diff options
author | Yehuda Katz + Carl Lerche <ykatz+clerche@engineyard.com> | 2009-09-24 14:01:31 -0700 |
---|---|---|
committer | Yehuda Katz + Carl Lerche <ykatz+clerche@engineyard.com> | 2009-09-24 16:11:41 -0700 |
commit | f0dd77c6be6a86fe384bb0015151e0a497973d39 (patch) | |
tree | ad81bb4c807c39edeaa37035882a16d9d95ebaa9 /railties/lib/rails | |
parent | 610b81beca461a6fa4f00c7023e0e4315eb2b8be (diff) | |
download | rails-f0dd77c6be6a86fe384bb0015151e0a497973d39.tar.gz rails-f0dd77c6be6a86fe384bb0015151e0a497973d39.tar.bz2 rails-f0dd77c6be6a86fe384bb0015151e0a497973d39.zip |
Move railties/lib/* into railties/lib/*
Diffstat (limited to 'railties/lib/rails')
256 files changed, 22421 insertions, 0 deletions
diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb new file mode 100644 index 0000000000..740d8a1767 --- /dev/null +++ b/railties/lib/rails/code_statistics.rb @@ -0,0 +1,107 @@ +class CodeStatistics #:nodoc: + + TEST_TYPES = %w(Units Functionals Unit\ tests Functional\ tests Integration\ tests) + + def initialize(*pairs) + @pairs = pairs + @statistics = calculate_statistics + @total = calculate_total if pairs.length > 1 + end + + def to_s + print_header + @pairs.each { |pair| print_line(pair.first, @statistics[pair.first]) } + print_splitter + + if @total + print_line("Total", @total) + print_splitter + end + + print_code_test_stats + end + + private + def calculate_statistics + @pairs.inject({}) { |stats, pair| stats[pair.first] = calculate_directory_statistics(pair.last); stats } + end + + def calculate_directory_statistics(directory, pattern = /.*\.rb$/) + stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } + + Dir.foreach(directory) do |file_name| + if File.stat(directory + "/" + file_name).directory? and (/^\./ !~ file_name) + newstats = calculate_directory_statistics(directory + "/" + file_name, pattern) + stats.each { |k, v| stats[k] += newstats[k] } + end + + next unless file_name =~ pattern + + f = File.open(directory + "/" + file_name) + + while line = f.gets + stats["lines"] += 1 + stats["classes"] += 1 if line =~ /class [A-Z]/ + stats["methods"] += 1 if line =~ /def [a-z]/ + stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ /^\s*#/ + end + end + + stats + end + + def calculate_total + total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } + @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } } + total + end + + def calculate_code + code_loc = 0 + @statistics.each { |k, v| code_loc += v['codelines'] unless TEST_TYPES.include? k } + code_loc + end + + def calculate_tests + test_loc = 0 + @statistics.each { |k, v| test_loc += v['codelines'] if TEST_TYPES.include? k } + test_loc + end + + def print_header + print_splitter + puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" + print_splitter + end + + def print_splitter + puts "+----------------------+-------+-------+---------+---------+-----+-------+" + end + + def print_line(name, statistics) + m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0 + loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0 + + start = if TEST_TYPES.include? name + "| #{name.ljust(20)} " + else + "| #{name.ljust(20)} " + end + + puts start + + "| #{statistics["lines"].to_s.rjust(5)} " + + "| #{statistics["codelines"].to_s.rjust(5)} " + + "| #{statistics["classes"].to_s.rjust(7)} " + + "| #{statistics["methods"].to_s.rjust(7)} " + + "| #{m_over_c.to_s.rjust(3)} " + + "| #{loc_over_m.to_s.rjust(5)} |" + end + + def print_code_test_stats + code = calculate_code + tests = calculate_tests + + puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}" + puts "" + end + end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb new file mode 100644 index 0000000000..841e98a0dc --- /dev/null +++ b/railties/lib/rails/commands.rb @@ -0,0 +1,17 @@ +commands = Dir["#{File.dirname(__FILE__)}/commands/*.rb"].collect { |file_path| File.basename(file_path).split(".").first } + +if commands.include?(ARGV.first) + require "#{File.dirname(__FILE__)}/commands/#{ARGV.shift}" +else + puts <<-USAGE +The 'run' provides a unified access point for all the default Rails' commands. + +Usage: ./script/run <command> [OPTIONS] + +Examples: + ./script/run generate controller Admin + ./script/run process reaper + +USAGE + puts "Choose: #{commands.join(", ")}" +end
\ No newline at end of file diff --git a/railties/lib/rails/commands/about.rb b/railties/lib/rails/commands/about.rb new file mode 100644 index 0000000000..bc2cfcb948 --- /dev/null +++ b/railties/lib/rails/commands/about.rb @@ -0,0 +1,3 @@ +require "#{RAILS_ROOT}/config/environment" +require 'rails/info' +puts Rails::Info diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb new file mode 100644 index 0000000000..31448bdf1a --- /dev/null +++ b/railties/lib/rails/commands/console.rb @@ -0,0 +1,45 @@ +irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' + +require 'optparse' + +options = { :sandbox => false, :irb => irb } +OptionParser.new do |opt| + opt.banner = "Usage: console [environment] [options]" + opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v } + opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |v| options[:irb] = v } + opt.on("--debugger", 'Enable ruby-debugging for the console.') { |v| options[:debugger] = v } + opt.parse!(ARGV) +end + +libs = " -r irb/completion" +libs << %( -r "#{RAILS_ROOT}/config/environment") +libs << " -r rails/console_app" +libs << " -r rails/console_sandbox" if options[:sandbox] +libs << " -r rails/console_with_helpers" + +if options[:debugger] + begin + require 'ruby-debug' + libs << " -r ruby-debug" + puts "=> Debugger enabled" + rescue Exception + puts "You need to install ruby-debug to run the console in debugging mode. With gems, use 'gem install ruby-debug'" + exit + end +end + +ENV['RAILS_ENV'] = case ARGV.first + when "p"; "production" + when "d"; "development" + when "t"; "test" + else + ARGV.first || ENV['RAILS_ENV'] || 'development' +end + +if options[:sandbox] + puts "Loading #{ENV['RAILS_ENV']} environment in sandbox (Rails #{Rails.version})" + puts "Any modifications you make will be rolled back on exit" +else + puts "Loading #{ENV['RAILS_ENV']} environment (Rails #{Rails.version})" +end +exec "#{options[:irb]} #{libs} --simple-prompt" diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb new file mode 100644 index 0000000000..e6f11a45db --- /dev/null +++ b/railties/lib/rails/commands/dbconsole.rb @@ -0,0 +1,87 @@ +require 'erb' +require 'yaml' +require 'optparse' + +include_password = false +options = {} + +OptionParser.new do |opt| + opt.banner = "Usage: dbconsole [options] [environment]" + opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| + include_password = true + end + + opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], + "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| + options['mode'] = mode + end + + opt.on("-h", "--header") do |h| + options['header'] = h + end + + opt.parse!(ARGV) + abort opt.to_s unless (0..1).include?(ARGV.size) +end + +env = ARGV.first || ENV['RAILS_ENV'] || 'development' +unless config = YAML::load(ERB.new(IO.read(RAILS_ROOT + "/config/database.yml")).result)[env] + abort "No database is configured for the environment '#{env}'" +end + + +def find_cmd(*commands) + dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) + commands += commands.map{|cmd| "#{cmd}.exe"} if RUBY_PLATFORM =~ /win32/ + + full_path_command = nil + found = commands.detect do |cmd| + dir = dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.executable? full_path_command + end + end + found ? full_path_command : abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") +end + +case config["adapter"] +when "mysql" + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'encoding' => '--default-character-set' + }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact + + if config['password'] && include_password + args << "--password=#{config['password']}" + elsif config['password'] && !config['password'].to_s.empty? + args << "-p" + end + + args << config['database'] + + exec(find_cmd('mysql', 'mysql5'), *args) + +when "postgresql" + ENV['PGUSER'] = config["username"] if config["username"] + ENV['PGHOST'] = config["host"] if config["host"] + ENV['PGPORT'] = config["port"].to_s if config["port"] + ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && include_password + exec(find_cmd('psql'), config["database"]) + +when "sqlite" + exec(find_cmd('sqlite'), config["database"]) + +when "sqlite3" + args = [] + + args << "-#{options['mode']}" if options['mode'] + args << "-header" if options['header'] + args << config['database'] + + exec(find_cmd('sqlite3'), *args) +else + abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" +end diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb new file mode 100644 index 0000000000..5013d30b83 --- /dev/null +++ b/railties/lib/rails/commands/destroy.rb @@ -0,0 +1,10 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'generators')) +require "#{RAILS_ROOT}/config/environment" + +if ARGV.size == 0 + Rails::Generators.help + exit +end + +name = ARGV.shift +Rails::Generators.invoke name, ARGV, :behavior => :revoke diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb new file mode 100755 index 0000000000..32cabcab10 --- /dev/null +++ b/railties/lib/rails/commands/generate.rb @@ -0,0 +1,10 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'generators')) +require "#{RAILS_ROOT}/config/environment" + +if ARGV.size == 0 + Rails::Generators.help + exit +end + +name = ARGV.shift +Rails::Generators.invoke name, ARGV, :behavior => :invoke diff --git a/railties/lib/rails/commands/ncgi/listener b/railties/lib/rails/commands/ncgi/listener new file mode 100755 index 0000000000..7079ef78a6 --- /dev/null +++ b/railties/lib/rails/commands/ncgi/listener @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby + +require 'stringio' +require 'fileutils' +require 'fcgi_handler' + +def message(s) + $stderr.puts "listener: #{s}" if ENV && ENV["DEBUG_GATEWAY"] +end + +class RemoteCGI < CGI + attr_accessor :stdinput, :stdoutput, :env_table + def initialize(env_table, input = nil, output = nil) + self.env_table = env_table + self.stdinput = input || StringIO.new + self.stdoutput = output || StringIO.new + super() + end + + def out(stream) # Ignore the requested output stream + super(stdoutput) + end +end + +class Listener + include DRbUndumped + + def initialize(timeout, socket_path) + @socket = File.expand_path(socket_path) + @mutex = Mutex.new + @active = false + @timeout = timeout + + @handler = RailsFCGIHandler.new + @handler.extend DRbUndumped + + message 'opening socket' + DRb.start_service("drbunix:#{@socket}", self) + + message 'entering process loop' + @handler.process! self + end + + def each_cgi(&cgi_block) + @cgi_block = cgi_block + message 'entering idle loop' + loop do + sleep @timeout rescue nil + die! unless @active + @active = false + end + end + + def process(env, input) + message 'received request' + @mutex.synchronize do + @active = true + + message 'creating input stream' + input_stream = StringIO.new(input) + message 'building CGI instance' + cgi = RemoteCGI.new(eval(env), input_stream) + + message 'yielding to fcgi handler' + @cgi_block.call cgi + message 'yield finished -- sending output' + + cgi.stdoutput.seek(0) + output = cgi.stdoutput.read + + return output + end + end + + def die! + message 'shutting down' + DRb.stop_service + FileUtils.rm_f @socket + Kernel.exit 0 + end +end + +socket_path = ARGV.shift +timeout = (ARGV.shift || 90).to_i + +Listener.new(timeout, socket_path) diff --git a/railties/lib/rails/commands/ncgi/tracker b/railties/lib/rails/commands/ncgi/tracker new file mode 100755 index 0000000000..4ca12d779b --- /dev/null +++ b/railties/lib/rails/commands/ncgi/tracker @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +require 'drb' +require 'thread' + +def message(s) + $stderr.puts "tracker: #{s}" if ENV && ENV["DEBUG_GATEWAY"] +end + +class Tracker + include DRbUndumped + + def initialize(instances, socket_path) + @instances = instances + @socket = File.expand_path(socket_path) + @active = false + + @listeners = [] + @instances.times { @listeners << Mutex.new } + + message "using #{@listeners.length} listeners" + message "opening socket at #{@socket}" + + @service = DRb.start_service("drbunix://#{@socket}", self) + end + + def with_listener + message "listener requested" + + mutex = has_lock = index = nil + 3.times do + @listeners.each_with_index do |mutex, index| + has_lock = mutex.try_lock + break if has_lock + end + break if has_lock + sleep 0.05 + end + + if has_lock + message "obtained listener #{index}" + @active = true + begin yield index + ensure + mutex.unlock + message "released listener #{index}" + end + else + message "dropping request because no listeners are available!" + end + end + + def background(check_interval = nil) + if check_interval + loop do + sleep check_interval + message "Idle for #{check_interval}, shutting down" unless @active + @active = false + Kernel.exit 0 + end + else DRb.thread.join + end + end +end + +socket_path = ARGV.shift +instances = ARGV.shift.to_i +t = Tracker.new(instances, socket_path) +t.background(ARGV.first ? ARGV.shift.to_i : 90) diff --git a/railties/lib/rails/commands/performance/benchmarker.rb b/railties/lib/rails/commands/performance/benchmarker.rb new file mode 100644 index 0000000000..e8804fe1bf --- /dev/null +++ b/railties/lib/rails/commands/performance/benchmarker.rb @@ -0,0 +1,24 @@ +if ARGV.empty? + puts "Usage: ./script/performance/benchmarker [times] 'Person.expensive_way' 'Person.another_expensive_way' ..." + exit 1 +end + +begin + N = Integer(ARGV.first) + ARGV.shift +rescue ArgumentError + N = 1 +end + +require RAILS_ROOT + '/config/environment' +require 'benchmark' +include Benchmark + +# Don't include compilation in the benchmark +ARGV.each { |expression| eval(expression) } + +bm(6) do |x| + ARGV.each_with_index do |expression, idx| + x.report("##{idx + 1}") { N.times { eval(expression) } } + end +end diff --git a/railties/lib/rails/commands/performance/profiler.rb b/railties/lib/rails/commands/performance/profiler.rb new file mode 100644 index 0000000000..7df840f197 --- /dev/null +++ b/railties/lib/rails/commands/performance/profiler.rb @@ -0,0 +1,50 @@ +if ARGV.empty? + $stderr.puts "Usage: ./script/performance/profiler 'Person.expensive_method(10)' [times] [flat|graph|graph_html]" + exit(1) +end + +# Keep the expensive require out of the profile. +$stderr.puts 'Loading Rails...' +require RAILS_ROOT + '/config/environment' + +# Define a method to profile. +if ARGV[1] and ARGV[1].to_i > 1 + eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end" +else + eval "def profile_me() #{ARGV[0]} end" +end + +# Use the ruby-prof extension if available. Fall back to stdlib profiler. +begin + begin + require "ruby-prof" + $stderr.puts 'Using the ruby-prof extension.' + RubyProf.measure_mode = RubyProf::WALL_TIME + RubyProf.start + profile_me + results = RubyProf.stop + if ARGV[2] + printer_class = RubyProf.const_get((ARGV[2] + "_printer").classify) + else + printer_class = RubyProf::FlatPrinter + end + printer = printer_class.new(results) + printer.print($stderr) + rescue LoadError + require "prof" + $stderr.puts 'Using the old ruby-prof extension.' + Prof.clock_mode = Prof::GETTIMEOFDAY + Prof.start + profile_me + results = Prof.stop + require 'rubyprof_ext' + Prof.print_profile(results, $stderr) + end +rescue LoadError + require 'profiler' + $stderr.puts 'Using the standard Ruby profiler.' + Profiler__.start_profile + profile_me + Profiler__.stop_profile + Profiler__.print_profile($stderr) +end diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb new file mode 100644 index 0000000000..159db707e7 --- /dev/null +++ b/railties/lib/rails/commands/plugin.rb @@ -0,0 +1,542 @@ +# Rails Plugin Manager. +# +# Installing plugins: +# +# $ ./script/plugin install continuous_builder asset_timestamping +# +# Specifying revisions: +# +# * Subversion revision is a single integer. +# +# * Git revision format: +# - full - 'refs/tags/1.8.0' or 'refs/heads/experimental' +# - short: 'experimental' (equivalent to 'refs/heads/experimental') +# 'tag 1.8.0' (equivalent to 'refs/tags/1.8.0') +# +# +# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com) +# and is licensed MIT: (http://www.opensource.org/licenses/mit-license.php) + +$verbose = false + + +require 'open-uri' +require 'fileutils' +require 'tempfile' + +include FileUtils + +class RailsEnvironment + attr_reader :root + + def initialize(dir) + @root = dir + end + + def self.find(dir=nil) + dir ||= pwd + while dir.length > 1 + return new(dir) if File.exist?(File.join(dir, 'config', 'environment.rb')) + dir = File.dirname(dir) + end + end + + def self.default + @default ||= find + end + + def self.default=(rails_env) + @default = rails_env + end + + def install(name_uri_or_plugin) + if name_uri_or_plugin.is_a? String + if name_uri_or_plugin =~ /:\/\// + plugin = Plugin.new(name_uri_or_plugin) + else + plugin = Plugins[name_uri_or_plugin] + end + else + plugin = name_uri_or_plugin + end + unless plugin.nil? + plugin.install + else + puts "Plugin not found: #{name_uri_or_plugin}" + end + end + + def use_svn? + require 'active_support/core_ext/kernel' + silence_stderr {`svn --version` rescue nil} + !$?.nil? && $?.success? + end + + def use_externals? + use_svn? && File.directory?("#{root}/vendor/plugins/.svn") + end + + def use_checkout? + # this is a bit of a guess. we assume that if the rails environment + # is under subversion then they probably want the plugin checked out + # instead of exported. This can be overridden on the command line + File.directory?("#{root}/.svn") + end + + def best_install_method + return :http unless use_svn? + case + when use_externals? then :externals + when use_checkout? then :checkout + else :export + end + end + + def externals + return [] unless use_externals? + ext = `svn propget svn:externals "#{root}/vendor/plugins"` + lines = ext.respond_to?(:lines) ? ext.lines : ext + lines.reject{ |line| line.strip == '' }.map do |line| + line.strip.split(/\s+/, 2) + end + end + + def externals=(items) + unless items.is_a? String + items = items.map{|name,uri| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n") + end + Tempfile.open("svn-set-prop") do |file| + file.write(items) + file.flush + system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"") + end + end + +end + +class Plugin + attr_reader :name, :uri + + def initialize(uri, name = nil) + @uri = uri + guess_name(uri) + end + + def self.find(name) + new(name) + end + + def to_s + "#{@name.ljust(30)}#{@uri}" + end + + def svn_url? + @uri =~ /svn(?:\+ssh)?:\/\/*/ + end + + def git_url? + @uri =~ /^git:\/\// || @uri =~ /\.git$/ + end + + def installed? + File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \ + or rails_env.externals.detect{ |name, repo| self.uri == repo } + end + + def install(method=nil, options = {}) + method ||= rails_env.best_install_method? + if :http == method + method = :export if svn_url? + method = :git if git_url? + end + + uninstall if installed? and options[:force] + + unless installed? + send("install_using_#{method}", options) + run_install_hook + else + puts "already installed: #{name} (#{uri}). pass --force to reinstall" + end + end + + def uninstall + path = "#{rails_env.root}/vendor/plugins/#{name}" + if File.directory?(path) + puts "Removing 'vendor/plugins/#{name}'" if $verbose + run_uninstall_hook + rm_r path + else + puts "Plugin doesn't exist: #{path}" + end + + if rails_env.use_externals? + # clean up svn:externals + externals = rails_env.externals + externals.reject!{|n,u| name == n or name == u} + rails_env.externals = externals + end + end + + def info + tmp = "#{rails_env.root}/_tmp_about.yml" + if svn_url? + cmd = "svn export #{@uri} \"#{rails_env.root}/#{tmp}\"" + puts cmd if $verbose + system(cmd) + end + open(svn_url? ? tmp : File.join(@uri, 'about.yml')) do |stream| + stream.read + end rescue "No about.yml found in #{uri}" + ensure + FileUtils.rm_rf tmp if svn_url? + end + + private + + def run_install_hook + install_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/install.rb" + load install_hook_file if File.exist? install_hook_file + end + + def run_uninstall_hook + uninstall_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/uninstall.rb" + load uninstall_hook_file if File.exist? uninstall_hook_file + end + + def install_using_export(options = {}) + svn_command :export, options + end + + def install_using_checkout(options = {}) + svn_command :checkout, options + end + + def install_using_externals(options = {}) + externals = rails_env.externals + externals.push([@name, uri]) + rails_env.externals = externals + install_using_checkout(options) + end + + def install_using_http(options = {}) + root = rails_env.root + mkdir_p "#{root}/vendor/plugins/#{@name}" + Dir.chdir "#{root}/vendor/plugins/#{@name}" do + puts "fetching from '#{uri}'" if $verbose + fetcher = RecursiveHTTPFetcher.new(uri, -1) + fetcher.quiet = true if options[:quiet] + fetcher.fetch + end + end + + def install_using_git(options = {}) + root = rails_env.root + mkdir_p(install_path = "#{root}/vendor/plugins/#{name}") + Dir.chdir install_path do + init_cmd = "git init" + init_cmd += " -q" if options[:quiet] and not $verbose + puts init_cmd if $verbose + system(init_cmd) + base_cmd = "git pull --depth 1 #{uri}" + base_cmd += " -q" if options[:quiet] and not $verbose + base_cmd += " #{options[:revision]}" if options[:revision] + puts base_cmd if $verbose + if system(base_cmd) + puts "removing: .git .gitignore" if $verbose + rm_rf %w(.git .gitignore) + else + rm_rf install_path + end + end + end + + def svn_command(cmd, options = {}) + root = rails_env.root + mkdir_p "#{root}/vendor/plugins" + base_cmd = "svn #{cmd} #{uri} \"#{root}/vendor/plugins/#{name}\"" + base_cmd += ' -q' if options[:quiet] and not $verbose + base_cmd += " -r #{options[:revision]}" if options[:revision] + puts base_cmd if $verbose + system(base_cmd) + end + + def guess_name(url) + @name = File.basename(url) + if @name == 'trunk' || @name.empty? + @name = File.basename(File.dirname(url)) + end + @name.gsub!(/\.git$/, '') if @name =~ /\.git$/ + end + + def rails_env + @rails_env || RailsEnvironment.default + end +end + +# load default environment and parse arguments +require 'optparse' +module Commands + + class Plugin + attr_reader :environment, :script_name, :sources + def initialize + @environment = RailsEnvironment.default + @rails_root = RailsEnvironment.default.root + @script_name = File.basename($0) + @sources = [] + end + + def environment=(value) + @environment = value + RailsEnvironment.default = value + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@script_name} [OPTIONS] command" + o.define_head "Rails plugin manager." + + o.separator "" + o.separator "GENERAL OPTIONS" + + o.on("-r", "--root=DIR", String, + "Set an explicit rails app directory.", + "Default: #{@rails_root}") { |rails_root| @rails_root = rails_root; self.environment = RailsEnvironment.new(@rails_root) } + o.on("-s", "--source=URL1,URL2", Array, + "Use the specified plugin repositories instead of the defaults.") { |sources| @sources = sources} + + o.on("-v", "--verbose", "Turn on verbose output.") { |verbose| $verbose = verbose } + o.on("-h", "--help", "Show this help message.") { puts o; exit } + + o.separator "" + o.separator "COMMANDS" + + o.separator " install Install plugin(s) from known repositories or URLs." + o.separator " remove Uninstall plugins." + + o.separator "" + o.separator "EXAMPLES" + o.separator " Install a plugin:" + o.separator " #{@script_name} install continuous_builder\n" + o.separator " Install a plugin from a subversion URL:" + o.separator " #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n" + o.separator " Install a plugin from a git URL:" + o.separator " #{@script_name} install git://github.com/SomeGuy/my_awesome_plugin.git\n" + o.separator " Install a plugin and add a svn:externals entry to vendor/plugins" + o.separator " #{@script_name} install -x continuous_builder\n" + end + end + + def parse!(args=ARGV) + general, sub = split_args(args) + options.parse!(general) + + command = general.shift + if command =~ /^(install|remove)$/ + command = Commands.const_get(command.capitalize).new(self) + command.parse!(sub) + else + puts "Unknown command: #{command}" + puts options + exit 1 + end + end + + def split_args(args) + left = [] + left << args.shift while args[0] and args[0] =~ /^-/ + left << args.shift if args[0] + return [left, args] + end + + def self.parse!(args=ARGV) + Plugin.new.parse!(args) + end + end + + class Install + def initialize(base_command) + @base_command = base_command + @method = :http + @options = { :quiet => false, :revision => nil, :force => false } + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} install PLUGIN [PLUGIN [PLUGIN] ...]" + o.define_head "Install one or more plugins." + o.separator "" + o.separator "Options:" + o.on( "-x", "--externals", + "Use svn:externals to grab the plugin.", + "Enables plugin updates and plugin versioning.") { |v| @method = :externals } + o.on( "-o", "--checkout", + "Use svn checkout to grab the plugin.", + "Enables updating but does not add a svn:externals entry.") { |v| @method = :checkout } + o.on( "-e", "--export", + "Use svn export to grab the plugin.", + "Exports the plugin, allowing you to check it into your local repository. Does not enable updates, or add an svn:externals entry.") { |v| @method = :export } + o.on( "-q", "--quiet", + "Suppresses the output from installation.", + "Ignored if -v is passed (./script/plugin -v install ...)") { |v| @options[:quiet] = true } + o.on( "-r REVISION", "--revision REVISION", + "Checks out the given revision from subversion or git.", + "Ignored if subversion/git is not used.") { |v| @options[:revision] = v } + o.on( "-f", "--force", + "Reinstalls a plugin if it's already installed.") { |v| @options[:force] = true } + o.separator "" + o.separator "You can specify plugin names as given in 'plugin list' output or absolute URLs to " + o.separator "a plugin repository." + end + end + + def determine_install_method + best = @base_command.environment.best_install_method + @method = :http if best == :http and @method == :export + case + when (best == :http and @method != :http) + msg = "Cannot install using subversion because `svn' cannot be found in your PATH" + when (best == :export and (@method != :export and @method != :http)) + msg = "Cannot install using #{@method} because this project is not under subversion." + when (best != :externals and @method == :externals) + msg = "Cannot install using externals because vendor/plugins is not under subversion." + end + if msg + puts msg + exit 1 + end + @method + end + + def parse!(args) + options.parse!(args) + environment = @base_command.environment + install_method = determine_install_method + puts "Plugins will be installed using #{install_method}" if $verbose + args.each do |name| + ::Plugin.find(name).install(install_method, @options) + end + rescue StandardError => e + puts "Plugin not found: #{args.inspect}" + puts e.inspect if $verbose + exit 1 + end + end + + class Remove + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} remove name [name]..." + o.define_head "Remove plugins." + end + end + + def parse!(args) + options.parse!(args) + root = @base_command.environment.root + args.each do |name| + ::Plugin.new(name).uninstall + end + end + end + + class Info + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} info name [name]..." + o.define_head "Shows plugin info at {url}/about.yml." + end + end + + def parse!(args) + options.parse!(args) + args.each do |name| + puts ::Plugin.find(name).info + puts + end + end + end +end + +class RecursiveHTTPFetcher + attr_accessor :quiet + def initialize(urls_to_fetch, level = 1, cwd = ".") + @level = level + @cwd = cwd + @urls_to_fetch = RUBY_VERSION >= '1.9' ? urls_to_fetch.lines : urls_to_fetch.to_a + @quiet = false + end + + def ls + @urls_to_fetch.collect do |url| + if url =~ /^svn(\+ssh)?:\/\/.*/ + `svn ls #{url}`.split("\n").map {|entry| "/#{entry}"} rescue nil + else + open(url) do |stream| + links("", stream.read) + end rescue nil + end + end.flatten + end + + def push_d(dir) + @cwd = File.join(@cwd, dir) + FileUtils.mkdir_p(@cwd) + end + + def pop_d + @cwd = File.dirname(@cwd) + end + + def links(base_url, contents) + links = [] + contents.scan(/href\s*=\s*\"*[^\">]*/i) do |link| + link = link.sub(/href="/i, "") + next if link =~ /svnindex.xsl$/ + next if link =~ /^(\w*:|)\/\// || link =~ /^\./ + links << File.join(base_url, link) + end + links + end + + def download(link) + puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet + open(link) do |stream| + File.open(File.join(@cwd, File.basename(link)), "wb") do |file| + file.write(stream.read) + end + end + end + + def fetch(links = @urls_to_fetch) + links.each do |l| + (l =~ /\/$/ || links == @urls_to_fetch) ? fetch_dir(l) : download(l) + end + end + + def fetch_dir(url) + @level += 1 + push_d(File.basename(url)) if @level > 0 + open(url) do |stream| + contents = stream.read + fetch(links(url, contents)) + end + pop_d if @level > 0 + @level -= 1 + end +end + +Commands::Plugin.parse! diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb new file mode 100644 index 0000000000..510128318a --- /dev/null +++ b/railties/lib/rails/commands/runner.rb @@ -0,0 +1,54 @@ +require 'optparse' + +options = { :environment => (ENV['RAILS_ENV'] || "development").dup } +code_or_file = nil + +ARGV.clone.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: #{$0} [options] ('Some.ruby(code)' or a filename)" + + opts.separator "" + + opts.on("-e", "--environment=name", String, + "Specifies the environment for the runner to operate under (test/development/production).", + "Default: development") { |v| options[:environment] = v } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { $stderr.puts opts; exit } + + if RUBY_PLATFORM !~ /mswin/ + opts.separator "" + opts.separator "You can also use runner as a shebang line for your scripts like this:" + opts.separator "-------------------------------------------------------------" + opts.separator "#!/usr/bin/env #{File.expand_path($0)}" + opts.separator "" + opts.separator "Product.find(:all).each { |p| p.price *= 2 ; p.save! }" + opts.separator "-------------------------------------------------------------" + end + + opts.order! { |o| code_or_file ||= o } rescue retry +end + +ARGV.delete(code_or_file) + +ENV["RAILS_ENV"] = options[:environment] +RAILS_ENV.replace(options[:environment]) if defined?(RAILS_ENV) + +require RAILS_ROOT + '/config/environment' + +begin + if code_or_file.nil? + $stderr.puts "Run '#{$0} -h' for help." + exit 1 + elsif File.exist?(code_or_file) + eval(File.read(code_or_file), nil, code_or_file) + else + eval(code_or_file) + end +ensure + if defined? Rails + Rails.logger.flush if Rails.logger.respond_to?(:flush) + end +end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb new file mode 100644 index 0000000000..823916b1dc --- /dev/null +++ b/railties/lib/rails/commands/server.rb @@ -0,0 +1,106 @@ +require 'action_controller' + +require 'fileutils' +require 'optparse' + +options = { + :Port => 3000, + :Host => "0.0.0.0", + :environment => (ENV['RAILS_ENV'] || "development").dup, + :config => RAILS_ROOT + "/config.ru", + :detach => false, + :debugger => false, + :path => nil +} + +ARGV.clone.options do |opts| + opts.on("-p", "--port=port", Integer, + "Runs Rails on the specified port.", "Default: #{options[:Port]}") { |v| options[:Port] = v } + opts.on("-b", "--binding=ip", String, + "Binds Rails to the specified ip.", "Default: #{options[:Host]}") { |v| options[:Host] = v } + opts.on("-c", "--config=file", String, + "Use custom rackup configuration file") { |v| options[:config] = v } + opts.on("-d", "--daemon", "Make server run as a Daemon.") { options[:detach] = true } + opts.on("-u", "--debugger", "Enable ruby-debugging for the server.") { options[:debugger] = true } + opts.on("-e", "--environment=name", String, + "Specifies the environment to run this server under (test/development/production).", + "Default: #{options[:environment]}") { |v| options[:environment] = v } + opts.on("-P", "--path=/path", String, "Runs Rails app mounted at a specific path.", "Default: #{options[:path]}") { |v| options[:path] = v } + + opts.separator "" + + opts.on("-h", "--help", "Show this help message.") { puts opts; exit } + + opts.parse! +end + +server = Rack::Handler.get(ARGV.first) rescue nil +unless server + begin + server = Rack::Handler::Mongrel + rescue LoadError => e + server = Rack::Handler::WEBrick + end +end + +puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" +puts "=> Rails #{Rails.version} application starting on http://#{options[:Host]}:#{options[:Port]}#{options[:path]}" + +%w(cache pids sessions sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(RAILS_ROOT, 'tmp', dir_to_make)) +end + +if options[:detach] + Process.daemon + pid = "#{RAILS_ROOT}/tmp/pids/server.pid" + File.open(pid, 'w'){ |f| f.write(Process.pid) } + at_exit { File.delete(pid) if File.exist?(pid) } +end + +ENV["RAILS_ENV"] = options[:environment] +RAILS_ENV.replace(options[:environment]) if defined?(RAILS_ENV) + +if File.exist?(options[:config]) + config = options[:config] + if config =~ /\.ru$/ + cfgfile = File.read(config) + if cfgfile[/^#\\(.*)/] + opts.parse!($1.split(/\s+/)) + end + inner_app = eval("Rack::Builder.new {( " + cfgfile + "\n )}.to_app", nil, config) + else + require config + inner_app = Object.const_get(File.basename(config, '.rb').capitalize) + end +else + require RAILS_ROOT + "/config/environment" + inner_app = ActionController::Dispatcher.new +end + +if options[:path].nil? + map_path = "/" +else + ActionController::Base.relative_url_root = options[:path] + map_path = options[:path] +end + +app = Rack::Builder.new { + use Rails::Rack::LogTailer unless options[:detach] + use Rails::Rack::Debugger if options[:debugger] + map map_path do + use Rails::Rack::Static + run inner_app + end +}.to_app + +puts "=> Call with -d to detach" + +trap(:INT) { exit } + +puts "=> Ctrl-C to shutdown server" + +begin + server.run(app, options.merge(:AccessLog => [])) +ensure + puts 'Exiting' +end diff --git a/railties/lib/rails/commands/update.rb b/railties/lib/rails/commands/update.rb new file mode 100644 index 0000000000..f3b3ad0775 --- /dev/null +++ b/railties/lib/rails/commands/update.rb @@ -0,0 +1,10 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'generators')) +require "#{RAILS_ROOT}/config/environment" + +if ARGV.size == 0 + Rails::Generators.help + exit +end + +name = ARGV.shift +Rails::Generators.invoke name, ARGV, :behavior => :skip diff --git a/railties/lib/rails/console_app.rb b/railties/lib/rails/console_app.rb new file mode 100644 index 0000000000..75e6f11ea3 --- /dev/null +++ b/railties/lib/rails/console_app.rb @@ -0,0 +1,31 @@ +require 'active_support/all' +require 'active_support/test_case' +require 'action_controller' + +# work around the at_exit hook in test/unit, which kills IRB +Test::Unit.run = true if Test::Unit.respond_to?(:run=) + +# reference the global "app" instance, created on demand. To recreate the +# instance, pass a non-false value as the parameter. +def app(create=false) + @app_integration_instance = nil if create + @app_integration_instance ||= new_session do |sess| + sess.host! "www.example.com" + end +end + +# create a new session. If a block is given, the new session will be yielded +# to the block before being returned. +def new_session + session = ActionController::Integration::Session.new + yield session if block_given? + session +end + +#reloads the environment +def reload! + puts "Reloading..." + ActionController::Dispatcher.new + ActionController::Dispatcher.router.reload + true +end diff --git a/railties/lib/rails/console_sandbox.rb b/railties/lib/rails/console_sandbox.rb new file mode 100644 index 0000000000..65a3d68619 --- /dev/null +++ b/railties/lib/rails/console_sandbox.rb @@ -0,0 +1,6 @@ +ActiveRecord::Base.connection.increment_open_transactions +ActiveRecord::Base.connection.begin_db_transaction +at_exit do + ActiveRecord::Base.connection.rollback_db_transaction + ActiveRecord::Base.connection.decrement_open_transactions +end diff --git a/railties/lib/rails/console_with_helpers.rb b/railties/lib/rails/console_with_helpers.rb new file mode 100644 index 0000000000..039db667c4 --- /dev/null +++ b/railties/lib/rails/console_with_helpers.rb @@ -0,0 +1,5 @@ +def helper + @helper ||= ApplicationController.helpers +end + +@controller = ApplicationController.new diff --git a/railties/lib/rails/dispatcher.rb b/railties/lib/rails/dispatcher.rb new file mode 100644 index 0000000000..7f9a6221d9 --- /dev/null +++ b/railties/lib/rails/dispatcher.rb @@ -0,0 +1,24 @@ +#-- +# Copyright (c) 2004-2009 David Heinemeier Hansson +# +# 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. +#++ +require 'action_controller/dispatch/dispatcher' +Dispatcher = ActionController::Dispatcher diff --git a/railties/lib/rails/fcgi_handler.rb b/railties/lib/rails/fcgi_handler.rb new file mode 100644 index 0000000000..ef6f3b094c --- /dev/null +++ b/railties/lib/rails/fcgi_handler.rb @@ -0,0 +1,239 @@ +require 'fcgi' +require 'logger' +require 'rails/dispatcher' +require 'rbconfig' + +class RailsFCGIHandler + SIGNALS = { + 'HUP' => :reload, + 'INT' => :exit_now, + 'TERM' => :exit_now, + 'USR1' => :exit, + 'USR2' => :restart + } + GLOBAL_SIGNALS = SIGNALS.keys - %w(USR1) + + attr_reader :when_ready + + attr_accessor :log_file_path + attr_accessor :gc_request_period + + # Initialize and run the FastCGI instance, passing arguments through to new. + def self.process!(*args, &block) + new(*args, &block).process! + end + + # Initialize the FastCGI instance with the path to a crash log + # detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log) + # and the number of requests to process between garbage collection runs + # (default nil for normal GC behavior.) Optionally, pass a block which + # takes this instance as an argument for further configuration. + def initialize(log_file_path = nil, gc_request_period = nil) + self.log_file_path = log_file_path || "#{RAILS_ROOT}/log/fastcgi.crash.log" + self.gc_request_period = gc_request_period + + # Yield for additional configuration. + yield self if block_given? + + # Safely install signal handlers. + install_signal_handlers + + @app = Dispatcher.new + + # Start error timestamp at 11 seconds ago. + @last_error_on = Time.now - 11 + end + + def process!(provider = FCGI) + mark_features! + + dispatcher_log :info, 'starting' + process_each_request provider + dispatcher_log :info, 'stopping gracefully' + + rescue Exception => error + case error + when SystemExit + dispatcher_log :info, 'stopping after explicit exit' + when SignalException + dispatcher_error error, 'stopping after unhandled signal' + else + # Retry if exceptions occur more than 10 seconds apart. + if Time.now - @last_error_on > 10 + @last_error_on = Time.now + dispatcher_error error, 'retrying after unhandled exception' + retry + else + dispatcher_error error, 'stopping after unhandled exception within 10 seconds of the last' + end + end + end + + protected + def process_each_request(provider) + request = nil + + catch :exit do + provider.each do |request| + process_request(request) + + case when_ready + when :reload + reload! + when :restart + close_connection(request) + restart! + when :exit + close_connection(request) + throw :exit + end + end + end + rescue SignalException => signal + raise unless signal.message == 'SIGUSR1' + close_connection(request) + end + + def process_request(request) + @processing, @when_ready = true, nil + gc_countdown + + with_signal_handler 'USR1' do + begin + ::Rack::Handler::FastCGI.serve(request, @app) + rescue SignalException, SystemExit + raise + rescue Exception => error + dispatcher_error error, 'unhandled dispatch error' + end + end + ensure + @processing = false + end + + def logger + @logger ||= Logger.new(@log_file_path) + end + + def dispatcher_log(level, msg) + time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S") + logger.send(level, "[#{time_str} :: #{$$}] #{msg}") + rescue Exception => log_error # Logger errors + STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n" + STDERR << " #{log_error.class}: #{log_error.message}\n" + end + + def dispatcher_error(e, msg = "") + error_message = + "Dispatcher failed to catch: #{e} (#{e.class})\n" + + " #{e.backtrace.join("\n ")}\n#{msg}" + dispatcher_log(:error, error_message) + end + + def install_signal_handlers + GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) } + end + + def install_signal_handler(signal, handler = nil) + if SIGNALS.include?(signal) && self.class.method_defined?(name = "#{SIGNALS[signal]}_handler") + handler ||= method(name).to_proc + + begin + trap(signal, handler) + rescue ArgumentError + dispatcher_log :warn, "Ignoring unsupported signal #{signal}." + end + else + dispatcher_log :warn, "Ignoring unsupported signal #{signal}." + end + end + + def with_signal_handler(signal) + install_signal_handler(signal) + yield + ensure + install_signal_handler(signal, 'DEFAULT') + end + + def exit_now_handler(signal) + dispatcher_log :info, "asked to stop immediately" + exit + end + + def exit_handler(signal) + dispatcher_log :info, "asked to stop ASAP" + if @processing + @when_ready = :exit + else + throw :exit + end + end + + def reload_handler(signal) + dispatcher_log :info, "asked to reload ASAP" + if @processing + @when_ready = :reload + else + reload! + end + end + + def restart_handler(signal) + dispatcher_log :info, "asked to restart ASAP" + if @processing + @when_ready = :restart + else + restart! + end + end + + def restart! + config = ::Config::CONFIG + ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT'] + command_line = [ruby, $0, ARGV].flatten.join(' ') + + dispatcher_log :info, "restarted" + + # close resources as they won't be closed by + # the OS when using exec + logger.close rescue nil + Rails.logger.close rescue nil + + exec(command_line) + end + + def reload! + run_gc! if gc_request_period + restore! + @when_ready = nil + dispatcher_log :info, "reloaded" + end + + # Make a note of $" so we can safely reload this instance. + def mark_features! + @features = $".clone + end + + def restore! + $".replace @features + Dispatcher.reset_application! + ActionController::Routing::Routes.reload + end + + def run_gc! + @gc_request_countdown = gc_request_period + GC.enable; GC.start; GC.disable + end + + def gc_countdown + if gc_request_period + @gc_request_countdown ||= gc_request_period + @gc_request_countdown -= 1 + run_gc! if @gc_request_countdown <= 0 + end + end + + def close_connection(request) + request.finish if request + end +end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb new file mode 100644 index 0000000000..0419a4e36c --- /dev/null +++ b/railties/lib/rails/generators.rb @@ -0,0 +1,299 @@ +activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" +$LOAD_PATH.unshift(activesupport_path) if File.directory?(activesupport_path) +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/metaclass' +require 'active_support/core_ext/array' +require 'active_support/core_ext/hash' +require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/string/inflections' + +# TODO: Do not always push on vendored thor +$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/vendor/thor-0.11.6/lib") +require 'rails/generators/base' +require 'rails/generators/named_base' + +module Rails + module Generators + DEFAULT_ALIASES = { + :rails => { + :actions => '-a', + :orm => '-o', + :resource_controller => '-c', + :scaffold_controller => '-c', + :stylesheets => '-y', + :template_engine => '-e', + :test_framework => '-t' + }, + + :test_unit => { + :fixture_replacement => '-r', + }, + + :plugin => { + :generator => '-g', + :tasks => '-r' + } + } + + DEFAULT_OPTIONS = { + :active_record => { + :migration => true, + :timestamps => true + }, + + :erb => { + :layout => true + }, + + :rails => { + :force_plural => false, + :helper => true, + :layout => true, + :orm => :active_record, + :integration_tool => :test_unit, + :performance_tool => :test_unit, + :resource_controller => :controller, + :scaffold_controller => :scaffold_controller, + :singleton => false, + :stylesheets => true, + :template_engine => :erb, + :test_framework => :test_unit + }, + + :test_unit => { + :fixture => true, + :fixture_replacement => nil + }, + + :plugin => { + :generator => false, + :tasks => false + } + } + + def self.aliases #:nodoc: + @aliases ||= DEFAULT_ALIASES.dup + end + + def self.options #:nodoc: + @options ||= DEFAULT_OPTIONS.dup + end + + # We have two scenarios here: when rubygems is loaded and when bundler is + # being used. If rubygems is loaded, we get all generators paths from loaded + # specs. Otherwise we just have to look into vendor/gems/gems. + # + def self.gems_generators_paths + paths = [] + + if defined?(Gem) && Gem.respond_to?(:loaded_specs) + Gem.loaded_specs.each do |name, spec| + generator_path = File.join(spec.full_gem_path, "lib/generators") + paths << generator_path if File.exist?(generator_path) + end + elsif defined?(RAILS_ROOT) + paths += Dir[File.join(RAILS_ROOT, "vendor", "gems", "gems", "*", "lib", "generators")] + end + + paths + end + + # Load paths from plugin. + # + def self.plugins_generators_paths + return [] unless defined?(RAILS_ROOT) + Dir[File.join(RAILS_ROOT, "vendor", "plugins", "*", "lib", "generators")] + end + + # Hold configured generators fallbacks. If a plugin developer wants a + # generator group to fallback to another group in case of missing generators, + # they can add a fallback. + # + # For example, shoulda is considered a test_framework and is an extension + # of test_unit. However, most part of shoulda generators are similar to + # test_unit ones. + # + # Shoulda then can tell generators to search for test_unit generators when + # some of them are not available by adding a fallback: + # + # Rails::Generators.fallbacks[:shoulda] = :test_unit + # + def self.fallbacks + @fallbacks ||= {} + end + + # Remove the color from output. + # + def self.no_color! + Thor::Base.shell = Thor::Shell::Basic + end + + # Generators load paths used on lookup. The lookup happens as: + # + # 1) lib generators + # 2) vendor/plugin generators + # 3) vendor/gems generators + # 4) ~/rails/generators + # 5) rubygems generators + # 6) builtin generators + # + # TODO Remove hardcoded paths for all, except (6). + # + def self.load_paths + @load_paths ||= begin + paths = [] + paths << File.join(RAILS_ROOT, "lib", "generators") if defined?(RAILS_ROOT) + paths << File.join(Thor::Util.user_home, ".rails", "generators") + paths += self.plugins_generators_paths + paths += self.gems_generators_paths + paths << File.expand_path(File.join(File.dirname(__FILE__), "generators")) + paths.uniq! + paths + end + end + load_paths # Cache load paths. Needed to avoid __FILE__ pointing to wrong paths. + + # Receives a namespace and tries different combinations to find a generator. + # + # ==== Examples + # + # find_by_namespace :webrat, :rails, :integration + # + # Will search for the following generators: + # + # "rails:generators:webrat", "webrat:generators:integration", "webrat" + # + # If the namespace has ":" included we consider that a absolute namespace + # was given and the lookup above does not happen. Just the name is searched. + # + # Finally, it deals with one kind of shortcut: + # + # find_by_namespace "test_unit:model" + # + # It will search for generators at: + # + # "test_unit:generators:model", "test_unit:model" + # + def self.find_by_namespace(name, base=nil, context=nil) #:nodoc: + name, attempts = name.to_s, [] + + case name.count(':') + when 1 + base, name = name.split(':') + return find_by_namespace(name, base) + when 0 + attempts << "#{base}:generators:#{name}" if base + attempts << "#{name}:generators:#{context}" if context + end + + attempts << name + unloaded = attempts - namespaces + lookup(unloaded) + + attempts.each do |namespace| + klass = Thor::Util.find_by_namespace(namespace) + return klass if klass + end + + invoke_fallbacks_for(name, base) + end + + # Receives a namespace, arguments and the behavior to invoke the generator. + # It's used as the default entry point for generate, destroy and update + # commands. + # + def self.invoke(namespace, args=ARGV, config={}) + if klass = find_by_namespace(namespace, "rails") + args << "--help" if klass.arguments.any? { |a| a.required? } && args.empty? + klass.start args, config + else + puts "Could not find generator #{namespace}." + end + end + + # Show help message with available generators. + # + def self.help + rails = Rails::Generators.builtin.map do |group, name| + name if group == "rails" + end + rails.compact! + rails.sort! + + puts "Please select a generator." + puts "Builtin: #{rails.join(', ')}." + + # Load paths and remove builtin + paths, others = load_paths.dup, [] + paths.pop + + paths.each do |path| + tail = [ "*", "*", "*_generator.rb" ] + + until tail.empty? + others += Dir[File.join(path, *tail)].collect do |file| + file.split('/')[-tail.size, 2].join(':').sub(/_generator\.rb$/, '') + end + tail.shift + end + end + + others.sort! + puts "Others: #{others.join(', ')}." unless others.empty? + end + + protected + + # Return all defined namespaces. + # + def self.namespaces #:nodoc: + Thor::Base.subclasses.map{ |klass| klass.namespace } + end + + # Keep builtin generators in an Array[Array[group, name]]. + # + def self.builtin #:nodoc: + Dir[File.dirname(__FILE__) + '/generators/*/*'].collect do |file| + file.split('/')[-2, 2] + end + end + + # Try callbacks for the given base. + # + def self.invoke_fallbacks_for(name, base) + return nil unless base && fallbacks[base.to_sym] + invoked_fallbacks = [] + + Array(fallbacks[base.to_sym]).each do |fallback| + next if invoked_fallbacks.include?(fallback) + invoked_fallbacks << fallback + + klass = find_by_namespace(name, fallback) + return klass if klass + end + + nil + end + + # Receives namespaces in an array and tries to find matching generators + # in the load path. + # + def self.lookup(attempts) #:nodoc: + attempts = attempts.map { |a| "#{a.split(":").last}_generator" }.uniq + attempts = "{#{attempts.join(',')}}.rb" + + self.load_paths.each do |path| + Dir[File.join(path, '**', attempts)].each do |file| + begin + require file + rescue Exception => e + warn "[WARNING] Could not load generator at #{file.inspect}. Error: #{e.message}" + end + end + end + end + + end +end + diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb new file mode 100644 index 0000000000..c4552dd399 --- /dev/null +++ b/railties/lib/rails/generators/actions.rb @@ -0,0 +1,270 @@ +require 'open-uri' + +module Rails + module Generators + module Actions + + # Install a plugin. You must provide either a Subversion url or Git url. + # + # For a Git-hosted plugin, you can specify a branch and + # whether it should be added as a submodule instead of cloned. + # + # For a Subversion-hosted plugin you can specify a revision. + # + # ==== Examples + # + # plugin 'restful-authentication', :git => 'git://github.com/technoweenie/restful-authentication.git' + # plugin 'restful-authentication', :git => 'git://github.com/technoweenie/restful-authentication.git', :branch => 'stable' + # plugin 'restful-authentication', :git => 'git://github.com/technoweenie/restful-authentication.git', :submodule => true + # plugin 'restful-authentication', :svn => 'svn://svnhub.com/technoweenie/restful-authentication/trunk' + # plugin 'restful-authentication', :svn => 'svn://svnhub.com/technoweenie/restful-authentication/trunk', :revision => 1234 + # + def plugin(name, options) + log :plugin, name + + if options[:git] && options[:submodule] + options[:git] = "-b #{options[:branch]} #{options[:git]}" if options[:branch] + in_root do + run "git submodule add #{options[:git]} vendor/plugins/#{name}", :verbose => false + end + elsif options[:git] || options[:svn] + options[:git] = "-b #{options[:branch]} #{options[:git]}" if options[:branch] + options[:svn] = "-r #{options[:revision]} #{options[:svn]}" if options[:revision] + in_root do + run_ruby_script "script/plugin install #{options[:svn] || options[:git]}", :verbose => false + end + else + log "! no git or svn provided for #{name}. Skipping..." + end + end + + # Adds an entry into config/environment.rb for the supplied gem. If env + # is specified, add the gem to the given environment. + # + # ==== Example + # + # gem "rspec", :env => :test + # gem "technoweenie-restful-authentication", :lib => "restful-authentication", :source => "http://gems.github.com/" + # + def gem(name, options={}) + log :gem, name + env = options.delete(:env) + + gems_code = "config.gem '#{name}'" + + if options.any? + opts = options.inject([]) {|result, h| result << [":#{h[0]} => #{h[1].inspect.gsub('"',"'")}"] }.sort.join(", ") + gems_code << ", #{opts}" + end + + environment gems_code, :env => env + end + + # Adds a line inside the Initializer block for config/environment.rb. + # + # If options :env is specified, the line is appended to the corresponding + # file in config/environments. + # + def environment(data=nil, options={}, &block) + sentinel = "Rails::Initializer.run do |config|" + data = block.call if !data && block_given? + + in_root do + if options[:env].nil? + inject_into_file 'config/environment.rb', "\n #{data}", :after => sentinel, :verbose => false + else + Array.wrap(options[:env]).each do|env| + append_file "config/environments/#{env}.rb", "\n#{data}", :verbose => false + end + end + end + end + + # Run a command in git. + # + # ==== Examples + # + # git :init + # git :add => "this.file that.rb" + # git :add => "onefile.rb", :rm => "badfile.cxx" + # + def git(command={}) + if command.is_a?(Symbol) + run "git #{command}" + else + command.each do |command, options| + run "git #{command} #{options}" + end + end + end + + # Create a new file in the vendor/ directory. Code can be specified + # in a block or a data string can be given. + # + # ==== Examples + # + # vendor("sekrit.rb") do + # sekrit_salt = "#{Time.now}--#{3.years.ago}--#{rand}--" + # "salt = '#{sekrit_salt}'" + # end + # + # vendor("foreign.rb", "# Foreign code is fun") + # + def vendor(filename, data=nil, &block) + log :vendor, filename + create_file("vendor/#{filename}", data, :verbose => false, &block) + end + + # Create a new file in the lib/ directory. Code can be specified + # in a block or a data string can be given. + # + # ==== Examples + # + # lib("crypto.rb") do + # "crypted_special_value = '#{rand}--#{Time.now}--#{rand(1337)}--'" + # end + # + # lib("foreign.rb", "# Foreign code is fun") + # + def lib(filename, data=nil, &block) + log :lib, filename + create_file("lib/#{filename}", data, :verbose => false, &block) + end + + # Create a new Rakefile with the provided code (either in a block or a string). + # + # ==== Examples + # + # rakefile("bootstrap.rake") do + # project = ask("What is the UNIX name of your project?") + # + # <<-TASK + # namespace :#{project} do + # task :bootstrap do + # puts "i like boots!" + # end + # end + # TASK + # end + # + # rakefile("seed.rake", "puts 'im plantin ur seedz'") + # + def rakefile(filename, data=nil, &block) + log :rakefile, filename + create_file("lib/tasks/#{filename}", data, :verbose => false, &block) + end + + # Create a new initializer with the provided code (either in a block or a string). + # + # ==== Examples + # + # initializer("globals.rb") do + # data = "" + # + # ['MY_WORK', 'ADMINS', 'BEST_COMPANY_EVAR'].each do + # data << "#{const} = :entp" + # end + # + # data + # end + # + # initializer("api.rb", "API_KEY = '123456'") + # + def initializer(filename, data=nil, &block) + log :initializer, filename + create_file("config/initializers/#{filename}", data, :verbose => false, &block) + end + + # Generate something using a generator from Rails or a plugin. + # The second parameter is the argument string that is passed to + # the generator or an Array that is joined. + # + # ==== Example + # + # generate(:authenticated, "user session") + # + def generate(what, *args) + log :generate, what + argument = args.map {|arg| arg.to_s }.flatten.join(" ") + + in_root { run_ruby_script("script/generate #{what} #{argument}", :verbose => false) } + end + + # Runs the supplied rake task + # + # ==== Example + # + # rake("db:migrate") + # rake("db:migrate", :env => "production") + # rake("gems:install", :sudo => true) + # + def rake(command, options={}) + log :rake, command + env = options[:env] || 'development' + sudo = options[:sudo] && RUBY_PLATFORM !~ /mswin|mingw/ ? 'sudo ' : '' + in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", :verbose => false) } + end + + # Just run the capify command in root + # + # ==== Example + # + # capify! + # + def capify! + log :capify, "" + in_root { run("#{extify(:capify)} .", :verbose => false) } + end + + # Add Rails to /vendor/rails + # + # ==== Example + # + # freeze! + # + def freeze!(args = {}) + log :vendor, "rails" + in_root { run("#{extify(:rake)} rails:freeze:edge", :verbose => false) } + end + + # Make an entry in Rails routing file conifg/routes.rb + # + # === Example + # + # route "map.root :controller => :welcome" + # + def route(routing_code) + log :route, routing_code + sentinel = "ActionController::Routing::Routes.draw do |map|" + + in_root do + inject_into_file 'config/routes.rb', "\n #{routing_code}\n", { :after => sentinel, :verbose => false } + end + end + + protected + + # Define log for backwards compatibility. If just one argument is sent, + # invoke say, otherwise invoke say_status. + # + def log(*args) + if args.size == 1 + say args.first.to_s + else + say_status *args + end + end + + # Add an extension to the given name based on the platform. + # + def extify(name) + if RUBY_PLATFORM =~ /mswin|mingw/ + "#{name}.bat" + else + name + end + end + + end + end +end diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb new file mode 100644 index 0000000000..1a849a0e02 --- /dev/null +++ b/railties/lib/rails/generators/active_model.rb @@ -0,0 +1,74 @@ +module Rails + module Generators + # ActiveModel is a class to be implemented by each ORM to allow Rails to + # generate customized controller code. + # + # The API has the same methods as ActiveRecord, but each method returns a + # string that matches the ORM API. + # + # For example: + # + # ActiveRecord::Generators::ActiveModel.find(Foo, "params[:id]") + # #=> "Foo.find(params[:id])" + # + # Datamapper::Generators::ActiveModel.find(Foo, "params[:id]") + # #=> "Foo.get(params[:id])" + # + # On initialization, the ActiveModel accepts the instance name that will + # receive the calls: + # + # builder = ActiveRecord::Generators::ActiveModel.new "@foo" + # builder.save #=> "@foo.save" + # + # The only exception in ActiveModel for ActiveRecord is the use of self.build + # instead of self.new. + # + class ActiveModel + attr_reader :name + + def initialize(name) + @name = name + end + + # GET index + def self.all(klass) + raise NotImplementedError + end + + # GET show + # GET edit + # PUT update + # DELETE destroy + def self.find(klass, params=nil) + raise NotImplementedError + end + + # GET new + # POST create + def self.build(klass, params=nil) + raise NotImplementedError + end + + # POST create + def save + raise NotImplementedError + end + + # PUT update + def update_attributes(params=nil) + raise NotImplementedError + end + + # POST create + # PUT update + def errors + raise NotImplementedError + end + + # DELETE destroy + def destroy + raise NotImplementedError + end + end + end +end diff --git a/railties/lib/rails/generators/active_record.rb b/railties/lib/rails/generators/active_record.rb new file mode 100644 index 0000000000..c03ea59c1b --- /dev/null +++ b/railties/lib/rails/generators/active_record.rb @@ -0,0 +1,56 @@ +require 'rails/generators/named_base' +require 'rails/generators/migration' +require 'rails/generators/active_model' + +module ActiveRecord + module Generators + class Base < Rails::Generators::NamedBase #:nodoc: + include Rails::Generators::Migration + + protected + # Implement the required interface for Rails::Generators::Migration. + # + def next_migration_number(dirname) #:nodoc: + if ActiveRecord::Base.timestamped_migrations + Time.now.utc.strftime("%Y%m%d%H%M%S") + else + "%.3d" % (current_migration_number(dirname) + 1) + end + end + end + + class ActiveModel < Rails::Generators::ActiveModel #:nodoc: + def self.all(klass) + "#{klass}.all" + end + + def self.find(klass, params=nil) + "#{klass}.find(#{params})" + end + + def self.build(klass, params=nil) + if params + "#{klass}.new(#{params})" + else + "#{klass}.new" + end + end + + def save + "#{name}.save" + end + + def update_attributes(params=nil) + "#{name}.update_attributes(#{params})" + end + + def errors + "#{name}.errors" + end + + def destroy + "#{name}.destroy" + end + end + end +end diff --git a/railties/lib/rails/generators/active_record/migration/migration_generator.rb b/railties/lib/rails/generators/active_record/migration/migration_generator.rb new file mode 100644 index 0000000000..f6159deeeb --- /dev/null +++ b/railties/lib/rails/generators/active_record/migration/migration_generator.rb @@ -0,0 +1,25 @@ +require 'rails/generators/active_record' + +module ActiveRecord + module Generators + class MigrationGenerator < Base + argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + + def create_migration_file + set_local_assigns! + migration_template "migration.rb", "db/migrate/#{file_name}.rb" + end + + protected + attr_reader :migration_action + + def set_local_assigns! + if file_name =~ /^(add|remove)_.*_(?:to|from)_(.*)/ + @migration_action = $1 + @table_name = $2.pluralize + end + end + + end + end +end diff --git a/railties/lib/rails/generators/active_record/migration/templates/migration.rb b/railties/lib/rails/generators/active_record/migration/templates/migration.rb new file mode 100644 index 0000000000..bbb7c53d86 --- /dev/null +++ b/railties/lib/rails/generators/active_record/migration/templates/migration.rb @@ -0,0 +1,11 @@ +class <%= migration_class_name %> < ActiveRecord::Migration + def self.up<% attributes.each do |attribute| %> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end -%> + <%- end %> + end + + def self.down<% attributes.reverse.each do |attribute| %> + <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end -%> + <%- end %> + end +end diff --git a/railties/lib/rails/generators/active_record/model/model_generator.rb b/railties/lib/rails/generators/active_record/model/model_generator.rb new file mode 100644 index 0000000000..3e72fbeca8 --- /dev/null +++ b/railties/lib/rails/generators/active_record/model/model_generator.rb @@ -0,0 +1,33 @@ +require 'rails/generators/active_record' + +module ActiveRecord + module Generators + class ModelGenerator < Base + argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + + check_class_collision + + class_option :migration, :type => :boolean + class_option :timestamps, :type => :boolean + class_option :parent, :type => :string, :desc => "The parent class for the generated model" + + def create_migration_file + return unless options[:migration] && options[:parent].nil? + migration_template "migration.rb", "db/migrate/create_#{table_name}.rb" + end + + def create_model_file + template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") + end + + hook_for :test_framework + + protected + + def parent_class_name + options[:parent] || "ActiveRecord::Base" + end + + end + end +end diff --git a/railties/lib/rails/generators/active_record/model/templates/migration.rb b/railties/lib/rails/generators/active_record/model/templates/migration.rb new file mode 100644 index 0000000000..1f68487304 --- /dev/null +++ b/railties/lib/rails/generators/active_record/model/templates/migration.rb @@ -0,0 +1,16 @@ +class <%= migration_class_name %> < ActiveRecord::Migration + def self.up + create_table :<%= table_name %> do |t| +<% for attribute in attributes -%> + t.<%= attribute.type %> :<%= attribute.name %> +<% end -%> +<% if options[:timestamps] %> + t.timestamps +<% end -%> + end + end + + def self.down + drop_table :<%= table_name %> + end +end diff --git a/railties/lib/rails/generators/active_record/model/templates/model.rb b/railties/lib/rails/generators/active_record/model/templates/model.rb new file mode 100644 index 0000000000..21ae29e9f2 --- /dev/null +++ b/railties/lib/rails/generators/active_record/model/templates/model.rb @@ -0,0 +1,5 @@ +class <%= class_name %> < <%= parent_class_name.classify %> +<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> + belongs_to :<%= attribute.name %> +<% end -%> +end diff --git a/railties/lib/rails/generators/active_record/observer/observer_generator.rb b/railties/lib/rails/generators/active_record/observer/observer_generator.rb new file mode 100644 index 0000000000..c1c0e3f25b --- /dev/null +++ b/railties/lib/rails/generators/active_record/observer/observer_generator.rb @@ -0,0 +1,15 @@ +require 'rails/generators/active_record' + +module ActiveRecord + module Generators + class ObserverGenerator < Base + check_class_collision :suffix => "Observer" + + def create_observer_file + template 'observer.rb', File.join('app/models', class_path, "#{file_name}_observer.rb") + end + + hook_for :test_framework + end + end +end diff --git a/railties/lib/rails/generators/active_record/observer/templates/observer.rb b/railties/lib/rails/generators/active_record/observer/templates/observer.rb new file mode 100644 index 0000000000..b9a3004161 --- /dev/null +++ b/railties/lib/rails/generators/active_record/observer/templates/observer.rb @@ -0,0 +1,2 @@ +class <%= class_name %>Observer < ActiveRecord::Observer +end diff --git a/railties/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/railties/lib/rails/generators/active_record/session_migration/session_migration_generator.rb new file mode 100644 index 0000000000..afcda2a98a --- /dev/null +++ b/railties/lib/rails/generators/active_record/session_migration/session_migration_generator.rb @@ -0,0 +1,24 @@ +require 'rails/generators/active_record' + +module ActiveRecord + module Generators + class SessionMigrationGenerator < Base + argument :name, :type => :string, :default => "add_sessions_table" + + def create_migration_file + migration_template "migration.rb", "db/migrate/#{file_name}.rb" + end + + protected + + def session_table_name + current_table_name = ActiveRecord::SessionStore::Session.table_name + if ["sessions", "session"].include?(current_table_name) + current_table_name = (ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session') + end + current_table_name + end + + end + end +end diff --git a/railties/lib/rails/generators/active_record/session_migration/templates/migration.rb b/railties/lib/rails/generators/active_record/session_migration/templates/migration.rb new file mode 100644 index 0000000000..919822af7b --- /dev/null +++ b/railties/lib/rails/generators/active_record/session_migration/templates/migration.rb @@ -0,0 +1,16 @@ +class <%= migration_class_name %> < ActiveRecord::Migration + def self.up + create_table :<%= session_table_name %> do |t| + t.string :session_id, :null => false + t.text :data + t.timestamps + end + + add_index :<%= session_table_name %>, :session_id + add_index :<%= session_table_name %>, :updated_at + end + + def self.down + drop_table :<%= session_table_name %> + end +end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb new file mode 100644 index 0000000000..720caa5b3f --- /dev/null +++ b/railties/lib/rails/generators/base.rb @@ -0,0 +1,349 @@ +require 'thor' +require 'rails/generators/actions' + +module Rails + module Generators + class Error < Thor::Error + end + + class Base < Thor::Group + include Thor::Actions + include Rails::Generators::Actions + + add_runtime_options! + + # Automatically sets the source root based on the class name. + # + def self.source_root + @_rails_source_root ||= File.expand_path(File.join(File.dirname(__FILE__), + base_name, generator_name, 'templates')) + end + + # Tries to get the description from a USAGE file one folder above the source + # root otherwise uses a default description. + # + def self.desc(description=nil) + return super if description + usage = File.expand_path(File.join(source_root, "..", "USAGE")) + + @desc ||= if File.exist?(usage) + File.read(usage) + else + "Description:\n Create #{base_name.humanize.downcase} files for #{generator_name} generator." + end + end + + # Convenience method to get the namespace from the class name. It's the + # same as Thor default except that the Generator at the end of the class + # is removed. + # + def self.namespace(name=nil) + return super if name + @namespace ||= super.sub(/_generator$/, '') + end + + # Invoke a generator based on the value supplied by the user to the + # given option named "name". A class option is created when this method + # is invoked and you can set a hash to customize it. + # + # ==== Examples + # + # module Rails::Generators + # class ControllerGenerator < Base + # hook_for :test_framework, :aliases => "-t" + # end + # end + # + # The example above will create a test framework option and will invoke + # a generator based on the user supplied value. + # + # For example, if the user invoke the controller generator as: + # + # ruby script/generate controller Account --test-framework=test_unit + # + # The controller generator will then try to invoke the following generators: + # + # "rails:generators:test_unit", "test_unit:generators:controller", "test_unit" + # + # In this case, the "test_unit:generators:controller" is available and is + # invoked. This allows any test framework to hook into Rails as long as it + # provides any of the hooks above. + # + # ==== Options + # + # This lookup can be customized with two options: :base and :as. The first + # is the root module value and in the example above defaults to "rails". + # The later defaults to the generator name, without the "Generator" ending. + # + # Let's suppose you are creating a generator that needs to invoke the + # controller generator from test unit. Your first attempt is: + # + # class AwesomeGenerator < Rails::Generators::Base + # hook_for :test_framework + # end + # + # The lookup in this case for test_unit as input is: + # + # "test_unit:generators:awesome", "test_unit" + # + # Which is not the desired the lookup. You can change it by providing the + # :as option: + # + # class AwesomeGenerator < Rails::Generators::Base + # hook_for :test_framework, :as => :controller + # end + # + # And now it will lookup at: + # + # "test_unit:generators:awesome", "test_unit" + # + # Similarly, if you want it to also lookup in the rails namespace, you just + # need to provide the :base value: + # + # class AwesomeGenerator < Rails::Generators::Base + # hook_for :test_framework, :base => :rails, :as => :controller + # end + # + # And the lookup is exactly the same as previously: + # + # "rails:generators:test_unit", "test_unit:generators:controller", "test_unit" + # + # ==== Switches + # + # All hooks come with switches for user interface. If the user don't want + # to use any test framework, he can do: + # + # ruby script/generate controller Account --skip-test-framework + # + # Or similarly: + # + # ruby script/generate controller Account --no-test-framework + # + # ==== Boolean hooks + # + # In some cases, you want to provide a boolean hook. For example, webrat + # developers might want to have webrat available on controller generator. + # This can be achieved as: + # + # Rails::Generators::ControllerGenerator.hook_for :webrat, :type => :boolean + # + # Then, if you want, webrat to be invoked, just supply: + # + # ruby script/generate controller Account --webrat + # + # The hooks lookup is similar as above: + # + # "rails:generators:webrat", "webrat:generators:controller", "webrat" + # + # ==== Custom invocations + # + # You can also supply a block to hook_for to customize how the hook is + # going to be invoked. The block receives two parameters, an instance + # of the current class and the klass to be invoked. + # + # For example, in the resource generator, the controller should be invoked + # with a pluralized class name. By default, it is invoked with the same + # name as the resource generator, which is singular. To change this, we + # can give a block to customize how the controller can be invoked. + # + # hook_for :resource_controller do |instance, controller| + # instance.invoke controller, [ instance.name.pluralize ] + # end + # + def self.hook_for(*names, &block) + options = names.extract_options! + in_base = options.delete(:in) || base_name + as_hook = options.delete(:as) || generator_name + + names.each do |name| + defaults = if options[:type] == :boolean + { } + elsif [true, false].include?(default_value_for_option(name, options)) + { :banner => "" } + else + { :desc => "#{name.to_s.humanize} to be invoked", :banner => "NAME" } + end + + unless class_options.key?(name) + class_option name, defaults.merge!(options) + end + + hooks[name] = [ in_base, as_hook ] + invoke_from_option name, options, &block + end + end + + # Remove a previously added hook. + # + # ==== Examples + # + # remove_hook_for :orm + # + def self.remove_hook_for(*names) + remove_invocation *names + + names.each do |name| + hooks.delete(name) + end + end + + # Make class option aware of Rails::Generators.options and Rails::Generators.aliases. + # + def self.class_option(name, options={}) #:nodoc: + options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc) + options[:aliases] = default_aliases_for_option(name, options) + options[:default] = default_value_for_option(name, options) + super(name, options) + end + + # Cache source root and add lib/generators/base/generator/templates to + # source paths. + # + def self.inherited(base) #:nodoc: + super + base.source_root # Cache source root + + if defined?(RAILS_ROOT) && base.name !~ /Base$/ + path = File.expand_path(File.join(RAILS_ROOT, 'lib', 'templates')) + if base.name.include?('::') + base.source_paths << File.join(path, base.base_name, base.generator_name) + else + base.source_paths << File.join(path, base.generator_name) + end + end + end + + protected + + # Check whether the given class names are already taken by user + # application or Ruby on Rails. + # + def class_collisions(*class_names) #:nodoc: + return unless behavior == :invoke + + class_names.flatten.each do |class_name| + class_name = class_name.to_s + next if class_name.strip.empty? + + # Split the class from its module nesting + nesting = class_name.split('::') + last_name = nesting.pop + + # Hack to limit const_defined? to non-inherited on 1.9 + extra = [] + extra << false unless Object.method(:const_defined?).arity == 1 + + # Extract the last Module in the nesting + last = nesting.inject(Object) do |last, nest| + break unless last.const_defined?(nest, *extra) + last.const_get(nest) + end + + if last && last.const_defined?(last_name.camelize, *extra) + raise Error, "The name '#{class_name}' is either already used in your application " << + "or reserved by Ruby on Rails. Please choose an alternative and run " << + "this generator again." + end + end + end + + # Use Rails default banner. + # + def self.banner + "#{$0} #{generator_name} #{self.arguments.map{ |a| a.usage }.join(' ')} [options]" + end + + # Sets the base_name taking into account the current class namespace. + # + def self.base_name + @base_name ||= self.name.split('::').first.underscore + end + + # Removes the namespaces and get the generator name. For example, + # Rails::Generators::MetalGenerator will return "metal" as generator name. + # + def self.generator_name + @generator_name ||= begin + klass_name = self.name.split('::').last + klass_name.sub!(/Generator$/, '') + klass_name.underscore + end + end + + # Return the default value for the option name given doing a lookup in + # Rails::Generators.options. + # + def self.default_value_for_option(name, options) + config = Rails::Generators.options + generator, base = generator_name.to_sym, base_name.to_sym + + if config[generator] && config[generator].key?(name) + config[generator][name] + elsif config[base] && config[base].key?(name) + config[base][name] + elsif config[:rails].key?(name) + config[:rails][name] + else + options[:default] + end + end + + # Return default aliases for the option name given doing a lookup in + # Rails::Generators.aliases. + # + def self.default_aliases_for_option(name, options) + config = Rails::Generators.aliases + generator, base = generator_name.to_sym, base_name.to_sym + + if config[generator] && config[generator].key?(name) + config[generator][name] + elsif config[base] && config[base].key?(name) + config[base][name] + elsif config[:rails].key?(name) + config[:rails][name] + else + options[:aliases] + end + end + + # Keep hooks configuration that are used on prepare_for_invocation. + # + def self.hooks #:nodoc: + @hooks ||= from_superclass(:hooks, {}) + end + + # Prepare class invocation to search on Rails namespace if a previous + # added hook is being used. + # + def self.prepare_for_invocation(name, value) #:nodoc: + if value && constants = self.hooks[name] + Rails::Generators.find_by_namespace(value, *constants) + else + super + end + end + + # Small macro to add ruby as an option to the generator with proper + # default value plus an instance helper method called shebang. + # + def self.add_shebang_option! + class_option :ruby, :type => :string, :aliases => "-r", :default => Thor::Util.ruby_command, + :desc => "Path to the Ruby binary of your choice", :banner => "PATH" + + no_tasks { + define_method :shebang do + @shebang ||= begin + command = if options[:ruby] == Thor::Util.ruby_command + "/usr/bin/env #{File.basename(Thor::Util.ruby_command)}" + else + options[:ruby] + end + "#!#{command}" + end + end + } + end + + end + end +end diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb new file mode 100644 index 0000000000..d468d012dc --- /dev/null +++ b/railties/lib/rails/generators/erb.rb @@ -0,0 +1,8 @@ +require 'rails/generators/named_base' + +module Erb + module Generators + class Base < Rails::Generators::NamedBase #:nodoc: + end + end +end diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb new file mode 100644 index 0000000000..f8780d9c33 --- /dev/null +++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb @@ -0,0 +1,21 @@ +require 'rails/generators/erb' + +module Erb + module Generators + class ControllerGenerator < Base + argument :actions, :type => :array, :default => [], :banner => "action action" + + def create_view_files + base_path = File.join("app/views", class_path, file_name) + empty_directory base_path + + actions.each do |action| + @action = action + @path = File.join(base_path, "#{action}.html.erb") + + template 'view.html.erb', @path + end + end + end + end +end diff --git a/railties/lib/rails/generators/erb/controller/templates/view.html.erb b/railties/lib/rails/generators/erb/controller/templates/view.html.erb new file mode 100644 index 0000000000..cd54d13d83 --- /dev/null +++ b/railties/lib/rails/generators/erb/controller/templates/view.html.erb @@ -0,0 +1,2 @@ +<h1><%= class_name %>#<%= @action %></h1> +<p>Find me in <%= @path %></p> diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb new file mode 100644 index 0000000000..5266259e2f --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -0,0 +1,20 @@ +require 'rails/generators/erb' + +module Erb + module Generators + class MailerGenerator < Base + argument :actions, :type => :array, :default => [], :banner => "method method" + + def create_view_folder + empty_directory File.join("app/views", file_path) + end + + def create_view_files + actions.each do |action| + @action, @path = action, File.join(file_path, action) + template "view.erb", File.join("app/views", "#{@path}.erb") + end + end + end + end +end diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.erb b/railties/lib/rails/generators/erb/mailer/templates/view.erb new file mode 100644 index 0000000000..fcce7bd805 --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/view.erb @@ -0,0 +1,3 @@ +<%= class_name %>#<%= @action %> + +Find me in app/views/<%= @path %> diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb new file mode 100644 index 0000000000..de5b0e9c5d --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -0,0 +1,53 @@ +require 'rails/generators/erb' +require 'rails/generators/resource_helpers' + +module Erb + module Generators + class ScaffoldGenerator < Base + include Rails::Generators::ResourceHelpers + + argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + + class_option :layout, :type => :boolean + class_option :singleton, :type => :boolean, :desc => "Supply to skip index view" + + def create_root_folder + empty_directory File.join("app/views", controller_file_path) + end + + def copy_index_file + return if options[:singleton] + copy_view :index + end + + def copy_edit_file + copy_view :edit + end + + def copy_show_file + copy_view :show + end + + def copy_new_file + copy_view :new + end + + def copy_form_file + copy_view :_form + end + + def copy_layout_file + return unless options[:layout] + template "layout.html.erb", + File.join("app/views/layouts", controller_class_path, "#{controller_file_name}.html.erb") + end + + protected + + def copy_view(view) + template "#{view}.html.erb", File.join("app/views", controller_file_path, "#{view}.html.erb") + end + + end + end +end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb new file mode 100644 index 0000000000..d02028d983 --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -0,0 +1,17 @@ +<%% form_for(@<%= singular_name %>) do |f| %> + <%%= f.error_messages %> + +<% for attribute in attributes -%> + <div class="field"> + <%%= f.label :<%= attribute.name %> %><br /> + <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %> + </div> +<% end -%> + <div class="actions"> + <%% if @<%= singular_name %>.new_record? %> + <%%= f.submit 'Create' %> + <%% else %> + <%%= f.submit 'Update' %> + <%% end %> + </div> +<%% end %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb new file mode 100644 index 0000000000..5bc507ffc8 --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb @@ -0,0 +1,6 @@ +<h1>Editing <%= singular_name %></h1> + +<%%= render 'form' %> + +<%%= link_to 'Show', @<%= singular_name %> %> | +<%%= link_to 'Back', <%= plural_name %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb new file mode 100644 index 0000000000..5e6a4af9e0 --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -0,0 +1,27 @@ +<h1>Listing <%= plural_name %></h1> + +<table> + <tr> +<% for attribute in attributes -%> + <th><%= attribute.human_name %></th> +<% end -%> + <th></th> + <th></th> + <th></th> + </tr> + +<%% @<%= plural_name %>.each do |<%= singular_name %>| %> + <tr> +<% for attribute in attributes -%> + <td><%%=h <%= singular_name %>.<%= attribute.name %> %></td> +<% end -%> + <td><%%= link_to 'Show', <%= singular_name %> %></td> + <td><%%= link_to 'Edit', edit_<%= singular_name %>_path(<%= singular_name %>) %></td> + <td><%%= link_to 'Destroy', <%= singular_name %>, :confirm => 'Are you sure?', :method => :delete %></td> + </tr> +<%% end %> +</table> + +<br /> + +<%%= link_to 'New <%= singular_name %>', new_<%= singular_name %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb new file mode 100644 index 0000000000..6460e5b599 --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/templates/layout.html.erb @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <title><%= controller_class_name %>: <%%= controller.action_name %></title> + <%%= stylesheet_link_tag 'scaffold' %> +</head> +<body> + +<p class="notice"><%%= flash[:notice] %></p> + +<%%= yield %> + +</body> +</html> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb new file mode 100644 index 0000000000..9a1c489331 --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb @@ -0,0 +1,5 @@ +<h1>New <%= singular_name %></h1> + +<%%= render 'form' %> + +<%%= link_to 'Back', <%= plural_name %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb new file mode 100644 index 0000000000..25567957be --- /dev/null +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb @@ -0,0 +1,10 @@ +<% for attribute in attributes -%> +<p> + <b><%= attribute.human_name %>:</b> + <%%=h @<%= singular_name %>.<%= attribute.name %> %> +</p> + +<% end -%> + +<%%= link_to 'Edit', edit_<%= singular_name %>_path(@<%= singular_name %>) %> | +<%%= link_to 'Back', <%= plural_name %>_path %> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb new file mode 100644 index 0000000000..e962308585 --- /dev/null +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -0,0 +1,48 @@ +module Rails + module Generators + class GeneratedAttribute + attr_accessor :name, :type + + def initialize(name, type) + @name, @type = name, type.to_sym + end + + def field_type + @field_type ||= case type + when :integer, :float, :decimal then :text_field + when :datetime, :timestamp, :time then :datetime_select + when :date then :date_select + when :string then :text_field + when :text then :text_area + when :boolean then :check_box + else + :text_field + end + end + + def default + @default ||= case type + when :integer then 1 + when :float then 1.5 + when :decimal then "9.99" + when :datetime, :timestamp, :time then Time.now.to_s(:db) + when :date then Date.today.to_s(:db) + when :string then "MyString" + when :text then "MyText" + when :boolean then false + when :references, :belongs_to then nil + else + "" + end + end + + def human_name + name.to_s.humanize + end + + def reference? + [ :references, :belongs_to ].include?(self.type) + end + end + end +end diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb new file mode 100644 index 0000000000..0a9151ecdf --- /dev/null +++ b/railties/lib/rails/generators/migration.rb @@ -0,0 +1,65 @@ +module Rails + module Generators + # Holds common methods for migrations. It assumes that migrations has the + # [0-9]*_name format and can be used by another frameworks (like Sequel) + # just by implementing the next migration number method. + # + module Migration + def self.included(base) #:nodoc: + base.send :attr_reader, :migration_number, + :migration_file_name, + :migration_class_name + end + + # Creates a migration template at the given destination. The difference + # to the default template method is that the migration number is appended + # to the destination file name. + # + # The migration number, migration file name, migration class name are + # available as instance variables in the template to be rendered. + # + # ==== Examples + # + # migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb" + # + def migration_template(source, destination=nil, config={}) + destination = File.expand_path(destination || source, self.destination_root) + + migration_dir = File.dirname(destination) + @migration_number = next_migration_number(migration_dir) + @migration_file_name = File.basename(destination).sub(/\.rb$/, '') + @migration_class_name = @migration_file_name.camelize + + destination = migration_exists?(migration_dir, @migration_file_name) + + if behavior == :invoke + raise Error, "Another migration is already named #{@migration_file_name}: #{destination}" if destination + destination = File.join(migration_dir, "#{@migration_number}_#{@migration_file_name}.rb") + end + + template(source, destination, config) + end + + protected + + def migration_lookup_at(dirname) #:nodoc: + Dir.glob("#{dirname}/[0-9]*_*.rb") + end + + def migration_exists?(dirname, file_name) #:nodoc: + migration_lookup_at(dirname).grep(/\d+_#{file_name}.rb$/).first + end + + def current_migration_number(dirname) #:nodoc: + migration_lookup_at(dirname).collect do |file| + File.basename(file).split("_").first.to_i + end.max.to_i + end + + def next_migration_number(dirname) #:nodoc: + raise NotImplementError + end + + end + end +end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb new file mode 100644 index 0000000000..0e5976f915 --- /dev/null +++ b/railties/lib/rails/generators/named_base.rb @@ -0,0 +1,101 @@ +require 'rails/generators/base' +require 'rails/generators/generated_attribute' + +module Rails + module Generators + class NamedBase < Base + argument :name, :type => :string + + attr_reader :class_name, :singular_name, :plural_name, :table_name, + :class_path, :file_path, :class_nesting_depth + + alias :file_name :singular_name + + def initialize(*args) #:nodoc: + super + assign_names!(self.name) + parse_attributes! if respond_to?(:attributes) + end + + protected + + def assign_names!(given_name) #:nodoc: + base_name, @class_path, @file_path, class_nesting, @class_nesting_depth = extract_modules(given_name) + class_name_without_nesting, @singular_name, @plural_name = inflect_names(base_name) + + @table_name = if pluralize_table_names? + plural_name + else + singular_name + end + + if class_nesting.empty? + @class_name = class_name_without_nesting + else + @table_name = class_nesting.underscore << "_" << @table_name + @class_name = "#{class_nesting}::#{class_name_without_nesting}" + end + + @table_name.gsub!('/', '_') + end + + # Convert attributes hash into an array with GeneratedAttribute objects. + # + def parse_attributes! #:nodoc: + self.attributes = (attributes || []).map do |key_value| + name, type = key_value.split(':') + Rails::Generators::GeneratedAttribute.new(name, type) + end + end + + # Extract modules from filesystem-style or ruby-style path. Both + # good/fun/stuff and Good::Fun::Stuff produce the same results. + # + def extract_modules(name) #:nodoc: + modules = name.include?('/') ? name.split('/') : name.split('::') + name = modules.pop + path = modules.map { |m| m.underscore } + + file_path = (path + [name.underscore]).join('/') + nesting = modules.map { |m| m.camelize }.join('::') + + [name, path, file_path, nesting, modules.size] + end + + # Receives name and return camelized, underscored and pluralized names. + # + def inflect_names(name) #:nodoc: + camel = name.camelize + under = camel.underscore + plural = under.pluralize + [camel, under, plural] + end + + def pluralize_table_names? + !defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names + end + + # Add a class collisions name to be checked on class initialization. You + # can supply a hash with a :prefix or :suffix to be tested. + # + # ==== Examples + # + # check_class_collision :suffix => "Observer" + # + # If the generator is invoked with class name Admin, it will check for + # the presence of "AdminObserver". + # + def self.check_class_collision(options={}) + define_method :check_class_collision do + name = if self.respond_to?(:controller_class_name) # for ScaffoldBase + controller_class_name + else + class_name + end + + class_collisions "#{options[:prefix]}#{name}#{options[:suffix]}" + end + end + end + end +end diff --git a/railties/lib/rails/generators/rails/app/USAGE b/railties/lib/rails/generators/rails/app/USAGE new file mode 100644 index 0000000000..36d6061a59 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/USAGE @@ -0,0 +1,9 @@ +Description: + The 'rails' command creates a new Rails application with a default + directory structure and configuration at the path you specify. + +Example: + rails ~/Code/Ruby/weblog + + This generates a skeletal Rails installation in ~/Code/Ruby/weblog. + See the README in the newly created application to get going. diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb new file mode 100644 index 0000000000..24c9a969f9 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -0,0 +1,225 @@ +require 'digest/md5' +require 'active_support/secure_random' +require 'rails/version' unless defined?(Rails::VERSION) + +module Rails::Generators + class AppGenerator < Base + DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + add_shebang_option! + + argument :app_path, :type => :string + + class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", + :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" + + class_option :freeze, :type => :boolean, :aliases => "-F", :default => false, + :desc => "Freeze Rails in vendor/rails from the gems" + + class_option :template, :type => :string, :aliases => "-m", + :desc => "Path to an application template (can be a filesystem path or URL)." + + class_option :with_dispatchers, :type => :boolean, :aliases => "-D", :default => false, + :desc => "Add CGI/FastCGI/mod_ruby dispatchers code" + + class_option :skip_activerecord, :type => :boolean, :aliases => "-O", :default => false, + :desc => "Skip ActiveRecord files" + + class_option :skip_testunit, :type => :boolean, :aliases => "-T", :default => false, + :desc => "Skip TestUnit files" + + class_option :skip_prototype, :type => :boolean, :aliases => "-J", :default => false, + :desc => "Skip Prototype files" + + # Add Rails options + # + class_option :version, :type => :boolean, :aliases => "-v", :group => :rails, + :desc => "Show Rails version number and quit" + + class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, + :desc => "Show this help message and quit" + + def initialize(*args) + super + if !options[:no_activerecord] && !DATABASES.include?(options[:database]) + raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." + end + end + + def create_root + self.destination_root = File.expand_path(app_path, destination_root) + empty_directory '.' + + set_default_accessors! + FileUtils.cd(destination_root) + end + + def create_root_files + copy_file "Rakefile" + copy_file "README" + end + + def create_app_files + directory "app" + end + + def create_config_files + empty_directory "config" + + inside "config" do + copy_file "routes.rb" + template "environment.rb" + + directory "environments" + directory "initializers" + directory "locales" + end + end + + def create_boot_file + copy_file "config/boot.rb" + end + + def create_activerecord_files + return if options[:skip_activerecord] + template "config/databases/#{options[:database]}.yml", "config/database.yml" + end + + def create_db_files + directory "db" + end + + def create_doc_files + directory "doc" + end + + def create_lib_files + empty_directory "lib" + empty_directory "lib/tasks" + end + + def create_log_files + empty_directory "log" + + inside "log" do + %w( server production development test ).each do |file| + create_file "#{file}.log" + chmod "#{file}.log", 0666, :verbose => false + end + end + end + + def create_public_files + directory "public", "public", :recursive => false # Do small steps, so anyone can overwrite it. + end + + def create_dispatch_files + return unless options[:with_dispatchers] + copy_file "dispatchers/config.ru", "config.ru" + + template "dispatchers/dispatch.rb", "public/dispatch.rb" + chmod "public/dispatch.rb", 0755, :verbose => false + + template "dispatchers/dispatch.rb", "public/dispatch.cgi" + chmod "public/dispatch.cgi", 0755, :verbose => false + + template "dispatchers/dispatch.fcgi", "public/dispatch.fcgi" + chmod "public/dispatch.fcgi", 0755, :verbose => false + end + + def create_public_image_files + directory "public/images" + end + + def create_public_stylesheets_files + directory "public/stylesheets" + end + + def create_prototype_files + return if options[:skip_prototype] + directory "public/javascripts" + end + + def create_script_files + directory "script" + chmod "script", 0755, :verbose => false + end + + def create_test_files + return if options[:skip_testunit] + directory "test" + end + + def create_tmp_files + empty_directory "tmp" + + inside "tmp" do + %w(sessions sockets cache pids).each do |dir| + empty_directory dir + end + end + end + + def create_vendor_files + empty_directory "vendor/plugins" + end + + def apply_rails_template + apply rails_template if rails_template + rescue Thor::Error, LoadError, Errno::ENOENT => e + raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" + end + + def freeze? + freeze! if options[:freeze] + end + + protected + + attr_accessor :rails_template + + def set_default_accessors! + app_name # Cache app name + + self.rails_template = case options[:template] + when /^http:\/\// + options[:template] + when String + File.expand_path(options[:template], Dir.pwd) + else + options[:template] + end + end + + # Define file as an alias to create_file for backwards compatibility. + # + def file(*args, &block) + create_file(*args, &block) + end + + def app_name + @app_name ||= File.basename(destination_root) + end + + def app_secret + ActiveSupport::SecureRandom.hex(64) + end + + def self.banner + "#{$0} #{self.arguments.map(&:usage).join(' ')} [options]" + end + + def mysql_socket + @mysql_socket ||= [ + "/tmp/mysql.sock", # default + "/var/run/mysqld/mysqld.sock", # debian/gentoo + "/var/tmp/mysql.sock", # freebsd + "/var/lib/mysql/mysql.sock", # fedora + "/opt/local/lib/mysql/mysql.sock", # fedora + "/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql + "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 + "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 + "/opt/lampp/var/mysql/mysql.sock" # xampp for linux + ].find { |f| File.exist?(f) } unless RUBY_PLATFORM =~ /(:?mswin|mingw)/ + end + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/README b/railties/lib/rails/generators/rails/app/templates/README new file mode 100644 index 0000000000..37ec8ea211 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/README @@ -0,0 +1,243 @@ +== Welcome to Rails + +Rails is a web-application framework that includes everything needed to create +database-backed web applications according to the Model-View-Control pattern. + +This pattern splits the view (also called the presentation) into "dumb" templates +that are primarily responsible for inserting pre-built data in between HTML tags. +The model contains the "smart" domain objects (such as Account, Product, Person, +Post) that holds all the business logic and knows how to persist themselves to +a database. The controller handles the incoming requests (such as Save New Account, +Update Product, Show Post) by manipulating the model and directing data to the view. + +In Rails, the model is handled by what's called an object-relational mapping +layer entitled Active Record. This layer allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. You can read more about Active Record in +link:files/vendor/rails/activerecord/README.html. + +The controller and view are handled by the Action Pack, which handles both +layers by its two parts: Action View and Action Controller. These two layers +are bundled in a single package due to their heavy interdependence. This is +unlike the relationship between the Active Record and Action Pack that is much +more separate. Each of these packages can be used independently outside of +Rails. You can read more about Action Pack in +link:files/vendor/rails/actionpack/README.html. + + +== Getting Started + +1. At the command prompt, start a new Rails application using the <tt>rails</tt> command + and your application name. Ex: rails myapp +2. Change directory into myapp and start the web server: <tt>script/server</tt> (run with --help for options) +3. Go to http://localhost:3000/ and get "Welcome aboard: You're riding the Rails!" +4. Follow the guidelines to start developing your application + + +== Web Servers + +By default, Rails will try to use Mongrel if it's are installed when started with script/server, otherwise Rails will use WEBrick, the webserver that ships with Ruby. But you can also use Rails +with a variety of other web servers. + +Mongrel is a Ruby-based webserver with a C component (which requires compilation) that is +suitable for development and deployment of Rails applications. If you have Ruby Gems installed, +getting up and running with mongrel is as easy as: <tt>gem install mongrel</tt>. +More info at: http://mongrel.rubyforge.org + +Say other Ruby web servers like Thin and Ebb or regular web servers like Apache or LiteSpeed or +Lighttpd or IIS. The Ruby web servers are run through Rack and the latter can either be setup to use +FCGI or proxy to a pack of Mongrels/Thin/Ebb servers. + +== Apache .htaccess example for FCGI/CGI + +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly" + + +== Debugging Rails + +Sometimes your application goes wrong. Fortunately there are a lot of tools that +will help you debug it and get it back on the rails. + +First area to check is the application log files. Have "tail -f" commands running +on the server.log and development.log. Rails will automatically display debugging +and runtime information to these files. Debugging info will also be shown in the +browser on requests from 127.0.0.1. + +You can also log your own messages directly into the log file from your code using +the Ruby logger class from inside your controllers. Example: + + class WeblogController < ActionController::Base + def destroy + @weblog = Weblog.find(params[:id]) + @weblog.destroy + logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") + end + end + +The result will be a message in your log file along the lines of: + + Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1 + +More information on how to use the logger is at http://www.ruby-doc.org/core/ + +Also, Ruby documentation can be found at http://www.ruby-lang.org/ including: + +* The Learning Ruby (Pickaxe) Book: http://www.ruby-doc.org/docs/ProgrammingRuby/ +* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) + +These two online (and free) books will bring you up to speed on the Ruby language +and also on programming in general. + + +== Debugger + +Debugger support is available through the debugger command when you start your Mongrel or +Webrick server with --debugger. This means that you can break out of execution at any point +in the code, investigate and change the model, AND then resume execution! +You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug' +Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find(:all) + debugger + end + end + +So the controller will accept the action, run the first line, then present you +with a IRB prompt in the server window. Here you can do things like: + + >> @posts.inspect + => "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>, + #<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" + >> @posts.first.title = "hello from a debugger" + => "hello from a debugger" + +...and even better is that you can examine how your runtime objects actually work: + + >> f = @posts.first + => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + +Finally, when you're ready to resume execution, you enter "cont" + + +== Console + +You can interact with the domain model by starting the console through <tt>script/console</tt>. +Here you'll have all parts of the application configured, just like it is when the +application is running. You can inspect domain models, change values, and save to the +database. Starting the script without arguments will launch it in the development environment. +Passing an argument will specify a different environment, like <tt>script/console production</tt>. + +To reload your controllers and models after launching the console run <tt>reload!</tt> + +== dbconsole + +You can go to the command line of your database directly through <tt>script/dbconsole</tt>. +You would be connected to the database with the credentials defined in database.yml. +Starting the script without arguments will connect you to the development database. Passing an +argument will connect you to a different database, like <tt>script/dbconsole production</tt>. +Currently works for mysql, postgresql and sqlite. + +== Description of Contents + +app + Holds all the code that's specific to this particular application. + +app/controllers + Holds controllers that should be named like weblogs_controller.rb for + automated URL mapping. All controllers should descend from ApplicationController + which itself descends from ActionController::Base. + +app/models + Holds models that should be named like post.rb. + Most models will descend from ActiveRecord::Base. + +app/views + Holds the template files for the view that should be named like + weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby + syntax. + +app/views/layouts + Holds the template files for layouts to be used with views. This models the common + header/footer method of wrapping views. In your views, define a layout using the + <tt>layout :default</tt> and create a file named default.html.erb. Inside default.html.erb, + call <% yield %> to render the view using this layout. + +app/helpers + Holds view helpers that should be named like weblogs_helper.rb. These are generated + for you automatically when using script/generate for controllers. Helpers can be used to + wrap functionality for your views into methods. + +config + Configuration files for the Rails environment, the routing map, the database, and other dependencies. + +db + Contains the database schema in schema.rb. db/migrate contains all + the sequence of Migrations for your schema. + +doc + This directory is where your application documentation will be stored when generated + using <tt>rake doc:app</tt> + +lib + Application specific libraries. Basically, any kind of custom code that doesn't + belong under controllers, models, or helpers. This directory is in the load path. + +public + The directory available for the web server. Contains subdirectories for images, stylesheets, + and javascripts. Also contains the dispatchers and the default HTML files. This should be + set as the DOCUMENT_ROOT of your web server. + +script + Helper scripts for automation and generation. + +test + Unit and functional tests along with fixtures. When using the script/generate scripts, template + test files will be generated for you and placed in this directory. + +vendor + External libraries that the application depends on. Also includes the plugins subdirectory. + If the app has frozen rails, those gems also go here, under vendor/rails/. + This directory is in the load path. diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile new file mode 100755 index 0000000000..bd4dec5389 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'rails/tasks' diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb new file mode 100644 index 0000000000..6635a3f487 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb @@ -0,0 +1,10 @@ +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + +class ApplicationController < ActionController::Base + helper :all # include all helpers, all the time + protect_from_forgery # See ActionController::RequestForgeryProtection for details + + # Scrub sensitive parameters from your log + # filter_parameter_logging :password +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb new file mode 100644 index 0000000000..22a7940eb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb @@ -0,0 +1,3 @@ +# Methods added to this helper will be available to all templates in the application. +module ApplicationHelper +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/.empty_directory b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/.empty_directory diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb new file mode 100644 index 0000000000..1e09cf08ec --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb @@ -0,0 +1,114 @@ +# Don't change this file! +# Configure your app in config/environment.rb and config/environments/*.rb + +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end + end + + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + # activesupport/lib + %w(railties/lib).each do |path| + $:.unshift("#{RAILS_ROOT}/vendor/rails/#{path}") + end + require "rails/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + Rails::GemDependency.add_frozen_gem_path + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'rails/initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion rescue nil + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + min_version = '1.3.2' + require 'rubygems' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end +end + +# All that for this: +Rails.boot! diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml new file mode 100644 index 0000000000..c0c3588be1 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml @@ -0,0 +1,28 @@ +# FrontBase versions 4.x +# +# Get the bindings: +# gem install ruby-frontbase + +development: + adapter: frontbase + host: localhost + database: <%= app_name %>_development + username: <%= app_name %> + password: '' + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: frontbase + host: localhost + database: <%= app_name %>_test + username: <%= app_name %> + password: '' + +production: + adapter: frontbase + host: localhost + database: <%= app_name %>_production + username: <%= app_name %> + password: '' diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml new file mode 100644 index 0000000000..a9716ddb44 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -0,0 +1,62 @@ +# IBM Dataservers +# +# Home Page +# http://rubyforge.org/projects/rubyibm/ +# +# To install the ibm_db gem: +# On Linux: +# Source the db2profile file and set the necessary environment variables: +# +# . /home/db2inst1/sqllib/db2profile +# export IBM_DB_DIR=/opt/ibm/db2/V9.1 +# export IBM_DB_LIB=/opt/ibm/db2/V9.1/lib32 +# +# Then issue the command: gem install ibm_db +# +# On Windows: +# Issue the command: gem install ibm_db +# If prompted, select the mswin32 option +# +# For more details on the installation refer to http://rubyforge.org/docman/view.php/2361/7682/IBM_DB_GEM.pdf +# +# For more details on the connection parameters below refer to: +# http://rubyibm.rubyforge.org/docs/adapter/0.9.0/rdoc/classes/ActiveRecord/ConnectionAdapters/IBM_DBAdapter.html + +development: + adapter: ibm_db + username: db2inst1 + password: + database: <%= app_name[0,4] %>_dev + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user + #application: my_application + #workstation: my_workstation + +test: + adapter: ibm_db + username: db2inst1 + password: + database: <%= app_name[0,4] %>_tst + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user + #application: my_application + #workstation: my_workstation + +production: + adapter: ibm_db + username: db2inst1 + password: + database: <%= app_name[0,4] %>_prd + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user + #application: my_application + #workstation: my_workstation
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml new file mode 100644 index 0000000000..6bf2f7b1fd --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -0,0 +1,60 @@ +# MySQL. Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install mysql +# On Mac OS X: +# sudo gem install mysql -- --with-mysql-dir=/usr/local/mysql +# On Mac OS X Leopard: +# sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config +# This sets the ARCHFLAGS environment variable to your native architecture +# On Windows: +# gem install mysql +# Choose the win32 build. +# Install MySQL and put its /bin directory on your path. +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +development: + adapter: mysql + encoding: utf8 + reconnect: false + database: <%= app_name %>_development + pool: 5 + username: root + password: +<% if mysql_socket -%> + socket: <%= mysql_socket %> +<% else -%> + host: localhost +<% end -%> + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: mysql + encoding: utf8 + reconnect: false + database: <%= app_name %>_test + pool: 5 + username: root + password: +<% if mysql_socket -%> + socket: <%= mysql_socket %> +<% else -%> + host: localhost +<% end -%> + +production: + adapter: mysql + encoding: utf8 + reconnect: false + database: <%= app_name %>_production + pool: 5 + username: root + password: +<% if mysql_socket -%> + socket: <%= mysql_socket %> +<% else -%> + host: localhost +<% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml new file mode 100644 index 0000000000..a1883f6256 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -0,0 +1,39 @@ +# Oracle/OCI 8i, 9, 10g +# +# Requires Ruby/OCI8: +# http://rubyforge.org/projects/ruby-oci8/ +# +# Specify your database using any valid connection syntax, such as a +# tnsnames.ora service name, or a SQL connect url string of the form: +# +# //host:[port][/service name] +# +# By default prefetch_rows (OCI_ATTR_PREFETCH_ROWS) is set to 100. And +# until true bind variables are supported, cursor_sharing is set by default +# to 'similar'. Both can be changed in the configation below; the defaults +# are equivalent to specifying: +# +# prefetch_rows: 100 +# cursor_sharing: similar +# + +development: + adapter: oracle + database: <%= app_name %>_development + username: <%= app_name %> + password: + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: oracle + database: <%= app_name %>_test + username: <%= app_name %> + password: + +production: + adapter: oracle + database: <%= app_name %>_production + username: <%= app_name %> + password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml new file mode 100644 index 0000000000..f600e054cf --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -0,0 +1,51 @@ +# PostgreSQL. Versions 7.4 and 8.x are supported. +# +# Install the ruby-postgres driver: +# gem install ruby-postgres +# On Mac OS X: +# gem install ruby-postgres -- --include=/usr/local/pgsql +# On Windows: +# gem install ruby-postgres +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +development: + adapter: postgresql + encoding: unicode + database: <%= app_name %>_development + pool: 5 + username: <%= app_name %> + password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # The server defaults to notice. + #min_messages: warning + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: postgresql + encoding: unicode + database: <%= app_name %>_test + pool: 5 + username: <%= app_name %> + password: + +production: + adapter: postgresql + encoding: unicode + database: <%= app_name %>_production + pool: 5 + username: <%= app_name %> + password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml new file mode 100644 index 0000000000..025d62a8d8 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml @@ -0,0 +1,22 @@ +# SQLite version 3.x +# gem install sqlite3-ruby (not necessary on OS X Leopard) +development: + adapter: sqlite3 + database: db/development.sqlite3 + pool: 5 + timeout: 5000 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: sqlite3 + database: db/test.sqlite3 + pool: 5 + timeout: 5000 + +production: + adapter: sqlite3 + database: db/production.sqlite3 + pool: 5 + timeout: 5000 diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb new file mode 100644 index 0000000000..adb3a3060a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb @@ -0,0 +1,53 @@ +# Be sure to restart your server when you modify this file + +# Specifies gem version of Rails to use when vendor/rails is not present +<%= '# ' if options[:freeze] %>RAILS_GEM_VERSION = '<%= Rails::VERSION::STRING %>' unless defined? RAILS_GEM_VERSION + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + + # Specify gems that this application depends on and have them installed with rake gems:install + # config.gem "bj" + # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" + # config.gem "sqlite3-ruby", :lib => "sqlite3" + # config.gem "aws-s3", :lib => "aws/s3" + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Skip frameworks you're not going to use. To use Rails without a database, + # you must remove the Active Record framework. +<% if options[:skip_activerecord] -%> + config.frameworks -= [ :active_record ] +<% else -%> + # config.frameworks -= [ :active_record, :active_resource, :action_mailer ] + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer +<% end -%> + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. + config.time_zone = 'UTC' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] + # config.i18n.default_locale = :de + + # Configure generators values. Many other options are available, be sure to + # check the documentation. + # config.generators do |g| + # g.orm :active_record + # g.template_engine :erb + # g.test_framework :test_unit, :fixture => true + # end +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb new file mode 100644 index 0000000000..85c9a6080e --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb @@ -0,0 +1,17 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# In the development environment your application's code is reloaded on +# every request. This slows down response time but is perfect for development +# since you don't have to restart the webserver when you make code changes. +config.cache_classes = false + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_view.debug_rjs = true +config.action_controller.perform_caching = false + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb new file mode 100644 index 0000000000..1fc9f6b923 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb @@ -0,0 +1,27 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests +config.cache_classes = true + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# See everything in the log (default is :info) +# config.log_level = :debug + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + +# Use a different cache store in production +# config.cache_store = :mem_cache_store + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors, bad email addresses will be ignored +# config.action_mailer.raise_delivery_errors = false + +# Enable threaded mode +# config.threadsafe!
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb new file mode 100644 index 0000000000..496eb9572b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb @@ -0,0 +1,27 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Disable request forgery protection in test environment +config.action_controller.allow_forgery_protection = false + +# Tell Action Mailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test + +# Use SQL instead of Active Record's schema dumper when creating the test database. +# This is necessary if your schema can't be completely dumped by the schema dumper, +# like if you have constraints or database-specific column types +# config.active_record.schema_format = :sql
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..839d4cde19 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers!
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb new file mode 100644 index 0000000000..d531b8bb82 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb @@ -0,0 +1,10 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format +# (all these examples are active by default): +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb new file mode 100644 index 0000000000..72aca7e441 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register_alias "text/html", :iphone diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_rails_defaults.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_rails_defaults.rb new file mode 100644 index 0000000000..8ec3186c84 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_rails_defaults.rb @@ -0,0 +1,19 @@ +# Be sure to restart your server when you modify this file. + +# These settings change the behavior of Rails 2 apps and will be defaults +# for Rails 3. You can remove this initializer when Rails 3 is released. + +if defined?(ActiveRecord) + # Include Active Record class name as root for JSON serialized output. + ActiveRecord::Base.include_root_in_json = true + + # Store the full class name (including module namespace) in STI type column. + ActiveRecord::Base.store_full_sti_class = true +end + +# Use ISO 8601 format for JSON serialized times and dates. +ActiveSupport.use_standard_json_time_format = true + +# Don't escape HTML entities in JSON, leave that for the #json_escape helper. +# if you're including raw json in an HTML page. +ActiveSupport.escape_html_entities_in_json = false
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt new file mode 100644 index 0000000000..4499ab84b6 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt @@ -0,0 +1,15 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying cookie session data integrity. +# If you change this key, all old sessions will become invalid! +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +ActionController::Base.session = { + :key => '_<%= app_name %>_session', + :secret => '<%= app_secret %>' +} + +# Use the database for sessions instead of the cookie-based default, +# which shouldn't be used to store highly confidential information +# (create the session table with "rake db:sessions:create") +# ActionController::Base.session_store = :active_record_store diff --git a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml new file mode 100644 index 0000000000..f265c068d8 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml @@ -0,0 +1,5 @@ +# Sample localization file for English. Add more files in this directory for other locales. +# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. + +en: + hello: "Hello world"
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb new file mode 100644 index 0000000000..ea14ce1bfc --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb @@ -0,0 +1,43 @@ +ActionController::Routing::Routes.draw do |map| + # The priority is based upon order of creation: first created -> highest priority. + + # Sample of regular route: + # map.connect 'products/:id', :controller => 'catalog', :action => 'view' + # Keep in mind you can assign values other than :controller and :action + + # Sample of named route: + # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' + # This route can be invoked with purchase_url(:id => product.id) + + # Sample resource route (maps HTTP verbs to controller actions automatically): + # map.resources :products + + # Sample resource route with options: + # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get } + + # Sample resource route with sub-resources: + # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller + + # Sample resource route with more complex sub-resources + # map.resources :products do |products| + # products.resources :comments + # products.resources :sales, :collection => { :recent => :get } + # end + + # Sample resource route within a namespace: + # map.namespace :admin do |admin| + # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb) + # admin.resources :products + # end + + # You can have the root of your site routed with map.root -- just remember to delete public/index.html. + # map.root :controller => "welcome" + + # See how all your routes lay out with "rake routes" + + # Install the default routes as the lowest priority. + # Note: These default routes make all actions in every controller accessible via GET requests. You should + # consider removing or commenting them out if you're using named routes and resources. + map.connect ':controller/:action/:id' + map.connect ':controller/:action/:id.:format' +end diff --git a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb new file mode 100644 index 0000000000..bc8695e6f0 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# +# Examples: +# +# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) +# Mayor.create(:name => 'Daley', :city => cities.first) diff --git a/railties/lib/rails/generators/rails/app/templates/dispatchers/config.ru b/railties/lib/rails/generators/rails/app/templates/dispatchers/config.ru new file mode 100644 index 0000000000..acbfe4e9ae --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/dispatchers/config.ru @@ -0,0 +1,7 @@ +# Rack Dispatcher + +# Require your environment file to bootstrap Rails +require File.dirname(__FILE__) + '/config/environment' + +# Dispatch the request +run ActionController::Dispatcher.new diff --git a/railties/lib/rails/generators/rails/app/templates/dispatchers/dispatch.fcgi b/railties/lib/rails/generators/rails/app/templates/dispatchers/dispatch.fcgi new file mode 100755 index 0000000000..f5b3b71875 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/dispatchers/dispatch.fcgi @@ -0,0 +1,24 @@ +<%= shebang %> +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/railties/lib/rails/generators/rails/app/templates/dispatchers/dispatch.rb b/railties/lib/rails/generators/rails/app/templates/dispatchers/dispatch.rb new file mode 100755 index 0000000000..48e888113a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/dispatchers/dispatch.rb @@ -0,0 +1,10 @@ +<%= shebang %> + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch diff --git a/railties/lib/rails/generators/rails/app/templates/dispatchers/gateway.cgi b/railties/lib/rails/generators/rails/app/templates/dispatchers/gateway.cgi new file mode 100755 index 0000000000..bdc1055a22 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/dispatchers/gateway.cgi @@ -0,0 +1,97 @@ +<%= shebang %> + +require 'drb' + +# This file includes an experimental gateway CGI implementation. It will work +# only on platforms which support both fork and sockets. +# +# To enable it edit public/.htaccess and replace dispatch.cgi with gateway.cgi. +# +# Next, create the directory log/drb_gateway and grant the apache user rw access +# to said directory. +# +# On the next request to your server, the gateway tracker should start up, along +# with a few listener processes. This setup should provide you with much better +# speeds than dispatch.cgi. +# +# Keep in mind that the first request made to the server will be slow, as the +# tracker and listeners will have to load. Also, the tracker and listeners will +# shutdown after a period if inactivity. You can set this value below -- the +# default is 90 seconds. + +TrackerSocket = File.expand_path(File.join(File.dirname(__FILE__), '../log/drb_gateway/tracker.sock')) +DieAfter = 90 # Seconds +Listeners = 3 + +def message(s) + $stderr.puts "gateway.cgi: #{s}" if ENV && ENV["DEBUG_GATEWAY"] +end + +def listener_socket(number) + File.expand_path(File.join(File.dirname(__FILE__), "../log/drb_gateway/listener_#{number}.sock")) +end + +unless File.exist? TrackerSocket + message "Starting tracker and #{Listeners} listeners" + fork do + Process.setsid + STDIN.reopen "/dev/null" + STDOUT.reopen "/dev/null", "a" + + root = File.expand_path(File.dirname(__FILE__) + '/..') + + message "starting tracker" + fork do + ARGV.clear + ARGV << TrackerSocket << Listeners.to_s << DieAfter.to_s + load File.join(root, 'script', 'tracker') + end + + message "starting listeners" + require File.join(root, 'config/environment.rb') + Listeners.times do |number| + fork do + ARGV.clear + ARGV << listener_socket(number) << DieAfter.to_s + load File.join(root, 'script', 'listener') + end + end + end + + message "waiting for tracker and listener to arise..." + ready = false + 10.times do + sleep 0.5 + break if (ready = File.exist?(TrackerSocket) && File.exist?(listener_socket(0))) + end + + if ready + message "tracker and listener are ready" + else + message "Waited 5 seconds, listener and tracker not ready... dropping request" + Kernel.exit 1 + end +end + +DRb.start_service + +message "connecting to tracker" +tracker = DRbObject.new_with_uri("drbunix:#{TrackerSocket}") + +input = $stdin.read +$stdin.close + +env = ENV.inspect + +output = nil +tracker.with_listener do |number| + message "connecting to listener #{number}" + socket = listener_socket(number) + listener = DRbObject.new_with_uri("drbunix:#{socket}") + output = listener.process(env, input) + message "listener #{number} has finished, writing output" +end + +$stdout.write output +$stdout.flush +$stdout.close diff --git a/railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP b/railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP new file mode 100644 index 0000000000..fe41f5cc24 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP @@ -0,0 +1,2 @@ +Use this README file to introduce your application and point to useful places in the API for learning more. +Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html new file mode 100644 index 0000000000..88ee108e90 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/404.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <title>The page you were looking for doesn't exist (404)</title> + <style type="text/css"> + body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } + div.dialog { + width: 25em; + padding: 0 4em; + margin: 4em auto 0 auto; + border: 1px solid #ccc; + border-right-color: #999; + border-bottom-color: #999; + } + h1 { font-size: 100%; color: #f00; line-height: 1.5em; } + </style> +</head> + +<body> + <!-- This file lives in public/404.html --> + <div class="dialog"> + <h1>The page you were looking for doesn't exist.</h1> + <p>You may have mistyped the address or the page may have moved.</p> + </div> +</body> +</html> diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html new file mode 100644 index 0000000000..9c3c96670b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/422.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <title>The change you wanted was rejected (422)</title> + <style type="text/css"> + body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } + div.dialog { + width: 25em; + padding: 0 4em; + margin: 4em auto 0 auto; + border: 1px solid #ccc; + border-right-color: #999; + border-bottom-color: #999; + } + h1 { font-size: 100%; color: #f00; line-height: 1.5em; } + </style> +</head> + +<body> + <!-- This file lives in public/422.html --> + <div class="dialog"> + <h1>The change you wanted was rejected.</h1> + <p>Maybe you tried to change something you didn't have access to.</p> + </div> +</body> +</html> diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html new file mode 100644 index 0000000000..f71c86e652 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <title>We're sorry, but something went wrong (500)</title> + <style type="text/css"> + body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; } + div.dialog { + width: 25em; + padding: 0 4em; + margin: 4em auto 0 auto; + border: 1px solid #ccc; + border-right-color: #999; + border-bottom-color: #999; + } + h1 { font-size: 100%; color: #f00; line-height: 1.5em; } + </style> +</head> + +<body> + <!-- This file lives in public/500.html --> + <div class="dialog"> + <h1>We're sorry, but something went wrong.</h1> + <p>We've been notified about this issue and we'll take a look at it shortly.</p> + </div> +</body> +</html> diff --git a/railties/lib/rails/generators/rails/app/templates/public/favicon.ico b/railties/lib/rails/generators/rails/app/templates/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/favicon.ico diff --git a/railties/lib/rails/generators/rails/app/templates/public/images/rails.png b/railties/lib/rails/generators/rails/app/templates/public/images/rails.png Binary files differnew file mode 100644 index 0000000000..d5edc04e65 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/images/rails.png diff --git a/railties/lib/rails/generators/rails/app/templates/public/index.html b/railties/lib/rails/generators/rails/app/templates/public/index.html new file mode 100644 index 0000000000..ff2dfd3193 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/index.html @@ -0,0 +1,279 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> + <title>Ruby on Rails: Welcome aboard</title> + <style type="text/css" media="screen"> + body { + margin: 0; + margin-bottom: 25px; + padding: 0; + background-color: #f0f0f0; + font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"; + font-size: 13px; + color: #333; + } + + h1 { + font-size: 28px; + color: #000; + } + + a {color: #03c} + a:hover { + background-color: #03c; + color: white; + text-decoration: none; + } + + + #page { + background-color: #f0f0f0; + width: 750px; + margin: 0; + margin-left: auto; + margin-right: auto; + } + + #content { + float: left; + background-color: white; + border: 3px solid #aaa; + border-top: none; + padding: 25px; + width: 500px; + } + + #sidebar { + float: right; + width: 175px; + } + + #footer { + clear: both; + } + + + #header, #about, #getting-started { + padding-left: 75px; + padding-right: 30px; + } + + + #header { + background-image: url("images/rails.png"); + background-repeat: no-repeat; + background-position: top left; + height: 64px; + } + #header h1, #header h2 {margin: 0} + #header h2 { + color: #888; + font-weight: normal; + font-size: 16px; + } + + + #about h3 { + margin: 0; + margin-bottom: 10px; + font-size: 14px; + } + + #about-content { + background-color: #ffd; + border: 1px solid #fc0; + margin-left: -11px; + } + #about-content table { + margin-top: 10px; + margin-bottom: 10px; + font-size: 11px; + border-collapse: collapse; + } + #about-content td { + padding: 10px; + padding-top: 3px; + padding-bottom: 3px; + } + #about-content td.name {color: #555} + #about-content td.value {color: #000} + + #about-content ul { + padding: 0; + list-style-type: none; + } + + #about-content.failure { + background-color: #fcc; + border: 1px solid #f00; + } + #about-content.failure p { + margin: 0; + padding: 10px; + } + + + #getting-started { + border-top: 1px solid #ccc; + margin-top: 25px; + padding-top: 15px; + } + #getting-started h1 { + margin: 0; + font-size: 20px; + } + #getting-started h2 { + margin: 0; + font-size: 14px; + font-weight: normal; + color: #333; + margin-bottom: 25px; + } + #getting-started ol { + margin-left: 0; + padding-left: 0; + } + #getting-started li { + font-size: 18px; + color: #888; + margin-bottom: 25px; + } + #getting-started li h2 { + margin: 0; + font-weight: normal; + font-size: 18px; + color: #333; + } + #getting-started li p { + color: #555; + font-size: 13px; + } + + + #search { + margin: 0; + padding-top: 10px; + padding-bottom: 10px; + font-size: 11px; + } + #search input { + font-size: 11px; + margin: 2px; + } + #search-text {width: 170px} + + + #sidebar ul { + margin-left: 0; + padding-left: 0; + } + #sidebar ul h3 { + margin-top: 25px; + font-size: 16px; + padding-bottom: 10px; + border-bottom: 1px solid #ccc; + } + #sidebar li { + list-style-type: none; + } + #sidebar ul.links li { + margin-bottom: 5px; + } + + </style> + <script type="text/javascript" src="javascripts/prototype.js"></script> + <script type="text/javascript" src="javascripts/effects.js"></script> + <script type="text/javascript"> + function about() { + if (Element.empty('about-content')) { + new Ajax.Updater('about-content', 'rails/info/properties', { + method: 'get', + onFailure: function() {Element.classNames('about-content').add('failure')}, + onComplete: function() {new Effect.BlindDown('about-content', {duration: 0.25})} + }); + } else { + new Effect[Element.visible('about-content') ? + 'BlindUp' : 'BlindDown']('about-content', {duration: 0.25}); + } + } + + window.onload = function() { + $('search-text').value = ''; + $('search').onsubmit = function() { + $('search-text').value = 'site:rubyonrails.org ' + $F('search-text'); + } + } + </script> + </head> + <body> + <div id="page"> + <div id="sidebar"> + <ul id="sidebar-items"> + <li> + <form id="search" action="http://www.google.com/search" method="get"> + <input type="hidden" name="hl" value="en" /> + <input type="text" id="search-text" name="q" value="site:rubyonrails.org " /> + <input type="submit" value="Search" /> the Rails site + </form> + </li> + + <li> + <h3>Join the community</h3> + <ul class="links"> + <li><a href="http://www.rubyonrails.org/">Ruby on Rails</a></li> + <li><a href="http://weblog.rubyonrails.org/">Official weblog</a></li> + <li><a href="http://wiki.rubyonrails.org/">Wiki</a></li> + </ul> + </li> + + <li> + <h3>Browse the documentation</h3> + <ul class="links"> + <li><a href="http://api.rubyonrails.org/">Rails API</a></li> + <li><a href="http://stdlib.rubyonrails.org/">Ruby standard library</a></li> + <li><a href="http://corelib.rubyonrails.org/">Ruby core</a></li> + <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li> + </ul> + </li> + </ul> + </div> + + <div id="content"> + <div id="header"> + <h1>Welcome aboard</h1> + <h2>You’re riding Ruby on Rails!</h2> + </div> + + <div id="about"> + <h3><a href="rails/info/properties" onclick="about(); return false">About your application’s environment</a></h3> + <div id="about-content" style="display: none"></div> + </div> + + <div id="getting-started"> + <h1>Getting started</h1> + <h2>Here’s how to get rolling:</h2> + + <ol> + <li> + <h2>Use <code>script/generate</code> to create your models and controllers</h2> + <p>To see all available options, run it without parameters.</p> + </li> + + <li> + <h2>Set up a default route and remove or rename this file</h2> + <p>Routes are set up in config/routes.rb.</p> + </li> + + <li> + <h2>Create your database</h2> + <p>Run <code>rake db:migrate</code> to create your database. If you're not using SQLite (the default), edit <code>config/database.yml</code> with your username and password.</p> + </li> + </ol> + </div> + </div> + + <div id="footer"> </div> + </div> + </body> +</html> diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/application.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/application.js new file mode 100644 index 0000000000..fe4577696b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/application.js @@ -0,0 +1,2 @@ +// Place your application-specific JavaScript functions and classes here +// This file is automatically included by javascript_include_tag :defaults diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/controls.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/controls.js new file mode 100644 index 0000000000..ca29aefdd1 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/controls.js @@ -0,0 +1,963 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { }; +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element); + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || { }; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + '<iframe id="' + this.update.id + '_iefix" '+ + 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + + 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index--; + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++; + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.value; + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(Autocompleter.Base, { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + + elem.substr(entry.length) + "</li>"); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" + + elem.substr(foundPos, entry.length) + "</strong>" + elem.substr( + foundPos + entry.length) + "</li>"); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); + return "<ul>" + ret.join('') + "</ul>"; + } + }, options || { }); + } +}); + +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007). + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +}; + +Ajax.InPlaceEditor = Class.create({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML.unescapeHTML(); + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw('Server returned an invalid collection representation.'); + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +});
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/dragdrop.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/dragdrop.js new file mode 100644 index 0000000000..07229f986f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/dragdrop.js @@ -0,0 +1,973 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +}; + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +}; + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create({ + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this._originallyAbsolute) + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this._originallyAbsolute) + Position.relativize(this.element); + delete this._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*this.options.snap }.bind(this)); + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight; + } + } + return { top: T, left: L, width: W, height: H }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +}); + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + element = $(element); + var s = Sortable.sortables[element.id]; + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || { }); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + }; + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + }; + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + }; + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child); + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + }; + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +}; + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +}; + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +}; + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +};
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/effects.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/effects.js new file mode 100644 index 0000000000..5a639d2dea --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/effects.js @@ -0,0 +1,1128 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + .5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; + }, + pulse: function(pos, pulses) { + return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || { }); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if (this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i<len;i++) + this.effects[i] && this.effects[i].loop(timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if (!Object.isString(queueName)) return queueName; + + return this.instances.get(queueName) || + this.instances.set(queueName, new Effect.ScopedQueue()); + } +}; +Effect.Queue = Effect.Queues.get('global'); + +Effect.Base = Class.create({ + position: null, + start: function(options) { + function codeForEvent(options,eventName){ + return ( + (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') + + (options[eventName] ? 'this.options.'+eventName+'(this);' : '') + ); + } + if (options && options.transition === false) options.transition = Effect.Transitions.linear; + this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { }); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn+(this.options.duration*1000); + this.fromToDelta = this.options.to-this.options.from; + this.totalTime = this.finishOn-this.startOn; + this.totalFrames = this.options.fps*this.options.duration; + + this.render = (function() { + function dispatch(effect, eventName) { + if (effect.options[eventName + 'Internal']) + effect.options[eventName + 'Internal'](effect); + if (effect.options[eventName]) + effect.options[eventName](effect); + } + + return function(pos) { + if (this.state === "idle") { + this.state = "running"; + dispatch(this, 'beforeSetup'); + if (this.setup) this.setup(); + dispatch(this, 'afterSetup'); + } + if (this.state === "running") { + pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from; + this.position = pos; + dispatch(this, 'beforeUpdate'); + if (this.update) this.update(pos); + dispatch(this, 'afterUpdate'); + } + }; + })(); + + this.event('beforeStart'); + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if (timePos >= this.startOn) { + if (timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if (this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if (effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or { } with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || { }); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if (this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if (/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if (!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()); } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }); + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}); }}); }}); }}); }}); }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) + ); +}; + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ); + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }, + oldOpacity = element.getInlineOpacity(), + transition = options.transition || Effect.Transitions.linear, + reverser = function(pos){ + return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); + }; + + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +}; + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + }; + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ); + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ); + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>'; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { + results[property] = css[property]; + return results; + }); + if (!styles.opacity) styles.opacity = element.getOpacity(); + return styles; + }; +} + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element); + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + }; + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods);
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js b/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js new file mode 100644 index 0000000000..dfe8ab4e13 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js @@ -0,0 +1,4320 @@ +/* Prototype JavaScript framework, version 1.6.0.3 + * (c) 2005-2008 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0.3', + + Browser: { + IE: !!(window.attachEvent && + navigator.userAgent.indexOf('Opera') === -1), + Opera: navigator.userAgent.indexOf('Opera') > -1, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && + navigator.userAgent.indexOf('KHTML') === -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div')['__proto__'] && + document.createElement('div')['__proto__'] !== + document.createElement('form')['__proto__'] + }, + + ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method); + + value.valueOf = method.valueOf.bind(method); + value.toString = method.toString.bind(method); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return !!(object && object.nodeType == 1); + }, + + isArray: function(object) { + return object != null && typeof object == "object" && + 'splice' in object && 'join' in object; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1] + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + defer: function() { + var args = [0.01].concat($A(arguments)); + return this.delay.apply(this, args); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + }, + unescapeHTML: function() { + return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + try { + this._each(function(value) { + iterator.call(context, value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator.call(context, value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator.call(context, value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator.call(context, value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + var result; + this.each(function(value, index) { + if (iterator.call(context, value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + var results = []; + this.each(function(value, index) { + if (iterator.call(context, value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator.call(context, value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + this.each(function(value, index) { + memo = iterator.call(context, memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator || Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator.call(context, value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + var results = []; + this.each(function(value, index) { + if (!iterator.call(context, value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#<Enumerable:' + this.toArray().inspect() + '>'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + $A = function(iterable) { + if (!iterable) return []; + // In Safari, only use the `toArray` method if it's not a NodeList. + // A NodeList is a function, has an function `item` property, and a numeric + // `length` property. Adapted from Google Doctype. + if (!(typeof iterable === 'function' && typeof iterable.length === + 'number' && typeof iterable.item === 'function') && iterable.toArray) + return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + }; +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + // simulating poorly supported hasOwnProperty + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.inject([], function(results, pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return results.concat(values.map(toQueryPair.curry(key))); + } else results.push(toQueryPair(key, values)); + return results; + }).join('&'); + }, + + inspect: function() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +}; + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); + +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); + if (element) this.Element.prototype = element.prototype; +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + element = $(element); + element.style.display = 'none'; + return element; + }, + + show: function(element) { + element = $(element); + element.style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, insert, tagName, childNodes; + + for (var position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + insert = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + insert(element, content); + continue; + } + + content = Object.toHTML(content); + + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $(element).select("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + return Object.isNumber(expression) ? element.descendants()[expression] : + Element.select(element, expression)[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (ancestor.contains) + return ancestor.contains(element) && ancestor !== element; + + while (element = element.parentNode) + if (element == ancestor) return true; + + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value || value == 'auto') { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = element.getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (Prototype.Browser.Opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + // IE doesn't report offsets correctly for static elements, so we change them + // to "relative" to get the values, then change them back. + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + // IE throws an error if element is not in document + try { element.offsetParent } + catch(e) { return $(document.body) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + // Trigger hasLayout on the offset parent so that IE6 reports + // accurate offsetTop and offsetLeft values for position: fixed. + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( + function(proceed, element) { + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + return proceed(element); + } + ); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName.toUpperCase() == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if ('outerHTML' in document.createElement('div')) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + tags: { + TABLE: ['<table>', '</table>', 1], + TBODY: ['<table><tbody>', '</tbody></table>', 2], + TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3], + TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4], + SELECT: ['<select>', '</select>', 1] + } +}; + +(function() { + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return !!(node && node.specified); + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div')['__proto__']) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div')['__proto__']; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName.toUpperCase(), property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName)['__proto__']; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }, B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + if (B.WebKit && !document.evaluate) { + // Safari <3.0 needs self.innerWidth/Height + dimensions[d] = self['inner' + D]; + } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) { + // Opera <9.5 needs document.body.clientWidth/Height + dimensions[d] = document.body['client' + D] + } else { + dimensions[d] = document.documentElement['client' + D]; + } + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } + + }, + + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(e)) + return false; + + return true; + }, + + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (!Selector._div) Selector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + var e = this.expression, results; + + switch (this.mode) { + case 'selectorsAPI': + // querySelectorAll queries document-wide, then filters to descendants + // of the context element. That's not what we want. + // Add an explicit context to the selector if necessary. + if (root !== document) { + var oldId = root.id, id = $(root).identify(); + e = "#" + id + " " + e; + } + + results = $A(root.querySelectorAll(e)).map(Element.extend); + root.id = oldId; + + return results; + case 'xpath': + return document._getElementsByXPath(this.xpath, root); + default: + return this.matcher(root); + } + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector:" + this.expression.inspect() + ">"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: +/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, + attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._countedByPrototype) { + n._countedByPrototype = Prototype.emptyFunction; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Selector.split(expressions.join(',')); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + Object.extend(Selector.handlers, { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }, + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML. + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } + }); +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, value) { + if (Object.isUndefined(value)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + event = Event.extend(event); + + var node = event.target, + type = event.type, + currentTarget = event.currentTarget; + + if (currentTarget && currentTarget.tagName) { + // Firefox screws up the "click" event when moving between radio buttons + // via arrow keys. It also screws up the "load" and "error" events on images, + // reporting the document as the target instead of the original image. + if (type === 'load' || type === 'error' || + (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' + && currentTarget.type === 'radio')) + node = currentTarget; + } + if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; + return Element.extend(node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + var docElement = document.documentElement, + body = document.body || { scrollLeft: 0, scrollTop: 0 }; + return { + x: event.pageX || (event.clientX + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)), + y: event.pageY || (event.clientY + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__']; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._prototypeEventID) return element._prototypeEventID[0]; + arguments.callee.id = arguments.callee.id || 1; + return element._prototypeEventID = [++arguments.callee.id]; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event); + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + + // Internet Explorer needs to remove event handlers on page unload + // in order to avoid memory leaks. + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + // Safari has a dummy event handler on page unload so that it won't + // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" + // object when page is returned to via the back button using its bfcache. + if (Prototype.Browser.WebKit) { + window.addEventListener('unload', Prototype.emptyFunction, false); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + var event; + if (document.createEvent) { + event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize(), + loaded: false +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer; + + function fireContentLoadedEvent() { + if (document.loaded) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + document.loaded = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>"); + $("__onDOMContentLoaded").onreadystatechange = function() { + if (this.readyState == "complete") { + this.onreadystatechange = null; + fireContentLoadedEvent(); + } + }; + } +})(); +/*------------------------------- DEPRECATED -------------------------------*/ + +Hash.toQueryString = Object.toQueryString; + +var Toggle = { display: Element.toggle }; + +Element.Methods.childOf = Element.Methods.descendantOf; + +var Insertion = { + Before: function(element, content) { + return Element.insert(element, {before:content}); + }, + + Top: function(element, content) { + return Element.insert(element, {top:content}); + }, + + Bottom: function(element, content) { + return Element.insert(element, {bottom:content}); + }, + + After: function(element, content) { + return Element.insert(element, {after:content}); + } +}; + +var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); + +// This should be moved to script.aculo.us; notice the deprecated methods +// further below, that map to the newer Element methods. +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = Element.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = Element.cumulativeScrollOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = Element.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + // Deprecation layer -- use newer Element methods now (1.5.2). + + cumulativeOffset: Element.Methods.cumulativeOffset, + + positionedOffset: Element.Methods.positionedOffset, + + absolutize: function(element) { + Position.prepare(); + return Element.absolutize(element); + }, + + relativize: function(element) { + Position.prepare(); + return Element.relativize(element); + }, + + realOffset: Element.Methods.cumulativeScrollOffset, + + offsetParent: Element.Methods.getOffsetParent, + + page: Element.Methods.viewportOffset, + + clone: function(source, target, options) { + options = options || { }; + return Element.clonePosition(target, source, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ + function iter(name) { + return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; + } + + instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? + function(element, className) { + className = className.toString().strip(); + var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); + return cond ? document._getElementsByXPath('.//*' + cond, element) : []; + } : function(element, className) { + className = className.toString().strip(); + var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); + if (!classNames && !className) return elements; + + var nodes = $(element).getElementsByTagName('*'); + className = ' ' + className + ' '; + + for (var i = 0, child, cn; child = nodes[i]; i++) { + if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || + (classNames && classNames.all(function(name) { + return !name.toString().blank() && cn.include(' ' + name + ' '); + })))) + elements.push(Element.extend(child)); + } + return elements; + }; + + return function(className, parentElement) { + return $(parentElement || document.body).getElementsByClassName(className); + }; +}(Element.Methods); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); + +/*--------------------------------------------------------------------------*/ + +Element.addMethods();
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt new file mode 100644 index 0000000000..085187fa58 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-Agent: * +# Disallow: / diff --git a/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory b/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory diff --git a/railties/lib/rails/generators/rails/app/templates/script/about.tt b/railties/lib/rails/generators/rails/app/templates/script/about.tt new file mode 100755 index 0000000000..1220676c08 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/about.tt @@ -0,0 +1,4 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +$LOAD_PATH.unshift "#{RAILTIES_PATH}/builtin/rails_info" +require 'rails/commands/about' diff --git a/railties/lib/rails/generators/rails/app/templates/script/console.tt b/railties/lib/rails/generators/rails/app/templates/script/console.tt new file mode 100755 index 0000000000..5aec193853 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/console.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/console' diff --git a/railties/lib/rails/generators/rails/app/templates/script/dbconsole.tt b/railties/lib/rails/generators/rails/app/templates/script/dbconsole.tt new file mode 100755 index 0000000000..632563f470 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/dbconsole.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/dbconsole' diff --git a/railties/lib/rails/generators/rails/app/templates/script/destroy.tt b/railties/lib/rails/generators/rails/app/templates/script/destroy.tt new file mode 100755 index 0000000000..c5f94862c3 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/destroy.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/destroy' diff --git a/railties/lib/rails/generators/rails/app/templates/script/generate.tt b/railties/lib/rails/generators/rails/app/templates/script/generate.tt new file mode 100755 index 0000000000..d466c94767 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/generate.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/generate' diff --git a/railties/lib/rails/generators/rails/app/templates/script/performance/benchmarker.tt b/railties/lib/rails/generators/rails/app/templates/script/performance/benchmarker.tt new file mode 100755 index 0000000000..3e03aaa767 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/performance/benchmarker.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../../config/boot', __FILE__) +require 'rails/commands/performance/benchmarker' diff --git a/railties/lib/rails/generators/rails/app/templates/script/performance/profiler.tt b/railties/lib/rails/generators/rails/app/templates/script/performance/profiler.tt new file mode 100755 index 0000000000..deada2f561 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/performance/profiler.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../../config/boot', __FILE__) +require 'rails/commands/performance/profiler' diff --git a/railties/lib/rails/generators/rails/app/templates/script/plugin.tt b/railties/lib/rails/generators/rails/app/templates/script/plugin.tt new file mode 100755 index 0000000000..f4081f56b6 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/plugin.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/plugin' diff --git a/railties/lib/rails/generators/rails/app/templates/script/runner.tt b/railties/lib/rails/generators/rails/app/templates/script/runner.tt new file mode 100755 index 0000000000..60c8c0bba1 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/runner.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/runner' diff --git a/railties/lib/rails/generators/rails/app/templates/script/server.tt b/railties/lib/rails/generators/rails/app/templates/script/server.tt new file mode 100755 index 0000000000..54e3346bec --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/script/server.tt @@ -0,0 +1,3 @@ +<%= shebang %> +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands/server' diff --git a/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory diff --git a/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory diff --git a/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory diff --git a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb new file mode 100644 index 0000000000..4b60558b43 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' +require 'performance_test_help' + +# Profiling results for each test method are written to tmp/performance. +class BrowsingTest < ActionController::PerformanceTest + def test_homepage + get '/' + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb new file mode 100644 index 0000000000..a16f587d8b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -0,0 +1,38 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require 'rails/test_help' + +class ActiveSupport::TestCase + # Transactional fixtures accelerate your tests by wrapping each test method + # in a transaction that's rolled back on completion. This ensures that the + # test database remains unchanged so your fixtures don't have to be reloaded + # between every test method. Fewer database queries means faster tests. + # + # Read Mike Clark's excellent walkthrough at + # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting + # + # Every Active Record database supports transactions except MyISAM tables + # in MySQL. Turn off transactional fixtures in this case; however, if you + # don't care one way or the other, switching from MyISAM to InnoDB tables + # is recommended. + # + # The only drawback to using transactional fixtures is when you actually + # need to test transactions. Since your test is bracketed by a transaction, + # any transactions started in your code will be automatically rolled back. + self.use_transactional_fixtures = true + + # Instantiated fixtures are slow, but give you @david where otherwise you + # would need people(:david). If you don't want to migrate your existing + # test cases which use the @david style and don't mind the speed hit (each + # instantiated fixtures translates to a database query per test method), + # then set this back to true. + self.use_instantiated_fixtures = false + + # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. + # + # Note: You'll currently still have to declare fixtures explicitly in integration tests + # -- they do not yet inherit this setting + fixtures :all + + # Add more helper methods to be used by all tests here... +end diff --git a/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory diff --git a/railties/lib/rails/generators/rails/controller/USAGE b/railties/lib/rails/generators/rails/controller/USAGE new file mode 100644 index 0000000000..6ed4b2edfc --- /dev/null +++ b/railties/lib/rails/generators/rails/controller/USAGE @@ -0,0 +1,18 @@ +Description: + Stubs out a new controller and its views. Pass the controller name, either + CamelCased or under_scored, and a list of views as arguments. + + To create a controller within a module, specify the controller name as a + path like 'parent_module/controller_name'. + + This generates a controller class in app/controllers and invokes helper, + template engine and test framework generators. + +Example: + `./script/generate controller CreditCard open debit credit close` + + Credit card controller with URLs like /credit_card/debit. + Controller: app/controllers/credit_card_controller.rb + Functional Test: test/functional/credit_card_controller_test.rb + Views: app/views/credit_card/debit.html.erb [...] + Helper: app/helpers/credit_card_helper.rb diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb new file mode 100644 index 0000000000..91470be833 --- /dev/null +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -0,0 +1,14 @@ +module Rails + module Generators + class ControllerGenerator < NamedBase + argument :actions, :type => :array, :default => [], :banner => "action action" + check_class_collision :suffix => "Controller" + + def create_controller_files + template 'controller.rb', File.join('app/controllers', class_path, "#{file_name}_controller.rb") + end + + hook_for :template_engine, :test_framework, :helper + end + end +end diff --git a/railties/lib/rails/generators/rails/controller/templates/controller.rb b/railties/lib/rails/generators/rails/controller/templates/controller.rb new file mode 100644 index 0000000000..cda2659e69 --- /dev/null +++ b/railties/lib/rails/generators/rails/controller/templates/controller.rb @@ -0,0 +1,7 @@ +class <%= class_name %>Controller < ApplicationController +<% for action in actions -%> + def <%= action %> + end + +<% end -%> +end diff --git a/railties/lib/rails/generators/rails/generator/USAGE b/railties/lib/rails/generators/rails/generator/USAGE new file mode 100644 index 0000000000..ca7d3f62d0 --- /dev/null +++ b/railties/lib/rails/generators/rails/generator/USAGE @@ -0,0 +1,11 @@ +Description: + Stubs out a new generator at lib/generators. Pass the generator name, either + CamelCased or under_scored, as an argument. + +Example: + `./script/generate generator Awesome` + + creates a standard awesome generator: + lib/generators/awesome/ + lib/generators/awesome/awesome_generator.rb + lib/generators/awesome/templates/ diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb new file mode 100644 index 0000000000..5b5d1884bc --- /dev/null +++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb @@ -0,0 +1,25 @@ +module Rails + module Generators + class GeneratorGenerator < NamedBase + check_class_collision :suffix => "Generator" + + class_option :namespace, :type => :boolean, :default => true, + :desc => "Namespace generator under lib/generators/name" + + def create_generator_files + directory '.', generator_dir + end + + protected + + def generator_dir + if options[:namespace] + File.join("lib", "generators", file_name) + else + File.join("lib", "generators") + end + end + + end + end +end diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt new file mode 100644 index 0000000000..675f00043f --- /dev/null +++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt @@ -0,0 +1,5 @@ +class <%= class_name %>Generator < Rails::Generators::NamedBase + def self.source_root + @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates')) + end +end diff --git a/railties/lib/rails/generators/rails/generator/templates/USAGE.tt b/railties/lib/rails/generators/rails/generator/templates/USAGE.tt new file mode 100644 index 0000000000..ea9f4f12cc --- /dev/null +++ b/railties/lib/rails/generators/rails/generator/templates/USAGE.tt @@ -0,0 +1,8 @@ +Description: + Explain the generator + +Example: + ./script/generate <%= file_name %> Thing + + This will create: + what/will/it/create diff --git a/railties/lib/rails/generators/rails/generator/templates/templates/.empty_directory b/railties/lib/rails/generators/rails/generator/templates/templates/.empty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/generator/templates/templates/.empty_directory diff --git a/railties/lib/rails/generators/rails/helper/USAGE b/railties/lib/rails/generators/rails/helper/USAGE new file mode 100644 index 0000000000..531c9b390a --- /dev/null +++ b/railties/lib/rails/generators/rails/helper/USAGE @@ -0,0 +1,17 @@ +Description: + Stubs out a new helper. Pass the helper name, either CamelCased + or under_scored. + + To create a helper within a module, specify the helper name as a + path like 'parent_module/helper_name'. + + This generates a helper class in app/helpers and invokes the configured + test framework. + +Example: + `./script/generate helper CreditCard` + + Credit card helper. + Helper: app/helpers/credit_card_helper.rb + Test: test/unit/helpers/credit_card_helper_test.rb + diff --git a/railties/lib/rails/generators/rails/helper/helper_generator.rb b/railties/lib/rails/generators/rails/helper/helper_generator.rb new file mode 100644 index 0000000000..ad66388591 --- /dev/null +++ b/railties/lib/rails/generators/rails/helper/helper_generator.rb @@ -0,0 +1,13 @@ +module Rails + module Generators + class HelperGenerator < NamedBase + check_class_collision :suffix => "Helper" + + def create_helper_files + template 'helper.rb', File.join('app/helpers', class_path, "#{file_name}_helper.rb") + end + + hook_for :test_framework + end + end +end diff --git a/railties/lib/rails/generators/rails/helper/templates/helper.rb b/railties/lib/rails/generators/rails/helper/templates/helper.rb new file mode 100644 index 0000000000..3fe2ecdc74 --- /dev/null +++ b/railties/lib/rails/generators/rails/helper/templates/helper.rb @@ -0,0 +1,2 @@ +module <%= class_name %>Helper +end diff --git a/railties/lib/rails/generators/rails/integration_test/USAGE b/railties/lib/rails/generators/rails/integration_test/USAGE new file mode 100644 index 0000000000..b76c35a702 --- /dev/null +++ b/railties/lib/rails/generators/rails/integration_test/USAGE @@ -0,0 +1,10 @@ +Description: + Stubs out a new integration test. Pass the name of the test, either + CamelCased or under_scored, as an argument. + + This generator invokes the current integration tool, which defaults to + TestUnit. + +Example: + `./script/generate integration_test GeneralStories` creates a GeneralStories + integration test in test/integration/general_stories_test.rb diff --git a/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb b/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb new file mode 100644 index 0000000000..363a327fcb --- /dev/null +++ b/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb @@ -0,0 +1,7 @@ +module Rails + module Generators + class IntegrationTestGenerator < NamedBase + hook_for :integration_tool, :as => :integration + end + end +end diff --git a/railties/lib/rails/generators/rails/mailer/USAGE b/railties/lib/rails/generators/rails/mailer/USAGE new file mode 100644 index 0000000000..c56095b2c8 --- /dev/null +++ b/railties/lib/rails/generators/rails/mailer/USAGE @@ -0,0 +1,15 @@ +Description: + Stubs out a new mailer and its views. Pass the mailer name, either + CamelCased or under_scored, and an optional list of emails as arguments. + + This generates a mailer class in app/models and invokes your template + engine and test framework generators. + +Example: + `./script/generate mailer Notifications signup forgot_password invoice` + + creates a Notifications mailer class, views, test, and fixtures: + Mailer: app/models/notifications.rb + Views: app/views/notifications/signup.erb [...] + Test: test/unit/test/unit/notifications_test.rb + Fixtures: test/fixtures/notifications/signup [...] diff --git a/railties/lib/rails/generators/rails/mailer/mailer_generator.rb b/railties/lib/rails/generators/rails/mailer/mailer_generator.rb new file mode 100644 index 0000000000..33f1665b83 --- /dev/null +++ b/railties/lib/rails/generators/rails/mailer/mailer_generator.rb @@ -0,0 +1,14 @@ +module Rails + module Generators + class MailerGenerator < NamedBase + argument :actions, :type => :array, :default => [], :banner => "method method" + check_class_collision + + def create_mailer_file + template "mailer.rb", File.join('app/models', class_path, "#{file_name}.rb") + end + + hook_for :template_engine, :test_framework + end + end +end diff --git a/railties/lib/rails/generators/rails/mailer/templates/mailer.rb b/railties/lib/rails/generators/rails/mailer/templates/mailer.rb new file mode 100644 index 0000000000..90e0b712d6 --- /dev/null +++ b/railties/lib/rails/generators/rails/mailer/templates/mailer.rb @@ -0,0 +1,14 @@ +class <%= class_name %> < ActionMailer::Base +<% for action in actions -%> + + def <%= action %>(sent_at = Time.now) + subject '<%= class_name %>#<%= action %>' + recipients '' + from '' + sent_on sent_at + + body :greeting => 'Hi,' + end + +<% end -%> +end diff --git a/railties/lib/rails/generators/rails/metal/USAGE b/railties/lib/rails/generators/rails/metal/USAGE new file mode 100644 index 0000000000..123ec6c03f --- /dev/null +++ b/railties/lib/rails/generators/rails/metal/USAGE @@ -0,0 +1,8 @@ +Description: + Cast some metal! + +Examples: + `./script/generate metal poller` + + This will create: + Metal: app/metal/poller.rb diff --git a/railties/lib/rails/generators/rails/metal/metal_generator.rb b/railties/lib/rails/generators/rails/metal/metal_generator.rb new file mode 100644 index 0000000000..fe4f945cad --- /dev/null +++ b/railties/lib/rails/generators/rails/metal/metal_generator.rb @@ -0,0 +1,11 @@ +module Rails + module Generators + class MetalGenerator < NamedBase + check_class_collision + + def create_metal_file + template "metal.rb", "app/metal/#{file_name}.rb" + end + end + end +end diff --git a/railties/lib/rails/generators/rails/metal/templates/metal.rb b/railties/lib/rails/generators/rails/metal/templates/metal.rb new file mode 100644 index 0000000000..e94982b69a --- /dev/null +++ b/railties/lib/rails/generators/rails/metal/templates/metal.rb @@ -0,0 +1,12 @@ +# Allow the metal piece to run in isolation +require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails) + +class <%= class_name %> + def self.call(env) + if env["PATH_INFO"] =~ /^\/<%= file_name %>/ + [200, {"Content-Type" => "text/html"}, ["Hello, World!"]] + else + [404, {"Content-Type" => "text/html"}, ["Not Found"]] + end + end +end diff --git a/railties/lib/rails/generators/rails/migration/USAGE b/railties/lib/rails/generators/rails/migration/USAGE new file mode 100644 index 0000000000..d91127aac3 --- /dev/null +++ b/railties/lib/rails/generators/rails/migration/USAGE @@ -0,0 +1,29 @@ +Description: + Stubs out a new database migration. Pass the migration name, either + CamelCased or under_scored, and an optional list of attribute pairs as arguments. + + A migration class is generated in db/migrate prefixed by a timestamp of the current date and time. + + You can name your migration in either of these formats to generate add/remove + column lines from supplied attributes: AddColumnsToTable or RemoveColumnsFromTable + +Example: + `./script/generate migration AddSslFlag` + + If the current date is May 14, 2008 and the current time 09:09:12, this creates the AddSslFlag migration + db/migrate/20080514090912_add_ssl_flag.rb + + `./script/generate migration AddTitleBodyToPost title:string body:text published:boolean` + + This will create the AddTitleBodyToPost in db/migrate/20080514090912_add_title_body_to_post.rb with + this in the Up migration: + + add_column :posts, :title, :string + add_column :posts, :body, :text + add_column :posts, :published, :boolean + + And this in the Down migration: + + remove_column :posts, :published + remove_column :posts, :body + remove_column :posts, :title diff --git a/railties/lib/rails/generators/rails/migration/migration_generator.rb b/railties/lib/rails/generators/rails/migration/migration_generator.rb new file mode 100644 index 0000000000..39fa5b63b1 --- /dev/null +++ b/railties/lib/rails/generators/rails/migration/migration_generator.rb @@ -0,0 +1,8 @@ +module Rails + module Generators + class MigrationGenerator < NamedBase #metagenerator + argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + hook_for :orm, :required => true + end + end +end diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE new file mode 100644 index 0000000000..b056d5df8b --- /dev/null +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -0,0 +1,30 @@ +Description: + Stubs out a new model. Pass the model name, either CamelCased or + under_scored, and an optional list of attribute pairs as arguments. + + Attribute pairs are field:type arguments specifying the + model's attributes. Timestamps are added by default, so you don't have to + specify them by hand as 'created_at:datetime updated_at:datetime'. + + You don't have to think up every attribute up front, but it helps to + sketch out a few so you can start working with the model immediately. + + This generator invokes your configured ORM and test framework, which + defaults to ActiveRecord and TestUnit. + + Finally, if --parent option is given, it's used as superclass of the + created model. This allows you create Single Table Inheritance models. + +Examples: + `./script/generate model account` + + For ActiveRecord and TestUnit it creates: + + Model: app/models/account.rb + Test: test/unit/account_test.rb + Fixtures: test/fixtures/accounts.yml + Migration: db/migrate/XXX_add_accounts.rb + + `./script/generate model post title:string body:text published:boolean` + + Creates a Post model with a string title, text body, and published flag. diff --git a/railties/lib/rails/generators/rails/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb new file mode 100644 index 0000000000..629d5eed3f --- /dev/null +++ b/railties/lib/rails/generators/rails/model/model_generator.rb @@ -0,0 +1,8 @@ +module Rails + module Generators + class ModelGenerator < NamedBase #metagenerator + argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + hook_for :orm, :required => true + end + end +end diff --git a/railties/lib/rails/generators/rails/model_subclass/model_subclass_generator.rb b/railties/lib/rails/generators/rails/model_subclass/model_subclass_generator.rb new file mode 100644 index 0000000000..4649709780 --- /dev/null +++ b/railties/lib/rails/generators/rails/model_subclass/model_subclass_generator.rb @@ -0,0 +1,11 @@ +module Rails + module Generators + class ModelSubclassGenerator < Base + desc "model_subclass is deprecated. Invoke model with --parent option instead." + + def say_deprecation_warn + say self.class.desc + end + end + end +end diff --git a/railties/lib/rails/generators/rails/observer/USAGE b/railties/lib/rails/generators/rails/observer/USAGE new file mode 100644 index 0000000000..9a20f55a89 --- /dev/null +++ b/railties/lib/rails/generators/rails/observer/USAGE @@ -0,0 +1,12 @@ +Description: + Stubs out a new observer. Pass the observer name, either CamelCased or + under_scored, as an argument. + + This generator only invokes your ORM and test framework generators. + +Example: + `./script/generate observer Account` + + For ActiveRecord and TestUnit it creates: + Observer: app/models/account_observer.rb + TestUnit: test/unit/account_observer_test.rb diff --git a/railties/lib/rails/generators/rails/observer/observer_generator.rb b/railties/lib/rails/generators/rails/observer/observer_generator.rb new file mode 100644 index 0000000000..f5cedee91f --- /dev/null +++ b/railties/lib/rails/generators/rails/observer/observer_generator.rb @@ -0,0 +1,7 @@ +module Rails + module Generators + class ObserverGenerator < NamedBase #metagenerator + hook_for :orm, :required => true + end + end +end diff --git a/railties/lib/rails/generators/rails/performance_test/USAGE b/railties/lib/rails/generators/rails/performance_test/USAGE new file mode 100644 index 0000000000..ee82578cdb --- /dev/null +++ b/railties/lib/rails/generators/rails/performance_test/USAGE @@ -0,0 +1,10 @@ +Description: + Stubs out a new performance test. Pass the name of the test, either + CamelCased or under_scored, as an argument. + + This generator invokes the current performance tool, which defaults to + TestUnit. + +Example: + `./script/generate performance_test GeneralStories` creates a GeneralStories + performance test in test/performance/general_stories_test.rb diff --git a/railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb b/railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb new file mode 100644 index 0000000000..d1c71ab8ed --- /dev/null +++ b/railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb @@ -0,0 +1,7 @@ +module Rails + module Generators + class PerformanceTestGenerator < NamedBase + hook_for :performance_tool, :as => :performance + end + end +end diff --git a/railties/lib/rails/generators/rails/plugin/USAGE b/railties/lib/rails/generators/rails/plugin/USAGE new file mode 100644 index 0000000000..8a17fa4dec --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/USAGE @@ -0,0 +1,13 @@ +Description: + Stubs out a new plugin at vendor/plugins. Pass the plugin name, either + CamelCased or under_scored, as an argument. + +Example: + `./script/generate plugin BrowserFilters` + + creates a standard browser_filters plugin: + vendor/plugins/browser_filters/README + vendor/plugins/browser_filters/init.rb + vendor/plugins/browser_filters/install.rb + vendor/plugins/browser_filters/lib/browser_filters.rb + vendor/plugins/browser_filters/test/browser_filters_test.rb diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb new file mode 100644 index 0000000000..bc614bc5d3 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -0,0 +1,47 @@ +require 'rails/generators/rails/generator/generator_generator' + +module Rails + module Generators + class PluginGenerator < NamedBase + class_option :tasks, :desc => "When supplied creates tasks base files." + + check_class_collision + + def create_root_files + directory '.', plugin_dir, :recursive => false + end + + def create_lib_files + directory 'lib', plugin_dir('lib'), :recursive => false + end + + def create_tasks_files + return unless options[:tasks] + directory 'tasks', plugin_dir('tasks') + end + + hook_for :generator do |instance, generator| + instance.inside instance.send(:plugin_dir), :verbose => true do + instance.invoke generator, [ instance.name ], :namespace => false + end + end + + hook_for :test_framework do |instance, test_framework| + instance.inside instance.send(:plugin_dir), :verbose => true do + instance.invoke test_framework + end + end + + protected + + def plugin_dir(join=nil) + if join + File.join(plugin_dir, join) + else + "vendor/plugins/#{file_name}" + end + end + + end + end +end diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE new file mode 100644 index 0000000000..8717df053d --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) <%= Date.today.year %> [name of plugin creator] + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/railties/lib/rails/generators/rails/plugin/templates/README b/railties/lib/rails/generators/rails/plugin/templates/README new file mode 100644 index 0000000000..702db07cb1 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/README @@ -0,0 +1,13 @@ +<%= class_name %> +<%= "=" * class_name.size %> + +Introduction goes here. + + +Example +======= + +Example goes here. + + +Copyright (c) <%= Date.today.year %> [name of plugin creator], released under the MIT license diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile new file mode 100644 index 0000000000..85e8ff1834 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -0,0 +1,23 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the <%= file_name %> plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the <%= file_name %> plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = '<%= class_name %>' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/railties/lib/rails/generators/rails/plugin/templates/init.rb b/railties/lib/rails/generators/rails/plugin/templates/init.rb new file mode 100644 index 0000000000..3c19a743c9 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/init.rb @@ -0,0 +1 @@ +# Include hook code here diff --git a/railties/lib/rails/generators/rails/plugin/templates/install.rb b/railties/lib/rails/generators/rails/plugin/templates/install.rb new file mode 100644 index 0000000000..f7732d3796 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/install.rb @@ -0,0 +1 @@ +# Install hook code here diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%file_name%.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%file_name%.rb.tt new file mode 100644 index 0000000000..d8d908a959 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%file_name%.rb.tt @@ -0,0 +1 @@ +# <%= class_name %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/tasks/%file_name%_tasks.rake.tt b/railties/lib/rails/generators/rails/plugin/templates/tasks/%file_name%_tasks.rake.tt new file mode 100644 index 0000000000..72920a9d3a --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/tasks/%file_name%_tasks.rake.tt @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :<%= file_name %> do +# # Task goes here +# end diff --git a/railties/lib/rails/generators/rails/plugin/templates/uninstall.rb b/railties/lib/rails/generators/rails/plugin/templates/uninstall.rb new file mode 100644 index 0000000000..9738333463 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/uninstall.rb @@ -0,0 +1 @@ +# Uninstall hook code here diff --git a/railties/lib/rails/generators/rails/resource/USAGE b/railties/lib/rails/generators/rails/resource/USAGE new file mode 100644 index 0000000000..936619b0db --- /dev/null +++ b/railties/lib/rails/generators/rails/resource/USAGE @@ -0,0 +1,23 @@ +Description: + Stubs out a new resource including an empty model and controller suitable + for a restful, resource-oriented application. Pass the singular model name, + either CamelCased or under_scored, as the first argument, and an optional + list of attribute pairs. + + Attribute pairs are field:type arguments specifying the + model's attributes. Timestamps are added by default, so you don't have to + specify them by hand as 'created_at:datetime updated_at:datetime'. + + You don't have to think up every attribute up front, but it helps to + sketch out a few so you can start working with the model immediately. + + This generator invokes your configured ORM and test framework, besides + creating helpers and add routes to config/routes.rb. + + Unlike the scaffold generator, the resource generator does not create + views or add any methods to the generated controller. + +Examples: + `./script/generate resource post` # no attributes + `./script/generate resource post title:string body:text published:boolean` + `./script/generate resource purchase order_id:integer amount:decimal` diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb new file mode 100644 index 0000000000..e49f9aea1b --- /dev/null +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -0,0 +1,34 @@ +require 'rails/generators/rails/model/model_generator' +require 'rails/generators/resource_helpers' + +module Rails + module Generators + class ResourceGenerator < ModelGenerator #metagenerator + include ResourceHelpers + + hook_for :resource_controller, :required => true do |base, controller| + base.invoke controller, [ base.controller_name, base.options[:actions] ] + end + + class_option :actions, :type => :array, :banner => "ACTION ACTION", :default => [], + :desc => "Actions for the resource controller" + + class_option :singleton, :type => :boolean, :desc => "Supply to create a singleton controller" + + def add_resource_route + route "map.resource#{:s unless options[:singleton]} :#{pluralize?(file_name)}" + end + + protected + + def pluralize?(name) + if options[:singleton] + name + else + name.pluralize + end + end + + end + end +end diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE new file mode 100644 index 0000000000..71edd2f469 --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -0,0 +1,29 @@ +Description: + Scaffolds an entire resource, from model and migration to controller and + views, along with a full test suite. The resource is ready to use as a + starting point for your RESTful, resource-oriented application. + + Pass the name of the model (in singular form), either CamelCased or + under_scored, as the first argument, and an optional list of attribute + pairs. + + Attribute pairs are field:type arguments specifying the + model's attributes. Timestamps are added by default, so you don't have to + specify them by hand as 'created_at:datetime updated_at:datetime'. + + You don't have to think up every attribute up front, but it helps to + sketch out a few so you can start working with the resource immediately. + + For example, 'scaffold post title:string body:text published:boolean' + gives you a model with those three attributes, a controller that handles + the create/show/update/destroy, forms to create and edit your posts, and + an index that lists them all, as well as a map.resources :posts + declaration in config/routes.rb. + + If you want to remove all the generated files, run + 'script/destroy scaffold ModelName'. + +Examples: + `./script/generate scaffold post` + `./script/generate scaffold post title:string body:text published:boolean` + `./script/generate scaffold purchase order_id:integer amount:decimal` diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb new file mode 100644 index 0000000000..779f933785 --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -0,0 +1,13 @@ +require 'rails/generators/rails/resource/resource_generator' + +module Rails + module Generators + class ScaffoldGenerator < ResourceGenerator #metagenerator + remove_hook_for :resource_controller + remove_class_option :actions + + hook_for :scaffold_controller, :required => true + hook_for :stylesheets + end + end +end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/USAGE b/railties/lib/rails/generators/rails/scaffold_controller/USAGE new file mode 100644 index 0000000000..d60a3c3680 --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold_controller/USAGE @@ -0,0 +1,20 @@ +Description: + Stubs out a scaffolded controller and its views. Pass the model name, + either CamelCased or under_scored, and a list of views as arguments. + The controller name is retrieved as a pluralized version of the model + name. + + To create a controller within a module, specify the model name as a + path like 'parent_module/controller_name'. + + This generates a controller class in app/controllers and invokes helper, + template engine and test framework generators. + +Example: + `./script/generate scaffold_controller CreditCard` + + Credit card controller with URLs like /credit_card/debit. + Controller: app/controllers/credit_cards_controller.rb + Functional Test: test/functional/credit_cards_controller_test.rb + Views: app/views/credit_cards/index.html.erb [...] + Helper: app/helpers/credit_cards_helper.rb diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb new file mode 100644 index 0000000000..e544e29892 --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -0,0 +1,27 @@ +require 'rails/generators/resource_helpers' + +module Rails + module Generators + class ScaffoldControllerGenerator < NamedBase + include ResourceHelpers + + check_class_collision :suffix => "Controller" + + class_option :orm, :banner => "NAME", :type => :string, :required => true, + :desc => "ORM to generate the controller for" + + class_option :singleton, :type => :boolean, :desc => "Supply to create a singleton controller" + + def create_controller_files + template 'controller.rb', File.join('app/controllers', class_path, "#{controller_file_name}_controller.rb") + end + + hook_for :template_engine, :test_framework, :as => :scaffold + + # Invoke the helper using the controller (pluralized) name. + hook_for :helper, :as => :scaffold do |base, invoked| + base.invoke invoked, [ base.controller_name ] + end + end + end +end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb new file mode 100644 index 0000000000..3cc8bbf8e7 --- /dev/null +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -0,0 +1,87 @@ +class <%= controller_class_name %>Controller < ApplicationController +<% unless options[:singleton] -%> + # GET /<%= table_name %> + # GET /<%= table_name %>.xml + def index + @<%= table_name %> = <%= orm_class.all(class_name) %> + + respond_to do |format| + format.html # index.html.erb + format.xml { render :xml => @<%= table_name %> } + end + end +<% end -%> + + # GET /<%= table_name %>/1 + # GET /<%= table_name %>/1.xml + def show + @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + + respond_to do |format| + format.html # show.html.erb + format.xml { render :xml => @<%= file_name %> } + end + end + + # GET /<%= table_name %>/new + # GET /<%= table_name %>/new.xml + def new + @<%= file_name %> = <%= orm_class.build(class_name) %> + + respond_to do |format| + format.html # new.html.erb + format.xml { render :xml => @<%= file_name %> } + end + end + + # GET /<%= table_name %>/1/edit + def edit + @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + end + + # POST /<%= table_name %> + # POST /<%= table_name %>.xml + def create + @<%= file_name %> = <%= orm_class.build(class_name, "params[:#{file_name}]") %> + + respond_to do |format| + if @<%= orm_instance.save %> + flash[:notice] = '<%= class_name %> was successfully created.' + format.html { redirect_to(@<%= file_name %>) } + format.xml { render :xml => @<%= file_name %>, :status => :created, :location => @<%= file_name %> } + else + format.html { render :action => "new" } + format.xml { render :xml => @<%= orm_instance.errors %>, :status => :unprocessable_entity } + end + end + end + + # PUT /<%= table_name %>/1 + # PUT /<%= table_name %>/1.xml + def update + @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + + respond_to do |format| + if @<%= orm_instance.update_attributes("params[:#{file_name}]") %> + flash[:notice] = '<%= class_name %> was successfully updated.' + format.html { redirect_to(@<%= file_name %>) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @<%= orm_instance.errors %>, :status => :unprocessable_entity } + end + end + end + + # DELETE /<%= table_name %>/1 + # DELETE /<%= table_name %>/1.xml + def destroy + @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %> + @<%= orm_instance.destroy %> + + respond_to do |format| + format.html { redirect_to(<%= table_name %>_url) } + format.xml { head :ok } + end + end +end diff --git a/railties/lib/rails/generators/rails/session_migration/USAGE b/railties/lib/rails/generators/rails/session_migration/USAGE new file mode 100644 index 0000000000..e106f6ecc8 --- /dev/null +++ b/railties/lib/rails/generators/rails/session_migration/USAGE @@ -0,0 +1,8 @@ +Description: + Creates a migration to add the sessions table used by the ORM session store. + Pass the migration name, either CamelCased or under_scored, as an argument. + + Before invoking this generator, be sure that your ORM supports session stores. + +Example: + `./script/generate session_migration CreateSessionTable` diff --git a/railties/lib/rails/generators/rails/session_migration/session_migration_generator.rb b/railties/lib/rails/generators/rails/session_migration/session_migration_generator.rb new file mode 100644 index 0000000000..258cc5b4a0 --- /dev/null +++ b/railties/lib/rails/generators/rails/session_migration/session_migration_generator.rb @@ -0,0 +1,8 @@ +module Rails + module Generators + class SessionMigrationGenerator < NamedBase #metagenerator + argument :name, :type => :string, :default => "add_sessions_table" + hook_for :orm, :required => true + end + end +end diff --git a/railties/lib/rails/generators/rails/stylesheets/USAGE b/railties/lib/rails/generators/rails/stylesheets/USAGE new file mode 100644 index 0000000000..d6a81e51d0 --- /dev/null +++ b/railties/lib/rails/generators/rails/stylesheets/USAGE @@ -0,0 +1,5 @@ +Description: + Copies scaffold stylesheets to public/stylesheets/. + +Examples: + `./script/generate stylesheets` diff --git a/railties/lib/rails/generators/rails/stylesheets/stylesheets_generator.rb b/railties/lib/rails/generators/rails/stylesheets/stylesheets_generator.rb new file mode 100644 index 0000000000..ce68443c39 --- /dev/null +++ b/railties/lib/rails/generators/rails/stylesheets/stylesheets_generator.rb @@ -0,0 +1,9 @@ +module Rails + module Generators + class StylesheetsGenerator < Base + def copy_stylesheets_file + template "scaffold.css", "public/stylesheets/scaffold.css" if behavior == :invoke + end + end + end +end diff --git a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css b/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css new file mode 100644 index 0000000000..d9fa2cf2dc --- /dev/null +++ b/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css @@ -0,0 +1,62 @@ +body { background-color: #fff; color: #333; } + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { color: #000; } +a:visited { color: #666; } +a:hover { color: #fff; background-color:#000; } + +div.field, div.actions { + margin-bottom: 10px; +} + +.notice { + color: green; +} + +.fieldWithErrors { + padding: 2px; + background-color: red; + display: table; +} + +#errorExplanation { + width: 400px; + border: 2px solid red; + padding: 7px; + padding-bottom: 12px; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#errorExplanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + background-color: #c00; + color: #fff; +} + +#errorExplanation p { + color: #333; + margin-bottom: 0; + padding: 5px; +} + +#errorExplanation ul li { + font-size: 12px; + list-style: square; +} + diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb new file mode 100644 index 0000000000..d4b0d4b945 --- /dev/null +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -0,0 +1,74 @@ +module Rails + module Generators + # Deal with controller names on scaffold and add some helpers to deal with + # ActiveModel. + # + module ResourceHelpers + def self.included(base) #:nodoc: + base.send :attr_reader, :controller_name, :controller_class_name, :controller_file_name, + :controller_class_path, :controller_file_path + + base.send :class_option, :force_plural, :type => :boolean, :desc => "Forces the use of a plural ModelName" + end + + # Set controller variables on initialization. + # + def initialize(*args) #:nodoc: + super + + if name == name.pluralize && !options[:force_plural] + say "Plural version of the model detected, using singularized version. Override with --force-plural." + name.replace name.singularize + assign_names!(self.name) + end + + @controller_name = name.pluralize + + base_name, @controller_class_path, @controller_file_path, class_nesting, class_nesting_depth = extract_modules(@controller_name) + class_name_without_nesting, @controller_file_name, controller_plural_name = inflect_names(base_name) + + @controller_class_name = if class_nesting.empty? + class_name_without_nesting + else + "#{class_nesting}::#{class_name_without_nesting}" + end + end + + protected + + # Loads the ORM::Generators::ActiveModel class. This class is responsable + # to tell scaffold entities how to generate an specific method for the + # ORM. Check Rails::Generators::ActiveModel for more information. + # + def orm_class + @orm_class ||= begin + # Raise an error if the class_option :orm was not defined. + unless self.class.class_options[:orm] + raise "You need to have :orm as class option to invoke orm_class and orm_instance" + end + + active_model = "#{options[:orm].to_s.classify}::Generators::ActiveModel" + + # If the orm was not loaded, try to load it at "generators/orm", + # for example "generators/active_record" or "generators/sequel". + begin + klass = active_model.constantize + rescue NameError + require "rails/generators/#{options[:orm]}" + end + + # Try once again after loading the file with success. + klass ||= active_model.constantize + rescue Exception => e + raise Error, "Could not load #{active_model}, skipping controller. Error: #{e.message}." + end + end + + # Initialize ORM::Generators::ActiveModel to access instance methods. + # + def orm_instance(name=file_name) + @orm_instance ||= @orm_class.new(name) + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit.rb b/railties/lib/rails/generators/test_unit.rb new file mode 100644 index 0000000000..62b9afaa2c --- /dev/null +++ b/railties/lib/rails/generators/test_unit.rb @@ -0,0 +1,8 @@ +require 'rails/generators/named_base' + +module TestUnit + module Generators + class Base < Rails::Generators::NamedBase #:nodoc: + end + end +end diff --git a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb new file mode 100644 index 0000000000..39816d9990 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb @@ -0,0 +1,14 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class ControllerGenerator < Base + check_class_collision :suffix => "ControllerTest" + + def create_test_files + template 'functional_test.rb', + File.join('test/functional', class_path, "#{file_name}_controller_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb new file mode 100644 index 0000000000..62fa5d86fd --- /dev/null +++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class <%= class_name %>ControllerTest < ActionController::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb new file mode 100644 index 0000000000..4ea80bf7be --- /dev/null +++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb @@ -0,0 +1,13 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class HelperGenerator < Base + check_class_collision :suffix => "HelperTest" + + def create_helper_files + template 'helper_test.rb', File.join('test/unit/helpers', class_path, "#{file_name}_helper_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb b/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb new file mode 100644 index 0000000000..591e40900e --- /dev/null +++ b/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class <%= class_name %>HelperTest < ActionView::TestCase +end diff --git a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb new file mode 100644 index 0000000000..32d0fac029 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb @@ -0,0 +1,13 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class IntegrationGenerator < Base + check_class_collision :suffix => "Test" + + def create_test_files + template 'integration_test.rb', File.join('test/integration', class_path, "#{file_name}_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb new file mode 100644 index 0000000000..2c57158b1c --- /dev/null +++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' + +class <%= class_name %>Test < ActionController::IntegrationTest + fixtures :all + + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb new file mode 100644 index 0000000000..7353e5d61a --- /dev/null +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -0,0 +1,21 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class MailerGenerator < Base + argument :actions, :type => :array, :default => [], :banner => "method method" + check_class_collision :suffix => "Test" + + def create_test_files + template "unit_test.rb", File.join('test/unit', class_path, "#{file_name}_test.rb") + end + + def create_fixtures_files + actions.each do |action| + @action, @path = action, File.join(file_path, action) + template "fixture", File.join("test/fixtures", @path) + end + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/fixture b/railties/lib/rails/generators/test_unit/mailer/templates/fixture new file mode 100644 index 0000000000..fcce7bd805 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/mailer/templates/fixture @@ -0,0 +1,3 @@ +<%= class_name %>#<%= @action %> + +Find me in app/views/<%= @path %> diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/unit_test.rb new file mode 100644 index 0000000000..4de94076e9 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/mailer/templates/unit_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' + +class <%= class_name %>Test < ActionMailer::TestCase +<% for action in actions -%> + test "<%= action %>" do + @expected.subject = '<%= class_name %>#<%= action %>' + @expected.body = read_fixture('<%= action %>') + @expected.date = Time.now + + assert_equal @expected.encoded, <%= class_name %>.create_<%= action %>(@expected.date).encoded + end + +<% end -%> +<% if actions.blank? -%> + # replace this with your real tests + test "the truth" do + assert true + end +<% end -%> +end diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb new file mode 100644 index 0000000000..609b815683 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -0,0 +1,24 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class ModelGenerator < Base + argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + class_option :fixture, :type => :boolean + + check_class_collision :suffix => "Test" + + def create_test_file + template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb") + end + + hook_for :fixture_replacement + + def create_fixture_file + if options[:fixture] && options[:fixture_replacement].nil? + template 'fixtures.yml', File.join('test/fixtures', "#{table_name}.yml") + end + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml new file mode 100644 index 0000000000..c21035113e --- /dev/null +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -0,0 +1,19 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html + +<% unless attributes.empty? -%> +one: +<% for attribute in attributes -%> + <%= attribute.name %>: <%= attribute.default %> +<% end -%> + +two: +<% for attribute in attributes -%> + <%= attribute.name %>: <%= attribute.default %> +<% end -%> +<% else -%> +# one: +# column: value +# +# two: +# column: value +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb new file mode 100644 index 0000000000..3e0bc29d3a --- /dev/null +++ b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class <%= class_name %>Test < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/railties/lib/rails/generators/test_unit/observer/observer_generator.rb b/railties/lib/rails/generators/test_unit/observer/observer_generator.rb new file mode 100644 index 0000000000..6cc1158c21 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/observer/observer_generator.rb @@ -0,0 +1,13 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class ObserverGenerator < Base + check_class_collision :suffix => "ObserverTest" + + def create_test_files + template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_observer_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb new file mode 100644 index 0000000000..03f6d5666e --- /dev/null +++ b/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class <%= class_name %>ObserverTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/railties/lib/rails/generators/test_unit/performance/performance_generator.rb b/railties/lib/rails/generators/test_unit/performance/performance_generator.rb new file mode 100644 index 0000000000..99edda5461 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/performance/performance_generator.rb @@ -0,0 +1,13 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class PerformanceGenerator < Base + check_class_collision :suffix => "Test" + + def create_test_files + template 'performance_test.rb', File.join('test/performance', class_path, "#{file_name}_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb b/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb new file mode 100644 index 0000000000..27c91b0fca --- /dev/null +++ b/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' +require 'performance_test_help' + +class <%= class_name %>Test < ActionController::PerformanceTest + # Replace this with your real tests. + def test_homepage + get '/' + end +end diff --git a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb new file mode 100644 index 0000000000..4d65cd7d89 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb @@ -0,0 +1,13 @@ +require 'rails/generators/test_unit' + +module TestUnit + module Generators + class PluginGenerator < Base + check_class_collision :suffix => "Test" + + def create_test_files + directory '.', 'test' + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt b/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt new file mode 100644 index 0000000000..3e0bc29d3a --- /dev/null +++ b/railties/lib/rails/generators/test_unit/plugin/templates/%file_name%_test.rb.tt @@ -0,0 +1,8 @@ +require 'test_helper' + +class <%= class_name %>Test < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb b/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb new file mode 100644 index 0000000000..348ec33582 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb @@ -0,0 +1,5 @@ +require 'rubygems' +require 'test/unit' +require 'active_support' +require 'active_support/test_case' + diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb new file mode 100644 index 0000000000..c0315c7fe6 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -0,0 +1,18 @@ +require 'rails/generators/test_unit' +require 'rails/generators/resource_helpers' + +module TestUnit + module Generators + class ScaffoldGenerator < Base + include Rails::Generators::ResourceHelpers + + class_option :singleton, :type => :boolean, :desc => "Supply to create a singleton controller" + check_class_collision :suffix => "ControllerTest" + + def create_test_files + template 'functional_test.rb', + File.join('test/functional', controller_class_path, "#{controller_file_name}_controller_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb new file mode 100644 index 0000000000..e4bf4035da --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' + +class <%= controller_class_name %>ControllerTest < ActionController::TestCase +<% unless options[:singleton] -%> + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:<%= table_name %>) + end +<% end -%> + + test "should get new" do + get :new + assert_response :success + end + + test "should create <%= file_name %>" do + assert_difference('<%= class_name %>.count') do + post :create, :<%= file_name %> => { } + end + + assert_redirected_to <%= file_name %>_path(assigns(:<%= file_name %>)) + end + + test "should show <%= file_name %>" do + get :show, :id => <%= table_name %>(:one).to_param + assert_response :success + end + + test "should get edit" do + get :edit, :id => <%= table_name %>(:one).to_param + assert_response :success + end + + test "should update <%= file_name %>" do + put :update, :id => <%= table_name %>(:one).to_param, :<%= file_name %> => { } + assert_redirected_to <%= file_name %>_path(assigns(:<%= file_name %>)) + end + + test "should destroy <%= file_name %>" do + assert_difference('<%= class_name %>.count', -1) do + delete :destroy, :id => <%= table_name %>(:one).to_param + end + + assert_redirected_to <%= table_name %>_path + end +end diff --git a/railties/lib/rails/initializer.rb b/railties/lib/rails/initializer.rb new file mode 100644 index 0000000000..b91e07d5c4 --- /dev/null +++ b/railties/lib/rails/initializer.rb @@ -0,0 +1,578 @@ +require "pathname" + +require 'rails/railties_path' +require 'rails/version' +require 'rails/gem_dependency' +require 'rails/rack' +require 'rails/paths' +require 'rails/core' +require 'rails/configuration' + +RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defined?(RAILS_ENV) + +module Rails + # Sanity check to make sure this file is only loaded once + # TODO: Get to the point where this can be removed. + raise "It looks like initializer.rb was required twice" if defined?(Initializer) + + class Initializer + class Error < StandardError ; end + + class Base + class << self + def run(&blk) + define_method(:run, &blk) + end + + def config=(config) + @@config = config + end + + def config + @@config + end + alias configuration config + + def gems_dependencies_loaded + config.gems_dependencies_loaded + end + + def plugin_loader + @plugin_loader ||= configuration.plugin_loader.new(self) + end + end + + def gems_dependencies_loaded + self.class.gems_dependencies_loaded + end + + def plugin_loader + self.class.plugin_loader + end + end + + class Runner + + attr_reader :names, :initializers + attr_accessor :config + alias configuration config + + def initialize(parent = nil) + @names = parent ? parent.names.dup : {} + @initializers = parent ? parent.initializers.dup : [] + end + + def add(name, options = {}, &block) + # If :before or :after is specified, set the index to the right spot + if other = options[:before] || options[:after] + raise Error, "The #{other.inspect} initializer does not exist" unless @names[other] + index = @initializers.index(@names[other]) + index += 1 if options[:after] + end + + @initializers.insert(index || -1, block) + @names[name] = block + end + + def delete(name) + @names[name].tap do |initializer| + @initializers.delete(initializer) + @names.delete(name) + end + end + + def run_initializer(initializer) + init_block = initializer.is_a?(Proc) ? initializer : @names[initializer] + container = Class.new(Base, &init_block).new + container.run if container.respond_to?(:run) + end + + def run(initializer = nil) + Rails.configuration = Base.config = @config + + if initializer + run_initializer(initializer) + else + @initializers.each {|block| run_initializer(block) } + end + end + end + + def self.default + @default ||= Runner.new + end + + def self.run(initializer = nil, config = nil) + default.config = config if config + default.config ||= Configuration.new + yield default.config if block_given? + default.run(initializer) + end + end + + # Check for valid Ruby version (1.8.2 or 1.8.4 or higher). This is done in an + # external file, so we can use it from the `rails` program as well without duplication. + Initializer.default.add :check_ruby_version do + require 'rails/ruby_version_check' + end + + # If Rails is vendored and RubyGems is available, install stub GemSpecs + # for Rails, Active Support, Active Record, Action Pack, Action Mailer, and + # Active Resource. This allows Gem plugins to depend on Rails even when + # the Gem version of Rails shouldn't be loaded. + Initializer.default.add :install_gem_spec_stubs do + unless Rails.respond_to?(:vendor_rails?) + abort %{Your config/boot.rb is outdated: Run "rake rails:update".} + end + + if Rails.vendor_rails? + begin; require "rubygems"; rescue LoadError; return; end + + %w(rails activesupport activerecord actionpack actionmailer activeresource).each do |stub| + Gem.loaded_specs[stub] ||= Gem::Specification.new do |s| + s.name = stub + s.version = Rails::VERSION::STRING + s.loaded_from = "" + end + end + end + end + + # Set the <tt>$LOAD_PATH</tt> based on the value of + # Configuration#load_paths. Duplicates are removed. + Initializer.default.add :set_load_path do + # TODO: Think about unifying this with the general Rails paths + configuration.framework_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) } + configuration.paths.add_to_load_path + $LOAD_PATH.uniq! + end + + # Requires all frameworks specified by the Configuration#frameworks + # list. By default, all frameworks (Active Record, Active Support, + # Action Pack, Action Mailer, and Active Resource) are loaded. + Initializer.default.add :require_frameworks do + begin + require 'active_support' + require 'active_support/core_ext/kernel/reporting' + require 'active_support/core_ext/logger' + + # TODO: This is here to make Sam Ruby's tests pass. Needs discussion. + require 'active_support/core_ext/numeric/bytes' + configuration.frameworks.each { |framework| require(framework.to_s) } + rescue LoadError => e + # Re-raise as RuntimeError because Mongrel would swallow LoadError. + raise e.to_s + end + end + + # Set the paths from which Rails will automatically load source files, and + # the load_once paths. + Initializer.default.add :set_autoload_paths do + require 'active_support/dependencies' + ActiveSupport::Dependencies.load_paths = configuration.load_paths.uniq + ActiveSupport::Dependencies.load_once_paths = configuration.load_once_paths.uniq + + extra = ActiveSupport::Dependencies.load_once_paths - ActiveSupport::Dependencies.load_paths + unless extra.empty? + abort <<-end_error + load_once_paths must be a subset of the load_paths. + Extra items in load_once_paths: #{extra * ','} + end_error + end + + # Freeze the arrays so future modifications will fail rather than do nothing mysteriously + configuration.load_once_paths.freeze + end + + # Adds all load paths from plugins to the global set of load paths, so that + # code from plugins can be required (explicitly or automatically via ActiveSupport::Dependencies). + Initializer.default.add :add_plugin_load_paths do + require 'active_support/dependencies' + plugin_loader.add_plugin_load_paths + end + + # Loads the environment specified by Configuration#environment_path, which + # is typically one of development, test, or production. + Initializer.default.add :load_environment do + silence_warnings do + next if @environment_loaded + next unless File.file?(configuration.environment_path) + + @environment_loaded = true + + config = configuration + constants = self.class.constants + + eval(IO.read(configuration.environment_path), binding, configuration.environment_path) + + (self.class.constants - constants).each do |const| + Object.const_set(const, self.class.const_get(const)) + end + end + end + + Initializer.default.add :add_gem_load_paths do + require 'rails/gem_dependency' + Rails::GemDependency.add_frozen_gem_path + unless config.gems.empty? + require "rubygems" + config.gems.each { |gem| gem.add_load_paths } + end + end + + # Preload all frameworks specified by the Configuration#frameworks. + # Used by Passenger to ensure everything's loaded before forking and + # to avoid autoload race conditions in JRuby. + Initializer.default.add :preload_frameworks do + if configuration.preload_frameworks + configuration.frameworks.each do |framework| + # String#classify and #constantize aren't available yet. + toplevel = Object.const_get(framework.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }) + toplevel.load_all! if toplevel.respond_to?(:load_all!) + end + end + end + + # For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the + # multibyte safe operations. Plugin authors supporting other encodings + # should override this behaviour and set the relevant +default_charset+ + # on ActionController::Base. + # + # For Ruby 1.9, UTF-8 is the default internal and external encoding. + Initializer.default.add :initialize_encoding do + if RUBY_VERSION < '1.9' + $KCODE='u' + else + Encoding.default_external = Encoding::UTF_8 + end + end + + # This initialization routine does nothing unless <tt>:active_record</tt> + # is one of the frameworks to load (Configuration#frameworks). If it is, + # this sets the database configuration from Configuration#database_configuration + # and then establishes the connection. + Initializer.default.add :initialize_database do + if configuration.frameworks.include?(:active_record) + ActiveRecord::Base.configurations = configuration.database_configuration + ActiveRecord::Base.establish_connection + end + end + + Initializer.default.add :initialize_cache do + unless defined?(RAILS_CACHE) + silence_warnings { Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(configuration.cache_store) } + + if RAILS_CACHE.respond_to?(:middleware) + # Insert middleware to setup and teardown local cache for each request + configuration.middleware.insert_after(:"Rack::Lock", RAILS_CACHE.middleware) + end + end + end + + Initializer.default.add :initialize_framework_caches do + if configuration.frameworks.include?(:action_controller) + ActionController::Base.cache_store ||= RAILS_CACHE + end + end + + Initializer.default.add :initialize_logger do + # if the environment has explicitly defined a logger, use it + next if Rails.logger + + unless logger = configuration.logger + begin + logger = ActiveSupport::BufferedLogger.new(configuration.log_path) + logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase) + if RAILS_ENV == "production" + logger.auto_flushing = false + end + rescue StandardError => e + logger = ActiveSupport::BufferedLogger.new(STDERR) + logger.level = ActiveSupport::BufferedLogger::WARN + logger.warn( + "Rails Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " + + "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." + ) + end + end + + # TODO: Why are we silencing warning here? + silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } + end + + # Sets the logger for Active Record, Action Controller, and Action Mailer + # (but only for those frameworks that are to be loaded). If the framework's + # logger is already set, it is not changed, otherwise it is set to use + # RAILS_DEFAULT_LOGGER. + Initializer.default.add :initialize_framework_logging do + for framework in ([ :active_record, :action_controller, :action_mailer ] & configuration.frameworks) + framework.to_s.camelize.constantize.const_get("Base").logger ||= Rails.logger + end + + ActiveSupport::Dependencies.logger ||= Rails.logger + Rails.cache.logger ||= Rails.logger + end + + # Sets the dependency loading mechanism based on the value of + # Configuration#cache_classes. + Initializer.default.add :initialize_dependency_mechanism do + # TODO: Remove files from the $" and always use require + ActiveSupport::Dependencies.mechanism = configuration.cache_classes ? :require : :load + end + + # Loads support for "whiny nil" (noisy warnings when methods are invoked + # on +nil+ values) if Configuration#whiny_nils is true. + Initializer.default.add :initialize_whiny_nils do + require('active_support/whiny_nil') if configuration.whiny_nils + end + + + # Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes. + # If assigned value cannot be matched to a TimeZone, an exception will be raised. + Initializer.default.add :initialize_time_zone do + if configuration.time_zone + zone_default = Time.__send__(:get_zone, configuration.time_zone) + + unless zone_default + raise \ + 'Value assigned to config.time_zone not recognized.' + + 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.' + end + + Time.zone_default = zone_default + + if configuration.frameworks.include?(:active_record) + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + end + end + end + + # Set the i18n configuration from config.i18n but special-case for the load_path which should be + # appended to what's already set instead of overwritten. + Initializer.default.add :initialize_i18n do + configuration.i18n.each do |setting, value| + if setting == :load_path + I18n.load_path += value + else + I18n.send("#{setting}=", value) + end + end + end + + # Initializes framework-specific settings for each of the loaded frameworks + # (Configuration#frameworks). The available settings map to the accessors + # on each of the corresponding Base classes. + Initializer.default.add :initialize_framework_settings do + configuration.frameworks.each do |framework| + base_class = framework.to_s.camelize.constantize.const_get("Base") + + configuration.send(framework).each do |setting, value| + base_class.send("#{setting}=", value) + end + end + configuration.active_support.each do |setting, value| + ActiveSupport.send("#{setting}=", value) + end + end + + # Sets +ActionController::Base#view_paths+ and +ActionMailer::Base#template_root+ + # (but only for those frameworks that are to be loaded). If the framework's + # paths have already been set, it is not changed, otherwise it is + # set to use Configuration#view_path. + Initializer.default.add :initialize_framework_views do + if configuration.frameworks.include?(:action_view) + view_path = ActionView::PathSet.type_cast(configuration.view_path) + ActionMailer::Base.template_root = view_path if configuration.frameworks.include?(:action_mailer) && ActionMailer::Base.view_paths.blank? + ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.blank? + end + end + + Initializer.default.add :initialize_metal do + # TODO: Make Rails and metal work without ActionController + if defined?(ActionController) + Rails::Rack::Metal.requested_metals = configuration.metals + Rails::Rack::Metal.metal_paths += plugin_loader.engine_metal_paths + + configuration.middleware.insert_before( + :"ActionDispatch::ParamsParser", + Rails::Rack::Metal, :if => Rails::Rack::Metal.metals.any?) + end + end + + Initializer.default.add :check_for_unbuilt_gems do + unbuilt_gems = config.gems.select {|gem| gem.frozen? && !gem.built? } + if unbuilt_gems.size > 0 + # don't print if the gems:build rake tasks are being run + unless $gems_build_rake_task + abort <<-end_error +The following gems have native components that need to be built +#{unbuilt_gems.map { |gemm| "#{gemm.name} #{gemm.requirement}" } * "\n "} + +You're running: +ruby #{Gem.ruby_version} at #{Gem.ruby} +rubygems #{Gem::RubyGemsVersion} at #{Gem.path * ', '} + +Run `rake gems:build` to build the unbuilt gems. + end_error + end + end + end + + Initializer.default.add :load_gems do + unless $gems_rake_task + config.gems.each { |gem| gem.load } + end + end + + # Loads all plugins in <tt>config.plugin_paths</tt>. <tt>plugin_paths</tt> + # defaults to <tt>vendor/plugins</tt> but may also be set to a list of + # paths, such as + # config.plugin_paths = ["#{RAILS_ROOT}/lib/plugins", "#{RAILS_ROOT}/vendor/plugins"] + # + # In the default implementation, as each plugin discovered in <tt>plugin_paths</tt> is initialized: + # * its +lib+ directory, if present, is added to the load path (immediately after the applications lib directory) + # * <tt>init.rb</tt> is evaluated, if present + # + # After all plugins are loaded, duplicates are removed from the load path. + # If an array of plugin names is specified in config.plugins, only those plugins will be loaded + # and they plugins will be loaded in that order. Otherwise, plugins are loaded in alphabetical + # order. + # + # if config.plugins ends contains :all then the named plugins will be loaded in the given order and all other + # plugins will be loaded in alphabetical order + Initializer.default.add :load_plugins do + plugin_loader.load_plugins + end + + # + # # pick up any gems that plugins depend on + Initializer.default.add :add_gem_load_paths do + require 'rails/gem_dependency' + # TODO: This seems extraneous + Rails::GemDependency.add_frozen_gem_path + unless config.gems.empty? + require "rubygems" + config.gems.each { |gem| gem.add_load_paths } + end + end + + # TODO: Figure out if this needs to run a second time + # load_gems + + Initializer.default.add :check_gem_dependencies do + unloaded_gems = config.gems.reject { |g| g.loaded? } + if unloaded_gems.size > 0 + configuration.gems_dependencies_loaded = false + # don't print if the gems rake tasks are being run + unless $gems_rake_task + abort <<-end_error +Missing these required gems: +#{unloaded_gems.map { |gemm| "#{gemm.name} #{gemm.requirement}" } * "\n "} + +You're running: +ruby #{Gem.ruby_version} at #{Gem.ruby} +rubygems #{Gem::RubyGemsVersion} at #{Gem.path * ', '} + +Run `rake gems:install` to install the missing gems. + end_error + end + else + configuration.gems_dependencies_loaded = true + end + end + + # # bail out if gems are missing - note that check_gem_dependencies will have + # # already called abort() unless $gems_rake_task is set + # return unless gems_dependencies_loaded + + Initializer.default.add :load_application_initializers do + if gems_dependencies_loaded + Dir["#{configuration.root_path}/config/initializers/**/*.rb"].sort.each do |initializer| + load(initializer) + end + end + end + + # Fires the user-supplied after_initialize block (Configuration#after_initialize) + Initializer.default.add :after_initialize do + if gems_dependencies_loaded + configuration.after_initialize_blocks.each do |block| + block.call + end + end + end + + # # Setup database middleware after initializers have run + Initializer.default.add :initialize_database_middleware do + if configuration.frameworks.include?(:active_record) + if configuration.frameworks.include?(:action_controller) && + ActionController::Base.session_store.name == 'ActiveRecord::SessionStore' + configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement + configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::QueryCache + else + configuration.middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement + configuration.middleware.use ActiveRecord::QueryCache + end + end + end + + # TODO: Make a DSL way to limit an initializer to a particular framework + + # # Prepare dispatcher callbacks and run 'prepare' callbacks + Initializer.default.add :prepare_dispatcher do + next unless configuration.frameworks.include?(:action_controller) + require 'rails/dispatcher' unless defined?(::Dispatcher) + Dispatcher.define_dispatcher_callbacks(configuration.cache_classes) + end + + # Routing must be initialized after plugins to allow the former to extend the routes + # --- + # If Action Controller is not one of the loaded frameworks (Configuration#frameworks) + # this does nothing. Otherwise, it loads the routing definitions and sets up + # loading module used to lazily load controllers (Configuration#controller_paths). + Initializer.default.add :initialize_routing do + next unless configuration.frameworks.include?(:action_controller) + + ActionController::Routing.controller_paths += configuration.controller_paths + ActionController::Routing::Routes.add_configuration_file(configuration.routes_configuration_file) + ActionController::Routing::Routes.reload! + end + # + # # Observers are loaded after plugins in case Observers or observed models are modified by plugins. + Initializer.default.add :load_observers do + if gems_dependencies_loaded && configuration.frameworks.include?(:active_record) + ActiveRecord::Base.instantiate_observers + end + end + + # Eager load application classes + Initializer.default.add :load_application_classes do + next if $rails_rake_task + + if configuration.cache_classes + configuration.eager_load_paths.each do |load_path| + matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/ + Dir.glob("#{load_path}/**/*.rb").sort.each do |file| + require_dependency file.sub(matcher, '\1') + end + end + end + end + + # Disable dependency loading during request cycle + Initializer.default.add :disable_dependency_loading do + if configuration.cache_classes && !configuration.dependency_loading + ActiveSupport::Dependencies.unhook! + end + end + + # Configure generators if they were already loaded + Initializer.default.add :initialize_generators do + if defined?(Rails::Generators) + Rails::Generators.no_color! unless config.generators.colorize_logging + Rails::Generators.aliases.deep_merge! config.generators.aliases + Rails::Generators.options.deep_merge! config.generators.options + end + end +end diff --git a/railties/lib/rails/initializer_old.rb b/railties/lib/rails/initializer_old.rb new file mode 100644 index 0000000000..cee5c7bcb6 --- /dev/null +++ b/railties/lib/rails/initializer_old.rb @@ -0,0 +1,1137 @@ +require 'logger' +require 'set' +require 'pathname' + +$LOAD_PATH.unshift File.dirname(__FILE__) +require 'railties_path' +require 'rails/version' +require 'rails/gem_dependency' +require 'rails/rack' + +RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defined?(RAILS_ENV) + +module Rails + class << self + # The Configuration instance used to configure the Rails environment + def configuration + @@configuration + end + + def configuration=(configuration) + @@configuration = configuration + end + + def initialized? + @initialized || false + end + + def initialized=(initialized) + @initialized ||= initialized + end + + def logger + if defined?(RAILS_DEFAULT_LOGGER) + RAILS_DEFAULT_LOGGER + else + nil + end + end + + def backtrace_cleaner + @@backtrace_cleaner ||= begin + # Relies on ActiveSupport, so we have to lazy load to postpone definition until AS has been loaded + require 'rails/backtrace_cleaner' + Rails::BacktraceCleaner.new + end + end + + def root + Pathname.new(RAILS_ROOT) if defined?(RAILS_ROOT) + end + + def env + @_env ||= ActiveSupport::StringInquirer.new(RAILS_ENV) + end + + def cache + RAILS_CACHE + end + + def version + VERSION::STRING + end + + def public_path + @@public_path ||= self.root ? File.join(self.root, "public") : "public" + end + + def public_path=(path) + @@public_path = path + end + end + + # The Initializer is responsible for processing the Rails configuration, such + # as setting the $LOAD_PATH, requiring the right frameworks, initializing + # logging, and more. It can be run either as a single command that'll just + # use the default configuration, like this: + # + # Rails::Initializer.run + # + # But normally it's more interesting to pass in a custom configuration + # through the block running: + # + # Rails::Initializer.run do |config| + # config.frameworks -= [ :action_mailer ] + # end + # + # This will use the default configuration options from Rails::Configuration, + # but allow for overwriting on select areas. + class Initializer + # The Configuration instance used by this Initializer instance. + attr_reader :configuration + + # The set of loaded plugins. + attr_reader :loaded_plugins + + # Whether or not all the gem dependencies have been met + attr_reader :gems_dependencies_loaded + + # Runs the initializer. By default, this will invoke the #process method, + # which simply executes all of the initialization routines. Alternately, + # you can specify explicitly which initialization routine you want: + # + # Rails::Initializer.run(:set_load_path) + # + # This is useful if you only want the load path initialized, without + # incurring the overhead of completely loading the entire environment. + def self.run(command = :process, configuration = Configuration.new) + yield configuration if block_given? + initializer = new configuration + initializer.send(command) + initializer + end + + # Create a new Initializer instance that references the given Configuration + # instance. + def initialize(configuration) + @configuration = configuration + @loaded_plugins = [] + end + + # Sequentially step through all of the available initialization routines, + # in order (view execution order in source). + def process + Rails.configuration = configuration + + check_ruby_version + install_gem_spec_stubs + set_load_path + add_gem_load_paths + + require_frameworks + set_autoload_paths + add_plugin_load_paths + load_environment + preload_frameworks + + initialize_encoding + initialize_database + + initialize_cache + initialize_framework_caches + + initialize_logger + initialize_framework_logging + + initialize_dependency_mechanism + initialize_whiny_nils + + initialize_time_zone + initialize_i18n + + initialize_framework_settings + initialize_framework_views + + initialize_metal + + add_support_load_paths + + check_for_unbuilt_gems + + load_gems + load_plugins + + # pick up any gems that plugins depend on + add_gem_load_paths + load_gems + check_gem_dependencies + + # bail out if gems are missing - note that check_gem_dependencies will have + # already called abort() unless $gems_rake_task is set + return unless gems_dependencies_loaded + + load_application_initializers + + # the framework is now fully initialized + after_initialize + + # Setup database middleware after initializers have run + initialize_database_middleware + + # Prepare dispatcher callbacks and run 'prepare' callbacks + prepare_dispatcher + + # Routing must be initialized after plugins to allow the former to extend the routes + initialize_routing + + # Observers are loaded after plugins in case Observers or observed models are modified by plugins. + load_observers + + # Load view path cache + load_view_paths + + # Load application classes + load_application_classes + + # Disable dependency loading during request cycle + disable_dependency_loading + + # Flag initialized + Rails.initialized = true + end + + # Check for valid Ruby version + # This is done in an external file, so we can use it + # from the `rails` program as well without duplication. + def check_ruby_version + require 'ruby_version_check' + end + + # If Rails is vendored and RubyGems is available, install stub GemSpecs + # for Rails, Active Support, Active Record, Action Pack, Action Mailer, and + # Active Resource. This allows Gem plugins to depend on Rails even when + # the Gem version of Rails shouldn't be loaded. + def install_gem_spec_stubs + unless Rails.respond_to?(:vendor_rails?) + abort %{Your config/boot.rb is outdated: Run "rake rails:update".} + end + + if Rails.vendor_rails? + begin; require "rubygems"; rescue LoadError; return; end + + stubs = %w(rails activesupport activerecord actionpack actionmailer activeresource) + stubs.reject! { |s| Gem.loaded_specs.key?(s) } + + stubs.each do |stub| + Gem.loaded_specs[stub] = Gem::Specification.new do |s| + s.name = stub + s.version = Rails::VERSION::STRING + s.loaded_from = "" + end + end + end + end + + # Set the <tt>$LOAD_PATH</tt> based on the value of + # Configuration#load_paths. Duplicates are removed. + def set_load_path + load_paths = configuration.load_paths + configuration.framework_paths + load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) } + $LOAD_PATH.uniq! + end + + # Set the paths from which Rails will automatically load source files, and + # the load_once paths. + def set_autoload_paths + require 'active_support/dependencies' + ActiveSupport::Dependencies.load_paths = configuration.load_paths.uniq + ActiveSupport::Dependencies.load_once_paths = configuration.load_once_paths.uniq + + extra = ActiveSupport::Dependencies.load_once_paths - ActiveSupport::Dependencies.load_paths + unless extra.empty? + abort <<-end_error + load_once_paths must be a subset of the load_paths. + Extra items in load_once_paths: #{extra * ','} + end_error + end + + # Freeze the arrays so future modifications will fail rather than do nothing mysteriously + configuration.load_once_paths.freeze + end + + # Requires all frameworks specified by the Configuration#frameworks + # list. By default, all frameworks (Active Record, Active Support, + # Action Pack, Action Mailer, and Active Resource) are loaded. + def require_frameworks + require 'active_support/all' + configuration.frameworks.each { |framework| require(framework.to_s) } + rescue LoadError => e + # Re-raise as RuntimeError because Mongrel would swallow LoadError. + raise e.to_s + end + + # Preload all frameworks specified by the Configuration#frameworks. + # Used by Passenger to ensure everything's loaded before forking and + # to avoid autoload race conditions in JRuby. + def preload_frameworks + if configuration.preload_frameworks + configuration.frameworks.each do |framework| + # String#classify and #constantize aren't available yet. + toplevel = Object.const_get(framework.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }) + toplevel.load_all! if toplevel.respond_to?(:load_all!) + end + end + end + + # Add the load paths used by support functions such as the info controller + def add_support_load_paths + end + + # Adds all load paths from plugins to the global set of load paths, so that + # code from plugins can be required (explicitly or automatically via ActiveSupport::Dependencies). + def add_plugin_load_paths + require 'active_support/dependencies' + plugin_loader.add_plugin_load_paths + end + + def add_gem_load_paths + require 'rails/gem_dependency' + Rails::GemDependency.add_frozen_gem_path + unless @configuration.gems.empty? + require "rubygems" + @configuration.gems.each { |gem| gem.add_load_paths } + end + end + + def load_gems + unless $gems_rake_task + @configuration.gems.each { |gem| gem.load } + end + end + + def check_for_unbuilt_gems + unbuilt_gems = @configuration.gems.select {|gem| gem.frozen? && !gem.built? } + if unbuilt_gems.size > 0 + # don't print if the gems:build rake tasks are being run + unless $gems_build_rake_task + abort <<-end_error +The following gems have native components that need to be built + #{unbuilt_gems.map { |gemm| "#{gemm.name} #{gemm.requirement}" } * "\n "} + +You're running: + ruby #{Gem.ruby_version} at #{Gem.ruby} + rubygems #{Gem::RubyGemsVersion} at #{Gem.path * ', '} + +Run `rake gems:build` to build the unbuilt gems. + end_error + end + end + end + + def check_gem_dependencies + unloaded_gems = @configuration.gems.reject { |g| g.loaded? } + if unloaded_gems.size > 0 + @gems_dependencies_loaded = false + # don't print if the gems rake tasks are being run + unless $gems_rake_task + abort <<-end_error +Missing these required gems: + #{unloaded_gems.map { |gemm| "#{gemm.name} #{gemm.requirement}" } * "\n "} + +You're running: + ruby #{Gem.ruby_version} at #{Gem.ruby} + rubygems #{Gem::RubyGemsVersion} at #{Gem.path * ', '} + +Run `rake gems:install` to install the missing gems. + end_error + end + else + @gems_dependencies_loaded = true + end + end + + # Loads all plugins in <tt>config.plugin_paths</tt>. <tt>plugin_paths</tt> + # defaults to <tt>vendor/plugins</tt> but may also be set to a list of + # paths, such as + # config.plugin_paths = ["#{RAILS_ROOT}/lib/plugins", "#{RAILS_ROOT}/vendor/plugins"] + # + # In the default implementation, as each plugin discovered in <tt>plugin_paths</tt> is initialized: + # * its +lib+ directory, if present, is added to the load path (immediately after the applications lib directory) + # * <tt>init.rb</tt> is evaluated, if present + # + # After all plugins are loaded, duplicates are removed from the load path. + # If an array of plugin names is specified in config.plugins, only those plugins will be loaded + # and they plugins will be loaded in that order. Otherwise, plugins are loaded in alphabetical + # order. + # + # if config.plugins ends contains :all then the named plugins will be loaded in the given order and all other + # plugins will be loaded in alphabetical order + def load_plugins + plugin_loader.load_plugins + end + + def plugin_loader + @plugin_loader ||= configuration.plugin_loader.new(self) + end + + # Loads the environment specified by Configuration#environment_path, which + # is typically one of development, test, or production. + def load_environment + silence_warnings do + return if @environment_loaded + @environment_loaded = true + + config = configuration + constants = self.class.constants + + eval(IO.read(configuration.environment_path), binding, configuration.environment_path) + + (self.class.constants - constants).each do |const| + Object.const_set(const, self.class.const_get(const)) + end + end + end + + def load_observers + if gems_dependencies_loaded && configuration.frameworks.include?(:active_record) + ActiveRecord::Base.instantiate_observers + end + end + + def load_view_paths + if configuration.frameworks.include?(:action_view) + if configuration.cache_classes + view_path = ActionView::FileSystemResolverWithFallback.new(configuration.view_path) + ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) + ActionMailer::Base.template_root = view_path if configuration.frameworks.include?(:action_mailer) + end + end + end + + # Eager load application classes + def load_application_classes + return if $rails_rake_task + if configuration.cache_classes + configuration.eager_load_paths.each do |load_path| + matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/ + Dir.glob("#{load_path}/**/*.rb").sort.each do |file| + require_dependency file.sub(matcher, '\1') + end + end + end + end + + # For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the + # multibyte safe operations. Plugin authors supporting other encodings + # should override this behaviour and set the relevant +default_charset+ + # on ActionController::Base. + # + # For Ruby 1.9, UTF-8 is the default internal and external encoding. + def initialize_encoding + if RUBY_VERSION < '1.9' + $KCODE='u' + else + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 + end + end + + # This initialization routine does nothing unless <tt>:active_record</tt> + # is one of the frameworks to load (Configuration#frameworks). If it is, + # this sets the database configuration from Configuration#database_configuration + # and then establishes the connection. + def initialize_database + if configuration.frameworks.include?(:active_record) + ActiveRecord::Base.configurations = configuration.database_configuration + ActiveRecord::Base.establish_connection + end + end + + def initialize_database_middleware + if configuration.frameworks.include?(:active_record) + if configuration.frameworks.include?(:action_controller) && + ActionController::Base.session_store.name == 'ActiveRecord::SessionStore' + configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement + configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::QueryCache + else + configuration.middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement + configuration.middleware.use ActiveRecord::QueryCache + end + end + end + + def initialize_cache + unless defined?(RAILS_CACHE) + silence_warnings { Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(configuration.cache_store) } + + if RAILS_CACHE.respond_to?(:middleware) + # Insert middleware to setup and teardown local cache for each request + configuration.middleware.insert_after(:"Rack::Lock", RAILS_CACHE.middleware) + end + end + end + + def initialize_framework_caches + if configuration.frameworks.include?(:action_controller) + ActionController::Base.cache_store ||= RAILS_CACHE + end + end + + # If the RAILS_DEFAULT_LOGGER constant is already set, this initialization + # routine does nothing. If the constant is not set, and Configuration#logger + # is not +nil+, this also does nothing. Otherwise, a new logger instance + # is created at Configuration#log_path, with a default log level of + # Configuration#log_level. + # + # If the log could not be created, the log will be set to output to + # +STDERR+, with a log level of +WARN+. + def initialize_logger + # if the environment has explicitly defined a logger, use it + return if Rails.logger + + unless logger = configuration.logger + begin + logger = ActiveSupport::BufferedLogger.new(configuration.log_path) + logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase) + if configuration.environment == "production" + logger.auto_flushing = false + end + rescue StandardError => e + logger = ActiveSupport::BufferedLogger.new(STDERR) + logger.level = ActiveSupport::BufferedLogger::WARN + logger.warn( + "Rails Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " + + "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." + ) + end + end + + silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } + end + + # Sets the logger for Active Record, Action Controller, and Action Mailer + # (but only for those frameworks that are to be loaded). If the framework's + # logger is already set, it is not changed, otherwise it is set to use + # RAILS_DEFAULT_LOGGER. + def initialize_framework_logging + for framework in ([ :active_record, :action_controller, :action_mailer ] & configuration.frameworks) + framework.to_s.camelize.constantize.const_get("Base").logger ||= Rails.logger + end + + ActiveSupport::Dependencies.logger ||= Rails.logger + Rails.cache.logger ||= Rails.logger + end + + # Sets +ActionController::Base#view_paths+ and +ActionMailer::Base#template_root+ + # (but only for those frameworks that are to be loaded). If the framework's + # paths have already been set, it is not changed, otherwise it is + # set to use Configuration#view_path. + def initialize_framework_views + if configuration.frameworks.include?(:action_view) + view_path = ActionView::PathSet.type_cast(configuration.view_path) + ActionMailer::Base.template_root = view_path if configuration.frameworks.include?(:action_mailer) && ActionMailer::Base.view_paths.blank? + ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.blank? + end + end + + # If Action Controller is not one of the loaded frameworks (Configuration#frameworks) + # this does nothing. Otherwise, it loads the routing definitions and sets up + # loading module used to lazily load controllers (Configuration#controller_paths). + def initialize_routing + return unless configuration.frameworks.include?(:action_controller) + + ActionController::Routing.controller_paths += configuration.controller_paths + ActionController::Routing::Routes.add_configuration_file(configuration.routes_configuration_file) + ActionController::Routing::Routes.reload! + end + + # Sets the dependency loading mechanism based on the value of + # Configuration#cache_classes. + def initialize_dependency_mechanism + ActiveSupport::Dependencies.mechanism = configuration.cache_classes ? :require : :load + end + + # Loads support for "whiny nil" (noisy warnings when methods are invoked + # on +nil+ values) if Configuration#whiny_nils is true. + def initialize_whiny_nils + require('active_support/whiny_nil') if configuration.whiny_nils + end + + # Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes. + # If assigned value cannot be matched to a TimeZone, an exception will be raised. + def initialize_time_zone + if configuration.time_zone + zone_default = Time.__send__(:get_zone, configuration.time_zone) + + unless zone_default + raise \ + 'Value assigned to config.time_zone not recognized.' + + 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.' + end + + Time.zone_default = zone_default + + if configuration.frameworks.include?(:active_record) + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + end + end + end + + # Set the i18n configuration from config.i18n but special-case for the load_path which should be + # appended to what's already set instead of overwritten. + def initialize_i18n + configuration.i18n.each do |setting, value| + if setting == :load_path + I18n.load_path += value + else + I18n.send("#{setting}=", value) + end + end + end + + def initialize_metal + Rails::Rack::Metal.requested_metals = configuration.metals + Rails::Rack::Metal.metal_paths += plugin_loader.engine_metal_paths + + configuration.middleware.insert_before( + :"ActionDispatch::ParamsParser", + Rails::Rack::Metal, :if => Rails::Rack::Metal.metals.any?) + end + + # Initializes framework-specific settings for each of the loaded frameworks + # (Configuration#frameworks). The available settings map to the accessors + # on each of the corresponding Base classes. + def initialize_framework_settings + configuration.frameworks.each do |framework| + base_class = framework.to_s.camelize.constantize.const_get("Base") + + configuration.send(framework).each do |setting, value| + base_class.send("#{setting}=", value) + end + end + configuration.active_support.each do |setting, value| + ActiveSupport.send("#{setting}=", value) + end + end + + # Fires the user-supplied after_initialize block (Configuration#after_initialize) + def after_initialize + if gems_dependencies_loaded + configuration.after_initialize_blocks.each do |block| + block.call + end + end + end + + def load_application_initializers + if gems_dependencies_loaded + Dir["#{configuration.root_path}/config/initializers/**/*.rb"].sort.each do |initializer| + load(initializer) + end + end + end + + def prepare_dispatcher + return unless configuration.frameworks.include?(:action_controller) + require 'dispatcher' unless defined?(::Dispatcher) + Dispatcher.define_dispatcher_callbacks(configuration.cache_classes) + end + + def disable_dependency_loading + if configuration.cache_classes && !configuration.dependency_loading + ActiveSupport::Dependencies.unhook! + end + end + end + + # The Configuration class holds all the parameters for the Initializer and + # ships with defaults that suites most Rails applications. But it's possible + # to overwrite everything. Usually, you'll create an Configuration file + # implicitly through the block running on the Initializer, but it's also + # possible to create the Configuration instance in advance and pass it in + # like this: + # + # config = Rails::Configuration.new + # Rails::Initializer.run(:process, config) + class Configuration + # The application's base directory. + attr_reader :root_path + + # A stub for setting options on ActionController::Base. + attr_accessor :action_controller + + # A stub for setting options on ActionMailer::Base. + attr_accessor :action_mailer + + # A stub for setting options on ActionView::Base. + attr_accessor :action_view + + # A stub for setting options on ActiveRecord::Base. + attr_accessor :active_record + + # A stub for setting options on ActiveResource::Base. + attr_accessor :active_resource + + # A stub for setting options on ActiveSupport. + attr_accessor :active_support + + # Whether to preload all frameworks at startup. + attr_accessor :preload_frameworks + + # Whether or not classes should be cached (set to false if you want + # application classes to be reloaded on each request) + attr_accessor :cache_classes + + # The list of paths that should be searched for controllers. (Defaults + # to <tt>app/controllers</tt>.) + attr_accessor :controller_paths + + # The path to the database configuration file to use. (Defaults to + # <tt>config/database.yml</tt>.) + attr_accessor :database_configuration_file + + # The path to the routes configuration file to use. (Defaults to + # <tt>config/routes.rb</tt>.) + attr_accessor :routes_configuration_file + + # The list of rails framework components that should be loaded. (Defaults + # to <tt>:active_record</tt>, <tt>:action_controller</tt>, + # <tt>:action_view</tt>, <tt>:action_mailer</tt>, and + # <tt>:active_resource</tt>). + attr_accessor :frameworks + + # An array of additional paths to prepend to the load path. By default, + # all +app+, +lib+, +vendor+ and mock paths are included in this list. + attr_accessor :load_paths + + # An array of paths from which Rails will automatically load from only once. + # All elements of this array must also be in +load_paths+. + attr_accessor :load_once_paths + + # An array of paths from which Rails will eager load on boot if cache + # classes is enabled. All elements of this array must also be in + # +load_paths+. + attr_accessor :eager_load_paths + + # The log level to use for the default Rails logger. In production mode, + # this defaults to <tt>:info</tt>. In development mode, it defaults to + # <tt>:debug</tt>. + attr_accessor :log_level + + # The path to the log file to use. Defaults to log/#{environment}.log + # (e.g. log/development.log or log/production.log). + attr_accessor :log_path + + # The specific logger to use. By default, a logger will be created and + # initialized using #log_path and #log_level, but a programmer may + # specifically set the logger to use via this accessor and it will be + # used directly. + attr_accessor :logger + + # The specific cache store to use. By default, the ActiveSupport::Cache::Store will be used. + attr_accessor :cache_store + + # The root of the application's views. (Defaults to <tt>app/views</tt>.) + attr_accessor :view_path + + # Set to +true+ if you want to be warned (noisily) when you try to invoke + # any method of +nil+. Set to +false+ for the standard Ruby behavior. + attr_accessor :whiny_nils + + # The list of plugins to load. If this is set to <tt>nil</tt>, all plugins will + # be loaded. If this is set to <tt>[]</tt>, no plugins will be loaded. Otherwise, + # plugins will be loaded in the order specified. + attr_reader :plugins + def plugins=(plugins) + @plugins = plugins.nil? ? nil : plugins.map { |p| p.to_sym } + end + + # The list of metals to load. If this is set to <tt>nil</tt>, all metals will + # be loaded in alphabetical order. If this is set to <tt>[]</tt>, no metals will + # be loaded. Otherwise metals will be loaded in the order specified + attr_accessor :metals + + # The path to the root of the plugins directory. By default, it is in + # <tt>vendor/plugins</tt>. + attr_accessor :plugin_paths + + # The classes that handle finding the desired plugins that you'd like to load for + # your application. By default it is the Rails::Plugin::FileSystemLocator which finds + # plugins to load in <tt>vendor/plugins</tt>. You can hook into gem location by subclassing + # Rails::Plugin::Locator and adding it onto the list of <tt>plugin_locators</tt>. + attr_accessor :plugin_locators + + # The class that handles loading each plugin. Defaults to Rails::Plugin::Loader, but + # a sub class would have access to fine grained modification of the loading behavior. See + # the implementation of Rails::Plugin::Loader for more details. + attr_accessor :plugin_loader + + # Enables or disables plugin reloading. You can get around this setting per plugin. + # If <tt>reload_plugins?</tt> is false, add this to your plugin's <tt>init.rb</tt> + # to make it reloadable: + # + # ActiveSupport::Dependencies.load_once_paths.delete lib_path + # + # If <tt>reload_plugins?</tt> is true, add this to your plugin's <tt>init.rb</tt> + # to only load it once: + # + # ActiveSupport::Dependencies.load_once_paths << lib_path + # + attr_accessor :reload_plugins + + # Returns true if plugin reloading is enabled. + def reload_plugins? + !!@reload_plugins + end + + # Enables or disables dependency loading during the request cycle. Setting + # <tt>dependency_loading</tt> to true will allow new classes to be loaded + # during a request. Setting it to false will disable this behavior. + # + # Those who want to run in a threaded environment should disable this + # option and eager load or require all there classes on initialization. + # + # If <tt>cache_classes</tt> is disabled, dependency loaded will always be + # on. + attr_accessor :dependency_loading + + # An array of gems that this rails application depends on. Rails will automatically load + # these gems during installation, and allow you to install any missing gems with: + # + # rake gems:install + # + # You can add gems with the #gem method. + attr_accessor :gems + + # Adds a single Gem dependency to the rails application. By default, it will require + # the library with the same name as the gem. Use :lib to specify a different name. + # + # # gem 'aws-s3', '>= 0.4.0' + # # require 'aws/s3' + # config.gem 'aws-s3', :lib => 'aws/s3', :version => '>= 0.4.0', \ + # :source => "http://code.whytheluckystiff.net" + # + # To require a library be installed, but not attempt to load it, pass :lib => false + # + # config.gem 'qrp', :version => '0.4.1', :lib => false + def gem(name, options = {}) + @gems << Rails::GemDependency.new(name, options) + end + + # Deprecated options: + def breakpoint_server(_ = nil) + $stderr.puts %( + ******************************************************************* + * config.breakpoint_server has been deprecated and has no effect. * + ******************************************************************* + ) + end + alias_method :breakpoint_server=, :breakpoint_server + + # Sets the default +time_zone+. Setting this will enable +time_zone+ + # awareness for Active Record models and set the Active Record default + # timezone to <tt>:utc</tt>. + attr_accessor :time_zone + + # Accessor for i18n settings. + attr_accessor :i18n + + # Create a new Configuration instance, initialized with the default + # values. + def initialize + set_root_path! + + self.frameworks = default_frameworks + self.load_paths = default_load_paths + self.load_once_paths = default_load_once_paths + self.eager_load_paths = default_eager_load_paths + self.log_path = default_log_path + self.log_level = default_log_level + self.view_path = default_view_path + self.controller_paths = default_controller_paths + self.preload_frameworks = default_preload_frameworks + self.cache_classes = default_cache_classes + self.dependency_loading = default_dependency_loading + self.whiny_nils = default_whiny_nils + self.plugins = default_plugins + self.plugin_paths = default_plugin_paths + self.plugin_locators = default_plugin_locators + self.plugin_loader = default_plugin_loader + self.database_configuration_file = default_database_configuration_file + self.routes_configuration_file = default_routes_configuration_file + self.gems = default_gems + self.i18n = default_i18n + + for framework in default_frameworks + self.send("#{framework}=", Rails::OrderedOptions.new) + end + self.active_support = Rails::OrderedOptions.new + end + + # Set the root_path to RAILS_ROOT and canonicalize it. + def set_root_path! + raise 'RAILS_ROOT is not set' unless defined?(::RAILS_ROOT) + raise 'RAILS_ROOT is not a directory' unless File.directory?(::RAILS_ROOT) + + @root_path = + # Pathname is incompatible with Windows, but Windows doesn't have + # real symlinks so File.expand_path is safe. + if RUBY_PLATFORM =~ /(:?mswin|mingw)/ + File.expand_path(::RAILS_ROOT) + + # Otherwise use Pathname#realpath which respects symlinks. + else + Pathname.new(::RAILS_ROOT).realpath.to_s + end + + Object.const_set(:RELATIVE_RAILS_ROOT, ::RAILS_ROOT.dup) unless defined?(::RELATIVE_RAILS_ROOT) + ::RAILS_ROOT.replace @root_path + end + + # Enable threaded mode. Allows concurrent requests to controller actions and + # multiple database connections. Also disables automatic dependency loading + # after boot, and disables reloading code on every request, as these are + # fundamentally incompatible with thread safety. + def threadsafe! + self.preload_frameworks = true + self.cache_classes = true + self.dependency_loading = false + self.action_controller.allow_concurrency = true + self + end + + # Loads and returns the contents of the #database_configuration_file. The + # contents of the file are processed via ERB before being sent through + # YAML::load. + def database_configuration + require 'erb' + YAML::load(ERB.new(IO.read(database_configuration_file)).result) + end + + # The path to the current environment's file (<tt>development.rb</tt>, etc.). By + # default the file is at <tt>config/environments/#{environment}.rb</tt>. + def environment_path + "#{root_path}/config/environments/#{environment}.rb" + end + + # Return the currently selected environment. By default, it returns the + # value of the RAILS_ENV constant. + def environment + ::RAILS_ENV + end + + # Adds a block which will be executed after rails has been fully initialized. + # Useful for per-environment configuration which depends on the framework being + # fully initialized. + def after_initialize(&after_initialize_block) + after_initialize_blocks << after_initialize_block if after_initialize_block + end + + # Returns the blocks added with Configuration#after_initialize + def after_initialize_blocks + @after_initialize_blocks ||= [] + end + + # Add a preparation callback that will run before every request in development + # mode, or before the first request in production. + # + # See Dispatcher#to_prepare. + def to_prepare(&callback) + after_initialize do + require 'dispatcher' unless defined?(::Dispatcher) + Dispatcher.to_prepare(&callback) + end + end + + def middleware + require 'action_controller' + ActionController::Dispatcher.middleware + end + + def builtin_directories + # Include builtins only in the development environment. + (environment == 'development') ? Dir["#{RAILTIES_PATH}/builtin/*/"] : [] + end + + def framework_paths + paths = %w(railties railties/lib activesupport/lib) + paths << 'actionpack/lib' if frameworks.include?(:action_controller) || frameworks.include?(:action_view) + + [:active_record, :action_mailer, :active_resource, :action_web_service].each do |framework| + paths << "#{framework.to_s.gsub('_', '')}/lib" if frameworks.include?(framework) + end + + paths.map { |dir| "#{framework_root_path}/#{dir}" }.select { |dir| File.directory?(dir) } + end + + private + def framework_root_path + defined?(::RAILS_FRAMEWORK_ROOT) ? ::RAILS_FRAMEWORK_ROOT : "#{root_path}/vendor/rails" + end + + def default_frameworks + [ :active_record, :action_controller, :action_view, :action_mailer, :active_resource ] + end + + def default_load_paths + paths = [] + + # Add the old mock paths only if the directories exists + paths.concat(Dir["#{root_path}/test/mocks/#{environment}"]) if File.exists?("#{root_path}/test/mocks/#{environment}") + + # Add the app's controller directory + paths.concat(Dir["#{root_path}/app/controllers/"]) + + # Followed by the standard includes. + paths.concat %w( + app + app/metal + app/models + app/controllers + app/helpers + app/services + lib + vendor + ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) } + + paths.concat builtin_directories + end + + # Doesn't matter since plugins aren't in load_paths yet. + def default_load_once_paths + [] + end + + def default_eager_load_paths + %w( + app/metal + app/models + app/controllers + app/helpers + ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) } + end + + def default_log_path + File.join(root_path, 'log', "#{environment}.log") + end + + def default_log_level + environment == 'production' ? :info : :debug + end + + def default_database_configuration_file + File.join(root_path, 'config', 'database.yml') + end + + def default_routes_configuration_file + File.join(root_path, 'config', 'routes.rb') + end + + def default_view_path + File.join(root_path, 'app', 'views') + end + + def default_controller_paths + paths = [File.join(root_path, 'app', 'controllers')] + paths.concat builtin_directories + paths + end + + def default_dependency_loading + true + end + + def default_preload_frameworks + false + end + + def default_cache_classes + true + end + + def default_whiny_nils + false + end + + def default_plugins + nil + end + + def default_plugin_paths + ["#{root_path}/vendor/plugins"] + end + + def default_plugin_locators + require 'rails/plugin/locator' + locators = [] + locators << Plugin::GemLocator if defined? Gem + locators << Plugin::FileSystemLocator + end + + def default_plugin_loader + require 'rails/plugin/loader' + Plugin::Loader + end + + def default_cache_store + if File.exist?("#{root_path}/tmp/cache/") + [ :file_store, "#{root_path}/tmp/cache/" ] + else + :memory_store + end + end + + def default_gems + [] + end + + def default_i18n + i18n = Rails::OrderedOptions.new + i18n.load_path = [] + + if File.exist?(File.join(RAILS_ROOT, 'config', 'locales')) + i18n.load_path << Dir[File.join(RAILS_ROOT, 'config', 'locales', '*.{rb,yml}')] + i18n.load_path.flatten! + end + + i18n + end + end +end + +# Needs to be duplicated from Active Support since its needed before Active +# Support is available. Here both Options and Hash are namespaced to prevent +# conflicts with other implementations AND with the classes residing in Active Support. +class Rails::OrderedOptions < Array #:nodoc: + def []=(key, value) + key = key.to_sym + + if pair = find_pair(key) + pair.pop + pair << value + else + self << [key, value] + end + end + + def [](key) + pair = find_pair(key.to_sym) + pair ? pair.last : nil + end + + def method_missing(name, *args) + if name.to_s =~ /(.*)=$/ + self[$1.to_sym] = args.first + else + self[name] + end + end + + private + def find_pair(key) + self.each { |i| return i if i.first == key } + return false + end +end + diff --git a/railties/lib/rails/performance_test_help.rb b/railties/lib/rails/performance_test_help.rb new file mode 100644 index 0000000000..4ac38981d0 --- /dev/null +++ b/railties/lib/rails/performance_test_help.rb @@ -0,0 +1,3 @@ +ActionController::Base.perform_caching = true +ActiveSupport::Dependencies.mechanism = :require +Rails.logger.level = ActiveSupport::BufferedLogger::INFO diff --git a/railties/lib/rails/railties_path.rb b/railties/lib/rails/railties_path.rb new file mode 100644 index 0000000000..e291fc23ea --- /dev/null +++ b/railties/lib/rails/railties_path.rb @@ -0,0 +1 @@ +RAILTIES_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')) diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb new file mode 100644 index 0000000000..68d3acc876 --- /dev/null +++ b/railties/lib/rails/ruby_version_check.rb @@ -0,0 +1,17 @@ +min_release = "1.8.2 (2004-12-25)" +ruby_release = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" +if ruby_release =~ /1\.8\.3/ + abort <<-end_message + + Rails does not work with Ruby version 1.8.3. + Please upgrade to version 1.8.4 or downgrade to 1.8.2. + + end_message +elsif ruby_release < min_release + abort <<-end_message + + Rails requires Ruby version #{min_release} or later. + You're running #{ruby_release}; please upgrade to continue. + + end_message +end diff --git a/railties/lib/rails/rubyprof_ext.rb b/railties/lib/rails/rubyprof_ext.rb new file mode 100644 index 0000000000..f6e90357ce --- /dev/null +++ b/railties/lib/rails/rubyprof_ext.rb @@ -0,0 +1,35 @@ +require 'prof' + +module Prof #:nodoc: + # Adapted from Shugo Maeda's unprof.rb + def self.print_profile(results, io = $stderr) + total = results.detect { |i| + i.method_class.nil? && i.method_id == :"#toplevel" + }.total_time + total = 0.001 if total < 0.001 + + io.puts " %% cumulative self self total" + io.puts " time seconds seconds calls ms/call ms/call name" + + sum = 0.0 + for r in results + sum += r.self_time + + name = if r.method_class.nil? + r.method_id.to_s + elsif r.method_class.is_a?(Class) + "#{r.method_class}##{r.method_id}" + else + "#{r.method_class}.#{r.method_id}" + end + io.printf "%6.2f %8.3f %8.3f %8d %8.2f %8.2f %s\n", + r.self_time / total * 100, + sum, + r.self_time, + r.count, + r.self_time * 1000 / r.count, + r.total_time * 1000 / r.count, + name + end + end +end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb new file mode 100644 index 0000000000..591fd6f6bd --- /dev/null +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -0,0 +1,102 @@ +# Implements the logic behind the rake tasks for annotations like +# +# rake notes +# rake notes:optimize +# +# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>. +# +# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that +# represent the line where the annotation lives, its tag, and its text. Note +# the filename is not stored. +# +# Annotations are looked for in comments and modulus whitespace they have to +# start with the tag optionally followed by a colon. Everything up to the end +# of the line (or closing ERb comment tag) is considered to be their text. +class SourceAnnotationExtractor + class Annotation < Struct.new(:line, :tag, :text) + + # Returns a representation of the annotation that looks like this: + # + # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # + # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. + # Otherwise the string contains just line and text. + def to_s(options={}) + s = "[%3d] " % line + s << "[#{tag}] " if options[:tag] + s << text + end + end + + # Prints all annotations with tag +tag+ under the root directories +app+, +lib+, + # and +test+ (recursively). Only filenames with extension +.builder+, +.rb+, + # +.rxml+, +.rjs+, +.rhtml+, or +.erb+ are taken into account. The +options+ + # hash is passed to each annotation's +to_s+. + # + # This class method is the single entry point for the rake tasks. + def self.enumerate(tag, options={}) + extractor = new(tag) + extractor.display(extractor.find, options) + end + + attr_reader :tag + + def initialize(tag) + @tag = tag + end + + # Returns a hash that maps filenames under +dirs+ (recursively) to arrays + # with their annotations. Only files with annotations are included, and only + # those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+ + # are taken into account. + def find(dirs=%w(app lib test)) + dirs.inject({}) { |h, dir| h.update(find_in(dir)) } + end + + # Returns a hash that maps filenames under +dir+ (recursively) to arrays + # with their annotations. Only files with annotations are included, and only + # those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+ + # are taken into account. + def find_in(dir) + results = {} + + Dir.glob("#{dir}/*") do |item| + next if File.basename(item)[0] == ?. + + if File.directory?(item) + results.update(find_in(item)) + elsif item =~ /\.(builder|(r(?:b|xml|js)))$/ + results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/)) + elsif item =~ /\.(rhtml|erb)$/ + results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/)) + end + end + + results + end + + # If +file+ is the filename of a file that contains annotations this method returns + # a hash with a single entry that maps +file+ to an array of its annotations. + # Otherwise it returns an empty hash. + def extract_annotations_from(file, pattern) + lineno = 0 + result = File.readlines(file).inject([]) do |list, line| + lineno += 1 + next list unless line =~ pattern + list << Annotation.new(lineno, $1, $2) + end + result.empty? ? {} : { file => result } + end + + # Prints the mapping from filenames to annotations in +results+ ordered by filename. + # The +options+ hash is passed to each annotation's +to_s+. + def display(results, options={}) + results.keys.sort.each do |file| + puts "#{file}:" + results[file].each do |note| + puts " * #{note.to_s(options)}" + end + puts + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb new file mode 100644 index 0000000000..aad965306c --- /dev/null +++ b/railties/lib/rails/tasks.rb @@ -0,0 +1,24 @@ +$VERBOSE = nil + +# Load Rails rakefile extensions +%w( + annotations + databases + documentation + framework + gems + log + middleware + misc + routes + statistics + testing + tmp +).each do |task| + load "rails/tasks/#{task}.rake" +end + +# Load any custom rakefile extensions +# TODO: Don't hardcode these paths. +Dir["#{RAILS_ROOT}/vendor/plugins/*/**/tasks/**/*.rake"].sort.each { |ext| load ext } +Dir["#{RAILS_ROOT}/lib/tasks/**/*.rake"].sort.each { |ext| load ext } diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake new file mode 100644 index 0000000000..79973b3145 --- /dev/null +++ b/railties/lib/rails/tasks/annotations.rake @@ -0,0 +1,20 @@ +require 'rails/source_annotation_extractor' + +desc "Enumerate all annotations" +task :notes do + SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", :tag => true +end + +namespace :notes do + ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| + desc "Enumerate all #{annotation} annotations" + task annotation.downcase.intern do + SourceAnnotationExtractor.enumerate annotation + end + end + + desc "Enumerate a custom annotation, specify with ANNOTATION=WTFHAX" + task :custom do + SourceAnnotationExtractor.enumerate ENV['ANNOTATION'] + end +end
\ No newline at end of file diff --git a/railties/lib/rails/tasks/databases.rake b/railties/lib/rails/tasks/databases.rake new file mode 100644 index 0000000000..ed015e7a67 --- /dev/null +++ b/railties/lib/rails/tasks/databases.rake @@ -0,0 +1,469 @@ +namespace :db do + task :load_config => :rails_env do + require 'active_record' + ActiveRecord::Base.configurations = Rails::Configuration.new.database_configuration + end + + namespace :create do + desc 'Create all the local databases defined in config/database.yml' + task :all => :load_config do + ActiveRecord::Base.configurations.each_value do |config| + # Skip entries that don't have a database key, such as the first entry here: + # + # defaults: &defaults + # adapter: mysql + # username: root + # password: + # host: localhost + # + # development: + # database: blog_development + # <<: *defaults + next unless config['database'] + # Only connect to local databases + local_database?(config) { create_database(config) } + end + end + end + + desc 'Create the database defined in config/database.yml for the current RAILS_ENV' + task :create => :load_config do + create_database(ActiveRecord::Base.configurations[RAILS_ENV]) + end + + def create_database(config) + begin + if config['adapter'] =~ /sqlite/ + if File.exist?(config['database']) + $stderr.puts "#{config['database']} already exists" + else + begin + # Create the SQLite database + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.connection + rescue + $stderr.puts $!, *($!.backtrace) + $stderr.puts "Couldn't create database for #{config.inspect}" + end + end + return # Skip the else clause of begin/rescue + else + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.connection + end + rescue + case config['adapter'] + when 'mysql' + @charset = ENV['CHARSET'] || 'utf8' + @collation = ENV['COLLATION'] || 'utf8_unicode_ci' + creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} + begin + ActiveRecord::Base.establish_connection(config.merge('database' => nil)) + ActiveRecord::Base.connection.create_database(config['database'], creation_options) + ActiveRecord::Base.establish_connection(config) + rescue Mysql::Error => sqlerr + if sqlerr.errno == Mysql::Error::ER_ACCESS_DENIED_ERROR + print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>" + root_password = $stdin.gets.strip + grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \ + "TO '#{config['username']}'@'localhost' " \ + "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" + ActiveRecord::Base.establish_connection(config.merge( + 'database' => nil, 'username' => 'root', 'password' => root_password)) + ActiveRecord::Base.connection.create_database(config['database'], creation_options) + ActiveRecord::Base.connection.execute grant_statement + ActiveRecord::Base.establish_connection(config) + else + $stderr.puts sqlerr.error + $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}" + $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] + end + end + when 'postgresql' + @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8' + begin + ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) + ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding)) + ActiveRecord::Base.establish_connection(config) + rescue + $stderr.puts $!, *($!.backtrace) + $stderr.puts "Couldn't create database for #{config.inspect}" + end + end + else + $stderr.puts "#{config['database']} already exists" + end + end + + namespace :drop do + desc 'Drops all the local databases defined in config/database.yml' + task :all => :load_config do + ActiveRecord::Base.configurations.each_value do |config| + # Skip entries that don't have a database key + next unless config['database'] + begin + # Only connect to local databases + local_database?(config) { drop_database(config) } + rescue Exception => e + puts "Couldn't drop #{config['database']} : #{e.inspect}" + end + end + end + end + + desc 'Drops the database for the current RAILS_ENV' + task :drop => :load_config do + config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] + begin + drop_database(config) + rescue Exception => e + puts "Couldn't drop #{config['database']} : #{e.inspect}" + end + end + + def local_database?(config, &block) + if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank? + yield + else + puts "This task only modifies local databases. #{config['database']} is on a remote host." + end + end + + + desc "Migrate the database through scripts in db/migrate and update db/schema.rb by invoking db:schema:dump. Target specific version with VERSION=x. Turn off output with VERBOSE=false." + task :migrate => :environment do + ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + namespace :migrate do + desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.' + task :redo => :environment do + if ENV["VERSION"] + Rake::Task["db:migrate:down"].invoke + Rake::Task["db:migrate:up"].invoke + else + Rake::Task["db:rollback"].invoke + Rake::Task["db:migrate"].invoke + end + end + + desc 'Resets your database using your migrations for the current environment' + task :reset => ["db:drop", "db:create", "db:migrate"] + + desc 'Runs the "up" for a given migration VERSION.' + task :up => :environment do + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + raise "VERSION is required" unless version + ActiveRecord::Migrator.run(:up, "db/migrate/", version) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + desc 'Runs the "down" for a given migration VERSION.' + task :down => :environment do + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + raise "VERSION is required" unless version + ActiveRecord::Migrator.run(:down, "db/migrate/", version) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + end + + desc 'Rolls the schema back to the previous version. Specify the number of steps with STEP=n' + task :rollback => :environment do + step = ENV['STEP'] ? ENV['STEP'].to_i : 1 + ActiveRecord::Migrator.rollback('db/migrate/', step) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + desc 'Pushes the schema to the next version. Specify the number of steps with STEP=n' + task :forward => :environment do + step = ENV['STEP'] ? ENV['STEP'].to_i : 1 + ActiveRecord::Migrator.forward('db/migrate/', step) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' + task :reset => [ 'db:drop', 'db:setup' ] + + desc "Retrieves the charset for the current environment's database" + task :charset => :environment do + config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] + case config['adapter'] + when 'mysql' + ActiveRecord::Base.establish_connection(config) + puts ActiveRecord::Base.connection.charset + when 'postgresql' + ActiveRecord::Base.establish_connection(config) + puts ActiveRecord::Base.connection.encoding + else + puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + end + + desc "Retrieves the collation for the current environment's database" + task :collation => :environment do + config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] + case config['adapter'] + when 'mysql' + ActiveRecord::Base.establish_connection(config) + puts ActiveRecord::Base.connection.collation + else + puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + end + + desc "Retrieves the current schema version number" + task :version => :environment do + puts "Current version: #{ActiveRecord::Migrator.current_version}" + end + + desc "Raises an error if there are pending migrations" + task :abort_if_pending_migrations => :environment do + if defined? ActiveRecord + pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations + + if pending_migrations.any? + puts "You have #{pending_migrations.size} pending migrations:" + pending_migrations.each do |pending_migration| + puts ' %4d %s' % [pending_migration.version, pending_migration.name] + end + abort %{Run "rake db:migrate" to update your database then try again.} + end + end + end + + desc 'Create the database, load the schema, and initialize with the seed data' + task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ] + + desc 'Load the seed data from db/seeds.rb' + task :seed => :environment do + seed_file = File.join(Rails.root, 'db', 'seeds.rb') + load(seed_file) if File.exist?(seed_file) + end + + namespace :fixtures do + desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." + task :load => :environment do + require 'active_record/fixtures' + ActiveRecord::Base.establish_connection(Rails.env) + base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') + fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir + + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file| + Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*')) + end + end + + desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." + task :identify => :environment do + require "active_record/fixtures" + + label, id = ENV["LABEL"], ENV["ID"] + raise "LABEL or ID required" if label.blank? && id.blank? + + puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label + + base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') + Dir["#{base_dir}/**/*.yml"].each do |file| + if data = YAML::load(ERB.new(IO.read(file)).result) + data.keys.each do |key| + key_id = Fixtures.identify(key) + + if key == label || key_id == id.to_i + puts "#{file}: #{key} (#{key_id})" + end + end + end + end + end + end + + namespace :schema do + desc "Create a db/schema.rb file that can be portably used against any DB supported by AR" + task :dump => :environment do + require 'active_record/schema_dumper' + File.open(ENV['SCHEMA'] || "#{RAILS_ROOT}/db/schema.rb", "w") do |file| + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end + Rake::Task["db:schema:dump"].reenable + end + + desc "Load a schema.rb file into the database" + task :load => :environment do + file = ENV['SCHEMA'] || "#{RAILS_ROOT}/db/schema.rb" + if File.exists?(file) + load(file) + else + abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{RAILS_ROOT}/config/environment.rb to prevent active_record from loading: config.frameworks -= [ :active_record ]} + end + end + end + + namespace :structure do + desc "Dump the database structure to a SQL file" + task :dump => :environment do + abcs = ActiveRecord::Base.configurations + case abcs[RAILS_ENV]["adapter"] + when "mysql", "oci", "oracle" + ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) + File.open("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } + when "postgresql" + ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"] + ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"] + ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"] + search_path = abcs[RAILS_ENV]["schema_search_path"] + unless search_path.blank? + search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ") + end + `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}` + raise "Error dumping database" if $?.exitstatus == 1 + when "sqlite", "sqlite3" + dbfile = abcs[RAILS_ENV]["database"] || abcs[RAILS_ENV]["dbfile"] + `#{abcs[RAILS_ENV]["adapter"]} #{dbfile} .schema > db/#{RAILS_ENV}_structure.sql` + when "sqlserver" + `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r` + `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r` + when "firebird" + set_firebird_env(abcs[RAILS_ENV]) + db_string = firebird_db_string(abcs[RAILS_ENV]) + sh "isql -a #{db_string} > #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql" + else + raise "Task not supported by '#{abcs["test"]["adapter"]}'" + end + + if ActiveRecord::Base.connection.supports_migrations? + File.open("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + end + end + end + + namespace :test do + desc "Recreate the test database from the current schema.rb" + task :load => 'db:test:purge' do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + Rake::Task["db:schema:load"].invoke + end + + desc "Recreate the test database from the current environment's database schema" + task :clone => %w(db:schema:dump db:test:load) + + desc "Recreate the test databases from the development structure" + task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do + abcs = ActiveRecord::Base.configurations + case abcs["test"]["adapter"] + when "mysql" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') + IO.readlines("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table| + ActiveRecord::Base.connection.execute(table) + end + when "postgresql" + ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] + ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] + ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] + `psql -U "#{abcs["test"]["username"]}" -f #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}` + when "sqlite", "sqlite3" + dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] + `#{abcs["test"]["adapter"]} #{dbfile} < #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql` + when "sqlserver" + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` + when "oci", "oracle" + ActiveRecord::Base.establish_connection(:test) + IO.readlines("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl| + ActiveRecord::Base.connection.execute(ddl) + end + when "firebird" + set_firebird_env(abcs["test"]) + db_string = firebird_db_string(abcs["test"]) + sh "isql -i #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql #{db_string}" + else + raise "Task not supported by '#{abcs["test"]["adapter"]}'" + end + end + + desc "Empty the test database" + task :purge => :environment do + abcs = ActiveRecord::Base.configurations + case abcs["test"]["adapter"] + when "mysql" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"], abcs["test"]) + when "postgresql" + ActiveRecord::Base.clear_active_connections! + drop_database(abcs['test']) + create_database(abcs['test']) + when "sqlite","sqlite3" + dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] + File.delete(dbfile) if File.exist?(dbfile) + when "sqlserver" + dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-') + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}` + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` + when "oci", "oracle" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| + ActiveRecord::Base.connection.execute(ddl) + end + when "firebird" + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.recreate_database! + else + raise "Task not supported by '#{abcs["test"]["adapter"]}'" + end + end + + desc 'Check for pending migrations and load the test schema' + task :prepare => 'db:abort_if_pending_migrations' do + if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? + Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].invoke + end + end + end + + namespace :sessions do + desc "Creates a sessions migration for use with ActiveRecord::SessionStore" + task :create => :environment do + raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations? + require 'rails/generators' + require 'rails/generators/rails/session_migration/session_migration_generator' + Rails::Generators::SessionMigrationGenerator.start [ ENV["MIGRATION"] || "add_sessions_table" ] + end + + desc "Clear the sessions table" + task :clear => :environment do + ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}" + end + end +end + +def drop_database(config) + case config['adapter'] + when 'mysql' + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.connection.drop_database config['database'] + when /^sqlite/ + require 'pathname' + path = Pathname.new(config['database']) + file = path.absolute? ? path.to_s : File.join(RAILS_ROOT, path) + + FileUtils.rm(file) + when 'postgresql' + ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) + ActiveRecord::Base.connection.drop_database config['database'] + end +end + +def session_table_name + ActiveRecord::SessionStore::Session.table_name +end + +def set_firebird_env(config) + ENV["ISC_USER"] = config["username"].to_s if config["username"] + ENV["ISC_PASSWORD"] = config["password"].to_s if config["password"] +end + +def firebird_db_string(config) + FireRuby::Database.db_string_for(config.symbolize_keys) +end diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake new file mode 100644 index 0000000000..db1939c45f --- /dev/null +++ b/railties/lib/rails/tasks/documentation.rake @@ -0,0 +1,88 @@ +namespace :doc do + desc "Generate documentation for the application. Set custom template with TEMPLATE=/path/to/rdoc/template.rb or title with TITLE=\"Custom Title\"" + Rake::RDocTask.new("app") { |rdoc| + rdoc.rdoc_dir = 'doc/app' + rdoc.template = ENV['template'] if ENV['template'] + rdoc.title = ENV['title'] || "Rails Application Documentation" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.options << '--charset' << 'utf-8' + rdoc.rdoc_files.include('doc/README_FOR_APP') + rdoc.rdoc_files.include('app/**/*.rb') + rdoc.rdoc_files.include('lib/**/*.rb') + } + + desc "Generate documentation for the Rails framework" + Rake::RDocTask.new("rails") { |rdoc| + rdoc.rdoc_dir = 'doc/api' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.title = "Rails Framework Documentation" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('vendor/rails/railties/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/railties/MIT-LICENSE') + rdoc.rdoc_files.include('vendor/rails/railties/README') + rdoc.rdoc_files.include('vendor/rails/railties/lib/{*.rb,commands/*.rb,generators/*.rb}') + rdoc.rdoc_files.include('vendor/rails/activerecord/README') + rdoc.rdoc_files.include('vendor/rails/activerecord/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activerecord/lib/active_record/**/*.rb') + rdoc.rdoc_files.exclude('vendor/rails/activerecord/lib/active_record/vendor/*') + rdoc.rdoc_files.include('vendor/rails/activeresource/README') + rdoc.rdoc_files.include('vendor/rails/activeresource/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activeresource/lib/active_resource.rb') + rdoc.rdoc_files.include('vendor/rails/activeresource/lib/active_resource/*') + rdoc.rdoc_files.include('vendor/rails/actionpack/README') + rdoc.rdoc_files.include('vendor/rails/actionpack/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_controller/**/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_view/**/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionmailer/README') + rdoc.rdoc_files.include('vendor/rails/actionmailer/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionmailer/lib/action_mailer/base.rb') + rdoc.rdoc_files.include('vendor/rails/activesupport/README') + rdoc.rdoc_files.include('vendor/rails/activesupport/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activesupport/lib/active_support/**/*.rb') + } + + plugins = FileList['vendor/plugins/**'].collect { |plugin| File.basename(plugin) } + + desc "Generate documentation for all installed plugins" + task :plugins => plugins.collect { |plugin| "doc:plugins:#{plugin}" } + + desc "Remove plugin documentation" + task :clobber_plugins do + rm_rf 'doc/plugins' rescue nil + end + + desc "Generate Rails guides" + task :guides do + require File.join(RAILTIES_PATH, "guides/rails_guides") + RailsGuides::Generator.new(File.join(RAILS_ROOT, "doc/guides")).generate + end + + namespace :plugins do + # Define doc tasks for each plugin + plugins.each do |plugin| + desc "Generate documentation for the #{plugin} plugin" + task(plugin => :environment) do + plugin_base = "vendor/plugins/#{plugin}" + options = [] + files = Rake::FileList.new + options << "-o doc/plugins/#{plugin}" + options << "--title '#{plugin.titlecase} Plugin Documentation'" + options << '--line-numbers' << '--inline-source' + options << '--charset' << 'utf-8' + options << '-T html' + + files.include("#{plugin_base}/lib/**/*.rb") + if File.exist?("#{plugin_base}/README") + files.include("#{plugin_base}/README") + options << "--main '#{plugin_base}/README'" + end + files.include("#{plugin_base}/CHANGELOG") if File.exist?("#{plugin_base}/CHANGELOG") + + options << files.to_s + + sh %(rdoc #{options * ' '}) + end + end + end +end diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake new file mode 100644 index 0000000000..17e16f26fd --- /dev/null +++ b/railties/lib/rails/tasks/framework.rake @@ -0,0 +1,133 @@ +namespace :rails do + namespace :freeze do + desc "Lock this application to the current gems (by unpacking them into vendor/rails)" + task :gems do + deps = %w(actionpack activerecord actionmailer activesupport activeresource) + require 'rubygems' + require 'rubygems/gem_runner' + + rails = (version = ENV['VERSION']) ? + Gem.cache.find_name('rails', "= #{version}").first : + Gem.cache.find_name('rails').sort_by { |g| g.version }.last + + version ||= rails.version + + unless rails + puts "No rails gem #{version} is installed. Do 'gem list rails' to see what you have available." + exit + end + + puts "Freezing to the gems for Rails #{rails.version}" + rm_rf "vendor/rails" + mkdir_p "vendor/rails" + + begin + chdir("vendor/rails") do + rails.dependencies.select { |g| deps.include? g.name }.each do |g| + Gem::GemRunner.new.run(["unpack", g.name, "--version", g.version_requirements.to_s]) + mv(Dir.glob("#{g.name}*").first, g.name) + end + + Gem::GemRunner.new.run(["unpack", "rails", "--version", "=#{version}"]) + FileUtils.mv(Dir.glob("rails*").first, "railties") + end + rescue Exception + rm_rf "vendor/rails" + raise + end + end + + desc 'Lock to latest Edge Rails, for a specific release use RELEASE=1.2.0' + task :edge do + require 'open-uri' + version = ENV["RELEASE"] || "edge" + target = "rails_#{version}.zip" + commits = "http://github.com/api/v1/yaml/rails/rails/commits/master" + url = "http://dev.rubyonrails.org/archives/#{target}" + + chdir 'vendor' do + latest_revision = YAML.load(open(commits))["commits"].first["id"] + + puts "Downloading Rails from #{url}" + File.open('rails.zip', 'wb') do |dst| + open url do |src| + while chunk = src.read(4096) + dst << chunk + end + end + end + + puts 'Unpacking Rails' + rm_rf 'rails' + `unzip rails.zip` + %w(rails.zip rails/Rakefile rails/cleanlogs.sh rails/pushgems.rb rails/release.rb).each do |goner| + rm_f goner + end + + touch "rails/REVISION_#{latest_revision}" + end + + puts 'Updating current scripts, javascripts, and configuration settings' + Rake::Task['rails:update'].invoke + end + end + + desc "Unlock this application from freeze of gems or edge and return to a fluid use of system gems" + task :unfreeze do + rm_rf "vendor/rails" + end + + desc "Update both configs, scripts and public/javascripts from Rails" + task :update => [ "update:configs", "update:javascripts", "update:scripts", "update:application_controller" ] + + desc "Applies the template supplied by LOCATION=/path/to/template" + task :template do + template = ENV["LOCATION"] + template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} + + require 'generators' + generator = Rails::Generators::App.new [ RAILS_ROOT ], {}, :destination_root => RAILS_ROOT + generator.apply template, :verbose => false + end + + namespace :update do + def invoke_from_app_generator(method) + require 'generators' + require 'rails/generators/rails/app/app_generator' + + generator = Rails::Generators::AppGenerator.new ["rails"], { :with_dispatchers => true }, + :destination_root => RAILS_ROOT + generator.invoke(method) + end + + desc "Update config/boot.rb from your current rails install" + task :configs do + invoke_from_app_generator :create_boot_file + end + + desc "Update Prototype javascripts from your current rails install" + task :javascripts do + invoke_from_app_generator :create_prototype_files + end + + desc "Generate dispatcher files in RAILS_ROOT/public" + task :generate_dispatchers do + invoke_from_app_generator :create_dispatch_files + end + + desc "Add new scripts to the application script/ directory" + task :scripts do + invoke_from_app_generator :create_script_files + end + + desc "Rename application.rb to application_controller.rb" + task :application_controller do + old_style = RAILS_ROOT + '/app/controllers/application.rb' + new_style = RAILS_ROOT + '/app/controllers/application_controller.rb' + if File.exists?(old_style) && !File.exists?(new_style) + FileUtils.mv(old_style, new_style) + puts "#{old_style} has been renamed to #{new_style}, update your SCM as necessary" + end + end + end +end diff --git a/railties/lib/rails/tasks/gems.rake b/railties/lib/rails/tasks/gems.rake new file mode 100644 index 0000000000..f1c34c7cca --- /dev/null +++ b/railties/lib/rails/tasks/gems.rake @@ -0,0 +1,78 @@ +desc "List the gems that this rails application depends on" +task :gems => 'gems:base' do + Rails.configuration.gems.each do |gem| + print_gem_status(gem) + end + puts + puts "I = Installed" + puts "F = Frozen" + puts "R = Framework (loaded before rails starts)" +end + +namespace :gems do + task :base do + $gems_rake_task = true + require 'rubygems' + require 'rubygems/gem_runner' + Rake::Task[:environment].invoke + end + + desc "Build any native extensions for unpacked gems" + task :build do + $gems_build_rake_task = true + frozen_gems.each { |gem| gem.build } + end + + namespace :build do + desc "Force the build of all gems" + task :force do + $gems_build_rake_task = true + frozen_gems.each { |gem| gem.build(:force => true) } + end + end + + desc "Installs all required gems." + task :install => :base do + current_gems.each { |gem| gem.install } + end + + desc "Unpacks all required gems into vendor/gems." + task :unpack => :install do + current_gems.each { |gem| gem.unpack } + end + + namespace :unpack do + desc "Unpacks all required gems and their dependencies into vendor/gems." + task :dependencies => :install do + current_gems.each { |gem| gem.unpack(:recursive => true) } + end + end + + desc "Regenerate gem specifications in correct format." + task :refresh_specs do + frozen_gems(false).each { |gem| gem.refresh } + end +end + +def current_gems + gems = Rails.configuration.gems + gems = gems.select { |gem| gem.name == ENV['GEM'] } unless ENV['GEM'].blank? + gems +end + +def frozen_gems(load_specs=true) + Dir[File.join(RAILS_ROOT, 'vendor', 'gems', '*-*')].map do |gem_dir| + Rails::GemDependency.from_directory_name(gem_dir, load_specs) + end +end + +def print_gem_status(gem, indent=1) + code = case + when gem.framework_gem? then 'R' + when gem.frozen? then 'F' + when gem.installed? then 'I' + else ' ' + end + puts " "*(indent-1)+" - [#{code}] #{gem.name} #{gem.requirement.to_s}" + gem.dependencies.each { |g| print_gem_status(g, indent+1) } +end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake new file mode 100644 index 0000000000..6e1334692e --- /dev/null +++ b/railties/lib/rails/tasks/log.rake @@ -0,0 +1,9 @@ +namespace :log do + desc "Truncates all *.log files in log/ to zero bytes" + task :clear do + FileList["log/*.log"].each do |log_file| + f = File.open(log_file, "w") + f.close + end + end +end diff --git a/railties/lib/rails/tasks/middleware.rake b/railties/lib/rails/tasks/middleware.rake new file mode 100644 index 0000000000..05f159184e --- /dev/null +++ b/railties/lib/rails/tasks/middleware.rake @@ -0,0 +1,7 @@ +desc 'Prints out your Rack middleware stack' +task :middleware => :environment do + ActionController::Dispatcher.middleware.active.each do |middleware| + puts "use #{middleware.inspect}" + end + puts "run ActionController::Dispatcher.new" +end diff --git a/railties/lib/rails/tasks/misc.rake b/railties/lib/rails/tasks/misc.rake new file mode 100644 index 0000000000..fb2fc31dc1 --- /dev/null +++ b/railties/lib/rails/tasks/misc.rake @@ -0,0 +1,67 @@ +task :default => :test +task :environment do + $rails_rake_task = true + require(File.join(RAILS_ROOT, 'config', 'environment')) +end + +task :rails_env do + unless defined? RAILS_ENV + RAILS_ENV = ENV['RAILS_ENV'] ||= 'development' + end +end + +desc 'Generate a crytographically secure secret key. This is typically used to generate a secret for cookie sessions.' +task :secret do + require 'active_support/secure_random' + puts ActiveSupport::SecureRandom.hex(64) +end + +namespace :time do + namespace :zones do + desc 'Displays names of all time zones recognized by the Rails TimeZone class, grouped by offset. Results can be filtered with optional OFFSET parameter, e.g., OFFSET=-6' + task :all do + build_time_zone_list(:all) + end + + desc 'Displays names of US time zones recognized by the Rails TimeZone class, grouped by offset. Results can be filtered with optional OFFSET parameter, e.g., OFFSET=-6' + task :us do + build_time_zone_list(:us_zones) + end + + desc 'Displays names of time zones recognized by the Rails TimeZone class with the same offset as the system local time' + task :local do + require 'active_support' + require 'active_support/time' + jan_offset = Time.now.beginning_of_year.utc_offset + jul_offset = Time.now.beginning_of_year.change(:month => 7).utc_offset + offset = jan_offset < jul_offset ? jan_offset : jul_offset + build_time_zone_list(:all, offset) + end + + # to find UTC -06:00 zones, OFFSET can be set to either -6, -6:00 or 21600 + def build_time_zone_list(method, offset = ENV['OFFSET']) + require 'active_support' + require 'active_support/time' + if offset + offset = if offset.to_s.match(/(\+|-)?(\d+):(\d+)/) + sign = $1 == '-' ? -1 : 1 + hours, minutes = $2.to_f, $3.to_f + ((hours * 3600) + (minutes.to_f * 60)) * sign + elsif offset.to_f.abs <= 13 + offset.to_f * 3600 + else + offset.to_f + end + end + previous_offset = nil + ActiveSupport::TimeZone.__send__(method).each do |zone| + if offset.nil? || offset == zone.utc_offset + puts "\n* UTC #{zone.formatted_offset} *" unless zone.utc_offset == previous_offset + puts zone.name + previous_offset = zone.utc_offset + end + end + puts "\n" + end + end +end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake new file mode 100644 index 0000000000..abbf3258c1 --- /dev/null +++ b/railties/lib/rails/tasks/routes.rake @@ -0,0 +1,18 @@ +desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.' +task :routes => :environment do + all_routes = ENV['CONTROLLER'] ? ActionController::Routing::Routes.routes.select { |route| route.defaults[:controller] == ENV['CONTROLLER'] } : ActionController::Routing::Routes.routes + routes = all_routes.collect do |route| + name = ActionController::Routing::Routes.named_routes.routes.index(route).to_s + verb = route.conditions[:method].to_s.upcase + segs = route.segments.inject("") { |str,s| str << s.to_s } + segs.chop! if segs.length > 1 + reqs = route.requirements.empty? ? "" : route.requirements.inspect + {:name => name, :verb => verb, :segs => segs, :reqs => reqs} + end + name_width = routes.collect {|r| r[:name]}.collect {|n| n.length}.max + verb_width = routes.collect {|r| r[:verb]}.collect {|v| v.length}.max + segs_width = routes.collect {|r| r[:segs]}.collect {|s| s.length}.max + routes.each do |r| + puts "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:segs].ljust(segs_width)} #{r[:reqs]}" + end +end diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake new file mode 100644 index 0000000000..2dcc7bdf9d --- /dev/null +++ b/railties/lib/rails/tasks/statistics.rake @@ -0,0 +1,17 @@ +STATS_DIRECTORIES = [ + %w(Controllers app/controllers), + %w(Helpers app/helpers), + %w(Models app/models), + %w(Libraries lib/), + %w(APIs app/apis), + %w(Integration\ tests test/integration), + %w(Functional\ tests test/functional), + %w(Unit\ tests test/unit) + +].collect { |name, dir| [ name, "#{RAILS_ROOT}/#{dir}" ] }.select { |name, dir| File.directory?(dir) } + +desc "Report code statistics (KLOCs, etc) from the application" +task :stats do + require 'rails/code_statistics' + CodeStatistics.new(*STATS_DIRECTORIES).to_s +end diff --git a/railties/lib/rails/tasks/testing.rake b/railties/lib/rails/tasks/testing.rake new file mode 100644 index 0000000000..fd5e52a05b --- /dev/null +++ b/railties/lib/rails/tasks/testing.rake @@ -0,0 +1,139 @@ +TEST_CHANGES_SINCE = Time.now - 600 + +# Look up tests for recently modified sources. +def recent_tests(source_pattern, test_path, touched_since = 10.minutes.ago) + FileList[source_pattern].map do |path| + if File.mtime(path) > touched_since + tests = [] + source_dir = File.dirname(path).split("/") + source_file = File.basename(path, '.rb') + + # Support subdirs in app/models and app/controllers + modified_test_path = source_dir.length > 2 ? "#{test_path}/" << source_dir[1..source_dir.length].join('/') : test_path + + # For modified files in app/ run the tests for it. ex. /test/functional/account_controller.rb + test = "#{modified_test_path}/#{source_file}_test.rb" + tests.push test if File.exist?(test) + + # For modified files in app, run tests in subdirs too. ex. /test/functional/account/*_test.rb + test = "#{modified_test_path}/#{File.basename(path, '.rb').sub("_controller","")}" + FileList["#{test}/*_test.rb"].each { |f| tests.push f } if File.exist?(test) + + return tests + + end + end.flatten.compact +end + + +# Recreated here from ActiveSupport because :uncommitted needs it before Rails is available +module Kernel + def silence_stderr + old_stderr = STDERR.dup + STDERR.reopen(RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'NUL:' : '/dev/null') + STDERR.sync = true + yield + ensure + STDERR.reopen(old_stderr) + end +end + +desc 'Run all unit, functional and integration tests' +task :test do + errors = %w(test:units test:functionals test:integration).collect do |task| + begin + Rake::Task[task].invoke + nil + rescue => e + task + end + end.compact + abort "Errors running #{errors.to_sentence(:locale => :en)}!" if errors.any? +end + +namespace :test do + Rake::TestTask.new(:recent => "db:test:prepare") do |t| + since = TEST_CHANGES_SINCE + touched = FileList['test/**/*_test.rb'].select { |path| File.mtime(path) > since } + + recent_tests('app/models/**/*.rb', 'test/unit', since) + + recent_tests('app/controllers/**/*.rb', 'test/functional', since) + + t.libs << 'test' + t.verbose = true + t.test_files = touched.uniq + end + Rake::Task['test:recent'].comment = "Test recent changes" + + Rake::TestTask.new(:uncommitted => "db:test:prepare") do |t| + def t.file_list + if File.directory?(".svn") + changed_since_checkin = silence_stderr { `svn status` }.map { |path| path.chomp[7 .. -1] } + elsif File.directory?(".git") + changed_since_checkin = silence_stderr { `git ls-files --modified --others` }.map { |path| path.chomp } + else + abort "Not a Subversion or Git checkout." + end + + models = changed_since_checkin.select { |path| path =~ /app[\\\/]models[\\\/].*\.rb$/ } + controllers = changed_since_checkin.select { |path| path =~ /app[\\\/]controllers[\\\/].*\.rb$/ } + + unit_tests = models.map { |model| "test/unit/#{File.basename(model, '.rb')}_test.rb" } + functional_tests = controllers.map { |controller| "test/functional/#{File.basename(controller, '.rb')}_test.rb" } + + unit_tests.uniq + functional_tests.uniq + end + + t.libs << 'test' + t.verbose = true + end + Rake::Task['test:uncommitted'].comment = "Test changes since last checkin (only Subversion and Git)" + + Rake::TestTask.new(:units => "db:test:prepare") do |t| + t.libs << "test" + t.pattern = 'test/unit/**/*_test.rb' + t.verbose = true + end + Rake::Task['test:units'].comment = "Run the unit tests in test/unit" + + Rake::TestTask.new(:functionals => "db:test:prepare") do |t| + t.libs << "test" + t.pattern = 'test/functional/**/*_test.rb' + t.verbose = true + end + Rake::Task['test:functionals'].comment = "Run the functional tests in test/functional" + + Rake::TestTask.new(:integration => "db:test:prepare") do |t| + t.libs << "test" + t.pattern = 'test/integration/**/*_test.rb' + t.verbose = true + end + Rake::Task['test:integration'].comment = "Run the integration tests in test/integration" + + Rake::TestTask.new(:benchmark => 'db:test:prepare') do |t| + t.libs << 'test' + t.pattern = 'test/performance/**/*_test.rb' + t.verbose = true + t.options = '-- --benchmark' + end + Rake::Task['test:benchmark'].comment = 'Benchmark the performance tests' + + Rake::TestTask.new(:profile => 'db:test:prepare') do |t| + t.libs << 'test' + t.pattern = 'test/performance/**/*_test.rb' + t.verbose = true + end + Rake::Task['test:profile'].comment = 'Profile the performance tests' + + Rake::TestTask.new(:plugins => :environment) do |t| + t.libs << "test" + + if ENV['PLUGIN'] + t.pattern = "vendor/plugins/#{ENV['PLUGIN']}/test/**/*_test.rb" + else + t.pattern = 'vendor/plugins/*/**/test/**/*_test.rb' + end + + t.verbose = true + end + Rake::Task['test:plugins'].comment = "Run the plugin tests in vendor/plugins/*/**/test (or specify with PLUGIN=name)" +end diff --git a/railties/lib/rails/tasks/tmp.rake b/railties/lib/rails/tasks/tmp.rake new file mode 100644 index 0000000000..fea15058bb --- /dev/null +++ b/railties/lib/rails/tasks/tmp.rake @@ -0,0 +1,37 @@ +namespace :tmp do + desc "Clear session, cache, and socket files from tmp/" + task :clear => [ "tmp:sessions:clear", "tmp:cache:clear", "tmp:sockets:clear"] + + desc "Creates tmp directories for sessions, cache, sockets, and pids" + task :create do + FileUtils.mkdir_p(%w( tmp/sessions tmp/cache tmp/sockets tmp/pids )) + end + + namespace :sessions do + desc "Clears all files in tmp/sessions" + task :clear do + FileUtils.rm(Dir['tmp/sessions/[^.]*']) + end + end + + namespace :cache do + desc "Clears all files and directories in tmp/cache" + task :clear do + FileUtils.rm_rf(Dir['tmp/cache/[^.]*']) + end + end + + namespace :sockets do + desc "Clears all files in tmp/sockets" + task :clear do + FileUtils.rm(Dir['tmp/sockets/[^.]*']) + end + end + + namespace :pids do + desc "Clears all files in tmp/pids" + task :clear do + FileUtils.rm(Dir['tmp/pids/[^.]*']) + end + end +end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb new file mode 100644 index 0000000000..8bd4475c7b --- /dev/null +++ b/railties/lib/rails/test_help.rb @@ -0,0 +1,41 @@ +# Make double-sure the RAILS_ENV is set to test, +# so fixtures are loaded to the right database +silence_warnings { RAILS_ENV = "test" } + +require 'rubygems' +gem "rack", "~> 1.0.0" +gem "rack-test", "~> 0.5.0" + +require 'test/unit' +require 'active_support/core_ext/kernel/requires' +require 'action_view/test_case' +require 'action_mailer/test_case' if defined?(ActionMailer) + +if defined?(ActiveRecord) + require 'active_record/test_case' + require 'active_record/fixtures' + + class ActiveSupport::TestCase + include ActiveRecord::TestFixtures + self.fixture_path = "#{RAILS_ROOT}/test/fixtures/" + self.use_instantiated_fixtures = false + self.use_transactional_fixtures = true + end + + ActionController::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + + def create_fixtures(*table_names, &block) + Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, {}, &block) + end +end + +begin + require_library_or_gem 'ruby-debug' + Debugger.start + if Debugger.respond_to?(:settings) + Debugger.settings[:autoeval] = true + Debugger.settings[:autolist] = 1 + end +rescue LoadError + # ruby-debug wasn't available so neither can the debugging be +end diff --git a/railties/lib/rails/vendor/bundler/LICENSE b/railties/lib/rails/vendor/bundler/LICENSE new file mode 100644 index 0000000000..41decca113 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2009 Engine Yard + +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.
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/README.markdown b/railties/lib/rails/vendor/bundler/README.markdown new file mode 100644 index 0000000000..26863e96f2 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/README.markdown @@ -0,0 +1,162 @@ +## Bundler : A gem to bundle gems + + Github: http://github.com/wycats/bundler + Mailing list: http://groups.google.com/group/ruby-bundler + IRC: #carlhuda on freenode + +## Intro + +Bundler is a tool that manages gem dependencies for your ruby application. It +takes a gem manifest file and is able to fetch, download, and install the gems +and all child dependencies specified in this manifest. It can manage any update +to the gem manifest file and update the bundled gems accordingly. It also lets +you run any ruby code in context of the bundled gem environment. + +## Disclaimer + +This project is under rapid development. It is usable today, but there will be +many changes in the near future, including to the Gemfile DSL. We will bump up +versions with changes though. We greatly appreciate feedback. + +## Installation + +Bundler has no dependencies. Just clone the git repository and install the gem +with the following rake task: + + rake install + +## Usage + +Bundler requires a gem manifest file to be created. This should be a file named +`Gemfile` located in the root directory of your application. After the manifest +has been created, in your shell, cd into your application's directory and run +`gem bundle`. This will start the bundling process. + +### Manifest file + +This is where you specify all of your application's dependencies. By default +this should be in a file named `Gemfile` located in your application's root +directory. The following is an example of a potential `Gemfile`. For more +information, please refer to Bundler::ManifestBuilder. + + # Specify a dependency on rails. When the bundler downloads gems, + # it will download rails as well as all of rails' dependencies (such as + # activerecord, actionpack, etc...) + # + # At least one dependency must be specified + gem "rails" + + # Specify a dependency on rack v.1.0.0. The version is optional. If present, + # it can be specified the same way as with rubygems' #gem method. + gem "rack", "1.0.0" + + # Specify a dependency rspec, but only activate that gem in the "testing" + # environment (read more about environments later). :except is also a valid + # option to specify environment restrictions. + gem "rspec", :only => :testing + + # Add http://gems.github.com as a source that the bundler will use + # to find gems listed in the manifest. By default, + # http://gems.rubyforge.org is already added to the list. + # + # This is an optional setting. + source "http://gems.github.com" + + # Specify where the bundled gems should be stashed. This directory will + # be a gem repository where all gems are downloaded to and installed to. + # + # This is an optional setting. + # The default is: vendor/gems + bundle_path "my/bundled/gems" + + # Specify where gem executables should be copied to. + # + # This is an optional setting. + # The default is: bin + bin_path "my/executables" + + # Specify that rubygems should be completely disabled. This means that it + # will be impossible to require it and that available gems will be + # limited exclusively to gems that have been bundled. + # + # The default is to automatically require rubygems. There is also a + # `disable_system_gems` option that will limit available rubygems to + # the ones that have been bundled. + disable_rubygems + +### Running Bundler + +Once a manifest file has been created, the only thing that needs to be done +is to run the `gem bundle` command anywhere in your application. The script +will load the manifest file, resole all the dependencies, download all +needed gems, and install them into the specified directory. + +Every time an update is made to the manifest file, run `gem bundle` again to +get the changes installed. This will only check the remote sources if your +currently installed gems do not satisfy the `Gemfile`. If you want to force +checking for updates on the remote sources, use the `--update` option. + +### Running your application + +The easiest way to run your application is to start it with an executable +copied to the specified bin directory (by default, simply bin). For example, +if the application in question is a rack app, start it with `bin/rackup`. +This will automatically set the gem environment correctly. + +Another way to run arbitrary ruby code in context of the bundled gems is to +run it with the `gem exec` command. For example: + + gem exec ruby my_ruby_script.rb + +Yet another way is to manually require the environment file first. This is +located in `[bundle_path]/environments/default.rb`. For example: + + ruby -r vendor/gems/environment.rb my_ruby_script.rb + +### Using Bundler with Rails today + +It should be possible to use Bundler with Rails today. Here are the steps +to follow. + +* In your rails app, create a Gemfile and specify the gems that your + application depends on. Make sure to specify rails as well: + + gem "rails", "2.1.2" + gem "will_paginate" + + # Optionally, you can disable system gems all together and only + # use bundled gems. + disable_system_gems + +* Run `gem bundle` + +* You can now use rails if you prepend `gem exec` to every call to `script/*` + but that isn't fun. + +* At the top of `config/boot.rb`, add the following line: + + require File.expand_path(File.join(File.dirname(__FILE__), '..', 'vendor', 'gems', 'environment')) + +In theory, this should be enough to get going. + +## To require rubygems or not + +Ideally, no gem would assume the presence of rubygems at runtime. Rubygems provides +enough features so that this isn't necessary. However, there are a number of gems +that require specific rubygem features. + +If the `disable_rubygems` option is used, Bundler will stub out the most common +of these features, but it is possible that things will not go as intended quite +yet. So, if you are brave, try your code without rubygems at runtime. + +## Known Issues + +* When a gem points to a git repository, the git repository will be cloned + every time Bundler does a gem dependency resolve. + +## Reporting bugs + +Please report all bugs on the github issue tracker for the project located +at: + + http://github.com/wycats/bundler/issues/
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/Rakefile b/railties/lib/rails/vendor/bundler/Rakefile new file mode 100644 index 0000000000..dc4c3d6d46 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/Rakefile @@ -0,0 +1,57 @@ +require 'rubygems' unless ENV['NO_RUBYGEMS'] +require 'rubygems/specification' +require 'date' + +spec = Gem::Specification.new do |s| + s.name = "bundler" + s.version = "0.5.0.pre" + s.author = "Yehuda Katz" + s.email = "wycats@gmail.com" + s.homepage = "http://github.com/wycats/bundler" + s.description = s.summary = "An easy way to vendor gem dependencies" + + s.platform = Gem::Platform::RUBY + s.has_rdoc = true + s.extra_rdoc_files = ["README.markdown", "LICENSE"] + + s.required_rubygems_version = ">= 1.3.5" + + s.require_path = 'lib' + s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("lib/**/*") +end + +task :default => :spec + +begin + require 'spec/rake/spectask' +rescue LoadError + task(:spec) { $stderr.puts '`gem install rspec` to run specs' } +else + desc "Run specs" + Spec::Rake::SpecTask.new do |t| + t.spec_files = FileList['spec/**/*_spec.rb'] - FileList['spec/fixtures/**/*_spec.rb'] + t.spec_opts = %w(-fs --color) + end +end + +begin + require 'rake/gempackagetask' +rescue LoadError + task(:gem) { $stderr.puts '`gem install rake` to package gems' } +else + Rake::GemPackageTask.new(spec) do |pkg| + pkg.gem_spec = spec + end +end + +desc "install the gem locally" +task :install => [:package] do + sh %{gem install pkg/#{spec.name}-#{spec.version}} +end + +desc "create a gemspec file" +task :make_spec do + File.open("#{spec.name}.gemspec", "w") do |file| + file.puts spec.to_ruby + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler.rb b/railties/lib/rails/vendor/bundler/lib/bundler.rb new file mode 100644 index 0000000000..1ede3517dd --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler.rb @@ -0,0 +1,34 @@ +require 'pathname' +require 'logger' +require 'set' +require 'erb' +# Required elements of rubygems +require "rubygems/remote_fetcher" +require "rubygems/installer" + +require "bundler/gem_bundle" +require "bundler/source" +require "bundler/finder" +require "bundler/gem_ext" +require "bundler/resolver" +require "bundler/environment" +require "bundler/dsl" +require "bundler/cli" +require "bundler/repository" +require "bundler/dependency" + +module Bundler + VERSION = "0.5.0" + + class << self + attr_writer :logger + + def logger + @logger ||= begin + logger = Logger.new(STDOUT, Logger::INFO) + logger.formatter = proc {|_,_,_,msg| "#{msg}\n" } + logger + end + end + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/cli.rb b/railties/lib/rails/vendor/bundler/lib/bundler/cli.rb new file mode 100644 index 0000000000..df9181fbc4 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/cli.rb @@ -0,0 +1,44 @@ +require "optparse" + +module Bundler + class CLI + def self.run(command, options = {}) + new(options).run(command) + rescue DefaultManifestNotFound => e + Bundler.logger.error "Could not find a Gemfile to use" + exit 2 + rescue InvalidEnvironmentName => e + Bundler.logger.error "Gemfile error: #{e.message}" + exit + rescue InvalidRepository => e + Bundler.logger.error e.message + exit + rescue VersionConflict => e + Bundler.logger.error e.message + exit + rescue GemNotFound => e + Bundler.logger.error e.message + exit + end + + def initialize(options) + @options = options + @manifest = Bundler::Environment.load(@options[:manifest]) + end + + def bundle + @manifest.install(@options[:update]) + end + + def exec + @manifest.setup_environment + # w0t? + super(*@options[:args]) + end + + def run(command) + send(command) + end + + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/commands/bundle_command.rb b/railties/lib/rails/vendor/bundler/lib/bundler/commands/bundle_command.rb new file mode 100644 index 0000000000..a1f9590f75 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/commands/bundle_command.rb @@ -0,0 +1,31 @@ +class Gem::Commands::BundleCommand < Gem::Command + + def initialize + super('bundle', 'Create a gem bundle based on your Gemfile', {:manifest => nil, :update => false}) + + add_option('-m', '--manifest MANIFEST', "Specify the path to the manifest file") do |manifest, options| + options[:manifest] = manifest + end + + add_option('-u', '--update', "Force a remote check for newer gems") do + options[:update] = true + end + end + + def usage + "#{program_name}" + end + + def description # :nodoc: + <<-EOF +Bundle stuff + EOF + end + + def execute + # Prevent the bundler from getting required unless it is actually being used + require 'bundler' + Bundler::CLI.run(:bundle, options) + end + +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/commands/exec_command.rb b/railties/lib/rails/vendor/bundler/lib/bundler/commands/exec_command.rb new file mode 100644 index 0000000000..228aa60619 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/commands/exec_command.rb @@ -0,0 +1,31 @@ +class Gem::Commands::ExecCommand < Gem::Command + + def initialize + super('exec', 'Run a command in context of a gem bundle', {:manifest => nil}) + + add_option('-m', '--manifest MANIFEST', "Specify the path to the manifest file") do |manifest, options| + options[:manifest] = manifest + end + end + + def usage + "#{program_name} COMMAND" + end + + def arguments # :nodoc: + "COMMAND command to run in context of the gem bundle" + end + + def description # :nodoc: + <<-EOF.gsub(' ', '') + Run in context of a bundle + EOF + end + + def execute + # Prevent the bundler from getting required unless it is actually being used + require 'bundler' + Bundler::CLI.run(:exec, options) + end + +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/dependency.rb b/railties/lib/rails/vendor/bundler/lib/bundler/dependency.rb new file mode 100644 index 0000000000..b627b58662 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/dependency.rb @@ -0,0 +1,56 @@ +module Bundler + class InvalidEnvironmentName < StandardError; end + + class Dependency + attr_reader :name, :version, :require_as, :only, :except + + def initialize(name, options = {}, &block) + options.each do |k, v| + options[k.to_s] = v + end + + @name = name + @version = options["version"] || ">= 0" + @require_as = Array(options["require_as"] || name) + @only = options["only"] + @except = options["except"] + @block = block + + if (@only && @only.include?("rubygems")) || (@except && @except.include?("rubygems")) + raise InvalidEnvironmentName, "'rubygems' is not a valid environment name" + end + end + + def in?(environment) + environment = environment.to_s + + return false unless !@only || @only.include?(environment) + return false if @except && @except.include?(environment) + true + end + + def to_s + to_gem_dependency.to_s + end + + def require(environment) + return unless in?(environment) + + @require_as.each do |file| + super(file) + end + + @block.call if @block + end + + def to_gem_dependency + @gem_dep ||= Gem::Dependency.new(name, version) + end + + def ==(o) + [name, version, require_as, only, except] == + [o.name, o.version, o.require_as, o.only, o.except] + end + + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/dsl.rb b/railties/lib/rails/vendor/bundler/lib/bundler/dsl.rb new file mode 100644 index 0000000000..d9a86ee1fd --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/dsl.rb @@ -0,0 +1,109 @@ +module Bundler + class ManifestFileNotFound < StandardError; end + + class Dsl + def initialize(environment) + @environment = environment + @sources = Hash.new { |h,k| h[k] = {} } + end + + def bundle_path(path) + path = Pathname.new(path) + @environment.gem_path = (path.relative? ? + @environment.root.join(path) : path).expand_path + end + + def bin_path(path) + path = Pathname.new(path) + @environment.bindir = (path.relative? ? + @environment.root.join(path) : path).expand_path + end + + def disable_rubygems + @environment.rubygems = false + end + + def disable_system_gems + @environment.system_gems = false + end + + def source(source) + source = GemSource.new(:uri => source) + unless @environment.sources.include?(source) + @environment.add_source(source) + end + end + + def only(env) + old, @only = @only, _combine_onlys(env) + yield + @only = old + end + + def except(env) + old, @except = @except, _combine_excepts(env) + yield + @except = old + end + + def clear_sources + @environment.clear_sources + end + + def gem(name, *args) + options = args.last.is_a?(Hash) ? args.pop : {} + version = args.last + + options[:only] = _combine_onlys(options[:only] || options["only"]) + options[:except] = _combine_excepts(options[:except] || options["except"]) + + dep = Dependency.new(name, options.merge(:version => version)) + + # OMG REFACTORZ. KTHX + if vendored_at = options[:vendored_at] + vendored_at = Pathname.new(vendored_at) + vendored_at = @environment.filename.dirname.join(vendored_at) if vendored_at.relative? + + @sources[:directory][vendored_at.to_s] ||= begin + source = DirectorySource.new( + :name => name, + :version => version, + :location => vendored_at + ) + @environment.add_priority_source(source) + true + end + elsif git = options[:git] + @sources[:git][git] ||= begin + source = GitSource.new( + :name => name, + :version => version, + :uri => git, + :ref => options[:commit] || options[:tag], + :branch => options[:branch] + ) + @environment.add_priority_source(source) + true + end + end + + @environment.dependencies << dep + end + + private + + def _combine_onlys(only) + return @only unless only + only = [only].flatten.compact.uniq.map { |o| o.to_s } + only &= @only if @only + only + end + + def _combine_excepts(except) + return @except unless except + except = [except].flatten.compact.uniq.map { |o| o.to_s } + except |= @except if @except + except + end + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/environment.rb b/railties/lib/rails/vendor/bundler/lib/bundler/environment.rb new file mode 100644 index 0000000000..f07a9e2c6f --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/environment.rb @@ -0,0 +1,111 @@ +require "rubygems/source_index" + +module Bundler + class DefaultManifestNotFound < StandardError; end + + class Environment + attr_reader :filename, :dependencies + attr_accessor :rubygems, :system_gems, :gem_path, :bindir + + def self.load(gemfile = nil) + gemfile = gemfile ? Pathname.new(gemfile) : default_manifest_file + + unless gemfile.file? + raise ManifestFileNotFound, "#{filename.inspect} does not exist" + end + + new(gemfile) + end + + def self.default_manifest_file + current = Pathname.new(Dir.pwd) + + until current.root? + filename = current.join("Gemfile") + return filename if filename.exist? + current = current.parent + end + + raise DefaultManifestNotFound + end + + def initialize(filename) #, sources, dependencies, bindir, path, rubygems, system_gems) + @filename = filename + @default_sources = [GemSource.new(:uri => "http://gems.rubyforge.org")] + @sources = [] + @priority_sources = [] + @dependencies = [] + @rubygems = true + @system_gems = true + + # Evaluate the Gemfile + builder = Dsl.new(self) + builder.instance_eval(File.read(filename)) + end + + def install(update = false) + begin + tmp_path = filename.dirname.join(".tmp") + FileUtils.mkdir_p(tmp_path) + sources.each { |s| s.tmp_path = tmp_path } + repository.install(gem_dependencies, sources, + :rubygems => rubygems, + :system_gems => system_gems, + :manifest => filename, + :update => update + ) + ensure + FileUtils.rm_rf(tmp_path) + end + Bundler.logger.info "Done." + end + + def setup_environment + unless system_gems + ENV["GEM_HOME"] = gem_path + ENV["GEM_PATH"] = gem_path + end + ENV["PATH"] = "#{bindir}:#{ENV["PATH"]}" + ENV["RUBYOPT"] = "-r#{gem_path}/environment #{ENV["RUBYOPT"]}" + end + + def root + filename.parent + end + + def gem_path + @gem_path ||= root.join("vendor", "gems") + end + + def bindir + @bindir ||= root.join("bin") + end + + def sources + @priority_sources + @sources + @default_sources + end + + def add_source(source) + @sources << source + end + + def add_priority_source(source) + @priority_sources << source + end + + def clear_sources + @sources.clear + @default_sources.clear + end + + private + + def repository + @repository ||= Repository.new(gem_path, bindir) + end + + def gem_dependencies + @gem_dependencies ||= dependencies.map { |d| d.to_gem_dependency } + end + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/finder.rb b/railties/lib/rails/vendor/bundler/lib/bundler/finder.rb new file mode 100644 index 0000000000..b77ca65709 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/finder.rb @@ -0,0 +1,51 @@ +module Bundler + # Finder behaves like a rubygems source index in that it responds + # to #search. It also resolves a list of dependencies finding the + # best possible configuration of gems that satisifes all requirements + # without causing any gem activation errors. + class Finder + + # Takes an array of gem sources and fetches the full index of + # gems from each one. It then combines the indexes together keeping + # track of the original source so that any resolved gem can be + # fetched from the correct source. + # + # ==== Parameters + # *sources<String>:: URI pointing to the gem repository + def initialize(*sources) + @cache = {} + @index = {} + @sources = sources + end + + # Searches for a gem that matches the dependency + # + # ==== Parameters + # dependency<Gem::Dependency>:: The gem dependency to search for + # + # ==== Returns + # [Gem::Specification]:: A collection of gem specifications + # matching the search + def search(dependency) + @cache[dependency.hash] ||= begin + find_by_name(dependency.name).select do |spec| + dependency =~ spec + end.sort_by {|s| s.version } + end + end + + private + + def find_by_name(name) + matches = @index[name] ||= begin + versions = {} + @sources.reverse_each do |source| + versions.merge! source.specs[name] || {} + end + versions + end + matches.values + end + + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/gem_bundle.rb b/railties/lib/rails/vendor/bundler/lib/bundler/gem_bundle.rb new file mode 100644 index 0000000000..80d7710683 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/gem_bundle.rb @@ -0,0 +1,11 @@ +module Bundler + class GemBundle < Array + def download(repository) + sort_by {|s| s.full_name.downcase }.each do |spec| + spec.source.download(spec, repository) + end + + self + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/gem_ext.rb b/railties/lib/rails/vendor/bundler/lib/bundler/gem_ext.rb new file mode 100644 index 0000000000..155ad04c7e --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/gem_ext.rb @@ -0,0 +1,25 @@ +module Gem + class Installer + def app_script_text(bin_file_name) + path = @gem_home + template = File.read(File.join(File.dirname(__FILE__), "templates", "app_script.erb")) + erb = ERB.new(template, nil, '-') + erb.result(binding) + end + end + + class Specification + attr_accessor :source + attr_accessor :location + + # Hack to fix github's strange marshal file + def specification_version + @specification_version && @specification_version.to_i + end + + alias full_gem_path_without_location full_gem_path + def full_gem_path + @location ? @location : full_gem_path_without_location + end + end +end diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/repository.rb b/railties/lib/rails/vendor/bundler/lib/bundler/repository.rb new file mode 100644 index 0000000000..1a1dc7497d --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/repository.rb @@ -0,0 +1,151 @@ +require "bundler/repository/gem_repository" +require "bundler/repository/directory_repository" + +module Bundler + class InvalidRepository < StandardError ; end + + class Repository + attr_reader :path + + def initialize(path, bindir) + FileUtils.mkdir_p(path) + + @path = Pathname.new(path) + @bindir = Pathname.new(bindir) + + @repos = { + :gem => Gems.new(@path, @bindir), + :directory => Directory.new(@path.join("dirs"), @bindir) + } + end + + def install(dependencies, sources, options = {}) + if options[:update] || !satisfies?(dependencies) + fetch(dependencies, sources) + expand(options) + else + # Remove any gems that are still around if the Gemfile changed without + # requiring new gems to be download (e.g. a line in the Gemfile was + # removed) + cleanup(Resolver.resolve(dependencies, [source_index])) + end + configure(options) + sync + end + + def gems + gems = [] + each_repo do |repo| + gems.concat repo.gems + end + gems + end + + def satisfies?(dependencies) + index = source_index + dependencies.all? { |dep| index.search(dep).size > 0 } + end + + def source_index + index = Gem::SourceIndex.new + + each_repo do |repo| + index.gems.merge!(repo.source_index.gems) + end + + index + end + + def add_spec(type, spec) + @repos[type].add_spec(spec) + end + + def download_path_for(type) + @repos[type].download_path_for + end + + private + + def cleanup(bundle) + each_repo do |repo| + repo.cleanup(bundle) + end + end + + def each_repo + @repos.each do |k, repo| + yield repo + end + end + + def fetch(dependencies, sources) + bundle = Resolver.resolve(dependencies, sources) + # Cleanup here to remove any gems that could cause problem in the expansion + # phase + # + # TODO: Try to avoid double cleanup + cleanup(bundle) + bundle.download(self) + end + + def sync + glob = gems.map { |g| g.executables }.flatten.join(',') + + (Dir[@bindir.join("*")] - Dir[@bindir.join("{#{glob}}")]).each do |file| + Bundler.logger.info "Deleting bin file: #{File.basename(file)}" + FileUtils.rm_rf(file) + end + end + + def expand(options) + each_repo do |repo| + repo.expand(options) + end + end + + def configure(options) + generate_environment(options) + end + + def generate_environment(options) + FileUtils.mkdir_p(path) + + specs = gems + load_paths = load_paths_for_specs(specs) + bindir = @bindir.relative_path_from(path).to_s + filename = options[:manifest].relative_path_from(path).to_s + spec_files = specs.inject({}) do |hash, spec| + relative = spec.loaded_from.relative_path_from(@path).to_s + hash.merge!(spec.name => relative) + end + + File.open(path.join("environment.rb"), "w") do |file| + template = File.read(File.join(File.dirname(__FILE__), "templates", "environment.erb")) + erb = ERB.new(template, nil, '-') + file.puts erb.result(binding) + end + end + + def load_paths_for_specs(specs) + load_paths = [] + specs.each do |spec| + gem_path = Pathname.new(spec.full_gem_path) + if spec.bindir + load_paths << gem_path.join(spec.bindir).relative_path_from(@path).to_s + end + spec.require_paths.each do |path| + load_paths << gem_path.join(path).relative_path_from(@path).to_s + end + end + load_paths + end + + def require_code(file, dep) + constraint = case + when dep.only then %{ if #{dep.only.inspect}.include?(env)} + when dep.except then %{ unless #{dep.except.inspect}.include?(env)} + end + "require #{file.inspect}#{constraint}" + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/repository/directory_repository.rb b/railties/lib/rails/vendor/bundler/lib/bundler/repository/directory_repository.rb new file mode 100644 index 0000000000..e97dd38dd5 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/repository/directory_repository.rb @@ -0,0 +1,46 @@ +module Bundler + class Repository + class Directory + attr_reader :path, :bindir + + def initialize(path, bindir) + @path = path + @bindir = bindir + + FileUtils.mkdir_p(path.to_s) + end + + def source_index + index = Gem::SourceIndex.from_gems_in(@path.join("specifications")) + index.each { |n, spec| spec.loaded_from = @path.join("specifications", "#{spec.full_name}.gemspec") } + index + end + + def gems + source_index.gems.values + end + + def add_spec(spec) + destination = path.join('specifications') + destination.mkdir unless destination.exist? + + File.open(destination.join("#{spec.full_name}.gemspec"), 'w') do |f| + f.puts spec.to_ruby + end + end + + def download_path_for + @path.join("dirs") + end + + # Checks whether a gem is installed + def expand(options) + # raise NotImplementedError + end + + def cleanup(gems) + # raise NotImplementedError + end + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/repository/gem_repository.rb b/railties/lib/rails/vendor/bundler/lib/bundler/repository/gem_repository.rb new file mode 100644 index 0000000000..90de49d83d --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/repository/gem_repository.rb @@ -0,0 +1,108 @@ +module Bundler + class Repository + class Gems + attr_reader :path, :bindir + + def initialize(path, bindir) + @path = path + @bindir = bindir + end + + # Returns the source index for all gems installed in the + # repository + def source_index + index = Gem::SourceIndex.from_gems_in(@path.join("specifications")) + index.each { |n, spec| spec.loaded_from = @path.join("specifications", "#{spec.full_name}.gemspec") } + index + end + + def gems + source_index.gems.values + end + + # Checks whether a gem is installed + def expand(options) + cached_gems.each do |name, version| + unless installed?(name, version) + install_cached_gem(name, version, options) + end + end + end + + def cleanup(gems) + glob = gems.map { |g| g.full_name }.join(',') + base = path.join("{cache,specifications,gems}") + + (Dir[base.join("*")] - Dir[base.join("{#{glob}}{.gemspec,.gem,}")]).each do |file| + if File.basename(file) =~ /\.gem$/ + name = File.basename(file, '.gem') + Bundler.logger.info "Deleting gem: #{name}" + end + FileUtils.rm_rf(file) + end + end + + def add_spec(spec) + raise NotImplementedError + end + + def download_path_for + path + end + + private + + def cache_path + @path.join("cache") + end + + def cache_files + Dir[cache_path.join("*.gem")] + end + + def cached_gems + cache_files.map do |f| + full_name = File.basename(f).gsub(/\.gem$/, '') + full_name.split(/-(?=[^-]+$)/) + end + end + + def spec_path + @path.join("specifications") + end + + def spec_files + Dir[spec_path.join("*.gemspec")] + end + + def gem_path + @path.join("gems") + end + + def gem_paths + Dir[gem_path.join("*")] + end + + def installed?(name, version) + spec_files.any? { |g| File.basename(g) == "#{name}-#{version}.gemspec" } && + gem_paths.any? { |g| File.basename(g) == "#{name}-#{version}" } + end + + def install_cached_gem(name, version, options = {}) + cached_gem = cache_path.join("#{name}-#{version}.gem") + # TODO: Add a warning if cached_gem is not a file + if cached_gem.file? + Bundler.logger.info "Installing #{name}-#{version}.gem" + installer = Gem::Installer.new(cached_gem.to_s, options.merge( + :install_dir => @path, + :ignore_dependencies => true, + :env_shebang => true, + :wrappers => true, + :bin_dir => @bindir + )) + installer.install + end + end + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/resolver.rb b/railties/lib/rails/vendor/bundler/lib/bundler/resolver.rb new file mode 100644 index 0000000000..2a6a6371c2 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/resolver.rb @@ -0,0 +1,189 @@ +# This is the latest iteration of the gem dependency resolving algorithm. As of now, +# it can resolve (as a success of failure) any set of gem dependencies we throw at it +# in a reasonable amount of time. The most iterations I've seen it take is about 150. +# The actual implementation of the algorithm is not as good as it could be yet, but that +# can come later. + +# Extending Gem classes to add necessary tracking information +module Gem + class Dependency + def required_by + @required_by ||= [] + end + end + class Specification + def required_by + @required_by ||= [] + end + end +end + +module Bundler + class GemNotFound < StandardError; end + class VersionConflict < StandardError; end + + class Resolver + + attr_reader :errors + + # Figures out the best possible configuration of gems that satisfies + # the list of passed dependencies and any child dependencies without + # causing any gem activation errors. + # + # ==== Parameters + # *dependencies<Gem::Dependency>:: The list of dependencies to resolve + # + # ==== Returns + # <GemBundle>,nil:: If the list of dependencies can be resolved, a + # collection of gemspecs is returned. Otherwise, nil is returned. + def self.resolve(requirements, sources) + Bundler.logger.info "Calculating dependencies..." + + resolver = new(sources) + result = catch(:success) do + resolver.resolve(requirements, {}) + output = resolver.errors.inject("") do |o, (conflict, (origin, requirement))| + o << " Conflict on: #{conflict.inspect}:\n" + o << " * #{conflict} (#{origin.version}) activated by #{origin.required_by.first}\n" + o << " * #{requirement} required by #{requirement.required_by.first}\n" + o << " All possible versions of origin requirements conflict." + end + raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{output}" + nil + end + result && GemBundle.new(result.values) + end + + def initialize(sources) + @errors = {} + @stack = [] + @specs = Hash.new { |h,k| h[k] = {} } + @cache = {} + @index = {} + + sources.reverse_each do |source| + source.gems.values.each do |spec| + # TMP HAX FOR OPTZ + spec.source = source + next unless Gem::Platform.match(spec.platform) + @specs[spec.name][spec.version] = spec + end + end + end + + def resolve(reqs, activated) + # If the requirements are empty, then we are in a success state. Aka, all + # gem dependencies have been resolved. + throw :success, activated if reqs.empty? + + # Sort requirements so that the ones that are easiest to resolve are first. + # Easiest to resolve is defined by: Is this gem already activated? Otherwise, + # check the number of child dependencies this requirement has. + reqs = reqs.sort_by do |req| + activated[req.name] ? 0 : search(req).size + end + + activated = activated.dup + # Pull off the first requirement so that we can resolve it + current = reqs.shift + + # Check if the gem has already been activated, if it has, we will make sure + # that the currently activated gem satisfies the requirement. + if existing = activated[current.name] + if current.version_requirements.satisfied_by?(existing.version) + @errors.delete(existing.name) + # Since the current requirement is satisfied, we can continue resolving + # the remaining requirements. + resolve(reqs, activated) + else + @errors[existing.name] = [existing, current] + # Since the current requirement conflicts with an activated gem, we need + # to backtrack to the current requirement's parent and try another version + # of it (maybe the current requirement won't be present anymore). If the + # current requirement is a root level requirement, we need to jump back to + # where the conflicting gem was activated. + parent = current.required_by.last || existing.required_by.last + # We track the spot where the current gem was activated because we need + # to keep a list of every spot a failure happened. + throw parent.name, existing.required_by.last.name + end + else + # There are no activated gems for the current requirement, so we are going + # to find all gems that match the current requirement and try them in decending + # order. We also need to keep a set of all conflicts that happen while trying + # this gem. This is so that if no versions work, we can figure out the best + # place to backtrack to. + conflicts = Set.new + + # Fetch all gem versions matching the requirement + # + # TODO: Warn / error when no matching versions are found. + matching_versions = search(current) + + if matching_versions.empty? + if current.required_by.empty? + raise GemNotFound, "Could not find gem '#{current}' in any of the sources" + end + Bundler.logger.warn "Could not find gem '#{current}' (required by '#{current.required_by.last}') in any of the sources" + end + + matching_versions.reverse_each do |spec| + conflict = resolve_requirement(spec, current, reqs.dup, activated.dup) + conflicts << conflict if conflict + end + # If the current requirement is a root level gem and we have conflicts, we + # can figure out the best spot to backtrack to. + if current.required_by.empty? && !conflicts.empty? + # Check the current "catch" stack for the first one that is included in the + # conflicts set. That is where the parent of the conflicting gem was required. + # By jumping back to this spot, we can try other version of the parent of + # the conflicting gem, hopefully finding a combination that activates correctly. + @stack.reverse_each do |savepoint| + if conflicts.include?(savepoint) + throw savepoint + end + end + end + end + end + + def resolve_requirement(spec, requirement, reqs, activated) + # We are going to try activating the spec. We need to keep track of stack of + # requirements that got us to the point of activating this gem. + spec.required_by.replace requirement.required_by + spec.required_by << requirement + + activated[spec.name] = spec + + # Now, we have to loop through all child dependencies and add them to our + # array of requirements. + spec.dependencies.each do |dep| + next if dep.type == :development + dep.required_by << requirement + reqs << dep + end + + # We create a savepoint and mark it by the name of the requirement that caused + # the gem to be activated. If the activated gem ever conflicts, we are able to + # jump back to this point and try another version of the gem. + length = @stack.length + @stack << requirement.name + retval = catch(requirement.name) do + resolve(reqs, activated) + end + # Since we're doing a lot of throw / catches. A push does not necessarily match + # up to a pop. So, we simply slice the stack back to what it was before the catch + # block. + @stack.slice!(length..-1) + retval + end + + def search(dependency) + @cache[dependency.hash] ||= begin + @specs[dependency.name].values.select do |spec| + dependency =~ spec + end.sort_by {|s| s.version } + end + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/runtime.rb b/railties/lib/rails/vendor/bundler/lib/bundler/runtime.rb new file mode 100644 index 0000000000..27e0254966 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/runtime.rb @@ -0,0 +1,2 @@ +require File.join(File.dirname(__FILE__), "runtime", "dsl") +require File.join(File.dirname(__FILE__), "runtime", "dependency")
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/source.rb b/railties/lib/rails/vendor/bundler/lib/bundler/source.rb new file mode 100644 index 0000000000..37828ca316 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/source.rb @@ -0,0 +1,150 @@ +module Bundler + # Represents a source of rubygems. Initially, this is only gem repositories, but + # eventually, this will be git, svn, HTTP + class Source + attr_accessor :tmp_path + end + + class GemSource < Source + attr_reader :uri + + def initialize(options) + @uri = options[:uri] + @uri = URI.parse(@uri) unless @uri.is_a?(URI) + raise ArgumentError, "The source must be an absolute URI" unless @uri.absolute? + end + + def gems + @specs ||= fetch_specs + end + + def ==(other) + uri == other.uri + end + + def to_s + @uri.to_s + end + + class RubygemsRetardation < StandardError; end + + def download(spec, repository) + Bundler.logger.info "Downloading #{spec.full_name}.gem" + + destination = repository.download_path_for(:gem) + + unless destination.writable? + raise RubygemsRetardation + end + + Gem::RemoteFetcher.fetcher.download(spec, uri, repository.download_path_for(:gem)) + end + + private + + def fetch_specs + Bundler.logger.info "Updating source: #{to_s}" + + deflated = Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/Marshal.4.8.Z") + inflated = Gem.inflate deflated + + index = Marshal.load(inflated) + index.gems + rescue Gem::RemoteFetcher::FetchError => e + raise ArgumentError, "#{to_s} is not a valid source: #{e.message}" + end + end + + class DirectorySource < Source + def initialize(options) + @name = options[:name] + @version = options[:version] + @location = options[:location] + @require_paths = options[:require_paths] || %w(lib) + end + + def gems + @gems ||= begin + specs = {} + + # Find any gemspec files in the directory and load those specs + Dir[@location.join('**', '*.gemspec')].each do |file| + path = Pathname.new(file).relative_path_from(@location).dirname + spec = eval(File.read(file)) + spec.require_paths.map! { |p| path.join(p) } + specs[spec.full_name] = spec + end + + # If a gemspec for the dependency was not found, add it to the list + if specs.keys.grep(/^#{Regexp.escape(@name)}/).empty? + case + when @version.nil? + raise ArgumentError, "If you use :at, you must specify the gem" \ + "and version you wish to stand in for" + when !Gem::Version.correct?(@version) + raise ArgumentError, "If you use :at, you must specify a gem and" \ + "version. You specified #{@version} for the version" + end + + default = Gem::Specification.new do |s| + s.name = @name + s.version = Gem::Version.new(@version) if @version + end + specs[default.full_name] = default + end + + specs + end + end + + def ==(other) + # TMP HAX + other.is_a?(DirectorySource) + end + + def to_s + "#{@name} (#{@version}) Located at: '#{@location}'" + end + + def download(spec, repository) + spec.require_paths.map! { |p| File.join(@location, p) } + repository.add_spec(:directory, spec) + end + end + + class GitSource < DirectorySource + def initialize(options) + super + @uri = options[:uri] + @ref = options[:ref] + @branch = options[:branch] + end + + def gems + FileUtils.mkdir_p(tmp_path.join("gitz")) + + # TMP HAX to get the *.gemspec reading to work + @location = tmp_path.join("gitz", @name) + + Bundler.logger.info "Cloning git repository at: #{@uri}" + `git clone #{@uri} #{@location} --no-hardlinks` + + if @ref + Dir.chdir(@location) { `git checkout #{@ref}` } + elsif @branch && @branch != "master" + Dir.chdir(@location) { `git checkout origin/#{@branch}` } + end + super + end + + def download(spec, repository) + dest = repository.download_path_for(:directory).join(@name) + spec.require_paths.map! { |p| File.join(dest, p) } + repository.add_spec(:directory, spec) + if spec.name == @name + FileUtils.mkdir_p(dest.dirname) + FileUtils.mv(tmp_path.join("gitz", spec.name), dest) + end + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/templates/app_script.erb b/railties/lib/rails/vendor/bundler/lib/bundler/templates/app_script.erb new file mode 100644 index 0000000000..3e47a53ca8 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/templates/app_script.erb @@ -0,0 +1,3 @@ +<%= shebang bin_file_name %> +require File.join(File.dirname(__FILE__), "<%= path.join("environment").relative_path_from(Pathname.new(bin_dir)) %>") +load File.join(File.dirname(__FILE__), "<%= path.join("gems", @spec.full_name, @spec.bindir, bin_file_name).relative_path_from(Pathname.new(bin_dir)) %>")
\ No newline at end of file diff --git a/railties/lib/rails/vendor/bundler/lib/bundler/templates/environment.erb b/railties/lib/rails/vendor/bundler/lib/bundler/templates/environment.erb new file mode 100644 index 0000000000..21f3de8854 --- /dev/null +++ b/railties/lib/rails/vendor/bundler/lib/bundler/templates/environment.erb @@ -0,0 +1,127 @@ +# DO NOT MODIFY THIS FILE +module Bundler + dir = File.dirname(__FILE__) + +<% unless options[:system_gems] -%> + ENV["GEM_HOME"] = dir + ENV["GEM_PATH"] = dir +<% end -%> + ENV["PATH"] = "#{dir}/<%= bindir %>:#{ENV["PATH"]}" + ENV["RUBYOPT"] = "-r#{__FILE__} #{ENV["RUBYOPT"]}" + +<% load_paths.each do |load_path| -%> + $LOAD_PATH.unshift File.expand_path("#{dir}/<%= load_path %>") +<% end -%> + + @gemfile = "#{dir}/<%= filename %>" + +<% if options[:rubygems] -%> + require "rubygems" + + @bundled_specs = {} +<% spec_files.each do |name, path| -%> + @bundled_specs["<%= name %>"] = eval(File.read("#{dir}/<%= path %>")) + @bundled_specs["<%= name %>"].loaded_from = "#{dir}/<%= path %>" +<% end -%> + + def self.add_specs_to_loaded_specs + Gem.loaded_specs.merge! @bundled_specs + end + + def self.add_specs_to_index + @bundled_specs.each do |name, spec| + Gem.source_index.add_spec spec + end + end + + add_specs_to_loaded_specs + add_specs_to_index +<% end -%> + + def self.require_env(env = nil) + context = Class.new do + def initialize(env) @env = env && env.to_s ; end + def method_missing(*) ; end + def only(env) + old, @only = @only, _combine_onlys(env) + yield + @only = old + end + def except(env) + old, @except = @except, _combine_excepts(env) + yield + @except = old + end + def gem(name, *args) + opt = args.last || {} + only = _combine_onlys(opt[:only] || opt["only"]) + except = _combine_excepts(opt[:except] || opt["except"]) + files = opt[:require_as] || opt["require_as"] || name + + return unless !only || only.any? {|e| e == @env } + return if except && except.any? {|e| e == @env } + + files.each { |f| require f } + yield if block_given? + true + end + private + def _combine_onlys(only) + return @only unless only + only = [only].flatten.compact.uniq.map { |o| o.to_s } + only &= @only if @only + only + end + def _combine_excepts(except) + return @except unless except + except = [except].flatten.compact.uniq.map { |o| o.to_s } + except |= @except if @except + except + end + end + context.new(env && env.to_s).instance_eval(File.read(@gemfile)) + end +end + +<% if options[:rubygems] -%> +module Gem + def source_index.refresh! + super + Bundler.add_specs_to_index + end +end +<% else -%> +$" << "rubygems.rb" + +module Kernel + def gem(*) + # Silently ignore calls to gem, since, in theory, everything + # is activated correctly already. + end +end + +# Define all the Gem errors for gems that reference them. +module Gem + def self.ruby ; <%= Gem.ruby.inspect %> ; end + class LoadError < ::LoadError; end + class Exception < RuntimeError; end + class CommandLineError < Exception; end + class DependencyError < Exception; end + class DependencyRemovalException < Exception; end + class GemNotInHomeException < Exception ; end + class DocumentError < Exception; end + class EndOfYAMLException < Exception; end + class FilePermissionError < Exception; end + class FormatException < Exception; end + class GemNotFoundException < Exception; end + class InstallError < Exception; end + class InvalidSpecificationException < Exception; end + class OperationNotSupportedError < Exception; end + class RemoteError < Exception; end + class RemoteInstallationCancelled < Exception; end + class RemoteInstallationSkipped < Exception; end + class RemoteSourceException < Exception; end + class VerificationError < Exception; end + class SystemExitException < SystemExit; end +end +<% end -%>
\ No newline at end of file diff --git a/railties/lib/rails/vendor/thor-0.11.6/CHANGELOG.rdoc b/railties/lib/rails/vendor/thor-0.11.6/CHANGELOG.rdoc new file mode 100644 index 0000000000..dba25b7205 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/CHANGELOG.rdoc @@ -0,0 +1,77 @@ +== TODO + +* Improve spec coverage for Thor::Runner + +== 0.11.x, released 2009-07-01 + +* Added a rake compatibility layer. It allows you to use spec and rdoc tasks on + Thor classes. + +* BACKWARDS INCOMPATIBLE: aliases are not generated automatically anymore + since it wrong behavior to the invocation system. + +* thor help now show information about any class/task. All those calls are + possible: + + thor help describe + thor help describe:amazing + + Or even with default namespaces: + + thor help :spec + +* Thor::Runner now invokes the default task if none is supplied: + + thor describe # invokes the default task, usually help + +* Thor::Runner now works with mappings: + + thor describe -h + +* Added some documentation and code refactoring. + +== 0.9.8, released 2008-10-20 + +* Fixed some tiny issues that were introduced lately. + +== 0.9.7, released 2008-10-13 + +* Setting global method options on the initialize method works as expected: + All other tasks will accept these global options in addition to their own. +* Added 'group' notion to Thor task sets (class Thor); by default all tasks + are in the 'standard' group. Running 'thor -T' will only show the standard + tasks - adding --all will show all tasks. You can also filter on a specific + group using the --group option: thor -T --group advanced + +== 0.9.6, released 2008-09-13 + +* Generic improvements + +== 0.9.5, released 2008-08-27 + +* Improve Windows compatibility +* Update (incorrect) README and task.thor sample file +* Options hash is now frozen (once returned) +* Allow magic predicates on options object. For instance: `options.force?` +* Add support for :numeric type +* BACKWARDS INCOMPATIBLE: Refactor Thor::Options. You cannot access shorthand forms in options hash anymore (for instance, options[:f]) +* Allow specifying optional args with default values: method_options(:user => "mislav") +* Don't write options for nil or false values. This allows, for example, turning color off when running specs. +* Exit with the status of the spec command to help CI stuff out some. + +== 0.9.4, released 2008-08-13 + +* Try to add Windows compatibility. +* BACKWARDS INCOMPATIBLE: options hash is now accessed as a property in your class and is not passed as last argument anymore +* Allow options at the beginning of the argument list as well as the end. +* Make options available with symbol keys in addition to string keys. +* Allow true to be passed to Thor#method_options to denote a boolean option. +* If loading a thor file fails, don't give up, just print a warning and keep going. +* Make sure that we re-raise errors if they happened further down the pipe than we care about. +* Only delete the old file on updating when the installation of the new one is a success +* Make it Ruby 1.8.5 compatible. +* Don't raise an error if a boolean switch is defined multiple times. +* Thor::Options now doesn't parse through things that look like options but aren't. +* Add URI detection to install task, and make sure we don't append ".thor" to URIs +* Add rake2thor to the gem binfiles. +* Make sure local Thorfiles override system-wide ones. diff --git a/railties/lib/rails/vendor/thor-0.11.6/LICENSE b/railties/lib/rails/vendor/thor-0.11.6/LICENSE new file mode 100644 index 0000000000..98722da459 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008 Yehuda Katz + +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.
\ No newline at end of file diff --git a/railties/lib/rails/vendor/thor-0.11.6/README.rdoc b/railties/lib/rails/vendor/thor-0.11.6/README.rdoc new file mode 100644 index 0000000000..f1106f02b6 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/README.rdoc @@ -0,0 +1,234 @@ += thor + +Map options to a class. Simply create a class with the appropriate annotations +and have options automatically map to functions and parameters. + +Example: + + class App < Thor # [1] + map "-L" => :list # [2] + + desc "install APP_NAME", "install one of the available apps" # [3] + method_options :force => :boolean, :alias => :string # [4] + def install(name) + user_alias = options[:alias] + if options.force? + # do something + end + # other code + end + + desc "list [SEARCH]", "list all of the available apps, limited by SEARCH" + def list(search="") + # list everything + end + end + +Thor automatically maps commands as such: + + thor app:install myname --force + +That gets converted to: + + App.new.install("myname") + # with {'force' => true} as options hash + +1. Inherit from Thor to turn a class into an option mapper +2. Map additional non-valid identifiers to specific methods. In this case, convert -L to :list +3. Describe the method immediately below. The first parameter is the usage information, and the second parameter is the description +4. Provide any additional options that will be available the instance method options. + +== Types for <tt>method_options</tt> + +* :boolean - is parsed as <tt>--option</tt> or <tt>--option=true</tt> +* :string - is parsed as <tt>--option=VALUE</tt> +* :numeric - is parsed as <tt>--option=N</tt> +* :array - is parsed as <tt>--option=one two three</tt> +* :hash - is parsed as <tt>--option=name:string age:integer</tt> + +Besides, method_option allows a default value to be given, examples: + + method_options :force => false + #=> Creates a boolean option with default value false + + method_options :alias => "bar" + #=> Creates a string option with default value "bar" + + method_options :threshold => 3.0 + #=> Creates a numeric option with default value 3.0 + +You can also supply <tt>:option => :required</tt> to mark an option as required. The +type is assumed to be string. If you want a required hash with default values +as option, you can use <tt>method_option</tt> which uses a more declarative style: + + method_option :attributes, :type => :hash, :default => {}, :required => true + +All arguments can be set to nil (except required arguments), by suppling a no or +skip variant. For example: + + thor app name --no-attributes + +In previous versions, aliases for options were created automatically, but now +they should be explicit. You can supply aliases in both short and declarative +styles: + + method_options %w( force -f ) => :boolean + +Or: + + method_option :force, :type => :boolean, :aliases => "-f" + +You can supply as many aliases as you want. + +NOTE: Type :optional available in Thor 0.9.0 was deprecated. Use :string or :boolean instead. + +== Namespaces + +By default, your Thor tasks are invoked using Ruby namespace. In the example +above, tasks are invoked as: + + thor app:install name --force + +However, you could namespace your class as: + + module Sinatra + class App < Thor + # tasks + end + end + +And then you should invoke your tasks as: + + thor sinatra:app:install name --force + +If desired, you can change the namespace: + + module Sinatra + class App < Thor + namespace :myapp + # tasks + end + end + +And then your tasks hould be invoked as: + + thor myapp:install name --force + +== Invocations + +Thor comes with a invocation-dependency system as well which allows a task to be +invoked only once. For example: + + class Counter < Thor + desc "one", "Prints 1, 2, 3" + def one + puts 1 + invoke :two + invoke :three + end + + desc "two", "Prints 2, 3" + def two + puts 2 + invoke :three + end + + desc "three", "Prints 3" + def three + puts 3 + end + end + +When invoking the task one: + + thor counter:one + +The output is "1 2 3", which means that the three task was invoked only once. +You can even invoke tasks from another class, so be sure to check the +documentation. + +== Thor::Group + +Thor has a special class called Thor::Group. The main difference to Thor class +is that it invokes all tasks at once. The example above could be rewritten in +Thor::Group as this: + + class Counter < Thor::Group + desc "Prints 1, 2, 3" + + def one + puts 1 + end + + def two + puts 2 + end + + def three + puts 3 + end + end + +When invoked: + + thor counter + +It prints "1 2 3" as well. Notice you should describe (using the method <tt>desc</tt>) +only the class and not each task anymore. Thor::Group is a great tool to create +generators, since you can define several steps which are invoked in the order they +are defined (Thor::Group is the tool use in generators in Rails 3.0). + +Besides, Thor::Group can parse arguments and options as Thor tasks: + + class Counter < Thor::Group + # number will be available as attr_accessor + argument :number, :type => :numeric, :desc => "The number to start counting" + desc "Prints the 'number' given upto 'number+2'" + + def one + puts number + 0 + end + + def two + puts number + 1 + end + + def three + puts number + 2 + end + end + +The counter above expects one parameter and has the folling outputs: + + thor counter 5 + # Prints "5 6 7" + + thor counter 11 + # Prints "11 12 13" + +You can also give options to Thor::Group, but instead of using <tt>method_option</tt> +and <tt>method_options</tt>, you should use <tt>class_option</tt> and <tt>class_options</tt>. +Both argument and class_options methods are available to Thor class as well. + +== Actions + +Thor comes with several actions which helps with script and generator tasks. You +might be familiar with them since some came from Rails Templates. They are: +<tt>say</tt>, <tt>ask</tt>, <tt>yes?</tt>, <tt>no?</tt>, <tt>add_file</tt>, +<tt>remove_file</tt>, <tt>copy_file</tt>, <tt>template</tt>, <tt>directory</tt>, +<tt>inside</tt>, <tt>run</tt>, <tt>inject_into_file</tt> and a couple more. + +To use them, you just need to include Thor::Actions in your Thor classes: + + class App < Thor + include Thor::Actions + # tasks + end + +Some actions like copy file requires that a class method called source_root is +defined in your class. This is the directory where your templates should be +placed. Be sure to check the documentation. + +== License + +See MIT LICENSE. diff --git a/railties/lib/rails/vendor/thor-0.11.6/bin/rake2thor b/railties/lib/rails/vendor/thor-0.11.6/bin/rake2thor new file mode 100755 index 0000000000..50c7410d80 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/bin/rake2thor @@ -0,0 +1,87 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'ruby2ruby' +require 'parse_tree' +if Ruby2Ruby::VERSION >= "1.2.0" + require 'parse_tree_extensions' +end +require 'rake' + +input = ARGV[0] || 'Rakefile' +output = ARGV[1] || 'Thorfile' + +$requires = [] + +module Kernel + def require_with_record(file) + $requires << file if caller[1] =~ /rake2thor:/ + require_without_record file + end + alias_method :require_without_record, :require + alias_method :require, :require_with_record +end + +load input + +@private_methods = [] + +def file_task_name(name) + "compile_" + name.gsub('/', '_slash_').gsub('.', '_dot_').gsub(/\W/, '_') +end + +def method_for_task(task) + file_task = task.is_a?(Rake::FileTask) + comment = task.instance_variable_get('@comment') + prereqs = task.instance_variable_get('@prerequisites').select(&Rake::Task.method(:task_defined?)) + actions = task.instance_variable_get('@actions') + name = task.name.gsub(/^([^:]+:)+/, '') + name = file_task_name(name) if file_task + meth = '' + + meth << "desc #{name.inspect}, #{comment.inspect}\n" if comment + meth << "def #{name}\n" + + meth << prereqs.map do |pre| + pre = pre.to_s + pre = file_task_name(pre) if Rake::Task[pre].is_a?(Rake::FileTask) + ' ' + pre + end.join("\n") + + meth << "\n\n" unless prereqs.empty? || actions.empty? + + meth << actions.map do |act| + act = act.to_ruby + unless act.gsub!(/^proc \{ \|(\w+)\|\n/, + " \\1 = Struct.new(:name).new(#{name.inspect}) # A crude mock Rake::Task object\n") + act.gsub!(/^proc \{\n/, '') + end + act.gsub(/\n\}$/, '') + end.join("\n") + + meth << "\nend" + + if file_task + @private_methods << meth + return + end + + meth +end + +body = Rake::Task.tasks.map(&method(:method_for_task)).compact.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n") + +unless @private_methods.empty? + body << "\n\n private\n\n" + body << @private_methods.map { |meth| meth.gsub(/^/, ' ') }.join("\n\n") +end + +requires = $requires.map { |r| "require #{r.inspect}" }.join("\n") + +File.open(output, 'w') { |f| f.write(<<END.lstrip) } +#{requires} + +class Default < Thor +#{body} +end +END diff --git a/railties/lib/rails/vendor/thor-0.11.6/bin/thor b/railties/lib/rails/vendor/thor-0.11.6/bin/thor new file mode 100755 index 0000000000..eaf849fb4a --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/bin/thor @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +# -*- mode: ruby -*- + +require File.join(File.dirname(__FILE__), '..', 'lib', 'thor') +require 'thor/runner' + +Thor::Runner.start diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor.rb new file mode 100644 index 0000000000..3b45c4e9b7 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor.rb @@ -0,0 +1,243 @@ +$:.unshift File.expand_path(File.dirname(__FILE__)) +require 'thor/base' +require 'thor/group' +require 'thor/actions' + +class Thor + class << self + # Sets the default task when thor is executed without an explicit task to be called. + # + # ==== Parameters + # meth<Symbol>:: name of the defaut task + # + def default_task(meth=nil) + case meth + when :none + @default_task = 'help' + when nil + @default_task ||= from_superclass(:default_task, 'help') + else + @default_task = meth.to_s + end + end + + # Defines the usage and the description of the next task. + # + # ==== Parameters + # usage<String> + # description<String> + # + def desc(usage, description, options={}) + if options[:for] + task = find_and_refresh_task(options[:for]) + task.usage = usage if usage + task.description = description if description + else + @usage, @desc = usage, description + end + end + + # Maps an input to a task. If you define: + # + # map "-T" => "list" + # + # Running: + # + # thor -T + # + # Will invoke the list task. + # + # ==== Parameters + # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given task. + # + def map(mappings=nil) + @map ||= from_superclass(:map, {}) + + if mappings + mappings.each do |key, value| + if key.respond_to?(:each) + key.each {|subkey| @map[subkey] = value} + else + @map[key] = value + end + end + end + + @map + end + + # Declares the options for the next task to be declared. + # + # ==== Parameters + # Hash[Symbol => Object]:: The hash key is the name of the option and the value + # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric + # or :required (string). If you give a value, the type of the value is used. + # + def method_options(options=nil) + @method_options ||= {} + build_options(options, @method_options) if options + @method_options + end + + # Adds an option to the set of class options. If :for is given as option, + # it allows you to change the options from a previous defined task. + # + # def previous_task + # # magic + # end + # + # method_options :foo => :bar, :for => :previous_task + # + # def next_task + # # magic + # end + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :default - Default value for this argument. It cannot be required and have default values. + # :aliases - Aliases for this option. + # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean. + # :group - The group for this options. Use by class options to output options in different levels. + # :banner - String to show on usage notes. + # + def method_option(name, options={}) + scope = if options[:for] + find_and_refresh_task(options[:for]).options + else + method_options + end + + build_option(name, options, scope) + end + + # Parses the task and options from the given args, instantiate the class + # and invoke the task. This method is used when the arguments must be parsed + # from an array. If you are inside Ruby and want to use a Thor class, you + # can simply initialize it: + # + # script = MyScript.new(args, options, config) + # script.invoke(:task, first_arg, second_arg, third_arg) + # + def start(given_args=ARGV, config={}) + super do + meth = normalize_task_name(given_args.shift) + task = all_tasks[meth] + + if task + args, opts = Thor::Options.split(given_args) + config.merge!(:task_options => task.options) + else + args, opts = given_args, {} + end + + task ||= Thor::Task::Dynamic.new(meth) + trailing = args[Range.new(arguments.size, -1)] + new(args, opts, config).invoke(task, trailing || []) + end + end + + # Prints help information. If a task name is given, it shows information + # only about the specific task. + # + # ==== Parameters + # meth<String>:: An optional task name to print usage information about. + # + # ==== Options + # namespace:: When true, shows the namespace in the output before the usage. + # skip_inherited:: When true, does not show tasks from superclass. + # + def help(shell, meth=nil, options={}) + meth, options = nil, meth if meth.is_a?(Hash) + + if meth + task = all_tasks[meth] + raise UndefinedTaskError, "task '#{meth}' could not be found in namespace '#{self.namespace}'" unless task + + shell.say "Usage:" + shell.say " #{banner(task, options[:namespace], false)}" + shell.say + class_options_help(shell, "Class", :Method => task.options.map { |_, o| o }) + shell.say task.description + else + list = (options[:short] ? tasks : all_tasks).map do |_, task| + item = [ banner(task, options[:namespace]) ] + item << "# #{task.short_description}" if task.short_description + item << " " + end + + options[:ident] ||= 2 + if options[:short] + shell.print_list(list, :ident => options[:ident]) + else + shell.say "Tasks:" + shell.print_list(list, :ident => options[:ident]) + end + + Thor::Util.thor_classes_in(self).each do |subclass| + namespace = options[:namespace] == true || subclass.namespace.gsub(/^#{self.namespace}:/, '') + subclass.help(shell, options.merge(:short => true, :namespace => namespace)) + end + + class_options_help(shell, "Class") unless options[:short] + end + end + + protected + + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Thor::Runner. It receives + # the task that is going to be invoked and a boolean which indicates if + # the namespace should be displayed as arguments. + # + def banner(task, namespace=true, show_options=true) + task.formatted_usage(self, namespace, show_options) + end + + def baseclass #:nodoc: + Thor + end + + def create_task(meth) #:nodoc: + if @usage && @desc + tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options) + @usage, @desc, @method_options = nil + true + elsif self.all_tasks[meth.to_s] || meth.to_sym == :method_missing + true + else + puts "[WARNING] Attempted to create task #{meth.inspect} without usage or description. " << + "Call desc if you want this method to be available as task or declare it inside a " << + "no_tasks{} block. Invoked from #{caller[1].inspect}." + false + end + end + + def initialize_added #:nodoc: + class_options.merge!(method_options) + @method_options = nil + end + + # Receives a task name (can be nil), and try to get a map from it. + # If a map can't be found use the sent name or the default task. + # + def normalize_task_name(meth) #:nodoc: + mapping = map[meth.to_s] + meth = mapping || meth || default_task + meth.to_s.gsub('-','_') # treat foo-bar > foo_bar + end + end + + include Thor::Base + + map HELP_MAPPINGS => :help + + desc "help [TASK]", "Describe available tasks or one specific task" + def help(task=nil) + self.class.help(shell, task, :namespace => task && task.include?(?:)) + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions.rb new file mode 100644 index 0000000000..d561ccb2aa --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions.rb @@ -0,0 +1,273 @@ +require 'fileutils' + +Dir[File.join(File.dirname(__FILE__), "actions", "*.rb")].each do |action| + require action +end + +class Thor + module Actions + attr_accessor :behavior + + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + # Hold source paths for one Thor instance. source_paths_for_search is the + # method responsible to gather source_paths from this current class, + # inherited paths and the source root. + # + def source_paths + @source_paths ||= [] + end + + # Returns the source paths in the following order: + # + # 1) This class source paths + # 2) Source root + # 3) Parents source paths + # + def source_paths_for_search + paths = [] + paths += self.source_paths + paths << self.source_root if self.respond_to?(:source_root) + paths += from_superclass(:source_paths, []) + paths + end + + # Add runtime options that help actions execution. + # + def add_runtime_options! + class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime, + :desc => "Run but do not make any changes" + + class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime, + :desc => "Overwrite files that already exist" + + class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime, + :desc => "Skip files that already exist" + + class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime, + :desc => "Supress status output" + end + end + + # Extends initializer to add more configuration options. + # + # ==== Configuration + # behavior<Symbol>:: The actions default behavior. Can be :invoke or :revoke. + # It also accepts :force, :skip and :pretend to set the behavior + # and the respective option. + # + # destination_root<String>:: The root directory needed for some actions. + # + def initialize(args=[], options={}, config={}) + self.behavior = case config[:behavior].to_s + when "force", "skip" + _cleanup_options_and_set(options, config[:behavior]) + :invoke + when "revoke" + :revoke + else + :invoke + end + + super + self.destination_root = config[:destination_root] + end + + # Wraps an action object and call it accordingly to the thor class behavior. + # + def action(instance) #:nodoc: + if behavior == :revoke + instance.revoke! + else + instance.invoke! + end + end + + # Returns the root for this thor class (also aliased as destination root). + # + def destination_root + @destination_stack.last + end + + # Sets the root for this thor class. Relatives path are added to the + # directory where the script was invoked and expanded. + # + def destination_root=(root) + @destination_stack ||= [] + @destination_stack[0] = File.expand_path(root || '') + end + + # Returns the given path relative to the absolute root (ie, root where + # the script started). + # + def relative_to_original_destination_root(path, remove_dot=true) + path = path.gsub(@destination_stack[0], '.') + remove_dot ? (path[2..-1] || '') : path + end + + # Holds source paths in instance so they can be manipulated. + # + def source_paths + @source_paths ||= self.class.source_paths_for_search + end + + # Receives a file or directory and search for it in the source paths. + # + def find_in_source_paths(file) + relative_root = relative_to_original_destination_root(destination_root, false) + + source_paths.each do |source| + source_file = File.expand_path(file, File.join(source, relative_root)) + return source_file if File.exists?(source_file) + end + + if source_paths.empty? + raise Error, "You don't have any source path defined for class #{self.class.name}. To fix this, " << + "you can define a source_root in your class." + else + raise Error, "Could not find #{file.inspect} in source paths." + end + end + + # Do something in the root or on a provided subfolder. If a relative path + # is given it's referenced from the current root. The full path is yielded + # to the block you provide. The path is set back to the previous path when + # the method exits. + # + # ==== Parameters + # dir<String>:: the directory to move to. + # config<Hash>:: give :verbose => true to log and use padding. + # + def inside(dir='', config={}, &block) + verbose = config.fetch(:verbose, false) + + say_status :inside, dir, verbose + shell.padding += 1 if verbose + @destination_stack.push File.expand_path(dir, destination_root) + + FileUtils.mkdir_p(destination_root) unless File.exist?(destination_root) + FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield } + + @destination_stack.pop + shell.padding -= 1 if verbose + end + + # Goes to the root and execute the given block. + # + def in_root + inside(@destination_stack.first) { yield } + end + + # Loads an external file and execute it in the instance binding. + # + # ==== Parameters + # path<String>:: The path to the file to execute. Can be a web address or + # a relative path from the source root. + # + # ==== Examples + # + # apply "http://gist.github.com/103208" + # + # apply "recipes/jquery.rb" + # + def apply(path, config={}) + verbose = config.fetch(:verbose, true) + path = find_in_source_paths(path) unless path =~ /^http\:\/\// + + say_status :apply, path, verbose + shell.padding += 1 if verbose + instance_eval(open(path).read) + shell.padding -= 1 if verbose + end + + # Executes a command. + # + # ==== Parameters + # command<String>:: the command to be executed. + # config<Hash>:: give :verbose => false to not log the status. Specify :with + # to append an executable to command executation. + # + # ==== Example + # + # inside('vendor') do + # run('ln -s ~/edge rails') + # end + # + def run(command, config={}) + return unless behavior == :invoke + + destination = relative_to_original_destination_root(destination_root, false) + desc = "#{command} from #{destination.inspect}" + + if config[:with] + desc = "#{File.basename(config[:with].to_s)} #{desc}" + command = "#{config[:with]} #{command}" + end + + say_status :run, desc, config.fetch(:verbose, true) + system(command) unless options[:pretend] + end + + # Executes a ruby script (taking into account WIN32 platform quirks). + # + # ==== Parameters + # command<String>:: the command to be executed. + # config<Hash>:: give :verbose => false to not log the status. + # + def run_ruby_script(command, config={}) + return unless behavior == :invoke + run "#{command}", config.merge(:with => Thor::Util.ruby_command) + end + + # Run a thor command. A hash of options can be given and it's converted to + # switches. + # + # ==== Parameters + # task<String>:: the task to be invoked + # args<Array>:: arguments to the task + # config<Hash>:: give :verbose => false to not log the status. Other options + # are given as parameter to Thor. + # + # ==== Examples + # + # thor :install, "http://gist.github.com/103208" + # #=> thor install http://gist.github.com/103208 + # + # thor :list, :all => true, :substring => 'rails' + # #=> thor list --all --substring=rails + # + def thor(task, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + verbose = config.key?(:verbose) ? config.delete(:verbose) : true + + args.unshift task + args.push Thor::Options.to_switches(config) + command = args.join(' ').strip + + run command, :with => :thor, :verbose => verbose + end + + protected + + # Allow current root to be shared between invocations. + # + def _shared_configuration #:nodoc: + super.merge!(:destination_root => self.destination_root) + end + + def _cleanup_options_and_set(options, key) #:nodoc: + case options + when Array + %w(--force -f --skip -s).each { |i| options.delete(i) } + options << "--#{key}" + when Hash + [:force, :skip, "force", "skip"].each { |i| options.delete(i) } + options.merge!(key => true) + end + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/create_file.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/create_file.rb new file mode 100644 index 0000000000..8f6badee27 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/create_file.rb @@ -0,0 +1,102 @@ +require 'thor/actions/empty_directory' + +class Thor + module Actions + + # Create a new file relative to the destination root with the given data, + # which is the return value of a block or a data string. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # data<String|NilClass>:: the data to append to the file. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # create_file "lib/fun_party.rb" do + # hostname = ask("What is the virtual hostname I should use?") + # "vhost.name = #{hostname}" + # end + # + # create_file "config/apach.conf", "your apache config" + # + def create_file(destination, data=nil, config={}, &block) + action CreateFile.new(self, destination, block || data.to_s, config) + end + alias :add_file :create_file + + # AddFile is a subset of Template, which instead of rendering a file with + # ERB, it gets the content from the user. + # + class CreateFile < EmptyDirectory #:nodoc: + attr_reader :data + + def initialize(base, destination, data, config={}) + @data = data + super(base, destination, config) + end + + # Checks if the content of the file at the destination is identical to the rendered result. + # + # ==== Returns + # Boolean:: true if it is identical, false otherwise. + # + def identical? + exists? && File.read(destination) == render + end + + # Holds the content to be added to the file. + # + def render + @render ||= if data.is_a?(Proc) + data.call + else + data + end + end + + def invoke! + invoke_with_conflict_check do + FileUtils.mkdir_p(File.dirname(destination)) + File.open(destination, 'w'){ |f| f.write render } + end + end + + protected + + # Now on conflict we check if the file is identical or not. + # + def on_conflict_behavior(&block) + if identical? + say_status :identical, :blue + else + options = base.options.merge(config) + force_or_skip_or_conflict(options[:force], options[:skip], &block) + end + end + + # If force is true, run the action, otherwise check if it's not being + # skipped. If both are false, show the file_collision menu, if the menu + # returns true, force it, otherwise skip. + # + def force_or_skip_or_conflict(force, skip, &block) + if force + say_status :force, :yellow + block.call unless pretend? + elsif skip + say_status :skip, :yellow + else + say_status :conflict, :red + force_or_skip_or_conflict(force_on_collision?, true, &block) + end + end + + # Shows the file collision menu to the user and gets the result. + # + def force_on_collision? + base.shell.file_collision(destination){ render } + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/directory.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/directory.rb new file mode 100644 index 0000000000..be5eb822ac --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/directory.rb @@ -0,0 +1,87 @@ +require 'thor/actions/empty_directory' + +class Thor + module Actions + + # Copies recursively the files from source directory to root directory. + # If any of the files finishes with .tt, it's considered to be a template + # and is placed in the destination without the extension .tt. If any + # empty directory is found, it's copied and all .empty_directory files are + # ignored. Remember that file paths can also be encoded, let's suppose a doc + # directory with the following files: + # + # doc/ + # components/.empty_directory + # README + # rdoc.rb.tt + # %app_name%.rb + # + # When invoked as: + # + # directory "doc" + # + # It will create a doc directory in the destination with the following + # files (assuming that the app_name is "blog"): + # + # doc/ + # components/ + # README + # rdoc.rb + # blog.rb + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # If :recursive => false, does not look for paths recursively. + # + # ==== Examples + # + # directory "doc" + # directory "doc", "docs", :recursive => false + # + def directory(source, destination=nil, config={}) + action Directory.new(self, source, destination || source, config) + end + + class Directory < EmptyDirectory #:nodoc: + attr_reader :source + + def initialize(base, source, destination=nil, config={}) + @source = File.expand_path(base.find_in_source_paths(source.to_s)) + super(base, destination, { :recursive => true }.merge(config)) + end + + def invoke! + base.empty_directory given_destination, config + execute! + end + + def revoke! + execute! + end + + protected + + def execute! + lookup = config[:recursive] ? File.join(source, '**') : source + lookup = File.join(lookup, '{*,.[a-z]*}') + + Dir[lookup].each do |file_source| + next if File.directory?(file_source) + file_destination = File.join(given_destination, file_source.gsub(source, '.')) + + case file_source + when /\.empty_directory$/ + base.empty_directory(File.dirname(file_destination), config) + when /\.tt$/ + base.template(file_source, file_destination[0..-4], config) + else + base.copy_file(file_source, file_destination, config) + end + end + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/empty_directory.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/empty_directory.rb new file mode 100644 index 0000000000..03c1fe4af1 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/empty_directory.rb @@ -0,0 +1,133 @@ +class Thor + module Actions + + # Creates an empty directory. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # empty_directory "doc" + # + def empty_directory(destination, config={}) + action EmptyDirectory.new(self, destination, config) + end + + # Class which holds create directory logic. This is the base class for + # other actions like create_file and directory. + # + # This implementation is based in Templater actions, created by Jonas Nicklas + # and Michael S. Klishin under MIT LICENSE. + # + class EmptyDirectory #:nodoc: + attr_reader :base, :destination, :given_destination, :relative_destination, :config + + # Initializes given the source and destination. + # + # ==== Parameters + # base<Thor::Base>:: A Thor::Base instance + # source<String>:: Relative path to the source of this file + # destination<String>:: Relative path to the destination of this file + # config<Hash>:: give :verbose => false to not log the status. + # + def initialize(base, destination, config={}) + @base, @config = base, { :verbose => true }.merge(config) + self.destination = destination + end + + # Checks if the destination file already exists. + # + # ==== Returns + # Boolean:: true if the file exists, false otherwise. + # + def exists? + ::File.exists?(destination) + end + + def invoke! + invoke_with_conflict_check do + ::FileUtils.mkdir_p(destination) + end + end + + def revoke! + say_status :remove, :red + ::FileUtils.rm_rf(destination) if !pretend? && exists? + end + + protected + + # Shortcut for pretend. + # + def pretend? + base.options[:pretend] + end + + # Sets the absolute destination value from a relative destination value. + # It also stores the given and relative destination. Let's suppose our + # script is being executed on "dest", it sets the destination root to + # "dest". The destination, given_destination and relative_destination + # are related in the following way: + # + # inside "bar" do + # empty_directory "baz" + # end + # + # destination #=> dest/bar/baz + # relative_destination #=> bar/baz + # given_destination #=> baz + # + def destination=(destination) + if destination + @given_destination = convert_encoded_instructions(destination.to_s) + @destination = ::File.expand_path(@given_destination, base.destination_root) + @relative_destination = base.relative_to_original_destination_root(@destination) + end + end + + # Filenames in the encoded form are converted. If you have a file: + # + # %class_name%.rb + # + # It gets the class name from the base and replace it: + # + # user.rb + # + def convert_encoded_instructions(filename) + filename.gsub(/%(.*?)%/) do |string| + instruction = $1.strip + base.respond_to?(instruction) ? base.send(instruction) : string + end + end + + # Receives a hash of options and just execute the block if some + # conditions are met. + # + def invoke_with_conflict_check(&block) + if exists? + on_conflict_behavior(&block) + else + say_status :create, :green + block.call unless pretend? + end + + destination + end + + # What to do when the destination file already exists. + # + def on_conflict_behavior(&block) + say_status :exist, :blue + end + + # Shortcut to say_status shell method. + # + def say_status(status, color) + base.shell.say_status status, relative_destination, color if config[:verbose] + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/file_manipulation.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/file_manipulation.rb new file mode 100644 index 0000000000..d77d90d448 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/file_manipulation.rb @@ -0,0 +1,219 @@ +require 'erb' +require 'open-uri' + +class Thor + module Actions + + # Copies the file from the relative source to the relative destination. If + # the destination is not given it's assumed to be equal to the source. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # copy_file "README", "doc/README" + # + # copy_file "doc/README" + # + def copy_file(source, destination=nil, config={}) + destination ||= source + source = File.expand_path(find_in_source_paths(source.to_s)) + + create_file destination, nil, config do + File.read(source) + end + end + + # Gets the content at the given address and places it at the given relative + # destination. If a block is given instead of destination, the content of + # the url is yielded and used as location. + # + # ==== Parameters + # source<String>:: the address of the given content. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # get "http://gist.github.com/103208", "doc/README" + # + # get "http://gist.github.com/103208" do |content| + # content.split("\n").first + # end + # + def get(source, destination=nil, config={}, &block) + source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ /^http\:\/\// + render = open(source).read + + destination ||= if block_given? + block.arity == 1 ? block.call(render) : block.call + else + File.basename(source) + end + + create_file destination, render, config + end + + # Gets an ERB template at the relative source, executes it and makes a copy + # at the relative destination. If the destination is not given it's assumed + # to be equal to the source removing .tt from the filename. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # template "README", "doc/README" + # + # template "doc/README" + # + def template(source, destination=nil, config={}) + destination ||= source + source = File.expand_path(find_in_source_paths(source.to_s)) + context = instance_eval('binding') + + create_file destination, nil, config do + ERB.new(::File.read(source), nil, '-').result(context) + end + end + + # Changes the mode of the given file or directory. + # + # ==== Parameters + # mode<Integer>:: the file mode + # path<String>:: the name of the file to change mode + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # chmod "script/*", 0755 + # + def chmod(path, mode, config={}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true) + FileUtils.chmod_R(mode, path) unless options[:pretend] + end + + # Prepend text to a file. Since it depends on inject_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # data<String>:: the data to prepend to the file, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # prepend_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # prepend_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def prepend_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config.merge!(:after => /\A/) + inject_into_file(path, *(args << config), &block) + end + + # Append text to a file. Since it depends on inject_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # data<String>:: the data to append to the file, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # append_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # append_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def append_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config.merge!(:before => /\z/) + inject_into_file(path, *(args << config), &block) + end + + # Injects text right after the class definition. Since it depends on + # inject_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # klass<String|Class>:: the class to be manipulated + # data<String>:: the data to append to the class, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # inject_into_class "app/controllers/application_controller.rb", " filter_parameter :password\n" + # + # inject_into_class "app/controllers/application_controller.rb", ApplicationController do + # " filter_parameter :password\n" + # end + # + def inject_into_class(path, klass, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config.merge!(:after => /class #{klass}\n|class #{klass} .*\n/) + inject_into_file(path, *(args << config), &block) + end + + # Run a regular expression replacement on a file. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # flag<Regexp|String>:: the regexp or string to be replaced + # replacement<String>:: the replacement, can be also given as a block + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1' + # + # gsub_file 'README', /rake/, :green do |match| + # match << " no more. Use thor!" + # end + # + def gsub_file(path, flag, *args, &block) + return unless behavior == :invoke + config = args.last.is_a?(Hash) ? args.pop : {} + + path = File.expand_path(path, destination_root) + say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) + + unless options[:pretend] + content = File.read(path) + content.gsub!(flag, *args, &block) + File.open(path, 'wb') { |file| file.write(content) } + end + end + + # Removes a file at the given location. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # remove_file 'README' + # remove_file 'app/controllers/application_controller.rb' + # + def remove_file(path, config={}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + + say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true) + ::FileUtils.rm_rf(path) if !options[:pretend] && File.exists?(path) + end + alias :remove_dir :remove_file + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/inject_into_file.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/inject_into_file.rb new file mode 100644 index 0000000000..0636ec6591 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/actions/inject_into_file.rb @@ -0,0 +1,101 @@ +require 'thor/actions/empty_directory' + +class Thor + module Actions + + # Injects the given content into a file. Different from gsub_file, this + # method is reversible. + # + # ==== Parameters + # destination<String>:: Relative path to the destination root + # data<String>:: Data to add to the file. Can be given as a block. + # config<Hash>:: give :verbose => false to not log the status and the flag + # for injection (:after or :before). + # + # ==== Examples + # + # inject_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n" + # + # inject_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do + # gems = ask "Which gems would you like to add?" + # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") + # end + # + def inject_into_file(destination, *args, &block) + if block_given? + data, config = block, args.shift + else + data, config = args.shift, args.shift + end + action InjectIntoFile.new(self, destination, data, config) + end + + class InjectIntoFile < EmptyDirectory #:nodoc: + attr_reader :replacement, :flag, :behavior + + def initialize(base, destination, data, config) + super(base, destination, { :verbose => true }.merge(config)) + + @behavior, @flag = if @config.key?(:after) + [:after, @config.delete(:after)] + else + [:before, @config.delete(:before)] + end + + @replacement = data.is_a?(Proc) ? data.call : data + @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) + end + + def invoke! + say_status :invoke + + content = if @behavior == :after + '\0' + replacement + else + replacement + '\0' + end + + replace!(/#{flag}/, content) + end + + def revoke! + say_status :revoke + + regexp = if @behavior == :after + content = '\1\2' + /(#{flag})(.*)(#{Regexp.escape(replacement)})/m + else + content = '\2\3' + /(#{Regexp.escape(replacement)})(.*)(#{flag})/m + end + + replace!(regexp, content) + end + + protected + + def say_status(behavior) + status = if flag == /\A/ + behavior == :invoke ? :prepend : :unprepend + elsif flag == /\z/ + behavior == :invoke ? :append : :unappend + else + behavior == :invoke ? :inject : :deinject + end + + super(status, config[:verbose]) + end + + # Adds the content to the file. + # + def replace!(regexp, string) + unless base.options[:pretend] + content = File.read(destination) + content.gsub!(regexp, string) + File.open(destination, 'wb') { |file| file.write(content) } + end + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/base.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/base.rb new file mode 100644 index 0000000000..700d794123 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/base.rb @@ -0,0 +1,517 @@ +require 'thor/core_ext/hash_with_indifferent_access' +require 'thor/core_ext/ordered_hash' +require 'thor/error' +require 'thor/shell' +require 'thor/invocation' +require 'thor/parser' +require 'thor/task' +require 'thor/util' + +class Thor + # Shortcuts for help. + HELP_MAPPINGS = %w(-h -? --help -D) + + # Thor methods that should not be overwritten by the user. + THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root + action add_file create_file in_root inside run run_ruby_script) + + module Base + attr_accessor :options + + # It receives arguments in an Array and two hashes, one for options and + # other for configuration. + # + # Notice that it does not check if all required arguments were supplied. + # It should be done by the parser. + # + # ==== Parameters + # args<Array[Object]>:: An array of objects. The objects are applied to their + # respective accessors declared with <tt>argument</tt>. + # + # options<Hash>:: An options hash that will be available as self.options. + # The hash given is converted to a hash with indifferent + # access, magic predicates (options.skip?) and then frozen. + # + # config<Hash>:: Configuration for this Thor class. + # + def initialize(args=[], options={}, config={}) + Thor::Arguments.parse(self.class.arguments, args).each do |key, value| + send("#{key}=", value) + end + + parse_options = self.class.class_options + + if options.is_a?(Array) + task_options = config.delete(:task_options) # hook for start + parse_options = parse_options.merge(task_options) if task_options + array_options, hash_options = options, {} + else + array_options, hash_options = [], options + end + + options = Thor::Options.parse(parse_options, array_options) + self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).merge!(hash_options) + self.options.freeze + end + + class << self + def included(base) #:nodoc: + base.send :extend, ClassMethods + base.send :include, Invocation + base.send :include, Shell + end + + # Returns the classes that inherits from Thor or Thor::Group. + # + # ==== Returns + # Array[Class] + # + def subclasses + @subclasses ||= [] + end + + # Returns the files where the subclasses are kept. + # + # ==== Returns + # Hash[path<String> => Class] + # + def subclass_files + @subclass_files ||= Hash.new{ |h,k| h[k] = [] } + end + + # Whenever a class inherits from Thor or Thor::Group, we should track the + # class and the file on Thor::Base. This is the method responsable for it. + # + def register_klass_file(klass) #:nodoc: + file = caller[1].match(/(.*):\d+/)[1] + Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass) + + file_subclasses = Thor::Base.subclass_files[File.expand_path(file)] + file_subclasses << klass unless file_subclasses.include?(klass) + end + end + + module ClassMethods + # Adds an argument to the class and creates an attr_accessor for it. + # + # Arguments are different from options in several aspects. The first one + # is how they are parsed from the command line, arguments are retrieved + # from position: + # + # thor task NAME + # + # Instead of: + # + # thor task --name=NAME + # + # Besides, arguments are used inside your code as an accessor (self.argument), + # while options are all kept in a hash (self.options). + # + # Finally, arguments cannot have type :default or :boolean but can be + # optional (supplying :optional => :true or :required => false), although + # you cannot have a required argument after a non-required argument. If you + # try it, an error is raised. + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :optional - If the argument is optional or not. + # :type - The type of the argument, can be :string, :hash, :array, :numeric. + # :default - Default value for this argument. It cannot be required and have default values. + # :banner - String to show on usage notes. + # + # ==== Errors + # ArgumentError:: Raised if you supply a required argument after a non required one. + # + def argument(name, options={}) + is_thor_reserved_word?(name, :argument) + no_tasks { attr_accessor name } + + required = if options.key?(:optional) + !options[:optional] + elsif options.key?(:required) + options[:required] + else + options[:default].nil? + end + + remove_argument name + + arguments.each do |argument| + next if argument.required? + raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " << + "the non-required argument #{argument.human_name.inspect}." + end if required + + arguments << Thor::Argument.new(name, options[:desc], required, options[:type], + options[:default], options[:banner]) + end + + # Returns this class arguments, looking up in the ancestors chain. + # + # ==== Returns + # Array[Thor::Argument] + # + def arguments + @arguments ||= from_superclass(:arguments, []) + end + + # Adds a bunch of options to the set of class options. + # + # class_options :foo => false, :bar => :required, :baz => :string + # + # If you prefer more detailed declaration, check class_option. + # + # ==== Parameters + # Hash[Symbol => Object] + # + def class_options(options=nil) + @class_options ||= from_superclass(:class_options, {}) + build_options(options, @class_options) if options + @class_options + end + + # Adds an option to the set of class options + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :default - Default value for this argument. + # :group - The group for this options. Use by class options to output options in different levels. + # :aliases - Aliases for this option. + # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean. + # :banner - String to show on usage notes. + # + def class_option(name, options={}) + build_option(name, options, class_options) + end + + # Removes a previous defined argument. If :undefine is given, undefine + # accessors as well. + # + # ==== Paremeters + # names<Array>:: Arguments to be removed + # + # ==== Examples + # + # remove_argument :foo + # remove_argument :foo, :bar, :baz, :undefine => true + # + def remove_argument(*names) + options = names.last.is_a?(Hash) ? names.pop : {} + + names.each do |name| + arguments.delete_if { |a| a.name == name.to_s } + undef_method name, "#{name}=" if options[:undefine] + end + end + + # Removes a previous defined class option. + # + # ==== Paremeters + # names<Array>:: Class options to be removed + # + # ==== Examples + # + # remove_class_option :foo + # remove_class_option :foo, :bar, :baz + # + def remove_class_option(*names) + names.each do |name| + class_options.delete(name) + end + end + + # Defines the group. This is used when thor list is invoked so you can specify + # that only tasks from a pre-defined group will be shown. Defaults to standard. + # + # ==== Parameters + # name<String|Symbol> + # + def group(name=nil) + case name + when nil + @group ||= from_superclass(:group, 'standard') + else + @group = name.to_s + end + end + + # Returns the tasks for this Thor class. + # + # ==== Returns + # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task + # objects as values. + # + def tasks + @tasks ||= Thor::CoreExt::OrderedHash.new + end + + # Returns the tasks for this Thor class and all subclasses. + # + # ==== Returns + # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task + # objects as values. + # + def all_tasks + @all_tasks ||= from_superclass(:all_tasks, Thor::CoreExt::OrderedHash.new) + @all_tasks.merge(tasks) + end + + # Removes a given task from this Thor class. This is usually done if you + # are inheriting from another class and don't want it to be available + # anymore. + # + # By default it only remove the mapping to the task. But you can supply + # :undefine => true to undefine the method from the class as well. + # + # ==== Parameters + # name<Symbol|String>:: The name of the task to be removed + # options<Hash>:: You can give :undefine => true if you want tasks the method + # to be undefined from the class as well. + # + def remove_task(*names) + options = names.last.is_a?(Hash) ? names.pop : {} + + names.each do |name| + tasks.delete(name.to_s) + all_tasks.delete(name.to_s) + undef_method name if options[:undefine] + end + end + + # All methods defined inside the given block are not added as tasks. + # + # So you can do: + # + # class MyScript < Thor + # no_tasks do + # def this_is_not_a_task + # end + # end + # end + # + # You can also add the method and remove it from the task list: + # + # class MyScript < Thor + # def this_is_not_a_task + # end + # remove_task :this_is_not_a_task + # end + # + def no_tasks + @no_tasks = true + yield + @no_tasks = false + end + + # Sets the namespace for the Thor or Thor::Group class. By default the + # namespace is retrieved from the class name. If your Thor class is named + # Scripts::MyScript, the help method, for example, will be called as: + # + # thor scripts:my_script -h + # + # If you change the namespace: + # + # namespace :my_scripts + # + # You change how your tasks are invoked: + # + # thor my_scripts -h + # + # Finally, if you change your namespace to default: + # + # namespace :default + # + # Your tasks can be invoked with a shortcut. Instead of: + # + # thor :my_task + # + def namespace(name=nil) + case name + when nil + @namespace ||= Thor::Util.namespace_from_thor_class(self, false) + else + @namespace = name.to_s + end + end + + # Default way to start generators from the command line. + # + def start(given_args=ARGV, config={}) + config[:shell] ||= Thor::Base.shell.new + yield + rescue Thor::Error => e + if given_args.include?("--debug") + raise e + else + config[:shell].error e.message + end + exit(1) if exit_on_failure? + end + + protected + + # Prints the class options per group. If an option does not belong to + # any group, it uses the ungrouped name value. This method provide to + # hooks to add extra options, one of them if the third argument called + # extra_group that should be a hash in the format :group => Array[Options]. + # + # The second is by returning a lambda used to print values. The lambda + # requires two options: the group name and the array of options. + # + def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc: + groups = {} + + class_options.each do |_, value| + groups[value.group] ||= [] + groups[value.group] << value + end + + printer = proc do |group_name, options| + list = [] + padding = options.collect{ |o| o.aliases.size }.max.to_i * 4 + + options.each do |option| + item = [ option.usage(padding) ] + item.push(option.description ? "# #{option.description}" : "") + + list << item + list << [ "", "# Default: #{option.default}" ] if option.show_default? + end + + unless list.empty? + shell.say(group_name ? "#{group_name} options:" : "Options:") + shell.print_table(list, :ident => 2) + shell.say "" + end + end + + # Deal with default group + global_options = groups.delete(nil) || [] + printer.call(ungrouped_name, global_options) if global_options + + # Print all others + groups = extra_group.merge(groups) if extra_group + groups.each(&printer) + printer + end + + # Raises an error if the word given is a Thor reserved word. + # + def is_thor_reserved_word?(word, type) #:nodoc: + return false unless THOR_RESERVED_WORDS.include?(word.to_s) + raise "#{word.inspect} is a Thor reserved word and cannot be defined as #{type}" + end + + # Build an option and adds it to the given scope. + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described in both class_option and method_option. + # + def build_option(name, options, scope) #:nodoc: + scope[name] = Thor::Option.new(name, options[:desc], options[:required], + options[:type], options[:default], options[:banner], + options[:group], options[:aliases]) + end + + # Receives a hash of options, parse them and add to the scope. This is a + # fast way to set a bunch of options: + # + # build_options :foo => true, :bar => :required, :baz => :string + # + # ==== Parameters + # Hash[Symbol => Object] + # + def build_options(options, scope) #:nodoc: + options.each do |key, value| + scope[key] = Thor::Option.parse(key, value) + end + end + + # Finds a task with the given name. If the task belongs to the current + # class, just return it, otherwise dup it and add the fresh copy to the + # current task hash. + # + def find_and_refresh_task(name) #:nodoc: + task = if task = tasks[name.to_s] + task + elsif task = all_tasks[name.to_s] + tasks[name.to_s] = task.clone + else + raise ArgumentError, "You supplied :for => #{name.inspect}, but the task #{name.inspect} could not be found." + end + end + + # Everytime someone inherits from a Thor class, register the klass + # and file into baseclass. + # + def inherited(klass) + Thor::Base.register_klass_file(klass) + end + + # Fire this callback whenever a method is added. Added methods are + # tracked as tasks by invoking the create_task method. + # + def method_added(meth) + meth = meth.to_s + + if meth == "initialize" + initialize_added + return + end + + # Return if it's not a public instance method + return unless public_instance_methods.include?(meth) || + public_instance_methods.include?(meth.to_sym) + + return if @no_tasks || !create_task(meth) + + is_thor_reserved_word?(meth, :task) + Thor::Base.register_klass_file(self) + end + + # Retrieves a value from superclass. If it reaches the baseclass, + # returns default. + # + def from_superclass(method, default=nil) + if self == baseclass || !superclass.respond_to?(method, true) + default + else + value = superclass.send(method) + value.dup if value + end + end + + # A flag that makes the process exit with status 1 if any error happens. + # + def exit_on_failure? + false + end + + # SIGNATURE: Sets the baseclass. This is where the superclass lookup + # finishes. + def baseclass #:nodoc: + end + + # SIGNATURE: Creates a new task if valid_task? is true. This method is + # called when a new method is added to the class. + def create_task(meth) #:nodoc: + end + + # SIGNATURE: Defines behavior when the initialize method is added to the + # class. + def initialize_added #:nodoc: + end + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/core_ext/hash_with_indifferent_access.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/core_ext/hash_with_indifferent_access.rb new file mode 100644 index 0000000000..78bc5cf4bf --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/core_ext/hash_with_indifferent_access.rb @@ -0,0 +1,75 @@ +class Thor + module CoreExt #:nodoc: + + # A hash with indifferent access and magic predicates. + # + # hash = Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true + # + # hash[:foo] #=> 'bar' + # hash['foo'] #=> 'bar' + # hash.foo? #=> true + # + class HashWithIndifferentAccess < ::Hash #:nodoc: + + def initialize(hash={}) + super() + hash.each do |key, value| + self[convert_key(key)] = value + end + end + + def [](key) + super(convert_key(key)) + end + + def []=(key, value) + super(convert_key(key), value) + end + + def delete(key) + super(convert_key(key)) + end + + def values_at(*indices) + indices.collect { |key| self[convert_key(key)] } + end + + def merge(other) + dup.merge!(other) + end + + def merge!(other) + other.each do |key, value| + self[convert_key(key)] = value + end + self + end + + protected + + def convert_key(key) + key.is_a?(Symbol) ? key.to_s : key + end + + # Magic predicates. For instance: + # + # options.force? # => !!options['force'] + # options.shebang # => "/usr/lib/local/ruby" + # options.test_framework?(:rspec) # => options[:test_framework] == :rspec + # + def method_missing(method, *args, &block) + method = method.to_s + if method =~ /^(\w+)\?$/ + if args.empty? + !!self[$1] + else + self[$1] == args.first + end + else + self[method] + end + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/core_ext/ordered_hash.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/core_ext/ordered_hash.rb new file mode 100644 index 0000000000..27fea5bb35 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/core_ext/ordered_hash.rb @@ -0,0 +1,100 @@ +class Thor + module CoreExt #:nodoc: + + if RUBY_VERSION >= '1.9' + class OrderedHash < ::Hash + end + else + # This class is based on the Ruby 1.9 ordered hashes. + # + # It keeps the semantics and most of the efficiency of normal hashes + # while also keeping track of the order in which elements were set. + # + class OrderedHash #:nodoc: + include Enumerable + + Node = Struct.new(:key, :value, :next, :prev) + + def initialize + @hash = {} + end + + def [](key) + @hash[key] && @hash[key].value + end + + def []=(key, value) + if node = @hash[key] + node.value = value + else + node = Node.new(key, value) + + if @first.nil? + @first = @last = node + else + node.prev = @last + @last.next = node + @last = node + end + end + + @hash[key] = node + value + end + + def delete(key) + if node = @hash[key] + prev_node = node.prev + next_node = node.next + + next_node.prev = prev_node if next_node + prev_node.next = next_node if prev_node + + @first = next_node if @first == node + @last = prev_node if @last == node + + value = node.value + end + + @hash.delete(key) + value + end + + def keys + self.map { |k, v| k } + end + + def values + self.map { |k, v| v } + end + + def each + return unless @first + yield [@first.key, @first.value] + node = @first + yield [node.key, node.value] while node = node.next + self + end + + def merge(other) + hash = self.class.new + + self.each do |key, value| + hash[key] = value + end + + other.each do |key, value| + hash[key] = value + end + + hash + end + + def empty? + @hash.empty? + end + end + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/error.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/error.rb new file mode 100644 index 0000000000..f9b31a35d1 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/error.rb @@ -0,0 +1,27 @@ +class Thor + # Thor::Error is raised when it's caused by wrong usage of thor classes. Those + # errors have their backtrace supressed and are nicely shown to the user. + # + # Errors that are caused by the developer, like declaring a method which + # overwrites a thor keyword, it SHOULD NOT raise a Thor::Error. This way, we + # ensure that developer errors are shown with full backtrace. + # + class Error < StandardError + end + + # Raised when a task was not found. + # + class UndefinedTaskError < Error + end + + # Raised when a task was found, but not invoked properly. + # + class InvocationError < Error + end + + class RequiredArgumentMissingError < InvocationError + end + + class MalformattedArgumentError < InvocationError + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/group.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/group.rb new file mode 100644 index 0000000000..1e59df2313 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/group.rb @@ -0,0 +1,263 @@ +# Thor has a special class called Thor::Group. The main difference to Thor class +# is that it invokes all tasks at once. It also include some methods that allows +# invocations to be done at the class method, which are not available to Thor +# tasks. +# +class Thor::Group + class << self + # The descrition for this Thor::Group. If none is provided, but a source root + # exists, tries to find the USAGE one folder above it, otherwise searches + # in the superclass. + # + # ==== Parameters + # description<String>:: The description for this Thor::Group. + # + def desc(description=nil) + case description + when nil + @desc ||= from_superclass(:desc, nil) + else + @desc = description + end + end + + # Start works differently in Thor::Group, it simply invokes all tasks + # inside the class. + # + def start(given_args=ARGV, config={}) + super do + if Thor::HELP_MAPPINGS.include?(given_args.first) + help(config[:shell]) + return + end + + args, opts = Thor::Options.split(given_args) + new(args, opts, config).invoke + end + end + + # Prints help information. + # + # ==== Options + # short:: When true, shows only usage. + # + def help(shell, options={}) + if options[:short] + shell.say banner + else + shell.say "Usage:" + shell.say " #{banner}" + shell.say + class_options_help(shell) + shell.say self.desc if self.desc + end + end + + # Stores invocations for this class merging with superclass values. + # + def invocations #:nodoc: + @invocations ||= from_superclass(:invocations, {}) + end + + # Stores invocation blocks used on invoke_from_option. + # + def invocation_blocks #:nodoc: + @invocation_blocks ||= from_superclass(:invocation_blocks, {}) + end + + # Invoke the given namespace or class given. It adds an instance + # method that will invoke the klass and task. You can give a block to + # configure how it will be invoked. + # + # The namespace/class given will have its options showed on the help + # usage. Check invoke_from_option for more information. + # + def invoke(*names, &block) + options = names.last.is_a?(Hash) ? names.pop : {} + verbose = options.fetch(:verbose, :white) + + names.each do |name| + invocations[name] = false + invocation_blocks[name] = block if block_given? + + class_eval <<-METHOD, __FILE__, __LINE__ + def _invoke_#{name.to_s.gsub(/\W/, '_')} + klass, task = self.class.prepare_for_invocation(nil, #{name.inspect}) + + if klass + say_status :invoke, #{name.inspect}, #{verbose.inspect} + block = self.class.invocation_blocks[#{name.inspect}] + _invoke_for_class_method klass, task, &block + else + say_status :error, %(#{name.inspect} [not found]), :red + end + end + METHOD + end + end + + # Invoke a thor class based on the value supplied by the user to the + # given option named "name". A class option must be created before this + # method is invoked for each name given. + # + # ==== Examples + # + # class GemGenerator < Thor::Group + # class_option :test_framework, :type => :string + # invoke_from_option :test_framework + # end + # + # ==== Boolean options + # + # In some cases, you want to invoke a thor class if some option is true or + # false. This is automatically handled by invoke_from_option. Then the + # option name is used to invoke the generator. + # + # ==== Preparing for invocation + # + # In some cases you want to customize how a specified hook is going to be + # invoked. You can do that by overwriting the class method + # prepare_for_invocation. The class method must necessarily return a klass + # and an optional task. + # + # ==== Custom invocations + # + # You can also supply a block to customize how the option is giong to be + # invoked. The block receives two parameters, an instance of the current + # class and the klass to be invoked. + # + def invoke_from_option(*names, &block) + options = names.last.is_a?(Hash) ? names.pop : {} + verbose = options.fetch(:verbose, :white) + + names.each do |name| + unless class_options.key?(name) + raise ArgumentError, "You have to define the option #{name.inspect} " << + "before setting invoke_from_option." + end + + invocations[name] = true + invocation_blocks[name] = block if block_given? + + class_eval <<-METHOD, __FILE__, __LINE__ + def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')} + return unless options[#{name.inspect}] + + value = options[#{name.inspect}] + value = #{name.inspect} if TrueClass === value + klass, task = self.class.prepare_for_invocation(#{name.inspect}, value) + + if klass + say_status :invoke, value, #{verbose.inspect} + block = self.class.invocation_blocks[#{name.inspect}] + _invoke_for_class_method klass, task, &block + else + say_status :error, %(\#{value} [not found]), :red + end + end + METHOD + end + end + + # Remove a previously added invocation. + # + # ==== Examples + # + # remove_invocation :test_framework + # + def remove_invocation(*names) + names.each do |name| + remove_task(name) + remove_class_option(name) + invocations.delete(name) + invocation_blocks.delete(name) + end + end + + # Overwrite class options help to allow invoked generators options to be + # shown recursively when invoking a generator. + # + def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc: + group_options = {} + + get_options_from_invocations(group_options, class_options) do |klass| + klass.send(:get_options_from_invocations, group_options, class_options) + end + + group_options.merge!(extra_group) if extra_group + super(shell, ungrouped_name, group_options) + end + + # Get invocations array and merge options from invocations. Those + # options are added to group_options hash. Options that already exists + # in base_options are not added twice. + # + def get_options_from_invocations(group_options, base_options) #:nodoc: + invocations.each do |name, from_option| + value = if from_option + option = class_options[name] + option.type == :boolean ? name : option.default + else + name + end + next unless value + + klass, task = prepare_for_invocation(name, value) + next unless klass && klass.respond_to?(:class_options) + + value = value.to_s + human_name = value.respond_to?(:classify) ? value.classify : value + + group_options[human_name] ||= [] + group_options[human_name] += klass.class_options.values.select do |option| + base_options[option.name.to_sym].nil? && option.group.nil? && + !group_options.values.flatten.any? { |i| i.name == option.name } + end + + yield klass if block_given? + end + end + + protected + + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Thor::Runner. + # + def banner + "#{self.namespace} #{self.arguments.map {|a| a.usage }.join(' ')}" + end + + def baseclass #:nodoc: + Thor::Group + end + + def create_task(meth) #:nodoc: + tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil) + true + end + end + + include Thor::Base + + protected + + # Shortcut to invoke with padding and block handling. Use internally by + # invoke and invoke_from_option class methods. + # + def _invoke_for_class_method(klass, task=nil, *args, &block) #:nodoc: + shell.padding += 1 + + result = if block_given? + if block.arity == 2 + block.call(self, klass) + else + block.call(self, klass, task) + end + else + invoke klass, task, *args + end + + shell.padding -= 1 + result + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/invocation.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/invocation.rb new file mode 100644 index 0000000000..32e6a72454 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/invocation.rb @@ -0,0 +1,178 @@ +class Thor + module Invocation + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + # Prepare for class methods invocations. This method must return a klass to + # have the invoked class options showed in help messages in generators. + # + def prepare_for_invocation(key, name) #:nodoc: + case name + when Symbol, String + Thor::Util.namespace_to_thor_class_and_task(name.to_s, false) + else + name + end + end + end + + # Make initializer aware of invocations and the initializer proc. + # + def initialize(args=[], options={}, config={}, &block) #:nodoc: + @_invocations = config[:invocations] || Hash.new { |h,k| h[k] = [] } + @_initializer = [ args, options, config ] + super + end + + # Receives a name and invokes it. The name can be a string (either "task" or + # "namespace:task"), a Thor::Task, a Class or a Thor instance. If the task + # cannot be guessed by name, it can also be supplied as second argument. + # + # You can also supply the arguments, options and configuration values for + # the task to be invoked, if none is given, the same values used to + # initialize the invoker are used to initialize the invoked. + # + # ==== Examples + # + # class A < Thor + # def foo + # invoke :bar + # invoke "b:hello", ["José"] + # end + # + # def bar + # invoke "b:hello", ["José"] + # end + # end + # + # class B < Thor + # def hello(name) + # puts "hello #{name}" + # end + # end + # + # You can notice that the method "foo" above invokes two tasks: "bar", + # which belongs to the same class and "hello" which belongs to the class B. + # + # By using an invocation system you ensure that a task is invoked only once. + # In the example above, invoking "foo" will invoke "b:hello" just once, even + # if it's invoked later by "bar" method. + # + # When class A invokes class B, all arguments used on A initialization are + # supplied to B. This allows lazy parse of options. Let's suppose you have + # some rspec tasks: + # + # class Rspec < Thor::Group + # class_option :mock_framework, :type => :string, :default => :rr + # + # def invoke_mock_framework + # invoke "rspec:#{options[:mock_framework]}" + # end + # end + # + # As you noticed, it invokes the given mock framework, which might have its + # own options: + # + # class Rspec::RR < Thor::Group + # class_option :style, :type => :string, :default => :mock + # end + # + # Since it's not rspec concern to parse mock framework options, when RR + # is invoked all options are parsed again, so RR can extract only the options + # that it's going to use. + # + # If you want Rspec::RR to be initialized with its own set of options, you + # have to do that explicitely: + # + # invoke "rspec:rr", [], :style => :foo + # + # Besides giving an instance, you can also give a class to invoke: + # + # invoke Rspec::RR, [], :style => :foo + # + def invoke(name=nil, task=nil, args=nil, opts=nil, config=nil) + task, args, opts, config = nil, task, args, opts if task.nil? || task.is_a?(Array) + args, opts, config = nil, args, opts if args.is_a?(Hash) + + object, task = _prepare_for_invocation(name, task) + klass, instance = _initialize_klass_with_initializer(object, args, opts, config) + + method_args = [] + current = @_invocations[klass] + + iterator = proc do |_, task| + unless current.include?(task.name) + current << task.name + task.run(instance, method_args) + end + end + + if task + args ||= [] + method_args = args[Range.new(klass.arguments.size, -1)] || [] + iterator.call(nil, task) + else + klass.all_tasks.map(&iterator) + end + end + + protected + + # Configuration values that are shared between invocations. + # + def _shared_configuration #:nodoc: + { :invocations => @_invocations } + end + + # Prepare for invocation in the instance level. In this case, we have to + # take into account that a just a task name from the current class was + # given or even a Thor::Task object. + # + def _prepare_for_invocation(name, sent_task=nil) #:nodoc: + if name.is_a?(Thor::Task) + task = name + elsif task = self.class.all_tasks[name.to_s] + object = self + else + object, task = self.class.prepare_for_invocation(nil, name) + task ||= sent_task + end + + # If the object was not set, use self and use the name as task. + object, task = self, name unless object + return object, _validate_task(object, task) + end + + # Check if the object given is a Thor class object and get a task object + # for it. + # + def _validate_task(object, task) #:nodoc: + klass = object.is_a?(Class) ? object : object.class + raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base + + task ||= klass.default_task if klass <= Thor + task = klass.all_tasks[task.to_s] || Thor::Task::Dynamic.new(task) if task && !task.is_a?(Thor::Task) + task + end + + # Initialize klass using values stored in the @_initializer. + # + def _initialize_klass_with_initializer(object, args, opts, config) #:nodoc: + if object.is_a?(Class) + klass = object + + stored_args, stored_opts, stored_config = @_initializer + args ||= stored_args.dup + opts ||= stored_opts.dup + + config ||= {} + config = stored_config.merge(_shared_configuration).merge!(config) + [ klass, klass.new(args, opts, config) ] + else + [ object.class, object ] + end + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser.rb new file mode 100644 index 0000000000..57a3f6e1a5 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser.rb @@ -0,0 +1,4 @@ +require 'thor/parser/argument' +require 'thor/parser/arguments' +require 'thor/parser/option' +require 'thor/parser/options' diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/argument.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/argument.rb new file mode 100644 index 0000000000..aa8ace4719 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/argument.rb @@ -0,0 +1,67 @@ +class Thor + class Argument #:nodoc: + VALID_TYPES = [ :numeric, :hash, :array, :string ] + + attr_reader :name, :description, :required, :type, :default, :banner + alias :human_name :name + + def initialize(name, description=nil, required=true, type=:string, default=nil, banner=nil) + class_name = self.class.name.split("::").last + + raise ArgumentError, "#{class_name} name can't be nil." if name.nil? + raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type) + + @name = name.to_s + @description = description + @required = required || false + @type = (type || :string).to_sym + @default = default + @banner = banner || default_banner + + validate! # Trigger specific validations + end + + def usage + required? ? banner : "[#{banner}]" + end + + def required? + required + end + + def show_default? + case default + when Array, String, Hash + !default.empty? + else + default + end + end + + protected + + def validate! + raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil? + end + + def valid_type?(type) + VALID_TYPES.include?(type.to_sym) + end + + def default_banner + case type + when :boolean + nil + when :string, :default + human_name.upcase + when :numeric + "N" + when :hash + "key:value" + when :array + "one two three" + end + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/arguments.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/arguments.rb new file mode 100644 index 0000000000..fb5d965e06 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/arguments.rb @@ -0,0 +1,145 @@ +class Thor + class Arguments #:nodoc: + NUMERIC = /(\d*\.\d+|\d+)/ + + # Receives an array of args and returns two arrays, one with arguments + # and one with switches. + # + def self.split(args) + arguments = [] + + args.each do |item| + break if item =~ /^-/ + arguments << item + end + + return arguments, args[Range.new(arguments.size, -1)] + end + + def self.parse(base, args) + new(base).parse(args) + end + + # Takes an array of Thor::Argument objects. + # + def initialize(arguments=[]) + @assigns, @non_assigned_required = {}, [] + @switches = arguments + + arguments.each do |argument| + if argument.default + @assigns[argument.human_name] = argument.default + elsif argument.required? + @non_assigned_required << argument + end + end + end + + def parse(args) + @pile = args.dup + + @switches.each do |argument| + break unless peek + @non_assigned_required.delete(argument) + @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name) + end + + check_requirement! + @assigns + end + + private + + def peek + @pile.first + end + + def shift + @pile.shift + end + + def unshift(arg) + unless arg.kind_of?(Array) + @pile.unshift(arg) + else + @pile = arg + @pile + end + end + + def current_is_value? + peek && peek.to_s !~ /^-/ + end + + # Runs through the argument array getting strings that contains ":" and + # mark it as a hash: + # + # [ "name:string", "age:integer" ] + # + # Becomes: + # + # { "name" => "string", "age" => "integer" } + # + def parse_hash(name) + return shift if peek.is_a?(Hash) + hash = {} + + while current_is_value? && peek.include?(?:) + key, value = shift.split(':') + hash[key] = value + end + hash + end + + # Runs through the argument array getting all strings until no string is + # found or a switch is found. + # + # ["a", "b", "c"] + # + # And returns it as an array: + # + # ["a", "b", "c"] + # + def parse_array(name) + return shift if peek.is_a?(Array) + array = [] + + while current_is_value? + array << shift + end + array + end + + # Check if the peel is numeric ofrmat and return a Float or Integer. + # Otherwise raises an error. + # + def parse_numeric(name) + return shift if peek.is_a?(Numeric) + + unless peek =~ NUMERIC && $& == peek + raise MalformattedArgumentError, "expected numeric value for '#{name}'; got #{peek.inspect}" + end + + $&.index('.') ? shift.to_f : shift.to_i + end + + # Parse string, i.e., just return the current value in the pile. + # + def parse_string(name) + shift + end + + # Raises an error if @non_assigned_required array is not empty. + # + def check_requirement! + unless @non_assigned_required.empty? + names = @non_assigned_required.map do |o| + o.respond_to?(:switch_name) ? o.switch_name : o.human_name + end.join("', '") + + class_name = self.class.name.split('::').last.downcase + raise RequiredArgumentMissingError, "no value provided for required #{class_name} '#{names}'" + end + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/option.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/option.rb new file mode 100644 index 0000000000..9e40ec73fa --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/option.rb @@ -0,0 +1,132 @@ +class Thor + class Option < Argument #:nodoc: + attr_reader :aliases, :group + + VALID_TYPES = [:boolean, :numeric, :hash, :array, :string] + + def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, group=nil, aliases=nil) + super(name, description, required, type, default, banner) + @aliases = [*aliases].compact + @group = group.to_s.capitalize if group + end + + # This parse quick options given as method_options. It makes several + # assumptions, but you can be more specific using the option method. + # + # parse :foo => "bar" + # #=> Option foo with default value bar + # + # parse [:foo, :baz] => "bar" + # #=> Option foo with default value bar and alias :baz + # + # parse :foo => :required + # #=> Required option foo without default value + # + # parse :foo => 2 + # #=> Option foo with default value 2 and type numeric + # + # parse :foo => :numeric + # #=> Option foo without default value and type numeric + # + # parse :foo => true + # #=> Option foo with default value true and type boolean + # + # The valid types are :boolean, :numeric, :hash, :array and :string. If none + # is given a default type is assumed. This default type accepts arguments as + # string (--foo=value) or booleans (just --foo). + # + # By default all options are optional, unless :required is given. + # + def self.parse(key, value) + if key.is_a?(Array) + name, *aliases = key + else + name, aliases = key, [] + end + + name = name.to_s + default = value + + type = case value + when Symbol + default = nil + + if VALID_TYPES.include?(value) + value + elsif required = (value == :required) + :string + elsif value == :optional + # TODO Remove this warning in the future. + warn "Optional type is deprecated. Choose :boolean or :string instead. Assumed to be :boolean." + :boolean + end + when TrueClass, FalseClass + :boolean + when Numeric + :numeric + when Hash, Array, String + value.class.name.downcase.to_sym + end + + self.new(name.to_s, nil, required, type, default, nil, nil, aliases) + end + + def switch_name + @switch_name ||= dasherized? ? name : dasherize(name) + end + + def human_name + @human_name ||= dasherized? ? undasherize(name) : name + end + + def usage(padding=0) + sample = if banner && !banner.to_s.empty? + "#{switch_name}=#{banner}" + else + switch_name + end + + sample = "[#{sample}]" unless required? + + if aliases.empty? + (" " * padding) << sample + else + "#{aliases.join(', ')}, #{sample}" + end + end + + # Allow some type predicates as: boolean?, string? and etc. + # + def method_missing(method, *args, &block) + given = method.to_s.sub(/\?$/, '').to_sym + if valid_type?(given) + self.type == given + else + super + end + end + + protected + + def validate! + raise ArgumentError, "An option cannot be boolean and required." if boolean? && required? + end + + def valid_type?(type) + VALID_TYPES.include?(type.to_sym) + end + + def dasherized? + name.index('-') == 0 + end + + def undasherize(str) + str.sub(/^-{1,2}/, '') + end + + def dasherize(str) + (str.length > 1 ? "--" : "-") + str.gsub('_', '-') + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/options.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/options.rb new file mode 100644 index 0000000000..75092308b5 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/parser/options.rb @@ -0,0 +1,142 @@ +class Thor + # This is a modified version of Daniel Berger's Getopt::Long class, licensed + # under Ruby's license. + # + class Options < Arguments #:nodoc: + LONG_RE = /^(--\w+[-\w+]*)$/ + SHORT_RE = /^(-[a-z])$/i + EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i + SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args + SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i + + # Receives a hash and makes it switches. + # + def self.to_switches(options) + options.map do |key, value| + case value + when true + "--#{key}" + when Array + "--#{key} #{value.map{ |v| v.inspect }.join(' ')}" + when Hash + "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}" + when nil, false + "" + else + "--#{key} #{value.inspect}" + end + end.join(" ") + end + + # Takes a hash of Thor::Option objects. + # + def initialize(options={}) + options = options.values + super(options) + @shorts, @switches = {}, {} + + options.each do |option| + @switches[option.switch_name] = option + + option.aliases.each do |short| + @shorts[short.to_s] ||= option.switch_name + end + end + end + + def parse(args) + @pile = args.dup + + while peek + if current_is_switch? + case shift + when SHORT_SQ_RE + unshift($1.split('').map { |f| "-#{f}" }) + next + when EQ_RE, SHORT_NUM + unshift($2) + switch = $1 + when LONG_RE, SHORT_RE + switch = $1 + end + + switch = normalize_switch(switch) + next unless option = switch_option(switch) + + @assigns[option.human_name] = parse_peek(switch, option) + else + shift + end + end + + check_requirement! + @assigns + end + + protected + + # Returns true if the current value in peek is a registered switch. + # + def current_is_switch? + case peek + when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM + switch?($1) + when SHORT_SQ_RE + $1.split('').any? { |f| switch?("-#{f}") } + end + end + + def switch?(arg) + switch_option(arg) || @shorts.key?(arg) + end + + def switch_option(arg) + if match = no_or_skip?(arg) + @switches[arg] || @switches["--#{match}"] + else + @switches[arg] + end + end + + def no_or_skip?(arg) + arg =~ /^--(no|skip)-([-\w]+)$/ + $2 + end + + # Check if the given argument is actually a shortcut. + # + def normalize_switch(arg) + @shorts.key?(arg) ? @shorts[arg] : arg + end + + # Parse boolean values which can be given as --foo=true, --foo or --no-foo. + # + def parse_boolean(switch) + if current_is_value? + ["true", "TRUE", "t", "T", true].include?(shift) + else + @switches.key?(switch) || !no_or_skip?(switch) + end + end + + # Parse the value at the peek analyzing if it requires an input or not. + # + def parse_peek(switch, option) + unless current_is_value? + if option.boolean? + # No problem for boolean types + elsif no_or_skip?(switch) + return nil # User set value to nil + elsif option.string? && !option.required? + return option.human_name # Return the option name + else + raise MalformattedArgumentError, "no value provided for option '#{switch}'" + end + end + + @non_assigned_required.delete(option) + send(:"parse_#{option.type}", switch) + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/rake_compat.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/rake_compat.rb new file mode 100644 index 0000000000..3ab6bb21f5 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/rake_compat.rb @@ -0,0 +1,67 @@ +require 'rake' + +class Thor + # Adds a compatibility layer to your Thor classes which allows you to use + # rake package tasks. For example, to use rspec rake tasks, one can do: + # + # require 'thor/rake_compat' + # + # class Default < Thor + # include Thor::RakeCompat + # + # Spec::Rake::SpecTask.new(:spec) do |t| + # t.spec_opts = ['--options', "spec/spec.opts"] + # t.spec_files = FileList['spec/**/*_spec.rb'] + # end + # end + # + module RakeCompat + def self.rake_classes + @rake_classes ||= [] + end + + def self.included(base) + # Hack. Make rakefile point to invoker, so rdoc task is generated properly. + Rake.application.instance_variable_set(:@rakefile, caller[0].match(/(.*):\d+/)[1]) + self.rake_classes << base + end + end +end + +class Object #:nodoc: + alias :rake_task :task + alias :rake_namespace :namespace + + def task(*args, &block) + task = rake_task(*args, &block) + + if klass = Thor::RakeCompat.rake_classes.last + non_namespaced_name = task.name.split(':').last + + description = non_namespaced_name + description << task.arg_names.map{ |n| n.to_s.upcase }.join(' ') + description.strip! + + klass.desc description, task.comment || non_namespaced_name + klass.class_eval <<-METHOD + def #{non_namespaced_name}(#{task.arg_names.join(', ')}) + Rake::Task[#{task.name.to_sym.inspect}].invoke(#{task.arg_names.join(', ')}) + end + METHOD + end + + task + end + + def namespace(name, &block) + if klass = Thor::RakeCompat.rake_classes.last + const_name = Thor::Util.camel_case(name.to_s).to_sym + klass.const_set(const_name, Class.new(Thor)) + new_klass = klass.const_get(const_name) + Thor::RakeCompat.rake_classes << new_klass + end + + rake_namespace(name, &block) + Thor::RakeCompat.rake_classes.pop + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/runner.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/runner.rb new file mode 100644 index 0000000000..43da09b336 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/runner.rb @@ -0,0 +1,299 @@ +require 'fileutils' +require 'open-uri' +require 'yaml' +require 'digest/md5' +require 'pathname' + +class Thor::Runner < Thor #:nodoc: + map "-T" => :list, "-i" => :install, "-u" => :update + + # Override Thor#help so it can give information about any class and any method. + # + def help(meth=nil) + if meth && !self.respond_to?(meth) + initialize_thorfiles(meth) + klass, task = Thor::Util.namespace_to_thor_class_and_task(meth) + # Send mapping -h because it works with Thor::Group too + klass.start(["-h", task].compact, :shell => self.shell) + else + super + end + end + + # If a task is not found on Thor::Runner, method missing is invoked and + # Thor::Runner is then responsable for finding the task in all classes. + # + def method_missing(meth, *args) + meth = meth.to_s + initialize_thorfiles(meth) + klass, task = Thor::Util.namespace_to_thor_class_and_task(meth) + args.unshift(task) if task + klass.start(args, :shell => shell) + end + + desc "install NAME", "Install an optionally named Thor file into your system tasks" + method_options :as => :string, :relative => :boolean + def install(name) + initialize_thorfiles + + # If a directory name is provided as the argument, look for a 'main.thor' + # task in said directory. + begin + if File.directory?(File.expand_path(name)) + base, package = File.join(name, "main.thor"), :directory + contents = open(base).read + else + base, package = name, :file + contents = open(name).read + end + rescue OpenURI::HTTPError + raise Error, "Error opening URI '#{name}'" + rescue Errno::ENOENT + raise Error, "Error opening file '#{name}'" + end + + say "Your Thorfile contains:" + say contents + + return false if no?("Do you wish to continue [y/N]?") + + as = options["as"] || begin + first_line = contents.split("\n")[0] + (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil + end + + unless as + basename = File.basename(name) + as = ask("Please specify a name for #{name} in the system repository [#{basename}]:") + as = basename if as.empty? + end + + location = if options[:relative] || name =~ /^http:\/\// + name + else + File.expand_path(name) + end + + thor_yaml[as] = { + :filename => Digest::MD5.hexdigest(name + as), + :location => location, + :namespaces => Thor::Util.namespaces_in_content(contents, base) + } + + save_yaml(thor_yaml) + say "Storing thor file in your system repository" + destination = File.join(thor_root, thor_yaml[as][:filename]) + + if package == :file + File.open(destination, "w") { |f| f.puts contents } + else + FileUtils.cp_r(name, destination) + end + + thor_yaml[as][:filename] # Indicate success + end + + desc "uninstall NAME", "Uninstall a named Thor module" + def uninstall(name) + raise Error, "Can't find module '#{name}'" unless thor_yaml[name] + say "Uninstalling #{name}." + FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}")) + + thor_yaml.delete(name) + save_yaml(thor_yaml) + + puts "Done." + end + + desc "update NAME", "Update a Thor file from its original location" + def update(name) + raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location] + + say "Updating '#{name}' from #{thor_yaml[name][:location]}" + + old_filename = thor_yaml[name][:filename] + self.options = self.options.merge("as" => name) + filename = install(thor_yaml[name][:location]) + + unless filename == old_filename + File.delete(File.join(thor_root, old_filename)) + end + end + + desc "installed", "List the installed Thor modules and tasks" + method_options :internal => :boolean + def installed + initialize_thorfiles(nil, true) + + klasses = Thor::Base.subclasses + klasses -= [Thor, Thor::Runner] unless options["internal"] + + display_klasses(true, klasses) + end + + desc "list [SEARCH]", "List the available thor tasks (--substring means .*SEARCH)" + method_options :substring => :boolean, :group => :string, :all => :boolean + def list(search="") + initialize_thorfiles + + search = ".*#{search}" if options["substring"] + search = /^#{search}.*/i + group = options[:group] || "standard" + + klasses = Thor::Base.subclasses.select do |k| + (options[:all] || k.group == group) && k.namespace =~ search + end + + display_klasses(false, klasses) + end + + private + + def thor_root + Thor::Util.thor_root + end + + def thor_yaml + @thor_yaml ||= begin + yaml_file = File.join(thor_root, "thor.yml") + yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file) + yaml || {} + end + end + + # Save the yaml file. If none exists in thor root, creates one. + # + def save_yaml(yaml) + yaml_file = File.join(thor_root, "thor.yml") + + unless File.exists?(yaml_file) + FileUtils.mkdir_p(thor_root) + yaml_file = File.join(thor_root, "thor.yml") + FileUtils.touch(yaml_file) + end + + File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml } + end + + def self.exit_on_failure? + true + end + + # Load the thorfiles. If relevant_to is supplied, looks for specific files + # in the thor_root instead of loading them all. + # + # By default, it also traverses the current path until find Thor files, as + # described in thorfiles. This look up can be skipped by suppliying + # skip_lookup true. + # + def initialize_thorfiles(relevant_to=nil, skip_lookup=false) + thorfiles(relevant_to, skip_lookup).each do |f| + Thor::Util.load_thorfile(f) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f)) + end + end + + # Finds Thorfiles by traversing from your current directory down to the root + # directory of your system. If at any time we find a Thor file, we stop. + # + # We also ensure that system-wide Thorfiles are loaded first, so local + # Thorfiles can override them. + # + # ==== Example + # + # If we start at /Users/wycats/dev/thor ... + # + # 1. /Users/wycats/dev/thor + # 2. /Users/wycats/dev + # 3. /Users/wycats <-- we find a Thorfile here, so we stop + # + # Suppose we start at c:\Documents and Settings\james\dev\thor ... + # + # 1. c:\Documents and Settings\james\dev\thor + # 2. c:\Documents and Settings\james\dev + # 3. c:\Documents and Settings\james + # 4. c:\Documents and Settings + # 5. c:\ <-- no Thorfiles found! + # + def thorfiles(relevant_to=nil, skip_lookup=false) + # Deal with deprecated thor when :namespaces: is available as constants + save_yaml(thor_yaml) if Thor::Util.convert_constants_to_namespaces(thor_yaml) + + thorfiles = [] + + unless skip_lookup + Pathname.pwd.ascend do |path| + thorfiles = Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten + break unless thorfiles.empty? + end + end + + files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Thor::Util.thor_root_glob) + files += thorfiles + files -= ["#{thor_root}/thor.yml"] + + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end + end + + # Load thorfiles relevant to the given method. If you provide "foo:bar" it + # will load all thor files in the thor.yaml that has "foo" e "foo:bar" + # namespaces registered. + # + def thorfiles_relevant_to(meth) + lookup = [ meth, meth.split(":")[0...-1].join(":") ] + + files = thor_yaml.select do |k, v| + v[:namespaces] && !(v[:namespaces] & lookup).empty? + end + + files.map { |k, v| File.join(thor_root, "#{v[:filename]}") } + end + + # Display information about the given klasses. If with_module is given, + # it shows a table with information extracted from the yaml file. + # + def display_klasses(with_modules=false, klasses=Thor.subclasses) + klasses -= [Thor, Thor::Runner] unless with_modules + raise Error, "No Thor tasks available" if klasses.empty? + + if with_modules && !thor_yaml.empty? + info = [] + labels = ["Modules", "Namespaces"] + + info << labels + info << [ "-" * labels[0].size, "-" * labels[1].size ] + + thor_yaml.each do |name, hash| + info << [ name, hash[:namespaces].join(", ") ] + end + + print_table info + say "" + end + + unless klasses.empty? + klasses.dup.each do |klass| + klasses -= Thor::Util.thor_classes_in(klass) + end + + klasses.each { |k| display_tasks(k) } + else + say "\033[1;34mNo Thor tasks available\033[0m" + end + end + + # Display tasks from the given Thor class. + # + def display_tasks(klass) + unless klass.tasks.empty? + base = klass.namespace + + color = base == "default" ? :magenta : :blue + say shell.set_color(base, color, true) + say "-" * base.length + + klass.help(shell, :short => true, :ident => 0, :namespace => true) + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell.rb new file mode 100644 index 0000000000..0d3f4d5951 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell.rb @@ -0,0 +1,72 @@ +require 'thor/shell/color' + +class Thor + module Base + # Returns the shell used in all Thor classes. Default to color one. + # + def self.shell + @shell ||= Thor::Shell::Color + end + + # Sets the shell used in all Thor classes. + # + def self.shell=(klass) + @shell = klass + end + end + + module Shell + SHELL_DELEGATED_METHODS = [:ask, :yes?, :no?, :say, :say_status, :print_list, :print_table] + + # Add shell to initialize config values. + # + # ==== Configuration + # shell<Object>:: An instance of the shell to be used. + # + # ==== Examples + # + # class MyScript < Thor + # argument :first, :type => :numeric + # end + # + # MyScript.new [1.0], { :foo => :bar }, :shell => Thor::Shell::Basic.new + # + def initialize(args=[], options={}, config={}) + super + self.shell = config[:shell] + self.shell.base ||= self if self.shell.respond_to?(:base) + end + + # Holds the shell for the given Thor instance. If no shell is given, + # it gets a default shell from Thor::Base.shell. + # + def shell + @shell ||= Thor::Base.shell.new + end + + # Sets the shell for this thor class. + # + def shell=(shell) + @shell = shell + end + + # Common methods that are delegated to the shell. + # + SHELL_DELEGATED_METHODS.each do |method| + module_eval <<-METHOD, __FILE__, __LINE__ + def #{method}(*args) + shell.#{method}(*args) + end + METHOD + end + + protected + + # Allow shell to be shared between invocations. + # + def _shared_configuration #:nodoc: + super.merge!(:shell => self.shell) + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell/basic.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell/basic.rb new file mode 100644 index 0000000000..ea9665380b --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell/basic.rb @@ -0,0 +1,219 @@ +require 'tempfile' + +class Thor + module Shell + class Basic + attr_accessor :base, :padding + + # Initialize base and padding to nil. + # + def initialize #:nodoc: + @base, @padding = nil, 0 + end + + # Sets the output padding, not allowing less than zero values. + # + def padding=(value) + @padding = [0, value].max + end + + # Ask something to the user and receives a response. + # + # ==== Example + # ask("What is your name?") + # + def ask(statement, color=nil) + say("#{statement} ", color) + $stdin.gets.strip + end + + # Say (print) something to the user. If the sentence ends with a whitespace + # or tab character, a new line is not appended (print + flush). Otherwise + # are passed straight to puts (behavior got from Highline). + # + # ==== Example + # say("I know you knew that.") + # + def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/)) + message = message.to_s + message = set_color(message, color) if color + + if force_new_line + $stdout.puts(message) + else + $stdout.print(message) + $stdout.flush + end + end + + # Say a status with the given color and appends the message. Since this + # method is used frequently by actions, it allows nil or false to be given + # in log_status, avoiding the message from being shown. If a Symbol is + # given in log_status, it's used as the color. + # + def say_status(status, message, log_status=true) + return if quiet? || log_status == false + spaces = " " * (padding + 1) + color = log_status.is_a?(Symbol) ? log_status : :green + + status = status.to_s.rjust(12) + status = set_color status, color, true if color + say "#{status}#{spaces}#{message}", nil, true + end + + # Make a question the to user and returns true if the user replies "y" or + # "yes". + # + def yes?(statement, color=nil) + ask(statement, color) =~ is?(:yes) + end + + # Make a question the to user and returns true if the user replies "n" or + # "no". + # + def no?(statement, color=nil) + !yes?(statement, color) + end + + # Prints a list of items. + # + # ==== Parameters + # list<Array[String, String, ...]> + # + # ==== Options + # mode:: Can be :rows or :inline. Defaults to :rows. + # ident:: Ident each item with the value given. + # + def print_list(list, options={}) + return if list.empty? + + ident = " " * (options[:ident] || 0) + content = case options[:mode] + when :inline + last = list.pop + "#{list.join(", ")}, and #{last}" + else # rows + ident + list.join("\n#{ident}") + end + + $stdout.puts content + end + + # Prints a table. + # + # ==== Parameters + # Array[Array[String, String, ...]] + # + # ==== Options + # ident<Integer>:: Ident the first column by ident value. + # + def print_table(table, options={}) + return if table.empty? + + formats = [] + 0.upto(table.first.length - 2) do |i| + maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size + formats << "%-#{maxima + 2}s" + end + + formats[0] = formats[0].insert(0, " " * options[:ident]) if options[:ident] + formats << "%s" + + table.each do |row| + row.each_with_index do |column, i| + $stdout.print formats[i] % column.to_s + end + $stdout.puts + end + end + + # Deals with file collision and returns true if the file should be + # overwriten and false otherwise. If a block is given, it uses the block + # response as the content for the diff. + # + # ==== Parameters + # destination<String>:: the destination file to solve conflicts + # block<Proc>:: an optional block that returns the value to be used in diff + # + def file_collision(destination) + return true if @always_force + options = block_given? ? "[Ynaqdh]" : "[Ynaqh]" + + while true + answer = ask %[Overwrite #{destination}? (enter "h" for help) #{options}] + + case answer + when is?(:yes), is?(:force) + return true + when is?(:no), is?(:skip) + return false + when is?(:always) + return @always_force = true + when is?(:quit) + say 'Aborting...' + raise SystemExit + when is?(:diff) + show_diff(destination, yield) if block_given? + say 'Retrying...' + else + say file_collision_help + end + end + end + + # Called if something goes wrong during the execution. This is used by Thor + # internally and should not be used inside your scripts. If someone went + # wrong, you can always raise an exception. If you raise a Thor::Error, it + # will be rescued and wrapped in the method below. + # + def error(statement) + $stderr.puts statement + end + + # Apply color to the given string with optional bold. Disabled in the + # Thor::Shell::Basic class. + # + def set_color(string, color, bold=false) #:nodoc: + string + end + + protected + + def is?(value) #:nodoc: + value = value.to_s + + if value.size == 1 + /\A#{value}\z/i + else + /\A(#{value}|#{value[0,1]})\z/i + end + end + + def file_collision_help #:nodoc: +<<HELP +Y - yes, overwrite +n - no, do not overwrite +a - all, overwrite this and all others +q - quit, abort +d - diff, show the differences between the old and the new +h - help, show this help +HELP + end + + def show_diff(destination, content) #:nodoc: + diff_cmd = ENV['THOR_DIFF'] || ENV['RAILS_DIFF'] || 'diff -u' + + Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp| + temp.write content + temp.rewind + system %(#{diff_cmd} "#{destination}" "#{temp.path}") + end + end + + def quiet? #:nodoc: + base && base.options[:quiet] + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell/color.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell/color.rb new file mode 100644 index 0000000000..24704f7885 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/shell/color.rb @@ -0,0 +1,108 @@ +require 'thor/shell/basic' + +class Thor + module Shell + # Inherit from Thor::Shell::Basic and add set_color behavior. Check + # Thor::Shell::Basic to see all available methods. + # + class Color < Basic + # Embed in a String to clear all previous ANSI sequences. + CLEAR = "\e[0m" + # The start of an ANSI bold sequence. + BOLD = "\e[1m" + + # Set the terminal's foreground ANSI color to black. + BLACK = "\e[30m" + # Set the terminal's foreground ANSI color to red. + RED = "\e[31m" + # Set the terminal's foreground ANSI color to green. + GREEN = "\e[32m" + # Set the terminal's foreground ANSI color to yellow. + YELLOW = "\e[33m" + # Set the terminal's foreground ANSI color to blue. + BLUE = "\e[34m" + # Set the terminal's foreground ANSI color to magenta. + MAGENTA = "\e[35m" + # Set the terminal's foreground ANSI color to cyan. + CYAN = "\e[36m" + # Set the terminal's foreground ANSI color to white. + WHITE = "\e[37m" + + # Set the terminal's background ANSI color to black. + ON_BLACK = "\e[40m" + # Set the terminal's background ANSI color to red. + ON_RED = "\e[41m" + # Set the terminal's background ANSI color to green. + ON_GREEN = "\e[42m" + # Set the terminal's background ANSI color to yellow. + ON_YELLOW = "\e[43m" + # Set the terminal's background ANSI color to blue. + ON_BLUE = "\e[44m" + # Set the terminal's background ANSI color to magenta. + ON_MAGENTA = "\e[45m" + # Set the terminal's background ANSI color to cyan. + ON_CYAN = "\e[46m" + # Set the terminal's background ANSI color to white. + ON_WHITE = "\e[47m" + + # Set color by using a string or one of the defined constants. If a third + # option is set to true, it also adds bold to the string. This is based + # on Highline implementation and it automatically appends CLEAR to the end + # of the returned String. + # + def set_color(string, color, bold=false) + color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol) + bold = bold ? BOLD : "" + "#{bold}#{color}#{string}#{CLEAR}" + end + + protected + + # Overwrite show_diff to show diff with colors if Diff::LCS is + # available. + # + def show_diff(destination, content) #:nodoc: + if diff_lcs_loaded? && ENV['THOR_DIFF'].nil? && ENV['RAILS_DIFF'].nil? + actual = File.read(destination).to_s.split("\n") + content = content.to_s.split("\n") + + Diff::LCS.sdiff(actual, content).each do |diff| + output_diff_line(diff) + end + else + super + end + end + + def output_diff_line(diff) #:nodoc: + case diff.action + when '-' + say "- #{diff.old_element.chomp}", :red, true + when '+' + say "+ #{diff.new_element.chomp}", :green, true + when '!' + say "- #{diff.old_element.chomp}", :red, true + say "+ #{diff.new_element.chomp}", :green, true + else + say " #{diff.old_element.chomp}", nil, true + end + end + + # Check if Diff::LCS is loaded. If it is, use it to create pretty output + # for diff. + # + def diff_lcs_loaded? #:nodoc: + return true if defined?(Diff::LCS) + return @diff_lcs_loaded unless @diff_lcs_loaded.nil? + + @diff_lcs_loaded = begin + require 'diff/lcs' + true + rescue LoadError + false + end + end + + end + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/task.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/task.rb new file mode 100644 index 0000000000..91c7564d3f --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/task.rb @@ -0,0 +1,122 @@ +class Thor + class Task < Struct.new(:name, :description, :usage, :options) + + # A dynamic task that handles method missing scenarios. + # + class Dynamic < Task + def initialize(name) + super(name.to_s, "A dynamically-generated task", name.to_s) + end + + def run(instance, args=[]) + unless (instance.methods & [name.to_s, name.to_sym]).empty? + raise Error, "could not find Thor class or task '#{name}'" + end + super + end + end + + def initialize(name, description, usage, options=nil) + super(name.to_s, description, usage, options || {}) + end + + def initialize_copy(other) #:nodoc: + super(other) + self.options = other.options.dup if other.options + end + + def short_description + description.split("\n").first if description + end + + # By default, a task invokes a method in the thor class. You can change this + # implementation to create custom tasks. + # + def run(instance, args=[]) + raise UndefinedTaskError, "the '#{name}' task of #{instance.class} is private" unless public_method?(instance) + instance.send(name, *args) + rescue ArgumentError => e + parse_argument_error(instance, e, caller) + rescue NoMethodError => e + parse_no_method_error(instance, e) + end + + # Returns the formatted usage. If a class is given, the class arguments are + # injected in the usage. + # + def formatted_usage(klass=nil, namespace=false, show_options=true) + formatted = if namespace.is_a?(String) + "#{namespace}:" + elsif klass && namespace + "#{klass.namespace.gsub(/^default/,'')}:" + else + "" + end + + formatted << formatted_arguments(klass) + formatted << " #{formatted_options}" if show_options + formatted.strip! + formatted + end + + # Injects the class arguments into the task usage. + # + def formatted_arguments(klass) + if klass && !klass.arguments.empty? + usage.to_s.gsub(/^#{name}/) do |match| + match << " " << klass.arguments.map{ |a| a.usage }.join(' ') + end + else + usage.to_s + end + end + + # Returns the options usage for this task. + # + def formatted_options + @formatted_options ||= options.map{ |_, o| o.usage }.sort.join(" ") + end + + protected + + # Given a target, checks if this class name is not a private/protected method. + # + def public_method?(instance) #:nodoc: + collection = instance.private_methods + instance.protected_methods + (collection & [name.to_s, name.to_sym]).empty? + end + + # Clean everything that comes from the Thor gempath and remove the caller. + # + def sans_backtrace(backtrace, caller) #:nodoc: + dirname = /^#{Regexp.escape(File.dirname(__FILE__))}/ + saned = backtrace.reject { |frame| frame =~ dirname } + saned -= caller + end + + def parse_argument_error(instance, e, caller) #:nodoc: + backtrace = sans_backtrace(e.backtrace, caller) + + if backtrace.empty? && e.message =~ /wrong number of arguments/ + if instance.is_a?(Thor::Group) + raise e, "'#{name}' was called incorrectly. Are you sure it has arity equals to 0?" + else + raise InvocationError, "'#{name}' was called incorrectly. Call as " << + "'#{formatted_usage(instance.class, true)}'" + end + else + raise e + end + end + + def parse_no_method_error(instance, e) #:nodoc: + if e.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/ + raise UndefinedTaskError, "The #{instance.class.namespace} namespace " << + "doesn't have a '#{name}' task" + else + raise e + end + end + + end +end diff --git a/railties/lib/rails/vendor/thor-0.11.6/lib/thor/util.rb b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/util.rb new file mode 100644 index 0000000000..fd820d7462 --- /dev/null +++ b/railties/lib/rails/vendor/thor-0.11.6/lib/thor/util.rb @@ -0,0 +1,251 @@ +require 'rbconfig' + +class Thor + module Sandbox #:nodoc: + end + + # This module holds several utilities: + # + # 1) Methods to convert thor namespaces to constants and vice-versa. + # + # Thor::Utils.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz" + # + # 2) Loading thor files and sandboxing: + # + # Thor::Utils.load_thorfile("~/.thor/foo") + # + module Util + + # Receives a namespace and search for it in the Thor::Base subclasses. + # + # ==== Parameters + # namespace<String>:: The namespace to search for. + # + def self.find_by_namespace(namespace) + namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/ + + Thor::Base.subclasses.find do |klass| + klass.namespace == namespace + end + end + + # Receives a constant and converts it to a Thor namespace. Since Thor tasks + # can be added to a sandbox, this method is also responsable for removing + # the sandbox namespace. + # + # This method should not be used in general because it's used to deal with + # older versions of Thor. On current versions, if you need to get the + # namespace from a class, just call namespace on it. + # + # ==== Parameters + # constant<Object>:: The constant to be converted to the thor path. + # + # ==== Returns + # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz" + # + def self.namespace_from_thor_class(constant, remove_default=true) + constant = constant.to_s.gsub(/^Thor::Sandbox::/, "") + constant = snake_case(constant).squeeze(":") + constant.gsub!(/^default/, '') if remove_default + constant + end + + # Given the contents, evaluate it inside the sandbox and returns the + # namespaces defined in the sandbox. + # + # ==== Parameters + # contents<String> + # + # ==== Returns + # Array[Object] + # + def self.namespaces_in_content(contents, file=__FILE__) + old_constants = Thor::Base.subclasses.dup + Thor::Base.subclasses.clear + + load_thorfile(file, contents) + + new_constants = Thor::Base.subclasses.dup + Thor::Base.subclasses.replace(old_constants) + + new_constants.map!{ |c| c.namespace } + new_constants.compact! + new_constants + end + + # Returns the thor classes declared inside the given class. + # + def self.thor_classes_in(klass) + Thor::Base.subclasses.select do |subclass| + klass.constants.include?(subclass.name.gsub("#{klass.name}::", '')) + end + end + + # Receives a string and convert it to snake case. SnakeCase returns snake_case. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def self.snake_case(str) + return str.downcase if str =~ /^[A-Z_]+$/ + str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/ + return $+.downcase + end + + # Receives a string and convert it to camel case. camel_case returns CamelCase. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def self.camel_case(str) + return str if str !~ /_/ && str =~ /[A-Z]+.*/ + str.split('_').map { |i| i.capitalize }.join + end + + # Receives a namespace and tries to retrieve a Thor or Thor::Group class + # from it. It first searches for a class using the all the given namespace, + # if it's not found, removes the highest entry and searches for the class + # again. If found, returns the highest entry as the class name. + # + # ==== Examples + # + # class Foo::Bar < Thor + # def baz + # end + # end + # + # class Baz::Foo < Thor::Group + # end + # + # Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task + # Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil + # Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz" + # + # ==== Parameters + # namespace<String> + # + # ==== Errors + # Thor::Error:: raised if the namespace cannot be found. + # + # Thor::Error:: raised if the namespace evals to a class which does not + # inherit from Thor or Thor::Group. + # + def self.namespace_to_thor_class_and_task(namespace, raise_if_nil=true) + if namespace.include?(?:) + pieces = namespace.split(":") + task = pieces.pop + klass = Thor::Util.find_by_namespace(pieces.join(":")) + end + + unless klass + klass, task = Thor::Util.find_by_namespace(namespace), nil + end + + raise Error, "could not find Thor class or task '#{namespace}'" if raise_if_nil && klass.nil? + return klass, task + end + + # Receives a path and load the thor file in the path. The file is evaluated + # inside the sandbox to avoid namespacing conflicts. + # + def self.load_thorfile(path, content=nil) + content ||= File.read(path) + + begin + Thor::Sandbox.class_eval(content, path) + rescue Exception => e + $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}" + end + end + + # Receives a yaml (hash) and updates all constants entries to namespace. + # This was added to deal with deprecated versions of Thor. + # + # TODO Deprecate this method in the future. + # + # ==== Returns + # TrueClass|FalseClass:: Returns true if any change to the yaml file was made. + # + def self.convert_constants_to_namespaces(yaml) + yaml_changed = false + + yaml.each do |k, v| + next unless v[:constants] && v[:namespaces].nil? + yaml_changed = true + yaml[k][:namespaces] = v[:constants].map{|c| Thor::Util.namespace_from_thor_class(c)} + end + + yaml_changed + end + + def self.user_home + @@user_home ||= if ENV["HOME"] + ENV["HOME"] + elsif ENV["USERPROFILE"] + ENV["USERPROFILE"] + elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"] + File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"]) + elsif ENV["APPDATA"] + ENV["APPDATA"] + else + begin + File.expand_path("~") + rescue + if File::ALT_SEPARATOR + "C:/" + else + "/" + end + end + end + end + + # Returns the root where thor files are located, dependending on the OS. + # + def self.thor_root + File.join(user_home, ".thor") + end + + # Returns the files in the thor root. On Windows thor_root will be something + # like this: + # + # C:\Documents and Settings\james\.thor + # + # If we don't #gsub the \ character, Dir.glob will fail. + # + def self.thor_root_glob + files = Dir["#{thor_root.gsub(/\\/, '/')}/*"] + + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end + end + + # Where to look for Thor files. + # + def self.globs_for(path) + ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] + end + + # Return the path to the ruby interpreter taking into account multiple + # installations and windows extensions. + # + def self.ruby_command + @ruby_command ||= begin + ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) + ruby << Config::CONFIG['EXEEXT'] + + # escape string in case path to ruby executable contain spaces. + ruby.sub!(/.*\s.*/m, '"\&"') + ruby + end + end + + end +end diff --git a/railties/lib/rails/webrick_server.rb b/railties/lib/rails/webrick_server.rb new file mode 100644 index 0000000000..2f60151b22 --- /dev/null +++ b/railties/lib/rails/webrick_server.rb @@ -0,0 +1,156 @@ +# Donated by Florian Gross + +require 'webrick' +require 'cgi' +require 'stringio' +require 'dispatcher' + +include WEBrick + +class CGI #:nodoc: + def stdinput + @stdin || $stdin + end + + def env_table + @env_table || ENV + end + + def initialize(type = "query", table = nil, stdin = nil) + @env_table, @stdin = table, stdin + + if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE") + Apache.request.setup_cgi_env + end + + extend QueryExtension + @multipart = false + if defined?(CGI_PARAMS) + warn "do not use CGI_PARAMS and CGI_COOKIES" + @params = CGI_PARAMS.dup + @cookies = CGI_COOKIES.dup + else + initialize_query() # set @params, @cookies + end + @output_cookies = nil + @output_hidden = nil + end +end + +# A custom dispatch servlet for use with WEBrick. It dispatches requests +# (using the Rails Dispatcher) to the appropriate controller/action. By default, +# it restricts WEBrick to a managing a single Rails request at a time, but you +# can change this behavior by setting ActionController::Base.allow_concurrency +# to true. +class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet + # Start the WEBrick server with the given options, mounting the + # DispatchServlet at <tt>/</tt>. + def self.dispatch(options = {}) + Socket.do_not_reverse_lookup = true # patch for OS X + + params = { :Port => options[:port].to_i, + :ServerType => options[:server_type], + :BindAddress => options[:ip] } + params[:MimeTypes] = options[:mime_types] if options[:mime_types] + + server = WEBrick::HTTPServer.new(params) + server.mount('/', DispatchServlet, options) + + trap("INT") { server.shutdown } + server.start + end + + def initialize(server, options) #:nodoc: + @server_options = options + @file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root]) + # Change to the RAILS_ROOT, since Webrick::Daemon.start does a Dir::cwd("/") + # OPTIONS['working_directory'] is an absolute path of the RAILS_ROOT, set in railties/lib/commands/servers/webrick.rb + Dir.chdir(OPTIONS['working_directory']) if defined?(OPTIONS) && File.directory?(OPTIONS['working_directory']) + super + end + + def service(req, res) #:nodoc: + unless handle_file(req, res) + unless handle_dispatch(req, res) + raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." + end + end + end + + def handle_file(req, res) #:nodoc: + begin + req = req.dup + path = req.path.dup + + # Add .html if the last path piece has no . in it + path << '.html' if path != '/' && (%r{(^|/)[^./]+$} =~ path) + path.gsub!('+', ' ') # Unescape + since FileHandler doesn't do so. + + req.instance_variable_set(:@path_info, path) # Set the modified path... + + @file_handler.send(:service, req, res) + return true + rescue HTTPStatus::PartialContent, HTTPStatus::NotModified => err + res.set_error(err) + return true + rescue => err + return false + end + end + + def handle_dispatch(req, res, origin = nil) #:nodoc: + data = StringIO.new + Dispatcher.dispatch( + CGI.new("query", create_env_table(req, origin), StringIO.new(req.body || "")), + ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, + data + ) + + header, body = extract_header_and_body(data) + + set_charset(header) + assign_status(res, header) + res.cookies.concat(header.delete('set-cookie') || []) + header.each { |key, val| res[key] = val.join(", ") } + + res.body = body + return true + rescue => err + p err, err.backtrace + return false + end + + private + def create_env_table(req, origin) + env = req.meta_vars.clone + env.delete "SCRIPT_NAME" + env["QUERY_STRING"] = req.request_uri.query + env["REQUEST_URI"] = origin if origin + return env + end + + def extract_header_and_body(data) + data.rewind + data = data.read + + raw_header, body = *data.split(/^[\xd\xa]{2}/on, 2) + header = WEBrick::HTTPUtils::parse_header(raw_header) + + return header, body + end + + def set_charset(header) + ct = header["content-type"] + if ct.any? { |x| x =~ /^text\// } && ! ct.any? { |x| x =~ /charset=/ } + ch = @server_options[:charset] || "UTF-8" + ct.find { |x| x =~ /^text\// } << ("; charset=" + ch) + end + end + + def assign_status(res, header) + if /^(\d+)/ =~ header['status'][0] + res.status = $1.to_i + header.delete('status') + end + end +end |