├── .foreman ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ └── jumpstart │ │ └── announcements.scss ├── channels │ └── application_cable │ │ └── connection.rb ├── controllers │ ├── announcements_controller.rb │ ├── application_controller.rb │ ├── home_controller.rb │ ├── madmin │ │ └── impersonates_controller.rb │ ├── notifications_controller.rb │ └── users │ │ └── omniauth_callbacks_controller.rb ├── helpers │ ├── announcements_helper.rb │ ├── avatar_helper.rb │ └── bootstrap_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ └── index.js ├── models │ ├── announcement.rb │ ├── service.rb │ └── user.rb └── views │ ├── announcements │ └── index.html.erb │ ├── devise │ ├── confirmations │ │ └── new.html.erb │ ├── mailer │ │ ├── confirmation_instructions.html.erb │ │ ├── email_changed.html.erb │ │ ├── password_change.html.erb │ │ ├── reset_password_instructions.html.erb │ │ └── unlock_instructions.html.erb │ ├── passwords │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── registrations │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── sessions │ │ └── new.html.erb │ ├── shared │ │ ├── _error_messages.html.erb │ │ └── _links.html.erb │ └── unlocks │ │ └── new.html.erb │ ├── home │ ├── index.html.erb │ ├── privacy.html.erb │ └── terms.html.erb │ ├── layouts │ └── application.html.erb │ ├── notifications │ └── index.html.erb │ └── shared │ ├── _footer.html.erb │ ├── _head.html.erb │ ├── _navbar.html.erb │ └── _notices.html.erb ├── config └── cable.yml ├── esbuild.config.mjs ├── github └── workflows │ └── verify.yml ├── lib └── templates │ └── erb │ └── scaffold │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb ├── template.rb └── test └── template_test.rb /.foreman: -------------------------------------------------------------------------------- 1 | procfile: Procfile.dev 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [excid3] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | services: 15 | postgres: 16 | image: postgres:latest 17 | env: 18 | POSTGRES_DB: test_app_production 19 | POSTGRES_USER: test_app 20 | POSTGRES_PASSWORD: password 21 | ports: ['5432:5432'] 22 | options: >- 23 | --health-cmd pg_isready 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | 28 | mysql: 29 | image: mysql:8 30 | env: 31 | MYSQL_ROOT_PASSWORD: password 32 | MYSQL_DATABASE: test_app_production 33 | MYSQL_USER: test_app 34 | MYSQL_PASSWORD: password 35 | ports: 36 | - 3306 37 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Setup Ruby 43 | uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: '3.2' 46 | bundler-cache: true 47 | 48 | - name: Setup Node 49 | uses: actions/setup-node@v3 50 | with: 51 | node-version: 'lts/*' 52 | 53 | - name: Install dependencies 54 | run: | 55 | npm install -g npm 56 | sudo apt-get -yqq install libsqlite3-dev libpq-dev libmysqlclient-dev 57 | gem install rails 58 | 59 | # Clean up git repo so the new rails template doesn't conflict 60 | - name: Remove git repo 61 | run: | 62 | rm -rf .git 63 | 64 | - name: Run tests 65 | env: 66 | TEST_APP_DATABASE_PASSWORD: password 67 | run: | 68 | rake 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .idea/ 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2021-12-27 2 | 3 | * Support Rails 7.0 4 | * Drop support for Rails 5.2 and earlier 5 | 6 | ### 2021-05-13 7 | 8 | * Upgrade to Bootstrap 5 9 | * Remove data-confirm-modal, not yet bootstrap 5 compatible 10 | * Drop jQuery 11 | * Include devise views directly 12 | 13 | ### 2021-03-04 14 | 15 | * Switch to Madmin for admin area 16 | 17 | ### 2020-10-24 18 | 19 | * Rescue from git configuration exception 20 | 21 | ### 2020-08-20 22 | 23 | * Add tests for generating postgres and mysql apps 24 | 25 | ### 2020-08-07 26 | 27 | * Refactor notifications to use the [Noticed gem](https://github.com/excid3/noticed) 28 | 29 | ### 2019-02-28 30 | 31 | * Adds support for Rails 6.0 32 | * Move all Javascript to Webpacker for Rails 5.2 and 6.0 33 | * Use Bootstrap, data-confirm-modal, and local-time from NPM packages 34 | * ProvidePlugin sets jQuery, $, and Rails variables for webpacker 35 | * Use https://github.com/excid3/administrate fork of Administrate 36 | * Adds fix for zeitwerk autoloader in Rails 6 37 | * Adds support for virtual attributes 38 | * Add Procfile, Procfile.dev and .foreman configs 39 | * Add welcome message and instructions after completion 40 | 41 | ### 2019-01-02 and before 42 | 43 | * Original version of Jumpstart 44 | * Supported Rails 5.2 only 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Oliver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails server 2 | worker: bundle exec sidekiq 3 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | worker: bundle exec sidekiq 3 | js: yarn build --reload 4 | css: yarn build:css --watch 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > 👉 This has evolved into [Jumpstart Pro](https://jumpstartrails.com), a Rails template that includes payments, team accounts, TailwindCSS, APIs, Hotwire Native, and much, much more. 3 | > Check it out at https://jumpstartrails.com 4 | 5 | # GoRails App Template 6 | 7 | This is a Rails template, so you pass it in as an option when creating a new app. 8 | 9 | #### Requirements 10 | 11 | You'll need the following installed to run the template successfully: 12 | 13 | * Ruby 2.5 or higher 14 | * bundler - `gem install bundler` 15 | * rails - `gem install rails` 16 | * Database - we recommend Postgres, but you can use MySQL, SQLite3, etc 17 | * Redis - For ActionCable support 18 | * ImageMagick or libvips for ActiveStorage variants 19 | * Yarn - `brew install yarn` or [Install Yarn](https://yarnpkg.com/en/docs/install) 20 | * Foreman (optional) - `gem install foreman` - helps run all your processes in development 21 | 22 | #### Creating a new app 23 | 24 | ```bash 25 | rails new myapp -d postgresql -m https://raw.githubusercontent.com/excid3/gorails-app-template/master/template.rb 26 | ``` 27 | 28 | Or if you have downloaded this repo, you can reference template.rb locally: 29 | 30 | ```bash 31 | rails new myapp -d postgresql -m template.rb 32 | ``` 33 | 34 | ❓Having trouble? Try adding `DISABLE_SPRING=1` before `rails new`. Spring will get confused if you create an app with the same name twice. 35 | 36 | #### Running your app 37 | 38 | ```bash 39 | bin/dev 40 | ``` 41 | 42 | You can also run them in separate terminals manually if you prefer. 43 | 44 | A separate `Procfile` is generated for deploying to production on Heroku. 45 | 46 | #### Authenticate with social networks 47 | 48 | We use the encrypted Rails Credentials for app_id and app_secrets when it comes to omniauth authentication. Edit them as so: 49 | 50 | ``` 51 | EDITOR=vim rails credentials:edit 52 | ``` 53 | 54 | Make sure your file follow this structure: 55 | 56 | ```yml 57 | secret_key_base: [your-key] 58 | development: 59 | github: 60 | app_id: something 61 | app_secret: something 62 | options: 63 | scope: 'user:email' 64 | whatever: true 65 | production: 66 | github: 67 | app_id: something 68 | app_secret: something 69 | options: 70 | scope: 'user:email' 71 | whatever: true 72 | ``` 73 | 74 | With the environment, the service and the app_id/app_secret. If this is done correctly, you should see login links 75 | for the services you have added to the encrypted credentials using `EDITOR=vim rails credentials:edit` 76 | 77 | #### Enabling Admin Panel 78 | App uses `madmin` [gem](https://github.com/excid3/madmin), so you need to run the madmin generator: 79 | 80 | ``` 81 | rails g madmin:install 82 | ``` 83 | 84 | This will install Madmin and generate resources for each of the models it finds. 85 | #### Redis set up 86 | ##### On OSX 87 | ``` 88 | brew update 89 | brew install redis 90 | brew services start redis 91 | ``` 92 | ##### Ubuntu 93 | ``` 94 | sudo apt-get install redis-server 95 | ``` 96 | 97 | #### Cleaning up 98 | 99 | ```bash 100 | rails db:drop 101 | spring stop 102 | cd .. 103 | rm -rf myapp 104 | ``` 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "lib" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../builds 2 | //= link_tree ../images 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/jumpstart/announcements.scss: -------------------------------------------------------------------------------- 1 | .announcement { 2 | strong { 3 | color: $gray-700; 4 | font-weight: 900; 5 | } 6 | } 7 | 8 | .unread-announcements:before { 9 | -moz-border-radius: 50%; 10 | -webkit-border-radius: 50%; 11 | border-radius: 50%; 12 | -moz-background-clip: padding-box; 13 | -webkit-background-clip: padding-box; 14 | background-clip: padding-box; 15 | background: $red; 16 | content: ''; 17 | display: inline-block; 18 | height: 8px; 19 | width: 8px; 20 | margin-right: 6px; 21 | } 22 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | identified_by :current_user, :true_user 4 | impersonates :user 5 | 6 | def connect 7 | self.current_user = find_verified_user 8 | logger.add_tags "ActionCable", "User #{current_user.id}" 9 | end 10 | 11 | protected 12 | 13 | def find_verified_user 14 | if (current_user = env['warden'].user) 15 | current_user 16 | else 17 | reject_unauthorized_connection 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/announcements_controller.rb: -------------------------------------------------------------------------------- 1 | class AnnouncementsController < ApplicationController 2 | before_action :mark_as_read, if: :user_signed_in? 3 | 4 | def index 5 | @announcements = Announcement.order(published_at: :desc) 6 | end 7 | 8 | private 9 | 10 | def mark_as_read 11 | current_user.update(announcements_last_read_at: Time.zone.now) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | impersonates :user 3 | include Pundit::Authorization 4 | 5 | protect_from_forgery with: :exception 6 | 7 | before_action :configure_permitted_parameters, if: :devise_controller? 8 | 9 | protected 10 | 11 | def configure_permitted_parameters 12 | devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) 13 | devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def index 3 | end 4 | 5 | def terms 6 | end 7 | 8 | def privacy 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/madmin/impersonates_controller.rb: -------------------------------------------------------------------------------- 1 | class Madmin::ImpersonatesController < Madmin::ApplicationController 2 | impersonates :user 3 | 4 | def impersonate 5 | user = User.find(params[:id]) 6 | impersonate_user(user) 7 | redirect_to root_path 8 | end 9 | 10 | def stop_impersonating 11 | stop_impersonating_user 12 | redirect_to root_path 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class NotificationsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def index 5 | @notifications = current_user.notifications.includes(:event) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/users/omniauth_callbacks_controller.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class OmniauthCallbacksController < Devise::OmniauthCallbacksController 3 | before_action :set_service, except: [:failure] 4 | before_action :set_user, except: [:failure] 5 | 6 | attr_reader :service, :user 7 | 8 | def failure 9 | redirect_to root_path, alert: "Something went wrong" 10 | end 11 | 12 | def facebook 13 | handle_auth "Facebook" 14 | end 15 | 16 | def twitter 17 | handle_auth "Twitter" 18 | end 19 | 20 | def github 21 | handle_auth "Github" 22 | end 23 | 24 | private 25 | 26 | def handle_auth(kind) 27 | if service.present? 28 | service.update(service_attrs) 29 | else 30 | user.services.create(service_attrs) 31 | end 32 | 33 | if user_signed_in? 34 | flash[:notice] = "Your #{kind} account was connected." 35 | redirect_to edit_user_registration_path 36 | else 37 | sign_in_and_redirect user, event: :authentication 38 | set_flash_message :notice, :success, kind: kind 39 | end 40 | end 41 | 42 | def auth 43 | request.env['omniauth.auth'] 44 | end 45 | 46 | def set_service 47 | @service = Service.where(provider: auth.provider, uid: auth.uid).first 48 | end 49 | 50 | def set_user 51 | if user_signed_in? 52 | @user = current_user 53 | elsif service.present? 54 | @user = service.user 55 | elsif User.where(email: auth.info.email).any? 56 | # 5. User is logged out and they login to a new account which doesn't match their old one 57 | flash[:alert] = "An account with this email already exists. Please sign in with that account before connecting your #{auth.provider.titleize} account." 58 | redirect_to new_user_session_path 59 | else 60 | @user = create_user 61 | end 62 | end 63 | 64 | def service_attrs 65 | expires_at = auth.credentials.expires_at.present? ? Time.at(auth.credentials.expires_at) : nil 66 | { 67 | provider: auth.provider, 68 | uid: auth.uid, 69 | expires_at: expires_at, 70 | access_token: auth.credentials.token, 71 | access_token_secret: auth.credentials.secret, 72 | } 73 | end 74 | 75 | def create_user 76 | User.create( 77 | email: auth.info.email, 78 | #name: auth.info.name, 79 | password: Devise.friendly_token[0,20] 80 | ) 81 | end 82 | 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /app/helpers/announcements_helper.rb: -------------------------------------------------------------------------------- 1 | module AnnouncementsHelper 2 | def unread_announcements(user) 3 | last_announcement = Announcement.order(published_at: :desc).first 4 | return if last_announcement.nil? 5 | 6 | # Highlight announcements for anyone not logged in, cuz tempting 7 | if user.nil? || user.announcements_last_read_at.nil? || user.announcements_last_read_at < last_announcement.published_at 8 | "unread-announcements" 9 | end 10 | end 11 | 12 | def announcement_class(type) 13 | { 14 | "new" => "text-success", 15 | "update" => "text-warning", 16 | "fix" => "text-danger", 17 | }.fetch(type, "text-success") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/helpers/avatar_helper.rb: -------------------------------------------------------------------------------- 1 | module AvatarHelper 2 | def avatar_path(object, options = {}) 3 | size = options[:size] || 180 4 | default_image = options[:default] || "mp" 5 | base_url = "https://secure.gravatar.com/avatar" 6 | base_url_params = "?s=#{size}&d=#{default_image}" 7 | 8 | if object.respond_to?(:avatar) && object.avatar.attached? && object.avatar.variable? 9 | object.avatar.variant(resize_to_fill: [size, size]) 10 | elsif object.respond_to?(:email) && object.email 11 | gravatar_id = Digest::MD5::hexdigest(object.email.downcase) 12 | "#{base_url}/#{gravatar_id}#{base_url_params}" 13 | else 14 | "#{base_url}/00000000000000000000000000000000#{base_url_params}" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/helpers/bootstrap_helper.rb: -------------------------------------------------------------------------------- 1 | module BootstrapHelper 2 | def bootstrap_class_for(flash_type) 3 | { 4 | success: "alert-success", 5 | error: "alert-danger", 6 | alert: "alert-warning", 7 | notice: "alert-primary" 8 | }.stringify_keys[flash_type.to_s] || flash_type.to_s 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Webpack, along with any other files 2 | // present in this directory. You're encouraged to place your actual application logic in 3 | // a relevant structure within app/javascript and only use these pack files to reference 4 | // that code so it'll be compiled. 5 | 6 | import "@hotwired/turbo-rails" 7 | require("@rails/activestorage").start() 8 | //require("trix") 9 | //require("@rails/actiontext") 10 | require("local-time").start() 11 | require("@rails/ujs").start() 12 | 13 | import './channels/**/*_channel.js' 14 | import "./controllers" 15 | 16 | import * as bootstrap from "bootstrap" 17 | 18 | document.addEventListener("turbo:load", () => { 19 | var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) 20 | var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { 21 | return new bootstrap.Tooltip(tooltipTriggerEl) 22 | }) 23 | 24 | var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) 25 | var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { 26 | return new bootstrap.Popover(popoverTriggerEl) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | import { application } from "./application" 2 | import controllers from './**/*_controller.js' 3 | controllers.forEach((controller) => { 4 | application.register(controller.name, controller.module.default) 5 | }) 6 | 7 | -------------------------------------------------------------------------------- /app/models/announcement.rb: -------------------------------------------------------------------------------- 1 | class Announcement < ApplicationRecord 2 | TYPES = %w{ new fix update } 3 | 4 | after_initialize :set_defaults 5 | 6 | validates :announcement_type, :description, :name, :published_at, presence: true 7 | validates :announcement_type, inclusion: { in: TYPES } 8 | 9 | def set_defaults 10 | self.published_at ||= Time.zone.now 11 | self.announcement_type ||= TYPES.first 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/service.rb: -------------------------------------------------------------------------------- 1 | class Service < ApplicationRecord 2 | belongs_to :user 3 | 4 | Devise.omniauth_configs.keys.each do |provider| 5 | scope provider, ->{ where(provider: provider) } 6 | end 7 | 8 | def client 9 | send("#{provider}_client") 10 | end 11 | 12 | def expired? 13 | expires_at? && expires_at <= Time.zone.now 14 | end 15 | 16 | def access_token 17 | send("#{provider}_refresh_token!", super) if expired? 18 | super 19 | end 20 | 21 | 22 | def twitter_client 23 | Twitter::REST::Client.new do |config| 24 | config.consumer_key = Rails.application.secrets.twitter_app_id 25 | config.consumer_secret = Rails.application.secrets.twitter_app_secret 26 | config.access_token = access_token 27 | config.access_token_secret = access_token_secret 28 | end 29 | end 30 | 31 | def twitter_refresh_token!(token); end 32 | end 33 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable and :omniauthable 4 | devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable 5 | 6 | has_one_attached :avatar 7 | has_person_name 8 | 9 | has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification" 10 | has_many :notification_mentions, as: :record, dependent: :destroy, class_name: "Noticed::Event" 11 | has_many :services 12 | end 13 | -------------------------------------------------------------------------------- /app/views/announcements/index.html.erb: -------------------------------------------------------------------------------- 1 |

What's New

2 | 3 |
4 |
5 | <% @announcements.each_with_index do |announcement, index| %> 6 | <% if index != 0 %> 7 |

8 | <% end %> 9 | 10 |
11 |
12 | <%= link_to announcements_path(anchor: dom_id(announcement)) do %> 13 | <%= announcement.published_at.strftime("%b %d") %> 14 | <% end %> 15 |
16 |
17 | <%= announcement.announcement_type.titleize %>: 18 | <%= announcement.name %> 19 | <%= simple_format announcement.description %> 20 |
21 |
22 | <% end %> 23 | 24 | <% if @announcements.empty? %> 25 |
Exciting stuff coming very soon!
26 | <% end %> 27 |
28 |
29 | -------------------------------------------------------------------------------- /app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Resend confirmation instructions

4 | 5 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> 6 | <%= render "devise/shared/error_messages", resource: resource %> 7 | 8 |
9 | <%= f.label :email, class: 'form-label' %>
10 | <%= f.email_field :email, autofocus: true, class: 'form-control', value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> 11 |
12 | 13 |
14 | <%= f.submit "Resend confirmation instructions", class: 'btn btn-primary btn-lg' %> 15 |
16 | <% end %> 17 | 18 |
19 | <%= render "devise/shared/links" %> 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @email %>!

2 | 3 |

You can confirm your account email through the link below:

4 | 5 |

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

6 | -------------------------------------------------------------------------------- /app/views/devise/mailer/email_changed.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @email %>!

2 | 3 | <% if @resource.try(:unconfirmed_email?) %> 4 |

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

5 | <% else %> 6 |

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/devise/mailer/password_change.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

We're contacting you to notify you that your password has been changed.

4 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Someone has requested a link to change your password. You can do this through the link below.

4 | 5 |

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

6 | 7 |

If you didn't request this, please ignore this email.

8 |

Your password won't change until you access the link above and create a new one.

9 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 |

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

8 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Change your password

4 | 5 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> 6 | <%= render "devise/shared/error_messages", resource: resource %> 7 | <%= f.hidden_field :reset_password_token %> 8 | 9 |
10 | <%= f.password_field :password, autofocus: true, autocomplete: "off", class: 'form-control', placeholder: "Password" %> 11 | <% if @minimum_password_length %> 12 |

<%= @minimum_password_length %> characters minimum

13 | <% end %> 14 |
15 | 16 |
17 | <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: "Confirm Password" %> 18 |
19 | 20 |
21 | <%= f.submit "Change my password", class: 'btn btn-primary btn-lg' %> 22 |
23 | <% end %> 24 | 25 |
26 | <%= render "devise/shared/links" %> 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Reset your password

4 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 5 | <%= render "devise/shared/error_messages", resource: resource %> 6 |

Enter your email address below and we will send you a link to reset your password.

7 | 8 |
9 | <%= f.email_field :email, autofocus: true, placeholder: 'Email address', class: 'form-control' %> 10 |
11 | 12 |
13 | <%= f.submit "Send password reset email", class: 'btn btn-primary btn-lg' %> 14 |
15 | <% end %> 16 |
17 | <%= render "devise/shared/links" %> 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Account

4 | 5 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> 6 | <%= render "devise/shared/error_messages", resource: resource %> 7 | 8 |
9 | <%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %> 10 |
11 | 12 |
13 | <%= f.email_field :email, class: 'form-control', placeholder: 'Email Address' %> 14 |
15 | 16 |
17 | <%= f.label :avatar, class: "form-label" %> 18 | <%= f.file_field :avatar, accept:'image/*' %> 19 |
20 | 21 | <%= image_tag avatar_path(f.object), class: "rounded border shadow-sm d-block mx-auto my-3" %> 22 | 23 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 24 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
25 | <% end %> 26 | 27 |
28 | <%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %> 29 |

Leave password blank if you don't want to change it

30 |
31 | 32 |
33 | <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %> 34 |
35 | 36 |
37 | <%= f.password_field :current_password, autocomplete: "off", class: 'form-control', placeholder: 'Current Password' %> 38 |

We need your current password to confirm your changes

39 |
40 | 41 |
42 | <%= f.submit "Save Changes", class: 'btn btn-lg btn-primary' %> 43 |
44 | <% end %> 45 |
46 | 47 |

<%= link_to "Deactivate my account", registration_path(resource_name), data: { confirm: "Are you sure? You cannot undo this." }, method: :delete %>

48 |
49 |
50 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sign Up

4 | 5 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> 6 | <%= render "devise/shared/error_messages", resource: resource %> 7 | 8 |
9 | <%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %> 10 |
11 | 12 |
13 | <%= f.email_field :email, autofocus: false, class: 'form-control', placeholder: "Email Address" %> 14 |
15 | 16 |
17 | <%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %> 18 |
19 | 20 |
21 | <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %> 22 |
23 | 24 |
25 | <%= f.submit "Sign up", class: "btn btn-primary btn-lg" %> 26 |
27 | <% end %> 28 | 29 |
30 | <%= render "devise/shared/links" %> 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Log in

4 | 5 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> 6 |
7 | <%= f.email_field :email, autofocus: true, placeholder: 'Email Address', class: 'form-control' %> 8 |
9 | 10 |
11 | <%= f.password_field :password, autocomplete: "off", placeholder: 'Password', class: 'form-control' %> 12 |
13 | 14 | <% if devise_mapping.rememberable? -%> 15 |
16 | 20 |
21 | <% end -%> 22 | 23 |
24 | <%= f.submit "Log in", class: "btn btn-primary btn-lg" %> 25 |
26 | <% end %> 27 | 28 |
29 | <%= render "devise/shared/links" %> 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /app/views/devise/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if resource.errors.any? %> 2 |
3 |
4 | <%= I18n.t("errors.messages.not_saved", 5 | count: resource.errors.count, 6 | resource: resource.class.model_name.human.downcase) 7 | %> 8 |
9 | 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end %> 4 | 5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end %> 8 | 9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end %> 12 | 13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end %> 16 | 17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end %> 20 | 21 | <%- if devise_mapping.omniauthable? %> 22 | <%- resource_class.omniauth_providers.each do |provider| %> 23 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
24 | <% end %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Resend unlock instructions

4 | 5 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> 6 | <%= render "devise/shared/error_messages", resource: resource %> 7 | 8 |
9 | <%= f.label :email, class: "form-label" %> 10 | <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %> 11 |
12 | 13 |
14 | <%= f.submit "Resend unlock instructions", class: "btn btn-lg btn-primary" %> 15 |
16 | <% end %> 17 | 18 | <%= render "devise/shared/links" %> 19 |
20 |
21 | -------------------------------------------------------------------------------- /app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome to Jumpstart!

2 |

Use this document as a way to quickly start any new project.
All you get is this text and a mostly barebones HTML document.

3 | -------------------------------------------------------------------------------- /app/views/home/privacy.html.erb: -------------------------------------------------------------------------------- 1 |

Privacy Policy

2 |

Use this for your Privacy Policy

3 | -------------------------------------------------------------------------------- /app/views/home/terms.html.erb: -------------------------------------------------------------------------------- 1 |

Terms of Service

2 |

Use this for your Terms of Service

3 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= render 'shared/head' %> 5 | 6 | 7 | 8 |
9 | <%= render 'shared/navbar' %> 10 | <%= render 'shared/notices' %> 11 | 12 |
13 | <%= yield %> 14 |
15 |
16 | 17 | <%= render 'shared/footer' %> 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/views/notifications/index.html.erb: -------------------------------------------------------------------------------- 1 |

Notifications

2 | 3 | 11 | -------------------------------------------------------------------------------- /app/views/shared/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /app/views/shared/_head.html.erb: -------------------------------------------------------------------------------- 1 | <%= Rails.configuration.application_name %> 2 | 3 | 4 | 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | 9 | <%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %> 10 | <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> 11 | -------------------------------------------------------------------------------- /app/views/shared/_navbar.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_user != true_user %> 2 |
3 | You're logged in as <%= current_user.name %> (<%= current_user.email %>) 4 | <%= link_to stop_impersonating_madmin_impersonates_path, method: :post do %><%= icon("fas", "times") %> Logout <% end %> 5 |
6 | <% end %> 7 | 8 | 53 | -------------------------------------------------------------------------------- /app/views/shared/_notices.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |msg_type, message| %> 2 |
3 | 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 4 | channel_prefix: streaming_logs_dev 5 | 6 | test: 7 | adapter: async 8 | 9 | production: 10 | adapter: redis 11 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 12 | channel_prefix: streaming_logs_production 13 | 14 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Esbuild is configured with 3 modes: 4 | // 5 | // `yarn build` - Build JavaScript and exit 6 | // `yarn build --watch` - Rebuild JavaScript on change 7 | // `yarn build --reload` - Reloads page when views, JavaScript, or stylesheets change 8 | // 9 | // Minify is enabled when "RAILS_ENV=production" 10 | // Sourcemaps are enabled in non-production environments 11 | 12 | import * as esbuild from "esbuild" 13 | import path from "path" 14 | import rails from "esbuild-rails" 15 | import chokidar from "chokidar" 16 | import http from "http" 17 | import { setTimeout } from "timers/promises" 18 | 19 | const clients = [] 20 | const entryPoints = [ 21 | "application.js" 22 | ] 23 | const watchDirectories = [ 24 | "./app/javascript/**/*.js", 25 | "./app/views/**/*.erb", 26 | "./app/assets/builds/**/*.css", // Wait for cssbundling changes 27 | ] 28 | const config = { 29 | absWorkingDir: path.join(process.cwd(), "app/javascript"), 30 | bundle: true, 31 | entryPoints: entryPoints, 32 | minify: process.env.RAILS_ENV == "production", 33 | outdir: path.join(process.cwd(), "app/assets/builds"), 34 | plugins: [rails()], 35 | sourcemap: process.env.RAILS_ENV != "production" 36 | } 37 | 38 | async function buildAndReload() { 39 | // Foreman & Overmind assign a separate PORT for each process 40 | const port = parseInt(process.env.PORT) 41 | const context = await esbuild.context({ 42 | ...config, 43 | banner: { 44 | js: ` (() => new EventSource("http://localhost:${port}").onmessage = () => location.reload())();`, 45 | } 46 | }) 47 | 48 | // Reload uses an HTTP server as an even stream to reload the browser 49 | http 50 | .createServer((req, res) => { 51 | return clients.push( 52 | res.writeHead(200, { 53 | "Content-Type": "text/event-stream", 54 | "Cache-Control": "no-cache", 55 | "Access-Control-Allow-Origin": "*", 56 | Connection: "keep-alive", 57 | }) 58 | ) 59 | }) 60 | .listen(port) 61 | 62 | await context.rebuild() 63 | console.log("[reload] initial build succeeded") 64 | 65 | let ready = false 66 | chokidar 67 | .watch(watchDirectories) 68 | .on("ready", () => { 69 | console.log("[reload] ready") 70 | ready = true 71 | }) 72 | .on("all", async (event, path) => { 73 | if (ready === false) return 74 | 75 | if (path.includes("javascript")) { 76 | try { 77 | await setTimeout(20) 78 | await context.rebuild() 79 | console.log("[reload] build succeeded") 80 | } catch (error) { 81 | console.error("[reload] build failed", error) 82 | } 83 | } 84 | clients.forEach((res) => res.write("data: update\n\n")) 85 | clients.length = 0 86 | }) 87 | } 88 | 89 | if (process.argv.includes("--reload")) { 90 | buildAndReload() 91 | } else if (process.argv.includes("--watch")) { 92 | let context = await esbuild.context({...config, logLevel: 'info'}) 93 | context.watch() 94 | } else { 95 | esbuild.build(config) 96 | } 97 | -------------------------------------------------------------------------------- /github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/andyw8/setup-rails for more information 2 | 3 | name: Verify 4 | on: [push] 5 | 6 | jobs: 7 | verify: 8 | uses: andyw8/setup-rails/.github/workflows/verify.yml@v1 -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%%= form_with(model: <%= model_resource_name %>) do |form| %> 2 | <%% if <%= singular_table_name %>.errors.any? %> 3 |
4 |

<%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:

5 | 6 | 11 |
12 | <%% end %> 13 | 14 | <% attributes.each do |attribute| -%> 15 |
16 | <% if attribute.password_digest? -%> 17 | <%%= form.label :password, class: 'form-label' %> 18 | <%%= form.password_field :password, class: 'form-control' %> 19 |
20 | 21 |
22 | <%%= form.label :password_confirmation, class: 'form-label' %> 23 | <%%= form.password_field :password_confirmation, class: 'form-control' %> 24 | <% else -%> 25 | <%%= form.label :<%= attribute.column_name %>, class: 'form-label' %> 26 | <% if attribute.field_type == "checkbox" -%> 27 | <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> 28 | <% else -%> 29 | <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: 'form-control' %> 30 | <% end -%> 31 | <% end -%> 32 |
33 | 34 | <% end -%> 35 |
36 | <%% if <%= model_resource_name %>.persisted? %> 37 |
38 | <%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, class: "text-danger", data: { confirm: 'Are you sure?' } %> 39 |
40 | <%% end %> 41 | 42 | <%%= form.submit class: 'btn btn-primary' %> 43 | 44 | <%% if <%= model_resource_name %>.persisted? %> 45 | <%%= link_to "Cancel", <%= model_resource_name %>, class: "btn btn-link" %> 46 | <%% else %> 47 | <%%= link_to "Cancel", <%= index_helper %>_path, class: "btn btn-link" %> 48 | <%% end %> 49 |
50 | <%% end %> 51 | -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit <%= singular_table_name.capitalize %>

2 | 3 | <%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> 4 | -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/index.html.erb: -------------------------------------------------------------------------------- 1 | <% name_attribute = attributes.find{ |a| a.name == "name" } %> 2 | <% has_name = !!name_attribute %> 3 | 4 |
5 |
6 |

<%= plural_table_name.capitalize %>

7 |
8 | 9 |
10 | <%%= link_to new_<%= singular_table_name %>_path, class: 'btn btn-primary' do %> 11 | Add New <%= human_name %> 12 | <%% end %> 13 |
14 |
15 | 16 |
17 | 18 | 19 | 20 | <% if has_name %> 21 | 22 | <% end %> 23 | 24 | <% attributes.without(name_attribute).each do |attribute| -%> 25 | 26 | <% end -%> 27 | <% unless has_name %> 28 | 29 | <% end %> 30 | 31 | 32 | 33 | 34 | <%% @<%= plural_table_name%>.each do |<%= singular_table_name %>| %> 35 | <%%= content_tag :tr, id: dom_id(<%= singular_table_name %>), class: dom_class(<%= singular_table_name %>) do %> 36 | <% if has_name %> 37 | 38 | <% end %> 39 | 40 | <% attributes.without(name_attribute).each do |attribute| -%> 41 | 42 | <% end -%> 43 | 44 | <% unless has_name %> 45 | 46 | <% end %> 47 | <%% end %> 48 | <%% end %> 49 | 50 |
Name<%= attribute.human_name %>
<%%= link_to <%= singular_table_name %>.name, <%= singular_table_name %> %><%%= <%= singular_table_name %>.<%= attribute.name %> %><%%= link_to 'Show', <%= singular_table_name %> %>
51 |
52 | -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/new.html.erb: -------------------------------------------------------------------------------- 1 |

New <%= singular_table_name %>

2 | 3 | <%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> 4 | -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/show.html.erb: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | <%- attributes.each do |attribute| -%> 13 |
<%= attribute.human_name %>:
14 |
<%%= @<%= singular_table_name %>.<%= attribute.name %> %>
15 | <%- end -%> 16 |
17 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "shellwords" 3 | 4 | # Copied from: https://github.com/mattbrictson/rails-template 5 | # Add this template directory to source_paths so that Thor actions like 6 | # copy_file and template resolve against our source files. If this file was 7 | # invoked remotely via HTTP, that means the files are not present locally. 8 | # In that case, use `git clone` to download them to a local temporary dir. 9 | def add_template_repository_to_source_path 10 | if __FILE__ =~ %r{\Ahttps?://} 11 | require "tmpdir" 12 | source_paths.unshift(tempdir = Dir.mktmpdir("jumpstart-")) 13 | at_exit { FileUtils.remove_entry(tempdir) } 14 | git clone: [ 15 | "--quiet", 16 | "https://github.com/excid3/jumpstart.git", 17 | tempdir 18 | ].map(&:shellescape).join(" ") 19 | 20 | if (branch = __FILE__[%r{jumpstart/(.+)/template.rb}, 1]) 21 | Dir.chdir(tempdir) { git checkout: branch } 22 | end 23 | else 24 | source_paths.unshift(File.dirname(__FILE__)) 25 | end 26 | end 27 | 28 | def read_gemfile? 29 | File.open("Gemfile").each_line do |line| 30 | return true if line.strip.start_with?("rails") && line.include?("6.") 31 | end 32 | end 33 | 34 | def rails_version 35 | @rails_version ||= Gem::Version.new(Rails::VERSION::STRING) || read_gemfile? 36 | end 37 | 38 | def rails_7_or_newer? 39 | Gem::Requirement.new(">= 7.0.0.alpha").satisfied_by? rails_version 40 | end 41 | 42 | unless rails_7_or_newer? 43 | say "\nJumpstart requires Rails 7 or newer. You are using #{rails_version}.", :green 44 | say "Please remove partially installed Jumpstart files #{original_app_name} and try again.", :green 45 | exit 1 46 | end 47 | 48 | def add_gems 49 | add_gem 'cssbundling-rails' 50 | add_gem 'devise', '~> 4.9' 51 | add_gem 'friendly_id', '~> 5.4' 52 | add_gem 'jsbundling-rails' 53 | add_gem 'madmin' 54 | add_gem 'name_of_person', github: "basecamp/name_of_person" # '~> 1.1' 55 | add_gem 'noticed', '~> 2.0' 56 | add_gem 'omniauth-facebook', '~> 8.0' 57 | add_gem 'omniauth-github', '~> 2.0' 58 | add_gem 'omniauth-twitter', '~> 1.4' 59 | add_gem 'pretender', '~> 0.3.4' 60 | add_gem 'pundit', '~> 2.1' 61 | add_gem 'sidekiq', '~> 6.2' 62 | add_gem 'sitemap_generator', '~> 6.1' 63 | add_gem 'whenever', require: false 64 | add_gem 'responders', github: 'heartcombo/responders', branch: 'main' 65 | end 66 | 67 | def set_application_name 68 | # Add Application Name to Config 69 | environment "config.application_name = Rails.application.class.module_parent_name" 70 | 71 | # Announce the user where they can change the application name in the future. 72 | puts "You can change application name inside: ./config/application.rb" 73 | end 74 | 75 | def add_users 76 | route "root to: 'home#index'" 77 | generate "devise:install" 78 | 79 | environment "config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }", env: 'development' 80 | generate :devise, "User", "first_name", "last_name", "announcements_last_read_at:datetime", "admin:boolean" 81 | 82 | # Set admin default to false 83 | in_root do 84 | migration = Dir.glob("db/migrate/*").max_by { |f| File.mtime(f) } 85 | gsub_file migration, /:admin/, ":admin, default: false" 86 | end 87 | 88 | gsub_file "config/initializers/devise.rb", / # config.secret_key = .+/, " config.secret_key = Rails.application.credentials.secret_key_base" 89 | 90 | inject_into_file("app/models/user.rb", "omniauthable, :", after: "devise :") 91 | end 92 | 93 | def add_authorization 94 | generate 'pundit:install' 95 | end 96 | 97 | def default_to_esbuild 98 | return if options[:javascript] == "esbuild" 99 | unless options[:skip_javascript] 100 | @options = options.merge(javascript: "esbuild") 101 | end 102 | end 103 | 104 | def add_javascript 105 | run "yarn add local-time esbuild-rails trix @hotwired/stimulus @hotwired/turbo-rails @rails/activestorage @rails/ujs @rails/request.js chokidar" 106 | end 107 | 108 | def copy_templates 109 | remove_file "app/assets/stylesheets/application.css" 110 | remove_file "app/javascript/application.js" 111 | remove_file "app/javascript/controllers/index.js" 112 | remove_file "Procfile.dev" 113 | 114 | copy_file "Procfile" 115 | copy_file "Procfile.dev" 116 | copy_file ".foreman" 117 | copy_file "esbuild.config.mjs" 118 | copy_file "app/javascript/application.js" 119 | copy_file "app/javascript/controllers/index.js" 120 | 121 | directory "app", force: true 122 | directory "config", force: true 123 | directory "lib", force: true 124 | 125 | route "get '/terms', to: 'home#terms'" 126 | route "get '/privacy', to: 'home#privacy'" 127 | end 128 | 129 | def add_sidekiq 130 | environment "config.active_job.queue_adapter = :sidekiq" 131 | 132 | insert_into_file "config/routes.rb", 133 | "require 'sidekiq/web'\n\n", 134 | before: "Rails.application.routes.draw do" 135 | 136 | content = <<~RUBY 137 | authenticate :user, lambda { |u| u.admin? } do 138 | mount Sidekiq::Web => '/sidekiq' 139 | 140 | namespace :madmin do 141 | resources :impersonates do 142 | post :impersonate, on: :member 143 | post :stop_impersonating, on: :collection 144 | end 145 | end 146 | end 147 | RUBY 148 | insert_into_file "config/routes.rb", "#{content}\n", after: "Rails.application.routes.draw do\n" 149 | end 150 | 151 | def add_announcements 152 | generate "model Announcement published_at:datetime announcement_type name description:text" 153 | route "resources :announcements, only: [:index]" 154 | end 155 | 156 | def add_notifications 157 | rails_command "noticed:install:migrations" 158 | route "resources :notifications, only: [:index]" 159 | end 160 | 161 | def add_multiple_authentication 162 | insert_into_file "config/routes.rb", ', controllers: { omniauth_callbacks: "users/omniauth_callbacks" }', after: " devise_for :users" 163 | 164 | generate "model Service user:references provider uid access_token access_token_secret refresh_token expires_at:datetime auth:text" 165 | 166 | template = """ 167 | env_creds = Rails.application.credentials[Rails.env.to_sym] || {} 168 | %i{ facebook twitter github }.each do |provider| 169 | if options = env_creds[provider] 170 | config.omniauth provider, options[:app_id], options[:app_secret], options.fetch(:options, {}) 171 | end 172 | end 173 | """.strip 174 | 175 | insert_into_file "config/initializers/devise.rb", " " + template + "\n\n", before: " # ==> Warden configuration" 176 | end 177 | 178 | def add_whenever 179 | run "wheneverize ." 180 | end 181 | 182 | def add_friendly_id 183 | generate "friendly_id" 184 | insert_into_file(Dir["db/migrate/**/*friendly_id_slugs.rb"].first, "[5.2]", after: "ActiveRecord::Migration") 185 | end 186 | 187 | def add_sitemap 188 | rails_command "sitemap:install" 189 | end 190 | 191 | def add_bootstrap 192 | rails_command "css:install:bootstrap" 193 | end 194 | 195 | def add_announcements_css 196 | insert_into_file 'app/assets/stylesheets/application.bootstrap.scss', '@import "jumpstart/announcements";' 197 | end 198 | 199 | def add_esbuild_script 200 | build_script = "node esbuild.config.mjs" 201 | 202 | case `npx -v`.to_f 203 | when 7.1...8.0 204 | run %(npm set-script build "#{build_script}") 205 | run %(yarn build) 206 | when (8.0..) 207 | run %(npm pkg set scripts.build="#{build_script}") 208 | run %(yarn build) 209 | else 210 | say %(Add "scripts": { "build": "#{build_script}" } to your package.json), :green 211 | end 212 | end 213 | 214 | def add_github_actions_ci 215 | copy_file "github/workflows/verify.yml", ".github/workflows/verify.yml" 216 | end 217 | 218 | def add_gem(name, *options) 219 | gem(name, *options) unless gem_exists?(name) 220 | end 221 | 222 | def gem_exists?(name) 223 | IO.read("Gemfile") =~ /^\s*gem ['"]#{name}['"]/ 224 | end 225 | 226 | # Main setup 227 | add_template_repository_to_source_path 228 | default_to_esbuild 229 | add_gems 230 | 231 | after_bundle do 232 | set_application_name 233 | add_users 234 | add_authorization 235 | add_javascript 236 | add_announcements 237 | add_notifications 238 | add_multiple_authentication 239 | add_sidekiq 240 | add_friendly_id 241 | add_bootstrap 242 | add_whenever 243 | add_sitemap 244 | add_announcements_css 245 | add_github_actions_ci 246 | rails_command "active_storage:install" 247 | 248 | # Make sure Linux is in the Gemfile.lock for deploying 249 | run "bundle lock --add-platform x86_64-linux" 250 | 251 | copy_templates 252 | 253 | add_esbuild_script 254 | 255 | # Commit everything to git 256 | unless ENV["SKIP_GIT"] 257 | git :init 258 | git add: "." 259 | # git commit will fail if user.email is not configured 260 | begin 261 | git commit: %( -m 'Initial commit' ) 262 | rescue StandardError => e 263 | puts e.message 264 | end 265 | end 266 | 267 | say 268 | say "Jumpstart app successfully created!", :blue 269 | say 270 | say "To get started with your new app:", :green 271 | say " cd #{original_app_name}" 272 | say 273 | say " # Update config/database.yml with your database credentials" 274 | say 275 | say " rails db:create" 276 | say " rails db:migrate" 277 | say " rails g madmin:install # Generate admin dashboards" 278 | say " gem install foreman" 279 | say " bin/dev" 280 | end 281 | -------------------------------------------------------------------------------- /test/template_test.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | 3 | class TemplateTest < Minitest::Test 4 | def setup 5 | system("[ -d test_app ] && rm -rf test_app") 6 | end 7 | 8 | def teardown 9 | setup 10 | end 11 | 12 | def test_generator_succeeds 13 | output, _err = capture_subprocess_io do 14 | system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb") 15 | end 16 | assert_includes output, "Jumpstart app successfully created!" 17 | 18 | output, _err = capture_subprocess_io do 19 | system("cd test_app && yarn build") 20 | end 21 | assert_includes output, "Done in " 22 | end 23 | 24 | # TODO: Fix these tests on CI so they don't fail on db:create 25 | # 26 | # def test_generator_with_postgres_succeeds 27 | # output, err = capture_subprocess_io do 28 | # system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb -d postgresql") 29 | # end 30 | # assert_includes output, "Jumpstart app successfully created!" 31 | # end 32 | 33 | # def test_generator_with_mysql_succeeds 34 | # output, err = capture_subprocess_io do 35 | # system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb -d mysql") 36 | # end 37 | # assert_includes output, "Jumpstart app successfully created!" 38 | # end 39 | end 40 | --------------------------------------------------------------------------------