require 'rubygems'
require 'optparse'
require 'bundler'
require 'bundler/cli'
class DevisableGenerator < Rails::Generators::Base
source_root File.expand_path("../templates", __FILE__)
@@available_configuration_options = {
'1' => 'database_authenticatable',
'2' => 'token_authenticatable',
'3' => 'oauthable',
'4' => 'confirmable',
'5' => 'recoverable',
'6' => 'registerable',
'7' => 'rememberable',
'8' => 'trackable',
'9' => 'timeoutable',
'10' => 'validatable',
'11' => 'lockable'
}
@@actual_configuration_options = {
'config' => ['1','5','6','7','8','10'],
'twitter_oauth' => [],
'facebook_oauth' => [],
'gems' => ['1.1.5','0.1.1','1.5.1','1.5.1'], #devise, warden_oauth, cancan, json_pure
'registration' => false,
'denied' => false,
'extra' => [],
'new' => false,
'sudo' => false,
'url' => 'http://localhost:3000'
}
#setups up configuration options from command line
#@param runtime_args [string,nil] arguments passed in via command line
def initialize(*runtime_args)
super(*runtime_args)
#@tut_args = runtime_args
#@@actual_configuration_options = @tut_args[1].split(',')
# -c, [--config] [string] # Configuration Options for devise. ie 1,2,3,7,11
# -o, [--oauth] [string] # Oauth secret and key seperated by , (comma). ie 123lk23jjsdfklsd,2l4kljknsdjlsdf
# -g, [--gems] [string] # Version of the gems you want to use seperated by Devise,Warden_oauth,Cancan. ie 1.1.5,0.1.1,1.5.1
# -r, [--registration] #add this option if you want to add a customized registration controller
# -d, [--denied] # add this option if you want to add a customized permission denied message
# -e, [--extra] [string] # Extra fields added to the user model. ie first_name:string,last_name:string
# -s, [--sudo] # add this option if gem commands need to use sudo
OptionParser.new do |opts|
opts.banner = "Usage: autoperf.rb [-c config]"
opts.on("-c", "--config [string]", String, "Devise configuration options") do |v|
@@actual_configuration_options['config'] = v.split(',')
end
opts.on("-t", "--twitteroauth [string]", String, "twitter oauth key and secret") do |v|
@@actual_configuration_options['twitter_oauth'] = v.split(',')
if @@actual_configuration_options['twitter_oauth'].length != 2
throw_error("You must have both key and secret seperated by comma for -to option.")
end
end
opts.on("-o", "--facebookoauth [string]", String, "facebook oauth key, secret, and client_id") do |v|
@@actual_configuration_options['facebook_oauth'] = v.split(',')
if @@actual_configuration_options['facebook_oauth'].length != 3
throw_error("You must have key, secret, and client_id seperated by comma for -fo option.")
end
end
opts.on("-g", "--gems [string]", String, "Gem Version options") do |v|
@@actual_configuration_options['gems'] = v.split(',')
if @@actual_configuration_options['gems'].length != 3
throw_error("You must have 3 gem versions seperated by comma for -g option. ie devise,warden,cancan")
end
end
opts.on("-d", "--denied [string]", String, "Add customized permission denied message") do |v|
@@actual_configuration_options['denied'] = true
end
opts.on("-e", "--extra [string]", String, "Extra fields added to the user model") do |v|
@@actual_configuration_options['extra'] = v.split(',')
end
opts.on("-n", "--new [string]", String, "Is New Project") do |v|
@@actual_configuration_options['new'] = true
end
opts.on("-s", "--sudo [string]", String, "Using sudo for gem commands") do |v|
@@actual_configuration_options['sudo'] = true
end
opts.on("-u", "--url [string]", String, "Root url for your app in development") do |v|
@@actual_configuration_options['url'] = v
end
opts.on("-C", "--cucumber [string]", String, "Include cucumber tests") do |v|
@@actual_configuration_options['cucumber'] = true
end
end.parse!
execute
end
#stops execution and prints error if an important enough error occurs
#@param error [string] The string that gets printed out when the error is thrown
def throw_error(error = '')
puts error
exit();
end
#replaces the last string 'end' in a file with the value of rep_str
#
#@param filename [string] file that will be manipulated
#@param rep_str [string] what the last end in the file will be replaced with
#@return nil
def replace_last_end_in_file_with(filename,rep_str)
gsub_file filename , /^(.*)end$/m, "\\1" + rep_str + "\nend"
end
#Adds all the necessary gems
def add_gems
sudo = ''
if @@actual_configuration_options['sudo']
sudo = 'sudo '
end
gem("devise", @@actual_configuration_options['gems'][0])
gem("warden_oauth", :version => @@actual_configuration_options['gems'][1], :git => "git://github.com/scottsampson/warden_oauth.git")
gem("cancan", @@actual_configuration_options['gems'][2])
gem("json_pure", @@actual_configuration_options['gems'][3])
gem("twitter_oauth", @@actual_configuration_options['gems'][4])
# cucumber only gems
if @@actual_configuration_options['cucumber']
gem("capybara")
gem("database_cleaner")
gem("cucumber-rails")
gem("cucumber")
gem("rspec-rails")
end
Bundler.with_clean_env do
output = system("bundle install")
if !output
err_str = "Gem conflict. Please make sure your gem versions in your Gemfile match the following:\n\n"
err_str += "devise - #{@@actual_configuration_options['gems'][0]}\n"
err_str += "warden_oauth - #{@@actual_configuration_options['gems'][1]}\n"
err_str += "cancan - #{@@actual_configuration_options['gems'][2]}\n"
err_str += "json_pure - #{@@actual_configuration_options['gems'][3]}\n\n\n"
throw_error(err_str)
end
end
end
#generates the devise user with option arguments from the 'extra' configuration options
def generate_devise_user
extra_args = ''
for i in 0...@@actual_configuration_options['extra'].length
extra_args << ' ' + @@actual_configuration_options['extra'][i]
end
unless extra_args.include?('username')
extra_args << ' username:string'
end
generate("devise:install")
generate("devise","user" + extra_args)
generate("devise:views")
end
#takes the options from the command line and
#replaces the existing devise configuration in the user model
#with the new one.
def generate_user_configuration
rep_str = ''
@@actual_configuration_options['config'].each do |option|
rep_str << ', :' + @@available_configuration_options[option]
end
rep_str = ' devise ' + rep_str[1..-1] + "\n"
gsub_file "app/models/user.rb" , /^[ ]*(devise)([^\n]*)(\n)(.*)(\s)/, rep_str
str = "attr_accessible :username\n"
insert_into_file "app/models/user.rb", str, :after => "# Setup accessible (or protected) attributes for your model\n"
rep_str = load_erb_string('partials/_user_model_methods.erb')
replace_last_end_in_file_with("app/models/user.rb",rep_str)
end
#grabs a certain migration file that already exists so it can be modified
#@param name excerpt of the name of the file you want
#@return the filename you were looking for
def get_migration(name)
basedir = './db/migrate'
Dir.chdir(basedir)
file = basedir + '/' + Dir.glob("*" + name + "*").first
Dir.chdir('../../')
return file
end
#loads a file from the templates folder and implements erb on it
#@param filename [string] Name of the file to be retrieved
#@return the data within the file after implementing erb
def load_erb_string(filename)
@twitter_secret = @@actual_configuration_options['twitter_oauth'][1]
@twitter_key = @@actual_configuration_options['twitter_oauth'][0]
@facebook_secret = @@actual_configuration_options['facebook_oauth'][1]
@facebook_key = @@actual_configuration_options['facebook_oauth'][0]
@facebook_client_id = @@actual_configuration_options['facebook_oauth'][2]
file_path = File.expand_path("../templates", __FILE__) + '/' + filename
erb_file = ERB.new(File.read(file_path),0,"%")
return erb_file.result(binding)
end
#loads a file from the templates folder WITHOUT implementing erb
#@param filename [string] Name of the file to be retrieved
#@return the data within the file
def load_file_string(filename)
file_path = File.expand_path("../templates", __FILE__) + '/' + filename
erb_file = File.read(file_path)
return erb_file
end
#checks for oauth
#if so adds oauth code
def check_for_oauth
@has_twitter_oauth = @@actual_configuration_options['twitter_oauth'].length > 0 ? true : false
@has_facebook_oauth = @@actual_configuration_options['facebook_oauth'].length > 0 ? true : false
@has_standard_authentication = @@actual_configuration_options['config'].include?('1') ? true : false
if @has_twitter_oauth || @had_facebook_oauth
@twitter_secret = @@actual_configuration_options['twitter_oauth'][1]
@twitter_key = @@actual_configuration_options['twitter_oauth'][0]
@facebook_secret = @@actual_configuration_options['facebook_oauth'][1]
@facebook_key = @@actual_configuration_options['facebook_oauth'][0]
@facebook_client_id = @@actual_configuration_options['facebook_oauth'][2]
@url = @@actual_configuration_options['url']
#/config/initializers/devise.rb
remove_file "config/initializers/devise.rb"
template "config/initializers/devise_initializer.erb", "config/initializers/devise.rb"
# remove user password from devise sign in page
unless @@actual_configuration_options['config'].include?('1')
gsub_file "app/views/devise/sessions/new.html.erb", "
<%= f.label :password %>
\n", ""
gsub_file "app/views/devise/sessions/new.html.erb", "<%= f.password_field :password %>
\n", ""
end
#add twitter_oauth
keys = load_erb_string('partials/_environments_development.erb')
append_to_file "config/environments/development.rb", keys
append_to_file "config/environments/test.rb", keys
#Add correct fields to the table
user_migration_file = get_migration("devise_create_user")
gsub_file user_migration_file, "# t.token_authenticatable", "t.token_authenticatable"
fields = load_erb_string('partials/_oauth_user_table_fields.erb')
insert_into_file user_migration_file, fields, :after => "t.token_authenticatable\n"
#add correct fields to user model
str = "attr_accessible :default_provider\n"
insert_into_file "app/models/user.rb", str, :after => "attr_accessible :username\n"
generate("model", "oauth_profile user_id:integer provider:string token:string secret:string username:string email:string name:string img_url:string")
insert_into_file "app/models/oauth_profile.rb", "belongs_to :user\n", :after => "class OauthProfile < ActiveRecord::Base\n"
insert_into_file "app/models/user.rb", "has_many :oauth_profiles\n", :after => "class User < ActiveRecord::Base\n"
end
# Link to "Login With Twitter/Facebook" somewhere in your view
login_links = load_erb_string('partials/_login_links.erb')
insert_into_file "app/views/layouts/application.html.erb", login_links, :after => "\n"
end
#adds confirmable code if the confirmable option for devise is included
def confirmable
if @@actual_configuration_options['config'].include?('4')
user_migration_file = get_migration("devise_create_user")
gsub_file user_migration_file, "# t.confirmable", "t.confirmable"
insert_into_file "app/controllers/application_controller.rb", "before_filter :mailer_set_url_options\n", :after => "class ApplicationController < ActionController::Base\n"
methods = load_erb_string('partials/_application_controller_methods.erb')
gsub_file "app/controllers/application_controller.rb", "end", methods + "\nend"
end
end
#adds lockable code if the lockable option for devise is included
def lockable
if @@actual_configuration_options['config'].include?('11')
user_migration_file = get_migration("devise_create_user")
gsub_file user_migration_file, "# t.lockable ", "t.lockable "
end
end
#adds the authenticate calls to the application controller
def add_authentication_check_to_controllers
insert_into_file "app/controllers/application_controller.rb", "before_filter :authenticate_user!, :except => []\n", :after => "class ApplicationController < ActionController::Base\n"
methods = load_erb_string('partials/_application_controller_methods2.erb')
insert_into_file "app/controllers/application_controller.rb",methods + "\n", :after => "before_filter :authenticate_user!, :except => []\n"
end
#installs cancan, updates the ability model, adds permission checks to views
def install_cancan
generate("cancan:ability")
#create the Role model
generate("scaffold","Role name:string")
generate("model","Permission role_id:integer model:string ability:string")
#create the Many To Many relationship between users and roles
generate("migration","UsersHaveAndBelongToManyRoles")
basedir = './db/migrate'
Dir.chdir(basedir)
file = basedir + '/' + Dir.glob("*users_have_and_belong_to_many*").first
Dir.chdir('../../')
self_up_migration = load_erb_string('partials/_migration_up.rb')
self_down_migration = load_erb_string('partials/_migration_down.rb')
insert_into_file file, self_up_migration, :after => "def self.up\n"
insert_into_file file, self_down_migration, :after => "def self.down\n"
insert_into_file "app/models/user.rb", "has_and_belongs_to_many :roles\n", :after => "class User < ActiveRecord::Base\n"
insert_into_file "app/models/role.rb", "has_and_belongs_to_many :users\n", :after => "class Role < ActiveRecord::Base\n"
insert_into_file "app/models/role.rb", "has_many :permissions\nvalidates_presence_of :name\n", :after => "has_and_belongs_to_many :users\n"
insert_into_file "app/controllers/roles_controller.rb", "before_filter :accessible_permissions, :only => [:new, :edit, :show, :update, :create]\nload_and_authorize_resource\n", :after => "class RolesController < ApplicationController\n"
insert_into_file "app/controllers/roles_controller.rb", "@role.save_permissions(params[:role_ids])\n", :after => "if @role.update_attributes(params[:role])\n"
insert_into_file "app/controllers/roles_controller.rb", "@role.save_permissions(params[:role_ids]) \n", :after => "if @role.save\n"
insert_into_file "app/models/permission.rb", "belongs_to :role\n", :after => "class Permission < ActiveRecord::Base\n"
rep_str = load_erb_string('partials/_permission_equals.rb')
insert_into_file "app/models/permission.rb", rep_str, :after => "belongs_to :role\n"
rep_str = load_erb_string('partials/_ability_class.rb')
gsub_file "app/models/ability.rb" , /^(.*)end$/m, rep_str
rep_str = load_erb_string('partials/_user_role.rb')
insert_into_file "app/models/user.rb", rep_str, :after => "has_and_belongs_to_many :roles\n"
end
#add a customized registration controller
def customize_registration_controller
template "app/controllers/registrations_controller.erb", "app/controllers/registrations_controller.rb"
gsub_file "config/routes.rb" , "devise_for :users\n", "devise_for :users, :controllers => { :registrations => \"registrations\" }\n"
end
#add a customize permission denied message for cancan
def customize_permission_denied_error
if @@actual_configuration_options['denied']
rep_str = load_erb_string('partials/_access_denied_flash.rb')
insert_into_file "app/controllers/application_controller.rb", rep_str, :after => "protect_from_forgery\n"
end
end
#creates the user tool instead of using scaffolding
def create_user_tool
@show_password = @@actual_configuration_options['config'].include?('1')
#TODO: Have to make sure if they have changed the devise_for :users that we account for it here.
insert_into_file "config/routes.rb", "resources :users", :after => "devise_for :users, :controllers => { :registrations => \"registrations\" }\n"
template "app/views/users/index.erb", "app/views/users/index.html.erb"
template "app/views/users/new.erb", "app/views/users/new.html.erb"
template "app/views/users/show.erb", "app/views/users/show.html.erb"
template "app/views/users/edit.erb", "app/views/users/edit.html.erb"
template "app/views/users/_form.erb", "app/views/users/_form.html.erb"
template "app/controllers/users_controller.erb", "app/controllers/users_controller.rb"
end
#uses scaffold to create the roles tool but then overrides many of the methods
def create_roles_tool
#TODO: Have to make sure if they have changed the devise_for :users that we account for it here.
rep_str = load_file_string('partials/_role_permission.rb')
insert_into_file "app/helpers/roles_helper.rb", rep_str, :after => "module RolesHelper\n"
#most of this is done in the scaffold above
gsub_file "app/views/roles/index.html.erb" , "<%= link_to 'Edit', edit_role_path(role) %> | ", "<%= link_to_if(can?(:edit, Role), 'Edit', edit_role_path(role)) %> | "
rep_str = load_erb_string('partials/_roles_index_delete.erb')
gsub_file "app/views/roles/index.html.erb" , "<%= link_to 'Destroy', role, :confirm => 'Are you sure?', :method => :delete %> | ", rep_str
gsub_file "app/views/roles/show.html.erb" , "<%= link_to 'Edit', edit_role_path(@role) %>", "<%= link_to_if(can?(:edit, Role), 'Edit', edit_role_path(@role)) %>"
gsub_file "app/views/roles/show.html.erb" , "<%= notice %>
", ""
gsub_file "app/views/roles/_form.html.erb" , /^[\s]*()[\s]+(<%= f.submit %>)[\s]+(<\/div>)/m, "
<%= f.label :permission %>
\n
\n <%= permissions_checkboxes(@role, :permission_ids, @accessible_permissions, @role.id) %>\n
\n
\n <%= f.submit %>\n
"
rep_str = load_erb_string('partials/_accessible_permissions_model.rb')
gsub_file "app/models/role.rb", /^(\s)*end\Z/m, rep_str
rep_str = load_erb_string('partials/_accessible_permissions_controller.rb')
gsub_file "app/controllers/roles_controller.rb" , /^(\s)*end\Z/m, rep_str
gsub_file "app/controllers/roles_controller.rb" , "format.html { redirect_to(roles_url) }", "format.html { redirect_to(roles_url, :notice => 'Role was successfully deleted.') }"
end
#adds the shared navigation to the layout view
def create_tool_navigation
empty_directory "app/views/shared"
template "app/views/shared/_admin_nav.erb", "app/views/shared/_admin_nav.html.erb"
rep_str = load_erb_string('partials/_application_current_tab.rb')
insert_into_file "app/helpers/application_helper.rb", rep_str, :after => "module ApplicationHelper\n"
end
#if the new option is selected removes the index file. Adds a welcome controller. Adds the root route
def new_project
if @@actual_configuration_options['new']
remove_file "public/index.html"
insert_into_file "config/routes.rb", "root :to => 'welcome#index'", :after => "resources :users\n"
template "app/controllers/welcome_controller.erb", "app/controllers/welcome_controller.rb"
empty_directory "app/views/welcome"
template "app/views/welcome/welcome_index.erb", "app/views/welcome/index.html.erb"
insert_into_file "config/routes.rb", "root :to => 'welcome#index'", :after => "resources :users\n"
rep_str = load_erb_string('partials/_application_flash.html.erb')
insert_into_file "app/views/layouts/application.html.erb", rep_str, :after => "<%= render 'shared/admin_nav' %>\n"
end
end
#adds the javascript for the role permissions checkboxes
def add_javascript
rep_str = load_erb_string('partials/_permission_manage.js')
append_to_file "public/javascripts/application.js", rep_str
end
#adds all the features and steps for cucumber tests
def add_cucumber_tests
if @@actual_configuration_options['cucumber']
generate('cucumber:install')
# features
template('cucumber/devise.feature', 'features/devise.feature')
template('cucumber/user.feature', 'features/user.feature')
template('cucumber/role.feature', 'features/role.feature')
# step definitions
template('cucumber/step_definitions/authentication_steps.rb', 'features/step_definitions/authentication_steps.rb')
template('cucumber/step_definitions/generic_steps.rb', 'features/step_definitions/generic_steps.rb')
template('cucumber/step_definitions/role_steps.rb', 'features/step_definitions/role_steps.rb')
template('cucumber/step_definitions/user_steps.rb', 'features/step_definitions/user_steps.rb')
# support
#empty_directory "features/support"
rep_str = load_erb_string('cucumber/support/_paths_partial.rb')
insert_into_file('features/support/paths.rb', rep_str, :after => "case page_name\n")
# this edit to the cucumber env file may be caused by rails 3.0.1 and can be removed after testing
gsub_file('features/support/env.rb', "require 'cucumber/rails/capybara_javascript_emulation'", "#require 'cucumber/rails/capybara_javascript_emulation'")
# rake task
rep_str = load_erb_string('cucumber/_rake_partial.rb')
insert_into_file('lib/tasks/cucumber.rake', rep_str, :after => "namespace :cucumber do\n")
gsub_file('lib/tasks/cucumber.rake', "task :cucumber => 'cucumber:ok'", "task :cucumber => ['cucumber:setup_js_with_vnc4server', 'cucumber:ok', 'cucumber:kill_js']")
end
end
#if you want to send the confirmable email you need to set the default mailer
def add_default_mailer_config
rep_str = "config.action_mailer.default_url_options = { :host => \"#{@@actual_configuration_options['url']}\" }\n"
insert_into_file "config/environments/development.rb", rep_str, :after => "config.action_mailer.raise_delivery_errors = false\n"
insert_into_file "config/environments/test.rb", rep_str, :after => "config.action_mailer.delivery_method = :test\n"
end
#add a little custom css for the flash notices
def modify_css
gsub_file('public/stylesheets/scaffold.css', "#notice {", "#notice, div.notice {")
end
#adds the rspec tests
def add_rspec_tests
rake('db:create')
generate('rspec:install')
['ability_spec','permission_spec','role_spec','user_spec'].each do |filename|
remove_file "spec/models/#{filename}.rb"
template("spec/models/#{filename}.erb", "spec/models/#{filename}.rb")
end
removing_files = ['spec/controllers/roles_controller_spec.rb','spec/helpers/roles_helper_spec.rb']
removing_files += ['spec/views/roles/edit.html.erb_spec.rb','spec/views/roles/index.html.erb_spec.rb']
removing_files += ['spec/views/roles/new.html.erb_spec.rb','spec/views/roles/show.html.erb_spec.rb','spec/requests/roles_spec.rb']
removing_files += ['spec/models/oauth_profile_spec.rb']
removing_files.each do |filename|
remove_file filename
end
template('spec/helpers/roles_helper_spec.erb', 'spec/helpers/roles_helper_spec.rb')
end
#feels a little procedural doesn't it.
#executes all the methods above in the correct order.
def execute
add_gems
generate_devise_user
generate_user_configuration
check_for_oauth
confirmable
lockable
add_authentication_check_to_controllers
install_cancan
customize_registration_controller
customize_permission_denied_error
create_user_tool
create_roles_tool
create_tool_navigation
new_project
add_javascript
add_cucumber_tests
add_default_mailer_config
modify_css
add_rspec_tests
end
end