commit d04a8dd9b3ba14b3aa25a28fd3556020847e876d Author: Celina <57647158+celina-lopez@users.noreply.github.com> Date: Wed Sep 25 11:20:22 2024 -0700 Add files via upload diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..97dec376 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +### 2021-12-27 + +* Support Rails 7.0 +* Drop support for Rails 5.2 and earlier + +### 2021-05-13 + +* Upgrade to Bootstrap 5 +* Remove data-confirm-modal, not yet bootstrap 5 compatible +* Drop jQuery +* Include devise views directly + +### 2021-03-04 + +* Switch to Madmin for admin area + +### 2020-10-24 + +* Rescue from git configuration exception + +### 2020-08-20 + +* Add tests for generating postgres and mysql apps + +### 2020-08-07 + +* Refactor notifications to use the [Noticed gem](https://github.com/excid3/noticed) + +### 2019-02-28 + +* Adds support for Rails 6.0 +* Move all Javascript to Webpacker for Rails 5.2 and 6.0 + * Use Bootstrap, data-confirm-modal, and local-time from NPM packages + * ProvidePlugin sets jQuery, $, and Rails variables for webpacker +* Use https://github.com/excid3/administrate fork of Administrate + * Adds fix for zeitwerk autoloader in Rails 6 + * Adds support for virtual attributes +* Add Procfile, Procfile.dev and .foreman configs +* Add welcome message and instructions after completion + +### 2019-01-02 and before + +* Original version of Jumpstart +* Supported Rails 5.2 only diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..67b94c37 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Chris Oliver + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..a9adec8f --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: bin/rails server +worker: bundle exec sidekiq diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 00000000..d44b54d3 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,4 @@ +web: bin/rails server -p 3000 +worker: bundle exec sidekiq +js: yarn build --reload +css: yarn build:css --watch diff --git a/README.md b/README.md new file mode 100644 index 00000000..129ed8a4 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +> [!NOTE] +> 👉 We've also built [Jumpstart Pro](https://jumpstartrails.com) which is a version of Jumpstart that includes payments with Stripe & Braintree, team accounts, TailwindCSS, and much more. + +# Jumpstart Rails Template + +All your Rails apps should start off with a bunch of great defaults. It's like Laravel Spark, for Rails. + +Want to see how it works? Check out [the Jumpstart walkthrough video](https://www.youtube.com/watch?v=ssOZpISfIfI): + +[![Jumpstart Ruby on Rails Template Walkthrough](https://i.imgur.com/pZDPbc7l.png)](https://www.youtube.com/watch?v=ssOZpISfIfI) + +## Getting Started + +Jumpstart is a Rails template, so you pass it in as an option when creating a new app. + +#### Requirements + +You'll need the following installed to run the template successfully: + +* Ruby 2.5 or higher +* bundler - `gem install bundler` +* rails - `gem install rails` +* Database - we recommend Postgres, but you can use MySQL, SQLite3, etc +* Redis - For ActionCable support +* ImageMagick or libvips for ActiveStorage variants +* Yarn - `brew install yarn` or [Install Yarn](https://yarnpkg.com/en/docs/install) +* Foreman (optional) - `gem install foreman` - helps run all your processes in development + +#### Creating a new app + +```bash +rails new myapp -d postgresql -m https://raw.githubusercontent.com/excid3/jumpstart/master/template.rb +``` + +Or if you have downloaded this repo, you can reference template.rb locally: + +```bash +rails new myapp -d postgresql -m template.rb +``` + +❓Having trouble? Try adding `DISABLE_SPRING=1` before `rails new`. Spring will get confused if you create an app with the same name twice. + +#### Running your app + +```bash +bin/dev +``` + +You can also run them in separate terminals manually if you prefer. + +A separate `Procfile` is generated for deploying to production on Heroku. + +#### Authenticate with social networks + +We use the encrypted Rails Credentials for app_id and app_secrets when it comes to omniauth authentication. Edit them as so: + +``` +EDITOR=vim rails credentials:edit +``` + +Make sure your file follow this structure: + +```yml +secret_key_base: [your-key] +development: + github: + app_id: something + app_secret: something + options: + scope: 'user:email' + whatever: true +production: + github: + app_id: something + app_secret: something + options: + scope: 'user:email' + whatever: true +``` + +With the environment, the service and the app_id/app_secret. If this is done correctly, you should see login links +for the services you have added to the encrypted credentials using `EDITOR=vim rails credentials:edit` + +#### Enabling Admin Panel +App uses `madmin` [gem](https://github.com/excid3/madmin), so you need to run the madmin generator: + +``` +rails g madmin:install +``` + +This will install Madmin and generate resources for each of the models it finds. +#### Redis set up +##### On OSX +``` +brew update +brew install redis +brew services start redis +``` +##### Ubuntu +``` +sudo apt-get install redis-server +``` + +#### Cleaning up + +```bash +rails db:drop +spring stop +cd .. +rm -rf myapp +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..e0622f85 --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +task :default => :test diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 00000000..decf0a9d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../builds +//= link_tree ../images diff --git a/app/assets/stylesheets/jumpstart/announcements.scss b/app/assets/stylesheets/jumpstart/announcements.scss new file mode 100644 index 00000000..a228f026 --- /dev/null +++ b/app/assets/stylesheets/jumpstart/announcements.scss @@ -0,0 +1,21 @@ +.announcement { + strong { + color: $gray-700; + font-weight: 900; + } +} + +.unread-announcements:before { + -moz-border-radius: 50%; + -webkit-border-radius: 50%; + border-radius: 50%; + -moz-background-clip: padding-box; + -webkit-background-clip: padding-box; + background-clip: padding-box; + background: $red; + content: ''; + display: inline-block; + height: 8px; + width: 8px; + margin-right: 6px; +} diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 00000000..39ed58a7 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,21 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user, :true_user + impersonates :user + + def connect + self.current_user = find_verified_user + logger.add_tags "ActionCable", "User #{current_user.id}" + end + + protected + + def find_verified_user + if (current_user = env['warden'].user) + current_user + else + reject_unauthorized_connection + end + end + end +end diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb new file mode 100644 index 00000000..1f671d1c --- /dev/null +++ b/app/controllers/announcements_controller.rb @@ -0,0 +1,13 @@ +class AnnouncementsController < ApplicationController + before_action :mark_as_read, if: :user_signed_in? + + def index + @announcements = Announcement.order(published_at: :desc) + end + + private + + def mark_as_read + current_user.update(announcements_last_read_at: Time.zone.now) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 00000000..ce537e97 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,15 @@ +class ApplicationController < ActionController::Base + impersonates :user + include Pundit::Authorization + + protect_from_forgery with: :exception + + before_action :configure_permitted_parameters, if: :devise_controller? + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) + devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar]) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 00000000..4bf0ee77 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,10 @@ +class HomeController < ApplicationController + def index + end + + def terms + end + + def privacy + end +end diff --git a/app/controllers/madmin/impersonates_controller.rb b/app/controllers/madmin/impersonates_controller.rb new file mode 100644 index 00000000..a329cee7 --- /dev/null +++ b/app/controllers/madmin/impersonates_controller.rb @@ -0,0 +1,14 @@ +class Madmin::ImpersonatesController < Madmin::ApplicationController + impersonates :user + + def impersonate + user = User.find(params[:id]) + impersonate_user(user) + redirect_to root_path + end + + def stop_impersonating + stop_impersonating_user + redirect_to root_path + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 00000000..2a73c5c5 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,7 @@ +class NotificationsController < ApplicationController + before_action :authenticate_user! + + def index + @notifications = current_user.notifications.includes(:event) + end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 00000000..25046f2f --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,84 @@ +module Users + class OmniauthCallbacksController < Devise::OmniauthCallbacksController + before_action :set_service, except: [:failure] + before_action :set_user, except: [:failure] + + attr_reader :service, :user + + def failure + redirect_to root_path, alert: "Something went wrong" + end + + def facebook + handle_auth "Facebook" + end + + def twitter + handle_auth "Twitter" + end + + def github + handle_auth "Github" + end + + private + + def handle_auth(kind) + if service.present? + service.update(service_attrs) + else + user.services.create(service_attrs) + end + + if user_signed_in? + flash[:notice] = "Your #{kind} account was connected." + redirect_to edit_user_registration_path + else + sign_in_and_redirect user, event: :authentication + set_flash_message :notice, :success, kind: kind + end + end + + def auth + request.env['omniauth.auth'] + end + + def set_service + @service = Service.where(provider: auth.provider, uid: auth.uid).first + end + + def set_user + if user_signed_in? + @user = current_user + elsif service.present? + @user = service.user + elsif User.where(email: auth.info.email).any? + # 5. User is logged out and they login to a new account which doesn't match their old one + flash[:alert] = "An account with this email already exists. Please sign in with that account before connecting your #{auth.provider.titleize} account." + redirect_to new_user_session_path + else + @user = create_user + end + end + + def service_attrs + expires_at = auth.credentials.expires_at.present? ? Time.at(auth.credentials.expires_at) : nil + { + provider: auth.provider, + uid: auth.uid, + expires_at: expires_at, + access_token: auth.credentials.token, + access_token_secret: auth.credentials.secret, + } + end + + def create_user + User.create( + email: auth.info.email, + #name: auth.info.name, + password: Devise.friendly_token[0,20] + ) + end + + end +end diff --git a/app/helpers/announcements_helper.rb b/app/helpers/announcements_helper.rb new file mode 100644 index 00000000..224e428f --- /dev/null +++ b/app/helpers/announcements_helper.rb @@ -0,0 +1,19 @@ +module AnnouncementsHelper + def unread_announcements(user) + last_announcement = Announcement.order(published_at: :desc).first + return if last_announcement.nil? + + # Highlight announcements for anyone not logged in, cuz tempting + if user.nil? || user.announcements_last_read_at.nil? || user.announcements_last_read_at < last_announcement.published_at + "unread-announcements" + end + end + + def announcement_class(type) + { + "new" => "text-success", + "update" => "text-warning", + "fix" => "text-danger", + }.fetch(type, "text-success") + end +end diff --git a/app/helpers/avatar_helper.rb b/app/helpers/avatar_helper.rb new file mode 100644 index 00000000..177dfe9f --- /dev/null +++ b/app/helpers/avatar_helper.rb @@ -0,0 +1,17 @@ +module AvatarHelper + def avatar_path(object, options = {}) + size = options[:size] || 180 + default_image = options[:default] || "mp" + base_url = "https://secure.gravatar.com/avatar" + base_url_params = "?s=#{size}&d=#{default_image}" + + if object.respond_to?(:avatar) && object.avatar.attached? && object.avatar.variable? + object.avatar.variant(resize_to_fill: [size, size]) + elsif object.respond_to?(:email) && object.email + gravatar_id = Digest::MD5::hexdigest(object.email.downcase) + "#{base_url}/#{gravatar_id}#{base_url_params}" + else + "#{base_url}/00000000000000000000000000000000#{base_url_params}" + end + end +end diff --git a/app/helpers/bootstrap_helper.rb b/app/helpers/bootstrap_helper.rb new file mode 100644 index 00000000..52127e20 --- /dev/null +++ b/app/helpers/bootstrap_helper.rb @@ -0,0 +1,10 @@ +module BootstrapHelper + def bootstrap_class_for(flash_type) + { + success: "alert-success", + error: "alert-danger", + alert: "alert-warning", + notice: "alert-primary" + }.stringify_keys[flash_type.to_s] || flash_type.to_s + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 00000000..81d6c6a2 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,28 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import "@hotwired/turbo-rails" +require("@rails/activestorage").start() +//require("trix") +//require("@rails/actiontext") +require("local-time").start() +require("@rails/ujs").start() + +import './channels/**/*_channel.js' +import "./controllers" + +import * as bootstrap from "bootstrap" + +document.addEventListener("turbo:load", () => { + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) + var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl) + }) + + var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) + var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl) + }) +}) diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 00000000..a538cbe0 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,6 @@ +import { application } from "./application" +import controllers from './**/*_controller.js' +controllers.forEach((controller) => { + application.register(controller.name, controller.module.default) +}) + diff --git a/app/models/announcement.rb b/app/models/announcement.rb new file mode 100644 index 00000000..dbeeea2d --- /dev/null +++ b/app/models/announcement.rb @@ -0,0 +1,13 @@ +class Announcement < ApplicationRecord + TYPES = %w{ new fix update } + + after_initialize :set_defaults + + validates :announcement_type, :description, :name, :published_at, presence: true + validates :announcement_type, inclusion: { in: TYPES } + + def set_defaults + self.published_at ||= Time.zone.now + self.announcement_type ||= TYPES.first + end +end diff --git a/app/models/service.rb b/app/models/service.rb new file mode 100644 index 00000000..fd5f8c5f --- /dev/null +++ b/app/models/service.rb @@ -0,0 +1,32 @@ +class Service < ApplicationRecord + belongs_to :user + + Devise.omniauth_configs.keys.each do |provider| + scope provider, ->{ where(provider: provider) } + end + + def client + send("#{provider}_client") + end + + def expired? + expires_at? && expires_at <= Time.zone.now + end + + def access_token + send("#{provider}_refresh_token!", super) if expired? + super + end + + + def twitter_client + Twitter::REST::Client.new do |config| + config.consumer_key = Rails.application.secrets.twitter_app_id + config.consumer_secret = Rails.application.secrets.twitter_app_secret + config.access_token = access_token + config.access_token_secret = access_token_secret + end + end + + def twitter_refresh_token!(token); end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..704f4c54 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,12 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable + + has_one_attached :avatar + has_person_name + + has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification" + has_many :notification_mentions, as: :record, dependent: :destroy, class_name: "Noticed::Event" + has_many :services +end diff --git a/app/views/announcements/index.html.erb b/app/views/announcements/index.html.erb new file mode 100644 index 00000000..175ccf35 --- /dev/null +++ b/app/views/announcements/index.html.erb @@ -0,0 +1,28 @@ +

What's New

+ +
+
+ <% @announcements.each_with_index do |announcement, index| %> + <% if index != 0 %> +

+ <% end %> + +
+
+ <%= link_to announcements_path(anchor: dom_id(announcement)) do %> + <%= announcement.published_at.strftime("%b %d") %> + <% end %> +
+
+ <%= announcement.announcement_type.titleize %>: + <%= announcement.name %> + <%= simple_format announcement.description %> +
+
+ <% end %> + + <% if @announcements.empty? %> +
Exciting stuff coming very soon!
+ <% end %> +
+
diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 00000000..6575189a --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,22 @@ +
+
+

Resend confirmation instructions

+ + <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email, class: 'form-label' %>
+ <%= f.email_field :email, autofocus: true, class: 'form-control', value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions", class: 'btn btn-primary btn-lg' %> +
+ <% end %> + +
+ <%= render "devise/shared/links" %> +
+
+
diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..dc55f64f --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

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

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 00000000..c9da7af4 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

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

+<% else %> +

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

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 00000000..b41daf47 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

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

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 00000000..f667dc12 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

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

+ +

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

+ +

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

+

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

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 00000000..41e148bf --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

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

+ +

Click the link below to unlock your account:

+ +

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

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 00000000..60ee31db --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,29 @@ +
+
+

Change your password

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

<%= @minimum_password_length %> characters minimum

+ <% end %> +
+ +
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: "Confirm Password" %> +
+ +
+ <%= f.submit "Change my password", class: 'btn btn-primary btn-lg' %> +
+ <% end %> + +
+ <%= render "devise/shared/links" %> +
+
+
diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 00000000..e61d4707 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,20 @@ +
+
+

Reset your password

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

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

+ +
+ <%= f.email_field :email, autofocus: true, placeholder: 'Email address', class: 'form-control' %> +
+ +
+ <%= f.submit "Send password reset email", class: 'btn btn-primary btn-lg' %> +
+ <% end %> +
+ <%= render "devise/shared/links" %> +
+
+
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 00000000..cce3da8d --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,49 @@ +
+
+

Account

+ + <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %> +
+ +
+ <%= f.email_field :email, class: 'form-control', placeholder: 'Email Address' %> +
+ +
+ <%= f.label :avatar, class: "form-label" %> + <%= f.file_field :avatar, accept:'image/*' %> +
+ + <%= image_tag avatar_path(f.object), class: "rounded border shadow-sm d-block mx-auto my-3" %> + + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %> +

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

+
+ +
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %> +
+ +
+ <%= f.password_field :current_password, autocomplete: "off", class: 'form-control', placeholder: 'Current Password' %> +

We need your current password to confirm your changes

+
+ +
+ <%= f.submit "Save Changes", class: 'btn btn-lg btn-primary' %> +
+ <% end %> +
+ +

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

+
+
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 00000000..221762a3 --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,33 @@ +
+
+

Sign Up

+ + <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %> +
+ +
+ <%= f.email_field :email, autofocus: false, class: 'form-control', placeholder: "Email Address" %> +
+ +
+ <%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %> +
+ +
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %> +
+ +
+ <%= f.submit "Sign up", class: "btn btn-primary btn-lg" %> +
+ <% end %> + +
+ <%= render "devise/shared/links" %> +
+
+
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 00000000..cb089411 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,32 @@ +
+
+

Log in

+ + <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.email_field :email, autofocus: true, placeholder: 'Email Address', class: 'form-control' %> +
+ +
+ <%= f.password_field :password, autocomplete: "off", placeholder: 'Password', class: 'form-control' %> +
+ + <% if devise_mapping.rememberable? -%> +
+ +
+ <% end -%> + +
+ <%= f.submit "Log in", class: "btn btn-primary btn-lg" %> +
+ <% end %> + +
+ <%= render "devise/shared/links" %> +
+
+
diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000..cc741938 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+
+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +
+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 00000000..96a94124 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 00000000..33a4fa72 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,20 @@ +
+
+

Resend unlock instructions

+ + <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email, class: "form-label" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %> +
+ +
+ <%= f.submit "Resend unlock instructions", class: "btn btn-lg btn-primary" %> +
+ <% end %> + + <%= render "devise/shared/links" %> +
+
diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 00000000..b4b4788d --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,2 @@ +

Welcome to Jumpstart!

+

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

diff --git a/app/views/home/privacy.html.erb b/app/views/home/privacy.html.erb new file mode 100644 index 00000000..a3ee85b6 --- /dev/null +++ b/app/views/home/privacy.html.erb @@ -0,0 +1,2 @@ +

Privacy Policy

+

Use this for your Privacy Policy

diff --git a/app/views/home/terms.html.erb b/app/views/home/terms.html.erb new file mode 100644 index 00000000..234d2356 --- /dev/null +++ b/app/views/home/terms.html.erb @@ -0,0 +1,2 @@ +

Terms of Service

+

Use this for your Terms of Service

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 00000000..9be6b640 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,19 @@ + + + + <%= render 'shared/head' %> + + + +
+ <%= render 'shared/navbar' %> + <%= render 'shared/notices' %> + +
+ <%= yield %> +
+
+ + <%= render 'shared/footer' %> + + diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb new file mode 100644 index 00000000..d8069d16 --- /dev/null +++ b/app/views/notifications/index.html.erb @@ -0,0 +1,10 @@ +

Notifications

+ + diff --git a/app/views/shared/_footer.html.erb b/app/views/shared/_footer.html.erb new file mode 100644 index 00000000..38604308 --- /dev/null +++ b/app/views/shared/_footer.html.erb @@ -0,0 +1,10 @@ + + diff --git a/app/views/shared/_head.html.erb b/app/views/shared/_head.html.erb new file mode 100644 index 00000000..1bc68f44 --- /dev/null +++ b/app/views/shared/_head.html.erb @@ -0,0 +1,10 @@ +<%= Rails.configuration.application_name %> + + + +<%= csrf_meta_tags %> +<%= csp_meta_tag %> + + +<%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %> +<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb new file mode 100644 index 00000000..bd02fa1b --- /dev/null +++ b/app/views/shared/_navbar.html.erb @@ -0,0 +1,52 @@ +<% if current_user != true_user %> +
+ You're logged in as <%= current_user.name %> (<%= current_user.email %>) + <%= link_to stop_impersonating_madmin_impersonates_path, method: :post do %><%= icon("fas", "times") %> Logout <% end %> +
+<% end %> + + diff --git a/app/views/shared/_notices.html.erb b/app/views/shared/_notices.html.erb new file mode 100644 index 00000000..fca73855 --- /dev/null +++ b/app/views/shared/_notices.html.erb @@ -0,0 +1,8 @@ +<% flash.each do |msg_type, message| %> +
+ +
+<% end %> diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..02c3701c --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,13 @@ +development: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: streaming_logs_dev + +test: + adapter: async + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: streaming_logs_production + diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 00000000..2fe760ba --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +// Esbuild is configured with 3 modes: +// +// `yarn build` - Build JavaScript and exit +// `yarn build --watch` - Rebuild JavaScript on change +// `yarn build --reload` - Reloads page when views, JavaScript, or stylesheets change +// +// Minify is enabled when "RAILS_ENV=production" +// Sourcemaps are enabled in non-production environments + +import * as esbuild from "esbuild" +import path from "path" +import rails from "esbuild-rails" +import chokidar from "chokidar" +import http from "http" +import { setTimeout } from "timers/promises" + +const clients = [] +const entryPoints = [ + "application.js" +] +const watchDirectories = [ + "./app/javascript/**/*.js", + "./app/views/**/*.erb", + "./app/assets/builds/**/*.css", // Wait for cssbundling changes +] +const config = { + absWorkingDir: path.join(process.cwd(), "app/javascript"), + bundle: true, + entryPoints: entryPoints, + minify: process.env.RAILS_ENV == "production", + outdir: path.join(process.cwd(), "app/assets/builds"), + plugins: [rails()], + sourcemap: process.env.RAILS_ENV != "production" +} + +async function buildAndReload() { + // Foreman & Overmind assign a separate PORT for each process + const port = parseInt(process.env.PORT) + const context = await esbuild.context({ + ...config, + banner: { + js: ` (() => new EventSource("http://localhost:${port}").onmessage = () => location.reload())();`, + } + }) + + // Reload uses an HTTP server as an even stream to reload the browser + http + .createServer((req, res) => { + return clients.push( + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Access-Control-Allow-Origin": "*", + Connection: "keep-alive", + }) + ) + }) + .listen(port) + + await context.rebuild() + console.log("[reload] initial build succeeded") + + let ready = false + chokidar + .watch(watchDirectories) + .on("ready", () => { + console.log("[reload] ready") + ready = true + }) + .on("all", async (event, path) => { + if (ready === false) return + + if (path.includes("javascript")) { + try { + await setTimeout(20) + await context.rebuild() + console.log("[reload] build succeeded") + } catch (error) { + console.error("[reload] build failed", error) + } + } + clients.forEach((res) => res.write("data: update\n\n")) + clients.length = 0 + }) +} + +if (process.argv.includes("--reload")) { + buildAndReload() +} else if (process.argv.includes("--watch")) { + let context = await esbuild.context({...config, logLevel: 'info'}) + context.watch() +} else { + esbuild.build(config) +} diff --git a/github/workflows/verify.yml b/github/workflows/verify.yml new file mode 100644 index 00000000..ee20a8f9 --- /dev/null +++ b/github/workflows/verify.yml @@ -0,0 +1,8 @@ +# See https://github.com/andyw8/setup-rails for more information + +name: Verify +on: [push] + +jobs: + verify: + uses: andyw8/setup-rails/.github/workflows/verify.yml@v1 \ No newline at end of file diff --git a/lib/templates/erb/scaffold/_form.html.erb b/lib/templates/erb/scaffold/_form.html.erb new file mode 100644 index 00000000..dd385640 --- /dev/null +++ b/lib/templates/erb/scaffold/_form.html.erb @@ -0,0 +1,50 @@ +<%%= form_with(model: <%= model_resource_name %>) do |form| %> + <%% if <%= singular_table_name %>.errors.any? %> +
+

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

+ + +
+ <%% end %> + +<% attributes.each do |attribute| -%> +
+<% if attribute.password_digest? -%> + <%%= form.label :password, class: 'form-label' %> + <%%= form.password_field :password, class: 'form-control' %> +
+ +
+ <%%= form.label :password_confirmation, class: 'form-label' %> + <%%= form.password_field :password_confirmation, class: 'form-control' %> +<% else -%> + <%%= form.label :<%= attribute.column_name %>, class: 'form-label' %> + <% if attribute.field_type == "checkbox" -%> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> + <% else -%> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: 'form-control' %> + <% end -%> +<% end -%> +
+ +<% end -%> +
+ <%% if <%= model_resource_name %>.persisted? %> +
+ <%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, class: "text-danger", data: { confirm: 'Are you sure?' } %> +
+ <%% end %> + + <%%= form.submit class: 'btn btn-primary' %> + + <%% if <%= model_resource_name %>.persisted? %> + <%%= link_to "Cancel", <%= model_resource_name %>, class: "btn btn-link" %> + <%% else %> + <%%= link_to "Cancel", <%= index_helper %>_path, class: "btn btn-link" %> + <%% end %> +
+<%% end %> diff --git a/lib/templates/erb/scaffold/edit.html.erb b/lib/templates/erb/scaffold/edit.html.erb new file mode 100644 index 00000000..50fe8dc5 --- /dev/null +++ b/lib/templates/erb/scaffold/edit.html.erb @@ -0,0 +1,3 @@ +

Edit <%= singular_table_name.capitalize %>

+ +<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> diff --git a/lib/templates/erb/scaffold/index.html.erb b/lib/templates/erb/scaffold/index.html.erb new file mode 100644 index 00000000..6c692d0b --- /dev/null +++ b/lib/templates/erb/scaffold/index.html.erb @@ -0,0 +1,51 @@ +<% name_attribute = attributes.find{ |a| a.name == "name" } %> +<% has_name = !!name_attribute %> + +
+
+

<%= plural_table_name.capitalize %>

+
+ +
+ <%%= link_to new_<%= singular_table_name %>_path, class: 'btn btn-primary' do %> + Add New <%= human_name %> + <%% end %> +
+
+ +
+ + + + <% if has_name %> + + <% end %> + + <% attributes.without(name_attribute).each do |attribute| -%> + + <% end -%> + <% unless has_name %> + + <% end %> + + + + + <%% @<%= plural_table_name%>.each do |<%= singular_table_name %>| %> + <%%= content_tag :tr, id: dom_id(<%= singular_table_name %>), class: dom_class(<%= singular_table_name %>) do %> + <% if has_name %> + + <% end %> + + <% attributes.without(name_attribute).each do |attribute| -%> + + <% end -%> + + <% unless has_name %> + + <% end %> + <%% end %> + <%% end %> + +
Name<%= attribute.human_name %>
<%%= link_to <%= singular_table_name %>.name, <%= singular_table_name %> %><%%= <%= singular_table_name %>.<%= attribute.name %> %><%%= link_to 'Show', <%= singular_table_name %> %>
+
diff --git a/lib/templates/erb/scaffold/new.html.erb b/lib/templates/erb/scaffold/new.html.erb new file mode 100644 index 00000000..fb7c45f4 --- /dev/null +++ b/lib/templates/erb/scaffold/new.html.erb @@ -0,0 +1,3 @@ +

New <%= singular_table_name %>

+ +<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> diff --git a/lib/templates/erb/scaffold/show.html.erb b/lib/templates/erb/scaffold/show.html.erb new file mode 100644 index 00000000..0b4616db --- /dev/null +++ b/lib/templates/erb/scaffold/show.html.erb @@ -0,0 +1,16 @@ + + +
+ <%- attributes.each do |attribute| -%> +
<%= attribute.human_name %>:
+
<%%= @<%= singular_table_name %>.<%= attribute.name %> %>
+ <%- end -%> +
diff --git a/template.rb b/template.rb new file mode 100644 index 00000000..d9844575 --- /dev/null +++ b/template.rb @@ -0,0 +1,280 @@ +require "fileutils" +require "shellwords" + +# Copied from: https://github.com/mattbrictson/rails-template +# Add this template directory to source_paths so that Thor actions like +# copy_file and template resolve against our source files. If this file was +# invoked remotely via HTTP, that means the files are not present locally. +# In that case, use `git clone` to download them to a local temporary dir. +def add_template_repository_to_source_path + if __FILE__ =~ %r{\Ahttps?://} + require "tmpdir" + source_paths.unshift(tempdir = Dir.mktmpdir("jumpstart-")) + at_exit { FileUtils.remove_entry(tempdir) } + git clone: [ + "--quiet", + "https://github.com/excid3/jumpstart.git", + tempdir + ].map(&:shellescape).join(" ") + + if (branch = __FILE__[%r{jumpstart/(.+)/template.rb}, 1]) + Dir.chdir(tempdir) { git checkout: branch } + end + else + source_paths.unshift(File.dirname(__FILE__)) + end +end + +def read_gemfile? + File.open("Gemfile").each_line do |line| + return true if line.strip.start_with?("rails") && line.include?("6.") + end +end + +def rails_version + @rails_version ||= Gem::Version.new(Rails::VERSION::STRING) || read_gemfile? +end + +def rails_7_or_newer? + Gem::Requirement.new(">= 7.0.0.alpha").satisfied_by? rails_version +end + +unless rails_7_or_newer? + say "\nJumpstart requires Rails 7 or newer. You are using #{rails_version}.", :green + say "Please remove partially installed Jumpstart files #{original_app_name} and try again.", :green + exit 1 +end + +def add_gems + add_gem 'cssbundling-rails' + add_gem 'devise', '~> 4.9' + add_gem 'friendly_id', '~> 5.4' + add_gem 'jsbundling-rails' + add_gem 'madmin' + add_gem 'name_of_person', github: "basecamp/name_of_person" # '~> 1.1' + add_gem 'noticed', '~> 2.0' + add_gem 'omniauth-facebook', '~> 8.0' + add_gem 'omniauth-github', '~> 2.0' + add_gem 'omniauth-twitter', '~> 1.4' + add_gem 'pretender', '~> 0.3.4' + add_gem 'pundit', '~> 2.1' + add_gem 'sidekiq', '~> 6.2' + add_gem 'sitemap_generator', '~> 6.1' + add_gem 'whenever', require: false + add_gem 'responders', github: 'heartcombo/responders', branch: 'main' +end + +def set_application_name + # Add Application Name to Config + environment "config.application_name = Rails.application.class.module_parent_name" + + # Announce the user where they can change the application name in the future. + puts "You can change application name inside: ./config/application.rb" +end + +def add_users + route "root to: 'home#index'" + generate "devise:install" + + environment "config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }", env: 'development' + generate :devise, "User", "first_name", "last_name", "announcements_last_read_at:datetime", "admin:boolean" + + # Set admin default to false + in_root do + migration = Dir.glob("db/migrate/*").max_by { |f| File.mtime(f) } + gsub_file migration, /:admin/, ":admin, default: false" + end + + gsub_file "config/initializers/devise.rb", / # config.secret_key = .+/, " config.secret_key = Rails.application.credentials.secret_key_base" + + inject_into_file("app/models/user.rb", "omniauthable, :", after: "devise :") +end + +def add_authorization + generate 'pundit:install' +end + +def default_to_esbuild + return if options[:javascript] == "esbuild" + unless options[:skip_javascript] + @options = options.merge(javascript: "esbuild") + end +end + +def add_javascript + run "yarn add local-time esbuild-rails trix @hotwired/stimulus @hotwired/turbo-rails @rails/activestorage @rails/ujs @rails/request.js chokidar" +end + +def copy_templates + remove_file "app/assets/stylesheets/application.css" + remove_file "app/javascript/application.js" + remove_file "app/javascript/controllers/index.js" + remove_file "Procfile.dev" + + copy_file "Procfile" + copy_file "Procfile.dev" + copy_file ".foreman" + copy_file "esbuild.config.mjs" + copy_file "app/javascript/application.js" + copy_file "app/javascript/controllers/index.js" + + directory "app", force: true + directory "config", force: true + directory "lib", force: true + + route "get '/terms', to: 'home#terms'" + route "get '/privacy', to: 'home#privacy'" +end + +def add_sidekiq + environment "config.active_job.queue_adapter = :sidekiq" + + insert_into_file "config/routes.rb", + "require 'sidekiq/web'\n\n", + before: "Rails.application.routes.draw do" + + content = <<~RUBY + authenticate :user, lambda { |u| u.admin? } do + mount Sidekiq::Web => '/sidekiq' + + namespace :madmin do + resources :impersonates do + post :impersonate, on: :member + post :stop_impersonating, on: :collection + end + end + end + RUBY + insert_into_file "config/routes.rb", "#{content}\n", after: "Rails.application.routes.draw do\n" +end + +def add_announcements + generate "model Announcement published_at:datetime announcement_type name description:text" + route "resources :announcements, only: [:index]" +end + +def add_notifications + rails_command "noticed:install:migrations" + route "resources :notifications, only: [:index]" +end + +def add_multiple_authentication + insert_into_file "config/routes.rb", ', controllers: { omniauth_callbacks: "users/omniauth_callbacks" }', after: " devise_for :users" + + generate "model Service user:references provider uid access_token access_token_secret refresh_token expires_at:datetime auth:text" + + template = """ + env_creds = Rails.application.credentials[Rails.env.to_sym] || {} + %i{ facebook twitter github }.each do |provider| + if options = env_creds[provider] + config.omniauth provider, options[:app_id], options[:app_secret], options.fetch(:options, {}) + end + end + """.strip + + insert_into_file "config/initializers/devise.rb", " " + template + "\n\n", before: " # ==> Warden configuration" +end + +def add_whenever + run "wheneverize ." +end + +def add_friendly_id + generate "friendly_id" + insert_into_file(Dir["db/migrate/**/*friendly_id_slugs.rb"].first, "[5.2]", after: "ActiveRecord::Migration") +end + +def add_sitemap + rails_command "sitemap:install" +end + +def add_bootstrap + rails_command "css:install:bootstrap" +end + +def add_announcements_css + insert_into_file 'app/assets/stylesheets/application.bootstrap.scss', '@import "jumpstart/announcements";' +end + +def add_esbuild_script + build_script = "node esbuild.config.mjs" + + case `npx -v`.to_f + when 7.1...8.0 + run %(npm set-script build "#{build_script}") + run %(yarn build) + when (8.0..) + run %(npm pkg set scripts.build="#{build_script}") + run %(yarn build) + else + say %(Add "scripts": { "build": "#{build_script}" } to your package.json), :green + end +end + +def add_github_actions_ci + copy_file "github/workflows/verify.yml", ".github/workflows/verify.yml" +end + +def add_gem(name, *options) + gem(name, *options) unless gem_exists?(name) +end + +def gem_exists?(name) + IO.read("Gemfile") =~ /^\s*gem ['"]#{name}['"]/ +end + +# Main setup +add_template_repository_to_source_path +default_to_esbuild +add_gems + +after_bundle do + set_application_name + add_users + add_authorization + add_javascript + add_announcements + add_notifications + add_multiple_authentication + add_sidekiq + add_friendly_id + add_bootstrap + add_whenever + add_sitemap + add_announcements_css + add_github_actions_ci + rails_command "active_storage:install" + + # Make sure Linux is in the Gemfile.lock for deploying + run "bundle lock --add-platform x86_64-linux" + + copy_templates + + add_esbuild_script + + # Commit everything to git + unless ENV["SKIP_GIT"] + git :init + git add: "." + # git commit will fail if user.email is not configured + begin + git commit: %( -m 'Initial commit' ) + rescue StandardError => e + puts e.message + end + end + + say + say "Jumpstart app successfully created!", :blue + say + say "To get started with your new app:", :green + say " cd #{original_app_name}" + say + say " # Update config/database.yml with your database credentials" + say + say " rails db:create" + say " rails db:migrate" + say " rails g madmin:install # Generate admin dashboards" + say " gem install foreman" + say " bin/dev" +end diff --git a/test/template_test.rb b/test/template_test.rb new file mode 100644 index 00000000..234dd1bc --- /dev/null +++ b/test/template_test.rb @@ -0,0 +1,39 @@ +require "minitest/autorun" + +class TemplateTest < Minitest::Test + def setup + system("[ -d test_app ] && rm -rf test_app") + end + + def teardown + setup + end + + def test_generator_succeeds + output, _err = capture_subprocess_io do + system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb") + end + assert_includes output, "Jumpstart app successfully created!" + + output, _err = capture_subprocess_io do + system("cd test_app && yarn build") + end + assert_includes output, "Done in " + end + + # TODO: Fix these tests on CI so they don't fail on db:create + # + # def test_generator_with_postgres_succeeds + # output, err = capture_subprocess_io do + # system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb -d postgresql") + # end + # assert_includes output, "Jumpstart app successfully created!" + # end + + # def test_generator_with_mysql_succeeds + # output, err = capture_subprocess_io do + # system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb -d mysql") + # end + # assert_includes output, "Jumpstart app successfully created!" + # end +end