Last active
August 29, 2015 14:17
-
-
Save nikhgupta/27ae08c38473648b07e1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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