Skip to content

Instantly share code, notes, and snippets.

@nikhgupta
Last active August 29, 2015 14:17
Show Gist options
  • Save nikhgupta/27ae08c38473648b07e1 to your computer and use it in GitHub Desktop.
Save nikhgupta/27ae08c38473648b07e1 to your computer and use it in GitHub Desktop.
# RubyOnRails application template for building rapid prototyping of
# admin backends using ActiveAdmin, and other gem components.
#
# All the components (or features) added by this template are optional,
# and can be chosen (or removed) either by supplying relevant arguments
# on the command-line, or by answering the queries created by this
# template.
#
# This template is, highly, opinionated, and intentionally, kept as
# simple as possible, so that even the starters can just follow through,
# and understand how it works, and later, customize it to their
# workflow.
#
# Author: Nikhil Gupta <[email protected]>
# Website: http://nikhgupta.com
# Package: Ruby On Rails
# URL: http://git.io/hM9X
# License: MIT
# Updated On: March 22, 2015
#
# Hint: I have the following lines specified in my ZSH configuration:
#
# new_admin_backend() {
# local template="https://gist.githubusercontent.com/nikhgupta/27ae08c38473648b07e1/raw/template.rb"
# rails new $@ -m "${template}" --github-flow --rspec --sidekiq \
# --active-admin --admin-user=user --admin-namespace=
# }
#
# TODO: Ensure that Github flow is installed on the system
# TODO: Add "--all" argument to select all possible features
#
# NOTE:
# - this template is primarily intended for rapid prototyping of admin backends.
# - high_voltage will be skipped, if active_admin is mounted on root namespace.
# - pundit is used for authorization and provides integration with active_admin.
# - support for test suites is lacking to aid in rapid prototyping.
#
# Other considerable gems:
# whenever, capybara, factory_girl, shoulda-matchers, simplecov, database_cleaner
#
# Exit early if skip-bundle was specified. A lot of the things will work
# unexpectedly, if we are unable to bundle the gems used by this template.
if options[:skip_bundle]
say_status "Warning", "Skipping template since it is incompatible with --skip-bundle argument.", :yellow
return
end
# Content/Messages
# ================
# This section sets up some variables for various messages, or content
# used in this template. This is included at the very start for easy
# customization of the messages.
readme_content = <<-CONTENT.gsub(/^ /, '').strip
# #{app_path.titleize}\n
This is a short description of your app. You MUST replace it, before
you even think about pushing this application to Github.
CONTENT
finished_message = <<-CONTENT.gsub(/^ /, '').strip
A new Rails application has been generated (and customized) for you.
You should, now, create your database, migrate it, and run your server.
\e[33mNote: Please, do not run command like: 'rake db:create db:migrate db:seed',
since for some reason (thread-safety?) 'db:seed' will miss upon a migration.
Try running: \e[34mrake db:migrate:reset db:migrate:status; rake db:seed\e[0m
CONTENT
home_page_html = <<-HTML.gsub(/^ /, '').strip
<h1>Welcome to your <%= Rails.application.class.parent_name.titleize %> application.</h1>
<p>You can edit this message inside: <code>app/views/pages/home.html.erb</code></p>
HTML
# Patch metaclass for the current generator to add new methods, and/or replace
# existing ones (dirty hacks). Namespacing these methods inside the metaclass
# feels more organized, than simply defining them in this template.
class << self
FEATURES = [:active_admin, :bootstrap, :github_flow, :high_voltage,
:pundit, :rspec, :sidekiq]
# Repatch the bundle_command to suppress the output.
# There could be a better way, but I am unaware of it.
# For now, this works.
old_bundle_command = instance_method(:bundle_command)
send(:define_method, :bundle_command) do |command|
old_bundle_command.bind(self).("#{command} > /dev/null")
end
# Allow specifying custom messages in generator and muting the verbose output
# from the actual commands.
def describe_task name, message, options = {}, &block
say_status name, message, options.fetch(:color, :magenta)
shell.mute { yield if block_given? }
end
# Parse extra arguments passed by the user on command-line, which have
# not yet been processed by rails, itself. This can be useful to adjust
# our template for user's requirement without asking too many questions.
#
def parsed_arguments
modified = @args.select{|arg| arg.start_with?("--")}.map do |arg|
key, val = arg.split("=")
_, bool, key = key.match(/^--(skip-|no-|)(.*)/).to_a
val = FEATURES.include?(key.underscore.to_sym) ? bool.empty? : val
[key.underscore.to_sym, val]
end
Hash[modified]
end
# Query the user for the requirement of a given feature, but do it
# smartly, i.e. only pest the user with a question when he has not
# supplied us with relevant arguments.
#
# Allows the user to state the requirement of a feature from within the
# `rails new` command, so that these features can be aliased by the user
# in their shell configuration, easily.
# For example, if we need to build an application that needs an
# ActiveAdmin backend, but not Sidekiq queue processing features, we can
# say:
#
# rails new test_app -m <template_path> --active-admin --skip-sidekiq
#
# And, alias it as follows in our shell configuration:
#
# new_admin_backend() {
# rails new $@ -m <template_path> --active-admin --skip-sidekiq
# }
#
def feature?(feature, question = nil)
return parsed_arguments[feature] if parsed_arguments.has_key?(feature)
return if question.blank?
skipped = question.present? && no?(question + " [Y/n]") ? "skip-" : ""
args.push("--#{skipped}#{feature.to_s.humanize.parameterize}")
end
# Ask user a question, and store the result in the arguments list for
# easy referencing later on.
#
# See: feature?
#
def query(key, question = nil, default = nil)
return parsed_arguments[key] if parsed_arguments.has_key?(key)
return if question.blank?
answer = ask("#{question} [#{default}]")
answer = default if answer.blank?
args.push("--#{key.to_s.humanize.parameterize}=#{answer}")
end
end
# User Inputs
# ===========
#
# Notify the user about what features have been enabled/disabled via CLI
(FEATURES & parsed_arguments.keys).each do |feature|
title = parsed_arguments[feature] ? :activated : :skipped
color = parsed_arguments[feature] ? :cyan : :yellow
say_status title, "- #{feature.to_s.titleize}", color
end
# Ask user for any inputs at the very start.
# Note: we should aim for not asking the user these questions, by
# allowing him to pass these values via CLI as arguments.
feature? :github_flow, "Use Github flow?"
feature? :active_admin, "Install ActiveAdmin for admin backend?"
feature? :sidekiq, "Install Sidekiq for background processing?"
feature? :rspec, "Use RSpec as testing framework?"
feature? :pundit, "Use Pundit for authorization?"
if feature? :active_admin
query(:admin_user, "What should be the user model for admin backend?", "admin_user")
query(:admin_namespace, "What should be the default namespace for admin backend?", "")
end
if query(:admin_namespace).present?
feature? :high_voltage, "Setup static homepage using HighVoltage?"
feature? :bootstrap, "Setup layouts using Twitter Bootstrap?"
end
# Show warnings to the user for incompatible features
if query(:admin_namespace).blank? && (feature?(:high_voltage) || feature?(:bootstrap))
say_status :warning, "Mounting active_admin to 'admin' namespace, due to incompatible feature list.", :yellow
@args.delete("--admin-namespace=")
@args << "--admin-namespace=admin"
end
# Start Customization
# ===================
# Ignore some files that may contain sensitive data from git repo,
# and add a shadow-copy in their place, instead.
describe_task :safeguard, "prevent sensitive config files from versioning" do
%w[ config/secrets.yml config/database.yml ].each do |file|
dest = File.basename(file, File.extname(file)) + ".example" + File.extname(file)
run %Q{ cp "#{file}" "#{File.join(File.dirname(file), dest)}" }
append_file ".gitignore", "\n#{file}"
end
end
# Update our README file by removing the `rdoc` version, and adding
# a new markdown based README file.
describe_task :readme, "Using markdown for README, instead." do
run "rm README.rdoc"
create_file "README.md", readme_content
end
# Setup components common to our test, and development environment
gem_group :test, :development do
gem "pry-rails"
if feature? :rspec
describe_task :rspec, "install and create requisite directory structure" do
gem "rspec-rails"
create_file "spec/models/.keep"
create_file "spec/support/.keep"
create_file "spec/routing/.keep"
end
after_bundle{ generate "rspec:install" }
end
end
# ActiveAdmin
# -----------
if feature? :active_admin
gem "devise"
gem "activeadmin", github: "gregbell/active_admin"
model_name = query(:admin_user).camelize
namespace = query(:admin_namespace).blank? ? "false" : ":#{query(:admin_namespace).underscore}"
# customize AA to behave as per user intended for.
custom_aa_config = <<-CONFIG.gsub(/^ {2}/, '').strip
# Custom configuration for ActiveAdmin (added via template)
config.site_title_link = "/"
config.default_namespace = #{namespace}
config.show_comments_in_menu = false
#{feature?(:pundit) ? "config.authorization_adapter = ActiveAdmin::PunditAdapter\n" : ""}
config.namespace(#{namespace}) do |namespace|
namespace.download_links = false
namespace.build_menu :default do |menu|
#{feature?(:sidekiq) ? "menu.add label: 'Monitor', url: ->{ sidekiq_web_path }, priority: 999, html_options: { target: :blank }, if: proc{ current_user.admin? }" : ""}
end
end
CONFIG
describe_task :devise, "add default_url_options to environments" do
environment "config.action_mailer.default_url_options = {host: 'localhost', port: 3000}\n", env: 'test'
environment "config.action_mailer.default_url_options = {host: 'localhost', port: 3000}\n", env: 'development'
environment "# TODO: uncomment this before deployment to production\n config.action_mailer.default_url_options = {host: 'localhost', port: 3000}\n", env: 'production'
end
after_bundle do
describe_task :active_admin, "install and modify configuration" do
# FIXME: find a way to suppress output from devise, too.
generate "active_admin:install", model_name, "--registerable"
insert_into_file "config/initializers/active_admin.rb", "\n\n #{custom_aa_config}\n", after: /ActiveAdmin\.setup do.*$/
append_file "app/assets/stylesheets/active_admin.css.scss", "#footer {\n p {\n display: none;\n }\n}"
end
describe_task :devise, "add admin field, and create default admin user" do
generate "migration", "AddAdminToUsers", "admin:boolean"
insert_into_file "app/admin/user.rb", " column :admin\n", after: "column :email\n"
append_file "db/seeds.rb", "\nUser.find_by(email: '[email protected]').update_attribute :admin, true"
append_file "db/seeds.rb", "\nUser.create!(email: '[email protected]', password: 'password', password_confirmation: 'password')"
end
end
end
# Pundit
# ------
if feature? :pundit
gem "pundit"
after_bundle do
generate "pundit:install"
describe_task :pundit, "activate and add default application policy" do
insert_into_file "app/controllers/application_controller.rb", "\n include Pundit\n", after: /class ApplicationController.*$/
insert_into_file "app/policies/application_policy.rb", "\n def destroy_all?\n user.admin?\n end\n", before: "\n def scope"
# update policies using gsub (saves the hassle of using an external template file)
gsub_file "app/policies/application_policy.rb", /def resolve.*?end/mi, "def resolve\n user.admin? ? scope.all : scope.where(user_id: @user)\n end\n"
gsub_file "app/policies/application_policy.rb", /def update\?.*?end/mi, "def update?\n show?\n end"
gsub_file "app/policies/application_policy.rb", /def destroy\?.*?end/mi, "def destroy?\n show?\n end"
gsub_file "app/policies/application_policy.rb", /def show\?.*?end/mi, "def show?\n user.admin? || scope.where(id: record.id).exists?\n end"
gsub_file "app/policies/application_policy.rb", "false", "true"
end
describe_task :pundit, "add user, active_admin comment and page policies" do
create_file "app/policies/user_policy.rb", "class UserPolicy < ApplicationPolicy\n\n def index?\n user.admin?\n end\n\n def create?\n user.admin?\n end\n\n class Scope < ApplicationPolicy::Scope\n def resolve\n user.admin? ? scope.all : scope.where(id: @user)\n end\n end\nend"
create_file "app/policies/active_admin/page_policy.rb", "module ActiveAdmin\n class PagePolicy < ApplicationPolicy\n def show?\n true\n end\n end\nend"
create_file "app/policies/active_admin/comment_policy.rb", "module ActiveAdmin\n class CommentPolicy < ApplicationPolicy\n end\nend"
end if feature?(:active_admin)
end
end
# Frontend
# --------
describe_task :high_voltage, "generate home page for the application" do
gem 'high_voltage'
create_file "app/views/pages/home.html.erb", home_page_html
route "root to: 'high_voltage/pages#show', id: 'home'"
end if feature?(:high_voltage)
after_bundle do
describe_task :frontend, "generate bootstrap templates" do
gem "bootstrap-generators"
generate "bootstrap:install", "--force"
# Update the application layout, so generated, to present a nice homepage
layout_file = "app/views/layouts/application.html.erb"
nav_html = "<ul class='nav navbar-nav'><li class='active'><%= link_to 'Home', root_path %></li></ul>"
nav_html += "\n<ul class='nav navbar-nav navbar-right'><li><%= link_to 'Login to #{query(:admin_namespace).titleize} Area', #{query(:admin_namespace)}_dashboard_path %></li></ul>" if feature?(:active_admin)
gsub_file layout_file, /project\s+name/i, @app_name.titleize
gsub_file layout_file, "Starter Template for Bootstrap", @app_name.titleize
gsub_file layout_file, /<ul class="nav navbar-nav">.*?<\/ul>/mi, nav_html
end
end if feature?(:bootstrap)
# Sidekiq
# -------
describe_task :sidekiq, "install and activate" do
gem "sidekiq"
gem "sinatra", require: false
sidekiq_config = <<-CONFIG.gsub(/^ {4}/, '').strip
---
:daemon: true
:concurrency: 5
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
staging:
:concurrency: 10
production:
:concurrency: 20
:queues:
- default
- [high_priority, 2]
CONFIG
create_file 'app/jobs/.keep'
create_file "config/sidekiq.yml", sidekiq_config
prepend_to_file "config/routes.rb", "require 'sidekiq/web'\n"
if feature? :active_admin
route "authenticate :user, lambda { |u| u.admin? } do\n mount Sidekiq::Web => '/monitor'\n end"
else
route "mount Sidekiq::Web => '/monitor'"
end
end if feature?(:sidekiq)
# FIXME: Possibly, a bug, but without it `after_bundle` won't work as expected.
describe_task :spring, "stopping spring, due to bug: https://github.com/rails/spring/issues/265" do
run "spring stop 1>/dev/null"
end
after_bundle do
describe_task :git, "create new repo and commit all customizations" do
git init: ". &>/dev/null"
git add: "."
git commit: "-qm 'generated custom Rails application, using template: http://git.io/hM9X'"
end
# create a new feature branch for the user for the initial setup
describe_task :git_flow, "activate and start new feature for user's setup" do
git flow: "init -d &>/dev/null"
git flow: "feature start setup &>/dev/null"
end if feature?(:github_flow)
# show a nice happy message :)
puts "\n\e[32m#{finished_message}\n\n\e[35mHappy Coding.. :)\e[0m"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment