├── .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 |
17 | <%= f.check_box :remember_me, class: "form-check-input" %>
18 | Remember me
19 |
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 |
10 | <% resource.errors.full_messages.each do |message| %>
11 | <%= message %>
12 | <% end %>
13 |
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 |
4 | <% @notifications.each do |notification| %>
5 | <%# Customize your notification format here. We typically recommend a `message` and `url` method on the Notifier classes. %>
6 | <%#= link_to notification.message, notification.url %>
7 |
8 | <%= notification.params %>
9 | <% end %>
10 |
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 |
9 |
10 | <%= link_to Rails.configuration.application_name, root_path, class: "navbar-brand" %>
11 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 | <%= link_to "What's New", announcements_path, class: "nav-link #{unread_announcements(current_user)}" %>
22 | <% if user_signed_in? %>
23 |
24 |
25 | <%= link_to notifications_path, class: "nav-link" do %>
26 |
27 | <% end %>
28 |
29 |
30 |
31 | <%= link_to "#", id: "navbar-dropdown", class: "nav-link dropdown-toggle", data: { target: "nav-account-dropdown", bs_toggle: "dropdown" }, aria: { haspopup: true, expanded: false } do %>
32 | <%= image_tag avatar_path(current_user, size: 40), height: 20, width: 20, class: "rounded" %>
33 | <% end %>
34 |
43 |
44 |
45 | <% else %>
46 | <%= link_to "Sign Up", new_user_registration_path, class: "nav-link" %>
47 | <%= link_to "Login", new_user_session_path, class: "nav-link" %>
48 | <% end %>
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/views/shared/_notices.html.erb:
--------------------------------------------------------------------------------
1 | <% flash.each do |msg_type, message| %>
2 |
3 |
4 | <%= message %>
5 |
6 |
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 |
7 | <%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
8 | <%%= message %>
9 | <%% end %>
10 |
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 | Name
22 | <% end %>
23 |
24 | <% attributes.without(name_attribute).each do |attribute| -%>
25 | <%= attribute.human_name %>
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 | <%%= link_to <%= singular_table_name %>.name, <%= singular_table_name %> %>
38 | <% end %>
39 |
40 | <% attributes.without(name_attribute).each do |attribute| -%>
41 | <%%= <%= singular_table_name %>.<%= attribute.name %> %>
42 | <% end -%>
43 |
44 | <% unless has_name %>
45 | <%%= link_to 'Show', <%= singular_table_name %> %>
46 | <% end %>
47 | <%% end %>
48 | <%% end %>
49 |
50 |
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 |
--------------------------------------------------------------------------------