Add files via upload

This commit is contained in:
Celina
2024-09-25 11:20:22 -07:00
committed by GitHub
commit d04a8dd9b3
57 changed files with 1492 additions and 0 deletions

44
CHANGELOG.md Normal file
View File

@@ -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

21
LICENSE Normal file
View File

@@ -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.

2
Procfile Normal file
View File

@@ -0,0 +1,2 @@
web: bin/rails server
worker: bundle exec sidekiq

4
Procfile.dev Normal file
View File

@@ -0,0 +1,4 @@
web: bin/rails server -p 3000
worker: bundle exec sidekiq
js: yarn build --reload
css: yarn build:css --watch

111
README.md Normal file
View File

@@ -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
```

9
Rakefile Normal file
View File

@@ -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

View File

@@ -0,0 +1,2 @@
//= link_tree ../builds
//= link_tree ../images

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,10 @@
class HomeController < ApplicationController
def index
end
def terms
end
def privacy
end
end

View File

@@ -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

View File

@@ -0,0 +1,7 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!
def index
@notifications = current_user.notifications.includes(:event)
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
})
})

View File

@@ -0,0 +1,6 @@
import { application } from "./application"
import controllers from './**/*_controller.js'
controllers.forEach((controller) => {
application.register(controller.name, controller.module.default)
})

View File

@@ -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

32
app/models/service.rb Normal file
View File

@@ -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

12
app/models/user.rb Normal file
View File

@@ -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

View File

@@ -0,0 +1,28 @@
<h1>What's New</h1>
<div class="card">
<div class="card-body">
<% @announcements.each_with_index do |announcement, index| %>
<% if index != 0 %>
<div class="row"><div class="col"><hr /></div></div>
<% end %>
<div class="row announcement" id="<%= dom_id(announcement) %>">
<div class="col-sm-1 text-center">
<%= link_to announcements_path(anchor: dom_id(announcement)) do %>
<strong><%= announcement.published_at.strftime("%b %d") %></strong>
<% end %>
</div>
<div class="col">
<strong class="<%= announcement_class(announcement.announcement_type) %>"><%= announcement.announcement_type.titleize %>:</strong>
<strong><%= announcement.name %></strong>
<%= simple_format announcement.description %>
</div>
</div>
<% end %>
<% if @announcements.empty? %>
<div>Exciting stuff coming very soon!</div>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h2 class="text-center">Resend confirmation instructions</h2>
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.label :email, class: 'form-label' %><br />
<%= f.email_field :email, autofocus: true, class: 'form-control', value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
</div>
<div class="actions d-grid">
<%= f.submit "Resend confirmation instructions", class: 'btn btn-primary btn-lg' %>
</div>
<% end %>
<div class="text-center">
<%= render "devise/shared/links" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<p>Welcome <%= @email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>

View File

@@ -0,0 +1,7 @@
<p>Hello <%= @email %>!</p>
<% if @resource.try(:unconfirmed_email?) %>
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
<% else %>
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
<% end %>

View File

@@ -0,0 +1,3 @@
<p>Hello <%= @resource.email %>!</p>
<p>We're contacting you to notify you that your password has been changed.</p>

View File

@@ -0,0 +1,8 @@
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>

View File

@@ -0,0 +1,7 @@
<p>Hello <%= @resource.email %>!</p>
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
<p>Click the link below to unlock your account:</p>
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>

View File

@@ -0,0 +1,29 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h2 class="text-center">Change your password</h2>
<%= 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 %>
<div class="mb-3">
<%= f.password_field :password, autofocus: true, autocomplete: "off", class: 'form-control', placeholder: "Password" %>
<% if @minimum_password_length %>
<p class="text-muted"><small><%= @minimum_password_length %> characters minimum</small></p>
<% end %>
</div>
<div class="mb-3">
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: "Confirm Password" %>
</div>
<div class="mb-3 d-grid">
<%= f.submit "Change my password", class: 'btn btn-primary btn-lg' %>
</div>
<% end %>
<div class="text-center">
<%= render "devise/shared/links" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h2 class="text-center">Reset your password</h2>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<p class="text-center">Enter your email address below and we will send you a link to reset your password.</p>
<div class="mb-3">
<%= f.email_field :email, autofocus: true, placeholder: 'Email address', class: 'form-control' %>
</div>
<div class="mb-3 d-grid">
<%= f.submit "Send password reset email", class: 'btn btn-primary btn-lg' %>
</div>
<% end %>
<div class="text-center">
<%= render "devise/shared/links" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,49 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h1 class="text-center">Account</h1>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %>
</div>
<div class="mb-3">
<%= f.email_field :email, class: 'form-control', placeholder: 'Email Address' %>
</div>
<div class="mb-3">
<%= f.label :avatar, class: "form-label" %>
<%= f.file_field :avatar, accept:'image/*' %>
</div>
<%= image_tag avatar_path(f.object), class: "rounded border shadow-sm d-block mx-auto my-3" %>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div class="alert alert-warning">Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<div class="mb-3">
<%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %>
<p class="form-text text-muted"><small>Leave password blank if you don't want to change it</small></p>
</div>
<div class="mb-3">
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %>
</div>
<div class="mb-3">
<%= f.password_field :current_password, autocomplete: "off", class: 'form-control', placeholder: 'Current Password' %>
<p class="form-text text-muted"><small>We need your current password to confirm your changes</small></p>
</div>
<div class="mb-3 d-grid">
<%= f.submit "Save Changes", class: 'btn btn-lg btn-primary' %>
</div>
<% end %>
<hr>
<p class="text-center"><%= link_to "Deactivate my account", registration_path(resource_name), data: { confirm: "Are you sure? You cannot undo this." }, method: :delete %></p>
</div>
</div>

View File

@@ -0,0 +1,33 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h1 class="text-center">Sign Up</h1>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %>
</div>
<div class="mb-3">
<%= f.email_field :email, autofocus: false, class: 'form-control', placeholder: "Email Address" %>
</div>
<div class="mb-3">
<%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %>
</div>
<div class="mb-3">
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %>
</div>
<div class="mb-3 d-grid">
<%= f.submit "Sign up", class: "btn btn-primary btn-lg" %>
</div>
<% end %>
<div class="text-center">
<%= render "devise/shared/links" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h1 class="text-center">Log in</h1>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="mb-3">
<%= f.email_field :email, autofocus: true, placeholder: 'Email Address', class: 'form-control' %>
</div>
<div class="mb-3">
<%= f.password_field :password, autocomplete: "off", placeholder: 'Password', class: 'form-control' %>
</div>
<% if devise_mapping.rememberable? -%>
<div class="form-check">
<label class="form-check-label">
<%= f.check_box :remember_me, class: "form-check-input" %>
Remember me
</label>
</div>
<% end -%>
<div class="mb-3 d-grid">
<%= f.submit "Log in", class: "btn btn-primary btn-lg" %>
</div>
<% end %>
<div class="text-center">
<%= render "devise/shared/links" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<% if resource.errors.any? %>
<div id="error_explanation" class="alert alert-danger">
<h6>
<%= I18n.t("errors.messages.not_saved",
count: resource.errors.count,
resource: resource.class.model_name.human.downcase)
%>
</h6>
<ul>
<% resource.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

View File

@@ -0,0 +1,25 @@
<%- if controller_name != 'sessions' %>
<%= link_to "Log in", new_session_path(resource_name) %><br />
<% end %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
<% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
<% 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) %><br />
<% 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 %><br />
<% end %>
<% end %>

View File

@@ -0,0 +1,20 @@
<div class="row">
<div class="col-lg-4 offset-lg-4">
<h1 class="text-center">Resend unlock instructions</h1>
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.submit "Resend unlock instructions", class: "btn btn-lg btn-primary" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<h1>Welcome to Jumpstart!</h1>
<p class="lead">Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.</p>

View File

@@ -0,0 +1,2 @@
<h1>Privacy Policy</h1>
<p class="lead">Use this for your Privacy Policy</p>

View File

@@ -0,0 +1,2 @@
<h1>Terms of Service</h1>
<p class="lead">Use this for your Terms of Service</p>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html class="h-100">
<head>
<%= render 'shared/head' %>
</head>
<body class="d-flex flex-column h-100">
<main class="flex-shrink-0">
<%= render 'shared/navbar' %>
<%= render 'shared/notices' %>
<div class="container mt-4 mx-auto">
<%= yield %>
</div>
</main>
<%= render 'shared/footer' %>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<h1>Notifications</h1>
<ul>
<% @notifications.each do |notification| %>
<%# Customize your notification format here. We typically recommend a `message` and `url` method on the Notifier classes. %>
<%#= link_to notification.message, notification.url %>
<li><%= notification.params %></li>
<% end %>
</ul>

View File

@@ -0,0 +1,10 @@
<footer class="bg-light footer mt-auto py-3">
<div class="container d-flex justify-content-between mx-auto text-muted">
<span>&copy; <%= Date.today.year %> Your Company</span>
<ul class="d-inline float-end list-inline mb-0">
<li class="list-inline-item mr-3"><%= link_to "Terms", terms_path %></li>
<li class="list-inline-item mr-3"><%= link_to "Privacy", privacy_path %></li>
</ul>
</div>
</footer>

View File

@@ -0,0 +1,10 @@
<title><%= Rails.configuration.application_name %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css" integrity="sha512-YWzhKL2whUzgiheMoBFwW8CKV4qpHQAEuvilg9FAn5VJUDwKZZxkJNuGM4XkWuk94WCrrwslk8yWNGmY1EduTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>

View File

@@ -0,0 +1,52 @@
<% if current_user != true_user %>
<div class="alert alert-warning text-center">
You're logged in as <b><%= current_user.name %> (<%= current_user.email %>)</b>
<%= link_to stop_impersonating_madmin_impersonates_path, method: :post do %><%= icon("fas", "times") %> Logout <% end %>
</div>
<% end %>
<nav class="navbar navbar-expand-md navbar-light bg-light">
<div class="container mx-auto">
<%= link_to Rails.configuration.application_name, root_path, class: "navbar-brand" %>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarsExample04" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarMain">
<ul class="navbar-nav me-auto mt-2 mt-md-0">
</ul>
<ul class="navbar-nav">
<li class="nav-item"><%= link_to "What's New", announcements_path, class: "nav-link #{unread_announcements(current_user)}" %></li>
<% if user_signed_in? %>
<li class="nav-item">
<%= link_to notifications_path, class: "nav-link" do %>
<span><i class="far fa-bell" aria-hidden="true"></i></span>
<% end %>
</li>
<li class="nav-item dropdown">
<%= link_to "#", id: "navbar-dropdown", class: "nav-link dropdown-toggle", data: { target: "nav-account-dropdown", bs_toggle: "dropdown" }, aria: { haspopup: true, expanded: false } do %>
<%= image_tag avatar_path(current_user, size: 40), height: 20, width: 20, class: "rounded" %>
<% end %>
<div id="nav-account-dropdown" class="dropdown-menu dropdown-menu-end" aria-labelledby="navbar-dropdown">
<%= link_to "Settings", edit_user_registration_path, class: "dropdown-item" %>
<% if current_user.admin? && respond_to?(:madmin_root_path) %>
<div class="dropdown-divider"></div>
<%= link_to "Admin Area", madmin_root_path, class: "dropdown-item" %>
<% end %>
<div class="dropdown-divider"></div>
<%= button_to "Logout", destroy_user_session_path, method: :delete, class: "dropdown-item" %>
</div>
</li>
<% else %>
<li class="nav-item"><%= link_to "Sign Up", new_user_registration_path, class: "nav-link" %></li>
<li class="nav-item"><%= link_to "Login", new_user_session_path, class: "nav-link" %></li>
<% end %>
</ul>
</div>
</div>
</nav>

View File

@@ -0,0 +1,8 @@
<% flash.each do |msg_type, message| %>
<div class="container mt-4 mx-auto">
<div class="alert <%= bootstrap_class_for(msg_type) %> alert-dismissible fade show" role="alert">
<%= message %>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
<% end %>

13
config/cable.yml Normal file
View File

@@ -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

96
esbuild.config.mjs Normal file
View File

@@ -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)
}

View File

@@ -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

View File

@@ -0,0 +1,50 @@
<%%= form_with(model: <%= model_resource_name %>) do |form| %>
<%% if <%= singular_table_name %>.errors.any? %>
<div id="error_explanation">
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
<ul>
<%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
<li><%%= message %></li>
<%% end %>
</ul>
</div>
<%% end %>
<% attributes.each do |attribute| -%>
<div class="mb-3">
<% if attribute.password_digest? -%>
<%%= form.label :password, class: 'form-label' %>
<%%= form.password_field :password, class: 'form-control' %>
</div>
<div class="mb-3">
<%%= 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 -%>
</div>
<% end -%>
<div class="mb-3">
<%% if <%= model_resource_name %>.persisted? %>
<div class="float-end">
<%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, class: "text-danger", data: { confirm: 'Are you sure?' } %>
</div>
<%% 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 %>
</div>
<%% end %>

View File

@@ -0,0 +1,3 @@
<h1>Edit <%= singular_table_name.capitalize %></h1>
<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %>

View File

@@ -0,0 +1,51 @@
<% name_attribute = attributes.find{ |a| a.name == "name" } %>
<% has_name = !!name_attribute %>
<div class="row">
<div class="col-sm-6">
<h1><%= plural_table_name.capitalize %></h1>
</div>
<div class="col-sm-6 text-end">
<%%= link_to new_<%= singular_table_name %>_path, class: 'btn btn-primary' do %>
Add New <%= human_name %>
<%% end %>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<% if has_name %>
<th>Name</th>
<% end %>
<% attributes.without(name_attribute).each do |attribute| -%>
<th><%= attribute.human_name %></th>
<% end -%>
<% unless has_name %>
<th></th>
<% end %>
</tr>
</thead>
<tbody>
<%% @<%= 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 %>
<td><%%= link_to <%= singular_table_name %>.name, <%= singular_table_name %> %></td>
<% end %>
<% attributes.without(name_attribute).each do |attribute| -%>
<td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
<% end -%>
<% unless has_name %>
<td><%%= link_to 'Show', <%= singular_table_name %> %></td>
<% end %>
<%% end %>
<%% end %>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,3 @@
<h1>New <%= singular_table_name %></h1>
<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %>

View File

@@ -0,0 +1,16 @@
<div class="page-header">
<%%= link_to <%= index_helper %>_path, class: 'btn btn-default' do %>
All <%= plural_table_name.capitalize %>
<%% end %>
<%%= link_to edit_<%= singular_table_name %>_path(@<%= singular_table_name %>), class: 'btn btn-primary' do %>
Edit
<%% end %>
<h1>Show <%= singular_table_name %></h1>
</div>
<dl class="dl-horizontal">
<%- attributes.each do |attribute| -%>
<dt><%= attribute.human_name %>:</dt>
<dd><%%= @<%= singular_table_name %>.<%= attribute.name %> %></dd>
<%- end -%>
</dl>

280
template.rb Normal file
View File

@@ -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

39
test/template_test.rb Normal file
View File

@@ -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