diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2010-08-10 18:15:12 +0100 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2010-08-10 18:15:12 +0100 |
commit | 62658500049fbb7a5e7d75537dd6f6a374204207 (patch) | |
tree | 8892d8305ced43866068a6c1c66548e465e45b38 | |
parent | cd2bbed9846d84a1230a1b9e52843eedca17b28d (diff) | |
parent | e86cced311539932420f9cda49d736606d106c28 (diff) | |
download | rails-62658500049fbb7a5e7d75537dd6f6a374204207.tar.gz rails-62658500049fbb7a5e7d75537dd6f6a374204207.tar.bz2 rails-62658500049fbb7a5e7d75537dd6f6a374204207.zip |
Merge branch 'master' of http://github.com/rails/rails
446 files changed, 10118 insertions, 10545 deletions
@@ -1,43 +1,47 @@ source 'http://rubygems.org' -gem "arel", :git => "git://github.com/rails/arel.git" -#gem "rack-mount", :git => "git://github.com/rails/rack-mount.git" +if ENV['AREL'] + gem "arel", :path => ENV['AREL'] +else + gem "arel", :git => "git://github.com/rails/arel.git" +end + gem "rails", :path => File.dirname(__FILE__) gem "rake", ">= 0.8.7" gem "mocha", ">= 0.9.8" -gem "rdoc", "2.2" +gem "rdoc", ">= 2.5.9" +gem "horo", ">= 1.0.1" -mri = !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" -if mri && RUBY_VERSION < '1.9' +# AS +gem "memcache-client", ">= 1.8.5" + +# AM +gem "text-format", "~> 1.0.0" + +platforms :mri_18 do gem "system_timer" gem "ruby-debug", ">= 0.10.3" end -if mri || RUBY_ENGINE == "rbx" +platforms :ruby do gem 'json' gem 'yajl-ruby' - gem "nokogiri", ">= 1.4.0" -elsif RUBY_ENGINE == "jruby" - gem "ruby-debug" - gem "jruby-openssl" -end - -# AS -gem "memcache-client", ">= 1.7.5" + gem "nokogiri", ">= 1.4.3.1" -# AM -gem "text-format", "~> 1.0.0" - -# AR -if mri || RUBY_ENGINE == "rbx" - gem "sqlite3-ruby", "~> 1.3.0", :require => 'sqlite3' + # AR + gem "sqlite3-ruby", "~> 1.3.1", :require => 'sqlite3' group :db do gem "pg", ">= 0.9.0" gem "mysql", ">= 2.8.1" + gem "mysql2", :git => 'git://github.com/brianmario/mysql2.git' end -elsif RUBY_ENGINE == "jruby" +end + +platforms :jruby do + gem "ruby-debug", ">= 0.10.3" + gem "activerecord-jdbcsqlite3-adapter" group :db do @@ -46,9 +50,11 @@ elsif RUBY_ENGINE == "jruby" end end -if ENV['CI'] - gem "nokogiri", ">= 1.4.0" +env 'CI' do + gem "nokogiri", ">= 1.4.3.1" - # fcgi gem doesn't compile on 1.9 - gem "fcgi", ">= 0.8.7" if RUBY_VERSION < '1.9.0' + platforms :ruby_18 do + # fcgi gem doesn't compile on 1.9 + gem "fcgi", ">= 0.8.8" + end end diff --git a/RAILS_VERSION b/RAILS_VERSION index c3eddb1401..7706a9b5ac 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -3.0.0.beta4 +3.0.0.rc diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 0000000000..090a6bb68c --- /dev/null +++ b/README.rdoc @@ -0,0 +1,67 @@ +== 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. Install Rails at the command prompt if you haven't yet: + + gem install rails + +2. At the command prompt, create a new Rails application: + + rails new myapp + + where "myapp" is the application name. + +3. Change directory to +myapp+ and start the web server: + + cd myapp; rails server + + Run with <tt>--help</tt> for options. + +4. Go to http://localhost:3000/ and you'll see: + + "Welcome aboard: You're riding Ruby on Rails!" + +5. Follow the guidelines to start developing your application. You can find +the following resources handy: + +* The README file created within your application. +* The {Getting Started Guide}[http://guides.rubyonrails.org/getting_started.html]. +* The {Ruby on Rails Tutorial Book}[http://railstutorial.org/book]. + + +== Contributing + +We encourage you to contribute to Ruby on Rails! Please check out the {Contributing to Rails +guide}[http://edgeguides.rubyonrails.org/contributing_to_rails.html] for guidelines about how +to proceed. {Join us}[http://contributors.rubyonrails.org]! + +== License + +Ruby on Rails is released under the MIT license. @@ -1,16 +1,41 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/gempackagetask' +# RDoc skips some files in the Rails tree due to its binary? predicate. This is a quick +# hack for edge docs, until we decide which is the correct way to address this issue. +# If not fixed in RDoc itself, via an option or something, we should probably move this +# to railties and use it also in doc:rails. +def hijack_rdoc! + require "rdoc/parser" + class << RDoc::Parser + def binary?(file) + s = File.read(file, 1024) or return false + + if s[0, 2] == Marshal.dump('')[0, 2] then + true + elsif file =~ /erb\.rb$/ then + false + elsif s.index("\x00") then # ORIGINAL is s.scan(/<%|%>/).length >= 4 || s.index("\x00") + true + elsif 0.respond_to? :fdiv then + s.count("^ -~\t\r\n").fdiv(s.size) > 0.3 + else # HACK 1.8.6 + (s.count("^ -~\t\r\n").to_f / s.size) > 0.3 + end + end + end +end + PROJECTS = %w(activesupport activemodel actionpack actionmailer activeresource activerecord railties) desc 'Run all tests by default' task :default => %w(test test:isolated) -%w(test test:isolated rdoc package gem).each do |task_name| +%w(test test:isolated package gem).each do |task_name| desc "Run #{task_name} task for all projects" task task_name do errors = [] @@ -62,59 +87,53 @@ task :install => :gem do end desc "Generate documentation for the Rails framework" -Rake::RDocTask.new do |rdoc| +RDoc::Task.new do |rdoc| + hijack_rdoc! + rdoc.rdoc_dir = 'doc/rdoc' rdoc.title = "Ruby on Rails Documentation" - rdoc.options << '--line-numbers' << '--inline-source' - rdoc.options << '-A cattr_accessor=object' - rdoc.options << '--charset' << 'utf-8' - rdoc.options << '--main' << 'railties/README' - - # Workaround: RDoc assumes that rdoc.template can be required, and that - # rdoc.template.upcase is a constant living in RDoc::Generator::HTML - # which holds the actual template class. - # - # We put 'doc/template' in the load path to be able to set the template - # to the string 'horo' and thus meet those RDoc's assumptions. - $:.unshift('doc/template') + rdoc.options << '-f' << 'horo' + rdoc.options << '-c' << 'utf-8' + rdoc.options << '-m' << 'README.rdoc' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : 'horo' + rdoc.rdoc_files.include('README.rdoc') rdoc.rdoc_files.include('railties/CHANGELOG') rdoc.rdoc_files.include('railties/MIT-LICENSE') - rdoc.rdoc_files.include('railties/README') + rdoc.rdoc_files.include('railties/README.rdoc') rdoc.rdoc_files.include('railties/lib/**/*.rb') rdoc.rdoc_files.exclude('railties/lib/rails/generators/**/templates/*') - rdoc.rdoc_files.include('activerecord/README') + rdoc.rdoc_files.include('activerecord/README.rdoc') rdoc.rdoc_files.include('activerecord/CHANGELOG') rdoc.rdoc_files.include('activerecord/lib/active_record/**/*.rb') rdoc.rdoc_files.exclude('activerecord/lib/active_record/vendor/*') - rdoc.rdoc_files.include('activeresource/README') + rdoc.rdoc_files.include('activeresource/README.rdoc') rdoc.rdoc_files.include('activeresource/CHANGELOG') rdoc.rdoc_files.include('activeresource/lib/active_resource.rb') rdoc.rdoc_files.include('activeresource/lib/active_resource/*') - rdoc.rdoc_files.include('actionpack/README') + rdoc.rdoc_files.include('actionpack/README.rdoc') rdoc.rdoc_files.include('actionpack/CHANGELOG') + rdoc.rdoc_files.include('actionpack/lib/abstract_controller/**/*.rb') rdoc.rdoc_files.include('actionpack/lib/action_controller/**/*.rb') rdoc.rdoc_files.include('actionpack/lib/action_dispatch/**/*.rb') rdoc.rdoc_files.include('actionpack/lib/action_view/**/*.rb') rdoc.rdoc_files.exclude('actionpack/lib/action_controller/vendor/*') - rdoc.rdoc_files.include('actionmailer/README') + rdoc.rdoc_files.include('actionmailer/README.rdoc') rdoc.rdoc_files.include('actionmailer/CHANGELOG') rdoc.rdoc_files.include('actionmailer/lib/action_mailer/base.rb') rdoc.rdoc_files.exclude('actionmailer/lib/action_mailer/vendor/*') - rdoc.rdoc_files.include('activesupport/README') + rdoc.rdoc_files.include('activesupport/README.rdoc') rdoc.rdoc_files.include('activesupport/CHANGELOG') rdoc.rdoc_files.include('activesupport/lib/active_support/**/*.rb') rdoc.rdoc_files.exclude('activesupport/lib/active_support/vendor/*') - rdoc.rdoc_files.include('activemodel/README') + rdoc.rdoc_files.include('activemodel/README.rdoc') rdoc.rdoc_files.include('activemodel/CHANGELOG') rdoc.rdoc_files.include('activemodel/lib/active_model/**/*.rb') end @@ -129,9 +148,6 @@ desc "Publish API docs for Rails as a whole and for each component" task :pdoc => :rdoc do require 'rake/contrib/sshpublisher' Rake::SshDirPublisher.new("rails@api.rubyonrails.org", "public_html/api", "doc/rdoc").upload - PROJECTS.each do |project| - system %(cd #{project} && #{$0} pdoc) - end end task :update_versions do diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index f0b9368874..76eab935e5 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* No material changes + + *Rails 3.0.0 [beta 4] (June 8th, 2010)* * subject is automatically looked up on I18n using mailer_name and action_name as scope as in t(".subject") [JK] diff --git a/actionmailer/README b/actionmailer/README.rdoc index 2a4d507d8a..b52c993f56 100644 --- a/actionmailer/README +++ b/actionmailer/README.rdoc @@ -74,9 +74,9 @@ Or you can just chain the methods together like: == Receiving emails -To receive emails, you need to implement a public instance method called receive that takes a +To receive emails, you need to implement a public instance method called <tt>receive</tt> that takes a tmail object as its single parameter. The Action Mailer framework has a corresponding class method, -which is also called receive, that accepts a raw, unprocessed email as a string, which it then turns +which is also called <tt>receive</tt>, that accepts a raw, unprocessed email as a string, which it then turns into the tmail object and calls the receive instance method. Example: @@ -119,36 +119,16 @@ The Base class has the full list of configuration options. Here's an example: :authentication => :plain # :plain, :login or :cram_md5 } -== Dependencies -Action Mailer requires that the Action Pack is either available to be required immediately -or is accessible as a GEM. +== Download and installation -Additionally, Action Mailer requires the Mail gem, http://github.com/mikel/mail +The latest version of Action Mailer can be installed with Rubygems: -== Bundled software + % [sudo] gem install actionmailer -* Text::Format 0.63 by Austin Ziegler released under OpenSource - Read more on http://www.halostatue.ca/ruby/Text__Format.html +Source code can be downloaded as part of the Rails project on GitHub -== Download - -The latest version of Action Mailer can be found at - -* http://rubyforge.org/project/showfiles.php?group_id=361 - -Documentation can be found at - -* http://actionmailer.rubyonrails.org - - -== Installation - -You can install Action Mailer with the following command. - - % [sudo] ruby install.rb - -from its distribution directory. +* http://github.com/rails/rails/tree/master/actionmailer/ == License @@ -158,10 +138,10 @@ Action Mailer is released under the MIT license. == Support -The Action Mailer homepage is http://www.rubyonrails.org. You can find -the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer. -And as Jim from Rake says: +API documentation is at + +* http://api.rubyonrails.com + +Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: - Feel free to submit commits or feature requests. If you send a patch, - remember to update the corresponding unit tests. If fact, I prefer - new feature to be submitted in the form of new unit tests. +* https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index f20e7ea928..a47426bd07 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,8 +1,8 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -26,13 +26,13 @@ namespace :test do end # Generate the RDoc documentation -Rake::RDocTask.new { |rdoc| +RDoc::Task.new { |rdoc| rdoc.rdoc_dir = 'doc' rdoc.title = "Action Mailer -- Easy email delivery and testing" - rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.options << '-f' << 'horo' + rdoc.options << '--main' << 'README.rdoc' + rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG') rdoc.rdoc_files.include('lib/action_mailer.rb') rdoc.rdoc_files.include('lib/action_mailer/*.rb') rdoc.rdoc_files.include('lib/action_mailer/delivery_method/*.rb') @@ -50,9 +50,3 @@ task :release => :package do Rake::Gemcutter::Tasks.new(spec).define Rake::Task['gem:push'].invoke end - -desc "Publish the API documentation" -task :pdoc => [:rdoc] do - require 'rake/contrib/sshpublisher' - Rake::SshDirPublisher.new("rails@api.rubyonrails.org", "public_html/am", "doc").upload -end diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 2b7c21b3f2..daf30e434a 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -13,12 +13,12 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'actionmailer' - s.files = Dir['CHANGELOG', 'README', 'MIT-LICENSE', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] s.require_path = 'lib' s.requirements << 'none' s.has_rdoc = true s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.2.3') + s.add_dependency('mail', '~> 2.2.5') end diff --git a/actionmailer/install.rb b/actionmailer/install.rb deleted file mode 100644 index 8d7c140c3b..0000000000 --- a/actionmailer/install.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rbconfig' -require 'find' -require 'ftools' - -include Config - -# this was adapted from rdoc's install.rb by way of Log4r - -$sitedir = CONFIG["sitelibdir"] -unless $sitedir - version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] - $libdir = File.join(CONFIG["libdir"], "ruby", version) - $sitedir = $:.find {|x| x =~ /site_ruby/ } - if !$sitedir - $sitedir = File.join($libdir, "site_ruby") - elsif $sitedir !~ Regexp.quote(version) - $sitedir = File.join($sitedir, version) - end -end - -# the actual gruntwork -Dir.chdir("lib") - -Find.find("action_mailer", "action_mailer.rb") { |f| - if f[-3..-1] == ".rb" - File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) - else - File::makedirs(File.join($sitedir, *f.split(/\//))) - end -} diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index ed4bea0c77..f742f982f2 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -41,16 +41,16 @@ module ActionMailer #:nodoc: # in the same manner as <tt>attachments[]=</tt> # # * <tt>headers[]=</tt> - Allows you to specify any header field in your email such - # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note, while most fields (like <tt>To:</tt> + # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note, while most fields like <tt>To:</tt> # <tt>From:</tt> can only appear once in an email header, other fields like <tt>X-Anything</tt> # can appear multiple times. If you want to change a field that can appear multiple times, - # you need to set it to nil first so that Mail knows you are replacing it, not adding - # another field of the same name.) + # you need to set it to nil first so that Mail knows you are replacing it and not adding + # another field of the same name. # # * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such # as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt> # - # * <tt>mail</tt> - Allows you to specify your email to send. + # * <tt>mail</tt> - Allows you to specify email to be sent. # # The hash passed to the mail method allows you to specify any header that a Mail::Message # will accept (any valid Email header including optional fields). @@ -66,7 +66,7 @@ module ActionMailer #:nodoc: # format.html # end # - # The block syntax is useful if also need to specify information specific to a part: + # The block syntax is also useful in providing information specific to a part: # # mail(:to => user.email) do |format| # format.text(:content_transfer_encoding => "base64") @@ -121,19 +121,18 @@ module ActionMailer #:nodoc: # # <%= users_url(:host => "example.com") %> # - # You will want to avoid using the <tt>name_of_route_path</tt> form of named routes because it doesn't + # You want to avoid using the <tt>name_of_route_path</tt> form of named routes because it doesn't # make sense to generate relative URLs in email messages. # # It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt> - # option in the <tt>ActionMailer::Base.default_url_options</tt> hash as follows: - # - # ActionMailer::Base.default_url_options[:host] = "example.com" - # - # This can also be set as a configuration option in <tt>config/environment.rb</tt>: + # option as a configuration option in <tt>config/application.rb</tt>: # # config.action_mailer.default_url_options = { :host => "example.com" } # - # If you do decide to set a default <tt>:host</tt> for your mailers you will want to use the + # Setting <tt>ActionMailer::Base.default_url_options</tt> directly is now deprecated, use the configuration + # option mentioned above to set the default host. + # + # If you do decide to set a default <tt>:host</tt> for your mailers you want to use the # <tt>:only_path => false</tt> option when using <tt>url_for</tt>. This will ensure that absolute URLs are # generated because the <tt>url_for</tt> view helper will, by default, generate relative URLs when a # <tt>:host</tt> option isn't explicitly provided. @@ -155,7 +154,7 @@ module ActionMailer #:nodoc: # detect and use multipart templates, where each template is named after the name of the action, followed # by the content type. Each such detected template will be added as separate part to the message. # - # For example, if the following templates existed: + # For example, if the following templates exist: # * signup_notification.text.plain.erb # * signup_notification.text.html.erb # * signup_notification.text.xml.builder @@ -172,8 +171,7 @@ module ActionMailer #:nodoc: # # = Attachments # - # You can see above how to make a multipart HTML / Text email, to send attachments is just - # as easy: + # Sending attachment in emails is easy: # # class ApplicationMailer < ActionMailer::Base # def welcome(recipient) @@ -190,10 +188,8 @@ module ActionMailer #:nodoc: # # = Inline Attachments # - # You can also specify that a file should be displayed inline with other HTML. For example a - # corporate logo or a photo or the like. - # - # To do this is simple, in the Mailer: + # You can also specify that a file should be displayed inline with other HTML. This is useful + # if you want to display a corporate logo or a photo. # # class ApplicationMailer < ActionMailer::Base # def welcome(recipient) @@ -497,10 +493,10 @@ module ActionMailer #:nodoc: # You can also search for specific attachments: # # # By Filename - # mail.attachments['filename.jpg'] #=> Mail::Part object or nil + # mail.attachments['filename.jpg'] # => Mail::Part object or nil # # # or by index - # mail.attachments[0] #=> Mail::Part (first attachment) + # mail.attachments[0] # => Mail::Part (first attachment) # def attachments @_message.attachments diff --git a/actionmailer/lib/action_mailer/deprecated_api.rb b/actionmailer/lib/action_mailer/deprecated_api.rb index 0070d8e016..7d57feba04 100644 --- a/actionmailer/lib/action_mailer/deprecated_api.rb +++ b/actionmailer/lib/action_mailer/deprecated_api.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/try' + module ActionMailer # This is the API which is deprecated and is going to be removed on Rails 3.1 release. # Part of the old API will be deprecated after 3.1, for a smoother deprecation process. diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index d7b09b2dc6..ce6d8cc5b5 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -10,21 +10,16 @@ module ActionMailer end initializer "action_mailer.set_configs" do |app| - paths = app.config.paths - am = app.config.action_mailer + paths = app.config.paths + options = app.config.action_mailer - am.assets_dir ||= paths.public.to_a.first - am.javascripts_dir ||= paths.public.javascripts.to_a.first - am.stylesheets_dir ||= paths.public.stylesheets.to_a.first + options.assets_dir ||= paths.public.to_a.first + options.javascripts_dir ||= paths.public.javascripts.to_a.first + options.stylesheets_dir ||= paths.public.stylesheets.to_a.first ActiveSupport.on_load(:action_mailer) do - self.config.merge!(am) - include app.routes.url_helpers - - app.config.action_mailer.each do |k,v| - send "#{k}=", v - end + options.each { |k,v| send("#{k}=", v) } end end end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index b91eed592a..f4d1bb59f1 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -28,7 +28,7 @@ module ActionMailer def determine_default_mailer(name) name.sub(/Test$/, '').constantize - rescue NameError => e + rescue NameError raise NonInferrableMailerError.new(name) end end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 8c19082f61..805c89be1d 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -3,7 +3,7 @@ module ActionMailer MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 5bc0491cff..fec0ecf477 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -2,139 +2,17 @@ require 'abstract_unit' require 'active_support/time' +require 'mailers/base_mailer' +require 'mailers/proc_mailer' +require 'mailers/asset_mailer' + class BaseTest < ActiveSupport::TestCase # TODO Add some tests for implicity layout render and url helpers # so we can get rid of old base tests altogether with old base. - class BaseMailer < ActionMailer::Base - self.mailer_name = "base_mailer" - - default :to => 'system@test.lindsaar.net', - :from => 'jose@test.plataformatec.com', - :reply_to => 'mikel@test.lindsaar.net' - - def welcome(hash = {}) - headers['X-SPAM'] = "Not SPAM" - mail({:subject => "The first email on new API!"}.merge!(hash)) - end - - def welcome_with_headers(hash = {}) - headers hash - mail - end - - def welcome_from_another_path(path) - mail(:template_name => "welcome", :template_path => path) - end - - def html_only(hash = {}) - mail(hash) - end - - def plain_text_only(hash = {}) - mail(hash) - end - - def inline_attachment - attachments.inline['logo.png'] = "\312\213\254\232" - mail - end - - def attachment_with_content(hash = {}) - attachments['invoice.pdf'] = 'This is test File content' - mail(hash) - end - - def attachment_with_hash - attachments['invoice.jpg'] = { :data => "\312\213\254\232)b", - :mime_type => "image/x-jpg", - :transfer_encoding => "base64" } - mail - end - - def attachment_with_hash_default_encoding - attachments['invoice.jpg'] = { :data => "\312\213\254\232)b", - :mime_type => "image/x-jpg" } - mail - end - - def implicit_multipart(hash = {}) - attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments) - mail(hash) - end - - def implicit_with_locale(hash = {}) - mail(hash) - end - - def explicit_multipart(hash = {}) - attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments) - mail(hash) do |format| - format.text { render :text => "TEXT Explicit Multipart" } - format.html { render :text => "HTML Explicit Multipart" } - end - end - - def explicit_multipart_templates(hash = {}) - mail(hash) do |format| - format.html - format.text - end - end - - def explicit_multipart_with_any(hash = {}) - mail(hash) do |format| - format.any(:text, :html){ render :text => "Format with any!" } - end - end - def explicit_multipart_with_options(include_html = false) - mail do |format| - format.text(:content_transfer_encoding => "base64"){ render "welcome" } - format.html{ render "welcome" } if include_html - end - end - - def explicit_multipart_with_one_template(hash = {}) - mail(hash) do |format| - format.html - format.text - end - end - - def implicit_different_template(template_name='') - mail(:template_name => template_name) - end - - def explicit_different_template(template_name='') - mail do |format| - format.text { render :template => "#{mailer_name}/#{template_name}" } - format.html { render :template => "#{mailer_name}/#{template_name}" } - end - end - - def different_layout(layout_name='') - mail do |format| - format.text { render :layout => layout_name } - format.html { render :layout => layout_name } - end - end - end - - class ProcMailer < ActionMailer::Base - default :to => 'system@test.lindsaar.net', - 'X-Proc-Method' => Proc.new { Time.now.to_i.to_s }, - :subject => Proc.new { give_a_greeting } - - def welcome - mail - end - - private - - def give_a_greeting - "Thanks for signing up this afternoon" - end - + def teardown + ActionMailer::Base.asset_host = nil + ActionMailer::Base.assets_dir = nil end test "method call to mail does not raise error" do @@ -570,6 +448,26 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Welcome from another path", mail.body.encoded) end + test "assets tags should use ActionMailer's asset_host settings" do + ActionMailer::Base.config.asset_host = "http://global.com" + ActionMailer::Base.config.assets_dir = "global/" + + mail = AssetMailer.welcome + + assert_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip) + end + + test "assets tags should use a Mailer's asset_host settings when available" do + ActionMailer::Base.config.asset_host = "global.com" + ActionMailer::Base.config.assets_dir = "global/" + + AssetMailer.asset_host = "http://local.com" + + mail = AssetMailer.welcome + + assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) + end + # Before and After hooks class MyObserver @@ -609,6 +507,18 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Thanks for signing up this afternoon", mail.subject) end + test "action methods should be refreshed after defining new method" do + class FooMailer < ActionMailer::Base + # this triggers action_methods + self.respond_to?(:foo) + + def notify + end + end + + assert_equal ["notify"], FooMailer.action_methods + end + protected # Execute the block setting the given values and restoring old values after diff --git a/actionmailer/test/fixtures/asset_mailer/welcome.html.erb b/actionmailer/test/fixtures/asset_mailer/welcome.html.erb new file mode 100644 index 0000000000..90d26130d9 --- /dev/null +++ b/actionmailer/test/fixtures/asset_mailer/welcome.html.erb @@ -0,0 +1 @@ +<%= image_tag "dummy.png" %>
\ No newline at end of file diff --git a/actionmailer/test/mailers/asset_mailer.rb b/actionmailer/test/mailers/asset_mailer.rb new file mode 100644 index 0000000000..f54a50d00d --- /dev/null +++ b/actionmailer/test/mailers/asset_mailer.rb @@ -0,0 +1,7 @@ +class AssetMailer < ActionMailer::Base + self.mailer_name = "asset_mailer" + + def welcome + mail + end +end diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb new file mode 100644 index 0000000000..2c6de36ccf --- /dev/null +++ b/actionmailer/test/mailers/base_mailer.rb @@ -0,0 +1,114 @@ +class BaseMailer < ActionMailer::Base + self.mailer_name = "base_mailer" + + default :to => 'system@test.lindsaar.net', + :from => 'jose@test.plataformatec.com', + :reply_to => 'mikel@test.lindsaar.net' + + def welcome(hash = {}) + headers['X-SPAM'] = "Not SPAM" + mail({:subject => "The first email on new API!"}.merge!(hash)) + end + + def welcome_with_headers(hash = {}) + headers hash + mail + end + + def welcome_from_another_path(path) + mail(:template_name => "welcome", :template_path => path) + end + + def html_only(hash = {}) + mail(hash) + end + + def plain_text_only(hash = {}) + mail(hash) + end + + def inline_attachment + attachments.inline['logo.png'] = "\312\213\254\232" + mail + end + + def attachment_with_content(hash = {}) + attachments['invoice.pdf'] = 'This is test File content' + mail(hash) + end + + def attachment_with_hash + attachments['invoice.jpg'] = { :data => "\312\213\254\232)b", + :mime_type => "image/x-jpg", + :transfer_encoding => "base64" } + mail + end + + def attachment_with_hash_default_encoding + attachments['invoice.jpg'] = { :data => "\312\213\254\232)b", + :mime_type => "image/x-jpg" } + mail + end + + def implicit_multipart(hash = {}) + attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments) + mail(hash) + end + + def implicit_with_locale(hash = {}) + mail(hash) + end + + def explicit_multipart(hash = {}) + attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments) + mail(hash) do |format| + format.text { render :text => "TEXT Explicit Multipart" } + format.html { render :text => "HTML Explicit Multipart" } + end + end + + def explicit_multipart_templates(hash = {}) + mail(hash) do |format| + format.html + format.text + end + end + + def explicit_multipart_with_any(hash = {}) + mail(hash) do |format| + format.any(:text, :html){ render :text => "Format with any!" } + end + end + + def explicit_multipart_with_options(include_html = false) + mail do |format| + format.text(:content_transfer_encoding => "base64"){ render "welcome" } + format.html{ render "welcome" } if include_html + end + end + + def explicit_multipart_with_one_template(hash = {}) + mail(hash) do |format| + format.html + format.text + end + end + + def implicit_different_template(template_name='') + mail(:template_name => template_name) + end + + def explicit_different_template(template_name='') + mail do |format| + format.text { render :template => "#{mailer_name}/#{template_name}" } + format.html { render :template => "#{mailer_name}/#{template_name}" } + end + end + + def different_layout(layout_name='') + mail do |format| + format.text { render :layout => layout_name } + format.html { render :layout => layout_name } + end + end +end diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb new file mode 100644 index 0000000000..6a79cd71fc --- /dev/null +++ b/actionmailer/test/mailers/proc_mailer.rb @@ -0,0 +1,16 @@ +class ProcMailer < ActionMailer::Base + default :to => 'system@test.lindsaar.net', + 'X-Proc-Method' => Proc.new { Time.now.to_i.to_s }, + :subject => Proc.new { give_a_greeting } + + def welcome + mail + end + + private + + def give_a_greeting + "Thanks for signing up this afternoon" + end + +end diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 32aba2091a..81abb8b5f1 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,4 +1,6 @@ -*Rails 3.0.0 [Release Candidate] (unreleased)* +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* Allow stylesheet/javascript extensions to be changed through railties. [Josh Kalderimis] * link_to, button_to, and tag/tag_options now rely on html_escape instead of escape_once. [fxn] @@ -31,6 +33,7 @@ * Removed textilize, textilize_without_paragraph and markdown helpers. [Santiago Pastorino] + *Rails 3.0.0 [beta 4] (June 8th, 2010)* * Remove middleware laziness [José Valim] diff --git a/actionpack/README b/actionpack/README.rdoc index 1a59f728cc..b297ceb0e2 100644 --- a/actionpack/README +++ b/actionpack/README.rdoc @@ -1,34 +1,35 @@ -= Action Pack -- On rails from request to response - -Action Pack splits the response to a web request into a controller part -(performing the logic) and a view part (rendering a template). This two-step -approach is known as an action, which will normally create, read, update, or -delete (CRUD for short) some sort of model part (often backed by a database) -before choosing either to render a template or redirecting to another action. - -Action Pack implements these actions as public methods on Action Controllers -and uses Action Views to implement the template rendering. Action Controllers -are then responsible for handling all the actions relating to a certain part -of an application. This grouping usually consists of actions for lists and for -CRUDs revolving around a single (or a few) model objects. So ContactsController -would be responsible for listing contacts, creating, deleting, and updating -contacts. A WeblogController could be responsible for both posts and comments. - -Action View templates are written using embedded Ruby in tags mingled in with -the HTML. To avoid cluttering the templates with code, a bunch of helper -classes provide common behavior for forms, dates, and strings. And it's easy -to add specific helpers to keep the separation as the application evolves. - -Note: Some of the features, such as scaffolding and form building, are tied to -ActiveRecord[http://activerecord.rubyonrails.org] (an object-relational -mapping package), but that doesn't mean that Action Pack depends on Active -Record. Action Pack is an independent package that can be used with any sort -of backend (Instiki[http://www.instiki.org], which is based on an older version -of Action Pack, used Madeleine for example). Read more about the role Action -Pack can play when used together with Active Record on -http://www.rubyonrails.org. - -A short rundown of the major features: += Action Pack -- From request to response + +Action Pack is a framework for handling and responding to web requests. It +provides mechanisms for *routing* (mapping request URLs to actions), defining +*controllers* that implement actions, and generating responses by rendering +*views*, which are templates of various formats. In short, Action Pack +provides the view and controller layers in the MVC paradigm. + +It consists of several modules: + +* Action Dispatch, which parses information about the web request, handles + routing as defined by the user, and does advanced processing related to HTTP + such as MIME-type negotiation, decoding parameters in POST/PUT bodies, + handling HTTP caching logic, cookies and sessions. + +* Action Controller, which provides a base controller class that can be + subclassed to implement filters and actions to handle requests. The result + of an action is typically content generated from views. + +* Action View, which handles view template lookup and rendering, and provides + view helpers that assist when building HTML forms, Atom feeds and more. + Template formats that Action View handles are ERb (embedded Ruby, typically + used to inline short Ruby snippets inside HTML), XML Builder and RJS + (dynamically generated JavaScript from Ruby code). + +With the Ruby on Rails framework, users only directly interface with the +Action Controller module. Necessary Action Dispatch functionality is activated +by default and Action View rendering is implicitly triggered by Action +Controller. However, these modules are designed to function on their own and +can be used outside of Rails. + +A short rundown of some of the major features: * Actions grouped in controller as methods instead of separate command objects and can therefore share helper methods @@ -40,26 +41,29 @@ A short rundown of the major features: def update @customer = find_customer - @customer.attributes = params[:customer] - @customer.save ? - redirect_to(:action => "show") : - render(:action => "edit") + if @customer.update_attributes(params[:customer]) + redirect_to :action => "show" + else + render :action => "edit" + end end private - def find_customer() Customer.find(params[:id]) end + def find_customer + Customer.find params[:id] + end end {Learn more}[link:classes/ActionController/Base.html] -* Embedded Ruby for templates (no new "easy" template language) +* ERb templates (static content mixed with dynamic output from ruby) <% for post in @posts %> Title: <%= post.title %> <% end %> - All post titles: <%= @posts.collect{ |p| p.title }.join ", " %> + All post titles: <%= @posts.collect{ |p| p.title }.join(", ") %> <% unless @person.is_client? %> Not for clients to see... @@ -68,7 +72,7 @@ A short rundown of the major features: {Learn more}[link:classes/ActionView.html] -* Builder-based templates (great for XML content, like RSS) +* "Builder" templates (great for XML content, like RSS) xml.rss("version" => "2.0") do xml.channel do @@ -93,11 +97,16 @@ A short rundown of the major features: {Learn more}[link:classes/ActionView/Base.html] -* Filters for pre and post processing of the response (as methods, procs, and classes) +* Filters for pre- and post-processing of the response class WeblogController < ActionController::Base + # filters as methods before_filter :authenticate, :cache, :audit + + # filter as a proc after_filter { |c| c.response.body = Gzip::compress(c.response.body) } + + # class filter after_filter LocalizeFilter def index @@ -120,16 +129,14 @@ A short rundown of the major features: * Helpers for forms, dates, action links, and text - <%= text_field "post", "title", "size" => 30 %> - <%= html_date_select(Date.today) %> + <%= text_field_tag "post", "title", "size" => 30 %> <%= link_to "New post", :controller => "post", :action => "new" %> <%= truncate(post.title, :length => 25) %> {Learn more}[link:classes/ActionView/Helpers.html] -* Layout sharing for template reuse (think simple version of Struts - Tiles[http://jakarta.apache.org/struts/userGuide/dev_tiles.html]) +* Layout sharing for template reuse class WeblogController < ActionController::Base layout "weblog_layout" @@ -150,22 +157,22 @@ A short rundown of the major features: {Learn more}[link:classes/ActionController/Layout/ClassMethods.html] -* Routing makes pretty urls incredibly easy +* Routing makes pretty URLs incredibly easy - map.connect 'clients/:client_name/:project_name/:controller/:action' + match 'clients/:client_name/:project_name/:controller/:action' - Accessing /clients/37signals/basecamp/project/dash calls ProjectController#dash with - { "client_name" => "37signals", "project_name" => "basecamp" } in params[:params] + Accessing "/clients/37signals/basecamp/project/index" calls ProjectController#index with + { "client_name" => "37signals", "project_name" => "basecamp" } in `params` - From that URL, you can rewrite the redirect in a number of ways: + From that action, you can write the redirect in a number of ways: redirect_to(:action => "edit") => - /clients/37signals/basecamp/project/dash + /clients/37signals/basecamp/project/edit redirect_to(:client_name => "nextangle", :project_name => "rails") => - /clients/nextangle/rails/project/dash + /clients/nextangle/rails/project/index - {Learn more}[link:classes/ActionController/Base.html] + {Learn more}[link:classes/ActionDispatch/Routing.html] * Easy testing of both controller and rendered template through ActionController::TestCase @@ -242,62 +249,6 @@ A short rundown of the major features: {Learn more}[link:classes/ActionController/Rescue.html] -* Scaffolding for Active Record model objects - - class AccountController < ActionController::Base - scaffold :account - end - - The AccountController now has the full CRUD range of actions and default - templates: list, show, destroy, new, create, edit, update - - {Learn more}[link:classes/ActionController/Scaffolding/ClassMethods.html] - - -* Form building for Active Record model objects - - The post object has a title (varchar), content (text), and - written_on (date) - - <%= form "post" %> - - ...will generate something like (the selects will have more options, of - course): - - <form action="create" method="POST"> - <p> - <b>Title:</b><br/> - <input type="text" name="post[title]" value="<%= @post.title %>" /> - </p> - <p> - <b>Content:</b><br/> - <textarea name="post[content]"><%= @post.title %></textarea> - </p> - <p> - <b>Written on:</b><br/> - <select name='post[written_on(3i)]'><option>18</option></select> - <select name='post[written_on(2i)]'><option value='7'>July</option></select> - <select name='post[written_on(1i)]'><option>2004</option></select> - </p> - - <input type="submit" value="Create"> - </form> - - This form generates a params[:post] array that can be used directly in a save action: - - class WeblogController < ActionController::Base - def create - post = Post.create(params[:post]) - redirect_to :action => "show", :id => post.id - end - end - - {Learn more}[link:classes/ActionView/Helpers/ActiveRecordHelper.html] - - -* Runs on top of WEBrick, Mongrel, CGI, FCGI, and mod_ruby - - == Simple example (from outside of Rails) This example will implement a simple weblog system using inline templates and @@ -364,24 +315,15 @@ new model). After creating the post, it'll redirect to the show page using an URL such as /weblog/5 (where 5 is the id of the post). -== Download +== Download and installation -The latest version of Action Pack can be found at +The latest version of Action Pack can be installed with Rubygems: -* http://rubyforge.org/project/showfiles.php?group_id=249 + % [sudo] gem install actionpack -Documentation can be found at +Source code can be downloaded as part of the Rails project on GitHub -* http://api.rubyonrails.com - - -== Installation - -You can install Action Pack with the following command. - - % [sudo] ruby install.rb - -from its distribution directory. +* http://github.com/rails/rails/tree/master/actionpack/ == License @@ -391,10 +333,10 @@ Action Pack is released under the MIT license. == Support -The Action Pack homepage is http://www.rubyonrails.org. You can find -the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack. -And as Jim from Rake says: +API documentation is at + +* http://api.rubyonrails.com + +Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: - Feel free to submit commits or feature requests. If you send a patch, - remember to update the corresponding unit tests. If fact, I prefer - new feature to be submitted in the form of new unit tests. +* https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets diff --git a/actionpack/Rakefile b/actionpack/Rakefile index aed5278e38..4af8ea167a 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,8 +1,8 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -38,16 +38,16 @@ end # Genereate the RDoc documentation -Rake::RDocTask.new { |rdoc| +RDoc::Task.new { |rdoc| rdoc.rdoc_dir = 'doc' rdoc.title = "Action Pack -- On rails from request to response" - rdoc.options << '--line-numbers' << '--inline-source' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' + rdoc.options << '-f' << 'horo' + rdoc.options << '--main' << 'README.rdoc' if ENV['DOC_FILES'] rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/)) else - rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') + rdoc.rdoc_files.include('README.rdoc', 'RUNNING_UNIT_TESTS', 'CHANGELOG') rdoc.rdoc_files.include(Dir['lib/**/*.rb'] - Dir['lib/*/vendor/**/*.rb']) rdoc.rdoc_files.exclude('lib/actionpack.rb') @@ -89,4 +89,4 @@ task :lines do end puts "Total: Lines #{total_lines}, LOC #{total_codelines}" -end
\ No newline at end of file +end diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index df0b5ac9a2..99deff234c 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'actionpack' - s.files = Dir['CHANGELOG', 'README', 'MIT-LICENSE', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] s.require_path = 'lib' s.requirements << 'none' @@ -25,7 +25,7 @@ Gem::Specification.new do |s| s.add_dependency('i18n', '~> 0.4.1') s.add_dependency('rack', '~> 1.2.1') s.add_dependency('rack-test', '~> 0.5.4') - #s.add_dependency('rack-mount', '~> 0.6.6') - s.add_dependency('tzinfo', '~> 0.3.16') + s.add_dependency('rack-mount', '~> 0.6.9') + s.add_dependency('tzinfo', '~> 0.3.22') s.add_dependency('erubis', '~> 2.6.6') end diff --git a/actionpack/install.rb b/actionpack/install.rb deleted file mode 100644 index d3b83c3b00..0000000000 --- a/actionpack/install.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rbconfig' -require 'find' -require 'ftools' - -include Config - -# this was adapted from rdoc's install.rb by way of Log4r - -$sitedir = CONFIG["sitelibdir"] -unless $sitedir - version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] - $libdir = File.join(CONFIG["libdir"], "ruby", version) - $sitedir = $:.find {|x| x =~ /site_ruby/ } - if !$sitedir - $sitedir = File.join($libdir, "site_ruby") - elsif $sitedir !~ Regexp.quote(version) - $sitedir = File.join($sitedir, version) - end -end - -# the actual gruntwork -Dir.chdir("lib") - -Find.find("action_controller", "action_controller.rb", "action_view", "action_view.rb") { |f| - if f[-3..-1] == ".rb" - File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) - else - File::makedirs(File.join($sitedir, *f.split(/\//))) - end -}
\ No newline at end of file diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 5990a1bbd0..c565c940a1 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -1,6 +1,7 @@ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) +require 'action_pack' require 'active_support/ruby/shim' require 'active_support/dependencies/autoload' require 'active_support/core_ext/class/attribute' diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb index 6d6f6ac607..9ca2fb742f 100644 --- a/actionpack/lib/abstract_controller/asset_paths.rb +++ b/actionpack/lib/abstract_controller/asset_paths.rb @@ -3,7 +3,7 @@ module AbstractController extend ActiveSupport::Concern included do - config_accessor :assets_dir, :javascripts_dir, :stylesheets_dir + config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, :stylesheets_dir end end end
\ No newline at end of file diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 8a8337858b..db0a6736e0 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -72,6 +72,13 @@ module AbstractController end end + # action_methods are cached and there is sometimes need to refresh + # them. clear_action_methods! allows you to do that, so next time + # you run action_methods, they will be recalculated + def clear_action_methods! + @action_methods = nil + end + # Returns the full controller name, underscored, without the ending Controller. # For instance, MyApp::MyPostsController would return "my_app/my_posts" for # controller_name. @@ -81,6 +88,11 @@ module AbstractController def controller_path @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? end + + def method_added(name) + super + clear_action_methods! + end end abstract! diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 1bd4572a47..ca0e5d6ff6 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -6,7 +6,6 @@ module ActionController autoload :Base autoload :Caching - autoload :PolymorphicRoutes autoload :Metal autoload :Middleware diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 1a2cbaab65..9dfffced75 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -28,7 +28,6 @@ module ActionController SessionManagement, Caching, MimeResponds, - PolymorphicRoutes, ImplicitRender, Cookies, @@ -64,9 +63,8 @@ module ActionController klass.helper :all end - config_accessor :asset_host, :asset_path ActiveSupport.run_load_hooks(:action_controller, self) end end -require "action_controller/deprecated/base"
\ No newline at end of file +require "action_controller/deprecated/base" diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index be7448ce01..b0eb24a4a8 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -63,7 +63,7 @@ module ActionController # def test_access_granted_from_xml # get( # "/notes/1.xml", nil, - # :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) # ) # # assert_equal 200, status diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index b2c119d7e4..b08d9a8434 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -1,3 +1,4 @@ +require 'benchmark' require 'abstract_controller/logger' module ActionController diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 4f384d1ec5..c6d4c6d936 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -12,27 +12,30 @@ module ActionController #:nodoc: end module ClassMethods - # Defines mimes that are rendered by default when invoking respond_with. + # Defines mime types that are rendered by default when invoking + # <tt>respond_with</tt>. # # Examples: # # respond_to :html, :xml, :json # - # All actions on your controller will respond to :html, :xml and :json. + # Specifies that all actions in the controller respond to requests + # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>. # - # But if you want to specify it based on your actions, you can use only and - # except: + # To specify on per-action basis, use <tt>:only</tt> and + # <tt>:except</tt> with an array of actions or a single action: # # respond_to :html # respond_to :xml, :json, :except => [ :edit ] # - # The definition above explicits that all actions respond to :html. And all - # actions except :edit respond to :xml and :json. - # - # You can specify also only parameters: + # This specifies that all actions respond to <tt>:html</tt> + # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and + # <tt>:json</tt>. # # respond_to :rjs, :only => :create # + # This specifies that the <tt>:create</tt> action and no other responds + # to <tt>:rjs</tt>. def respond_to(*mimes) options = mimes.extract_options! @@ -49,7 +52,7 @@ module ActionController #:nodoc: self.mimes_for_respond_to = new.freeze end - # Clear all mimes in respond_to. + # Clear all mime types in <tt>respond_to</tt>. # def clear_respond_to self.mimes_for_respond_to = ActiveSupport::OrderedHash.new.freeze @@ -145,7 +148,7 @@ module ActionController #:nodoc: # and accept Rails' defaults, life will be much easier. # # If you need to use a MIME type which isn't supported by default, you can register your own handlers in - # environment.rb as follows. + # config/initializers/mime_types.rb as follows. # # Mime::Type.register "image/jpg", :jpg # diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index bbca1b2179..eb037aa1b0 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -3,6 +3,15 @@ module ActionController #:nodoc: extend ActiveSupport::Concern include ActiveSupport::Rescuable + def rescue_with_handler(exception) + if (exception.respond_to?(:original_exception) && + (orig_exception = exception.original_exception) && + handler_for_rescue(orig_exception)) + exception = orig_exception + end + super(exception) + end + private def process_action(*args) super diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 753af3dc58..d75b46dace 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/file/path' + module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, # instead of rendering. diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb deleted file mode 100644 index bee50a7a3b..0000000000 --- a/actionpack/lib/action_controller/polymorphic_routes.rb +++ /dev/null @@ -1,183 +0,0 @@ -module ActionController - # Polymorphic URL helpers are methods for smart resolution to a named route call when - # given an Active Record model instance. They are to be used in combination with - # ActionController::Resources. - # - # These methods are useful when you want to generate correct URL or path to a RESTful - # resource without having to know the exact type of the record in question. - # - # Nested resources and/or namespaces are also supported, as illustrated in the example: - # - # polymorphic_url([:admin, @article, @comment]) - # - # results in: - # - # admin_article_comment_url(@article, @comment) - # - # == Usage within the framework - # - # Polymorphic URL helpers are used in a number of places throughout the Rails framework: - # - # * <tt>url_for</tt>, so you can use it with a record as the argument, e.g. - # <tt>url_for(@article)</tt>; - # * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write - # <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form - # action; - # * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write - # <tt>redirect_to(post)</tt> in your controllers; - # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs - # for feed entries. - # - # == Prefixed polymorphic helpers - # - # In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a - # number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt> - # in options. Those are: - # - # * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt> - # * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt> - # - # Example usage: - # - # edit_polymorphic_path(@post) # => "/posts/1/edit" - # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" - module PolymorphicRoutes - # Constructs a call to a named RESTful route for the given record and returns the - # resulting URL string. For example: - # - # # calls post_url(post) - # polymorphic_url(post) # => "http://example.com/posts/1" - # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" - # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" - # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" - # polymorphic_url(Comment) # => "http://example.com/comments" - # - # ==== Options - # - # * <tt>:action</tt> - Specifies the action prefix for the named route: - # <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix. - # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>. - # Default is <tt>:url</tt>. - # - # ==== Examples - # - # # an Article record - # polymorphic_url(record) # same as article_url(record) - # - # # a Comment record - # polymorphic_url(record) # same as comment_url(record) - # - # # it recognizes new records and maps to the collection - # record = Comment.new - # polymorphic_url(record) # same as comments_url() - # - # # the class of a record will also map to the collection - # polymorphic_url(Comment) # same as comments_url() - # - def polymorphic_url(record_or_hash_or_array, options = {}) - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 - end - - record = extract_record(record_or_hash_or_array) - record = record.to_model if record.respond_to?(:to_model) - - args = case record_or_hash_or_array - when Hash; [ record_or_hash_or_array ] - when Array; record_or_hash_or_array.dup - else [ record_or_hash_or_array ] - end - - inflection = if options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:persisted?) && !record.persisted?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end - - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, inflection, options) - - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - __send__(named_route, *args) - end - - # Returns the path component of a URL for the given record. It uses - # <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>. - def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) - end - - %w(edit new).each do |action| - module_eval <<-EOT, __FILE__, __LINE__ + 1 - def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}")) # options.merge(:action => "edit")) - end # end - # - def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) - end # end - EOT - end - - private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' - end - - def routing_type(options) - options[:routing_type] || :url - end - - def build_named_route_call(records, inflection, options = {}) - unless records.is_a?(Array) - record = extract_record(records) - route = '' - else - record = records.pop - route = records.inject("") do |string, parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - string << "#{parent}_" - else - string << RecordIdentifier.__send__("plural_class_name", parent).singularize - string << "_" - end - end - end - - if record.is_a?(Symbol) || record.is_a?(String) - route << "#{record}_" - else - route << RecordIdentifier.__send__("plural_class_name", record) - route = route.singularize if inflection == :singular - route << "_" - route << "index_" if RecordIdentifier.uncountable?(record) && inflection == :plural - end - - action_prefix(options) + route + routing_type(options).to_s - end - - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end - end - end -end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 9261422f0b..cd2dfafbe6 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -5,8 +5,6 @@ require "action_view/railtie" require "active_support/deprecation/proxy_wrappers" require "active_support/deprecation" -require "action_controller/railties/url_helpers" - module ActionController class Railtie < Rails::Railtie config.action_controller = ActiveSupport::OrderedOptions.new @@ -33,21 +31,6 @@ module ActionController end end - initializer "action_controller.set_configs" do |app| - paths = app.config.paths - ac = app.config.action_controller - - ac.assets_dir ||= paths.public.to_a.first - ac.javascripts_dir ||= paths.public.javascripts.to_a.first - ac.stylesheets_dir ||= paths.public.stylesheets.to_a.first - ac.page_cache_directory ||= paths.public.to_a.first - ac.helpers_path ||= paths.app.helpers.to_a - - ActiveSupport.on_load(:action_controller) do - self.config.merge!(ac) - end - end - initializer "action_controller.logger" do ActiveSupport.on_load(:action_controller) { self.logger ||= Rails.logger } end @@ -56,11 +39,23 @@ module ActionController ActiveSupport.on_load(:action_controller) { self.cache_store ||= RAILS_CACHE } end - initializer "action_controller.url_helpers" do |app| + initializer "action_controller.set_configs" do |app| + paths = app.config.paths + options = app.config.action_controller + + options.assets_dir ||= paths.public.to_a.first + options.javascripts_dir ||= paths.public.javascripts.to_a.first + options.stylesheets_dir ||= paths.public.stylesheets.to_a.first + options.page_cache_directory ||= paths.public.to_a.first + options.helpers_path ||= paths.app.helpers.to_a + ActiveSupport.on_load(:action_controller) do - extend ::ActionController::Railties::UrlHelpers.with(app.routes) + include app.routes.url_helpers + options.each { |k,v| send("#{k}=", v) } end + end + initializer "action_controller.deprecated_routes" do |app| message = "ActionController::Routing::Routes is deprecated. " \ "Instead, use Rails.application.routes" diff --git a/actionpack/lib/action_controller/railties/url_helpers.rb b/actionpack/lib/action_controller/railties/url_helpers.rb deleted file mode 100644 index 9df5665542..0000000000 --- a/actionpack/lib/action_controller/railties/url_helpers.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActionController - module Railties - module UrlHelpers - def self.with(routes) - Module.new do - define_method(:inherited) do |klass| - super(klass) - klass.send(:include, routes.url_helpers) - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index d20c3b64c5..3de40b0de3 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -46,7 +46,7 @@ module ActionController # dom_class(post, :edit) # => "edit_post" # dom_class(Person, :edit) # => "edit_person" def dom_class(record_or_class, prefix = nil) - singular = singular_class_name(record_or_class) + singular = ActiveModel::Naming.singular(record_or_class) prefix ? "#{prefix}#{JOIN}#{singular}" : singular end @@ -67,6 +67,8 @@ module ActionController end end + protected + # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id. # This can be overwritten to customize the default generated string representation if desired. # If you need to read back a key from a dom_id in order to query for the underlying database record, @@ -85,34 +87,5 @@ module ActionController def sanitize_dom_id(candidate_id) candidate_id # TODO implement conversion to valid DOM id values end - - # Returns the plural class name of a record or class. Examples: - # - # plural_class_name(post) # => "posts" - # plural_class_name(Highrise::Person) # => "highrise_people" - def plural_class_name(record_or_class) - model_name_from_record_or_class(record_or_class).plural - end - - # Returns the singular class name of a record or class. Examples: - # - # singular_class_name(post) # => "post" - # singular_class_name(Highrise::Person) # => "highrise_person" - def singular_class_name(record_or_class) - model_name_from_record_or_class(record_or_class).singular - end - - # Identifies whether the class name of a record or class is uncountable. Examples: - # - # uncountable?(Sheep) # => true - # uncountable?(Post) => false - def uncountable?(record_or_class) - plural_class_name(record_or_class) == singular_class_name(record_or_class) - end - - private - def model_name_from_record_or_class(record_or_class) - (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name - end end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 650eb16ac0..e306697f4b 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -365,7 +365,7 @@ module ActionController def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - returning __send__(request_method, action, parameters, session, flash) do + __send__(request_method, action, parameters, session, flash).tap do @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index cdf81c6648..aeec934be8 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -24,9 +24,14 @@ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) +activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__) +$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path) + require 'active_support' require 'active_support/dependencies/autoload' +require 'action_pack' +require 'active_model' require 'rack' module Rack @@ -42,6 +47,7 @@ module ActionDispatch end autoload_under 'middleware' do + autoload :BestStandardsSupport autoload :Callbacks autoload :Cookies autoload :Flash @@ -63,6 +69,7 @@ module ActionDispatch autoload :Headers autoload :MimeNegotiation autoload :Parameters + autoload :ParameterFilter autoload :FilterParameters autoload :Upload autoload :UploadedFile, 'action_dispatch/http/upload' diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 152aaa2e67..1ab48ae04d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -26,88 +26,32 @@ module ActionDispatch module FilterParameters extend ActiveSupport::Concern - @@compiled_parameter_filter_for = {} + @@parameter_filter_for = {} # Return a hash of parameters with all sensitive data replaced. def filtered_parameters - @filtered_parameters ||= if filtering_parameters? - process_parameter_filter(parameters) - else - parameters.dup - end + @filtered_parameters ||= parameter_filter.filter(parameters) end - alias :fitered_params :filtered_parameters # Return a hash of request.env with all sensitive data replaced. def filtered_env - filtered_env = @env.dup - filtered_env.each do |key, value| - if (key =~ /RAW_POST_DATA/i) - filtered_env[key] = '[FILTERED]' - elsif value.is_a?(Hash) - filtered_env[key] = process_parameter_filter(value) - end - end - filtered_env + @filtered_env ||= env_filter.filter(@env) end protected - def filtering_parameters? #:nodoc: - @env["action_dispatch.parameter_filter"].present? + def parameter_filter + parameter_filter_for(@env["action_dispatch.parameter_filter"]) end - def process_parameter_filter(params) #:nodoc: - compiled_parameter_filter_for(@env["action_dispatch.parameter_filter"]).call(params) + def env_filter + parameter_filter_for(Array.wrap(@env["action_dispatch.parameter_filter"]) << /RAW_POST_DATA/) end - def compile_parameter_filter(filters) #:nodoc: - strings, regexps, blocks = [], [], [] - - filters.each do |item| - case item - when NilClass - when Proc - blocks << item - when Regexp - regexps << item - else - strings << item.to_s - end - end - - regexps << Regexp.new(strings.join('|'), true) unless strings.empty? - [regexps, blocks] - end - - def compiled_parameter_filter_for(filters) #:nodoc: - @@compiled_parameter_filter_for[filters] ||= begin - regexps, blocks = compile_parameter_filter(filters) - - lambda do |original_params| - filtered_params = {} - - original_params.each do |key, value| - if regexps.find { |r| key =~ r } - value = '[FILTERED]' - elsif value.is_a?(Hash) - value = process_parameter_filter(value) - elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? process_parameter_filter(v) : v } - elsif blocks.present? - key = key.dup - value = value.dup if value.duplicable? - blocks.each { |b| b.call(key, value) } - end - - filtered_params[key] = value - end - - filtered_params - end - end + def parameter_filter_for(filters) + @@parameter_filter_for[filters] ||= ParameterFilter.new(filters) end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb new file mode 100644 index 0000000000..1480e8f77c --- /dev/null +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -0,0 +1,72 @@ +module ActionDispatch + module Http + class ParameterFilter + + def initialize(filters) + @filters = filters + end + + def filter(params) + if enabled? + compiled_filter.call(params) + else + params.dup + end + end + + private + + def enabled? + @filters.present? + end + + def compiled_filter + @compiled_filter ||= begin + regexps, blocks = compile_filter + + lambda do |original_params| + filtered_params = {} + + original_params.each do |key, value| + if regexps.find { |r| key =~ r } + value = '[FILTERED]' + elsif value.is_a?(Hash) + value = filter(value) + elsif value.is_a?(Array) + value = value.map { |v| v.is_a?(Hash) ? filter(v) : v } + elsif blocks.present? + key = key.dup + value = value.dup if value.duplicable? + blocks.each { |b| b.call(key, value) } + end + + filtered_params[key] = value + end + + filtered_params + end + end + end + + def compile_filter + strings, regexps, blocks = [], [], [] + + @filters.each do |item| + case item + when NilClass + when Proc + blocks << item + when Regexp + regexps << item + else + strings << item.to_s + end + end + + regexps << Regexp.new(strings.join('|'), true) unless strings.empty? + [regexps, blocks] + end + + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb new file mode 100644 index 0000000000..69adcc419f --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb @@ -0,0 +1,22 @@ +module ActionDispatch + class BestStandardsSupport + def initialize(app, type = true) + @app = app + + @header = case type + when true + "IE=Edge,chrome=1" + when :builtin + "IE=Edge" + when false + nil + end + end + + def call(env) + status, headers, body = @app.call(env) + headers["X-UA-Compatible"] = @header + [status, headers, body] + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index d69ba39728..4d33cd3b0c 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -45,10 +45,10 @@ module ActionDispatch # * <tt>:value</tt> - The cookie's value or list of values (as an array). # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root # of the application. - # * <tt>:domain</tt> - The domain for which this cookie applies so you can - # restrict to the domain level. If you use a schema like www.example.com + # * <tt>:domain</tt> - The domain for which this cookie applies so you can + # restrict to the domain level. If you use a schema like www.example.com # and want to share session with user.example.com set <tt>:domain</tt> - # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with + # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with # <tt>:all</tt> again when deleting keys. # # :domain => nil # Does not sets cookie domain. (default) @@ -63,7 +63,7 @@ module ActionDispatch class Cookies HTTP_HEADER = "Set-Cookie".freeze TOKEN_KEY = "action_dispatch.secret_token".freeze - + # Raised when storing more than 4K of session data. class CookieOverflow < StandardError; end @@ -101,7 +101,7 @@ module ActionDispatch def handle_options(options) #:nodoc: options[:path] ||= "/" - + if options[:domain] == :all @host =~ DOMAIN_REGEXP options[:domain] = ".#{$2}.#{$3}" @@ -122,7 +122,7 @@ module ActionDispatch value = super(key.to_s, value) handle_options(options) - + @set_cookies[key] = options @delete_cookies.delete(key) value @@ -151,7 +151,7 @@ module ActionDispatch # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: # # cookies.permanent.signed[:remember_me] = current_user.id - # # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent @permanent ||= PermanentCookieJar.new(self, @secret) end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 64f4d1d532..dd82294644 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -24,9 +24,9 @@ module ActionDispatch def [](key) if key == :id - load_session_id! unless super(:id) || has_session_id? + load_session_id! unless key?(:id) || has_session_id? end - super(key) + super end private diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index dce47c63bc..ca1494425f 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -35,7 +35,7 @@ module ActionDispatch # such as 'MD5', 'RIPEMD160', 'SHA256', etc. # # To generate a secret key for an existing application, run - # "rake secret" and set the key in config/environment.rb. + # "rake secret" and set the key in config/initializers/secret_token.rb. # # Note that changing digest or secret invalidates all existing sessions! class CookieStore < AbstractStore diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 4618f3befc..41078eced7 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -46,7 +46,7 @@ module ActionDispatch end def insert(index, *args, &block) - index = self.index(index) unless index.is_a?(Integer) + index = assert_index(index, :before) middleware = self.class::Middleware.new(*args, &block) super(index, middleware) end @@ -54,9 +54,8 @@ module ActionDispatch alias_method :insert_before, :insert def insert_after(index, *args, &block) - i = index.is_a?(Integer) ? index : self.index(index) - raise "No such middleware to insert after: #{index.inspect}" unless i - insert(i + 1, *args, &block) + index = assert_index(index, :after) + insert(index + 1, *args, &block) end def swap(target, *args, &block) @@ -79,5 +78,13 @@ module ActionDispatch raise "MiddlewareStack#build requires an app" unless app reverse.inject(app) { |a, e| e.build(a) } end + + protected + + def assert_index(index, where) + i = index.is_a?(Integer) ? index : self.index(index) + raise "No such middleware to insert #{where}: #{index.inspect}" unless i + i + end end end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index ed93211255..a3af37947a 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -7,10 +7,11 @@ module ActionDispatch config.action_dispatch.x_sendfile_header = "" config.action_dispatch.ip_spoofing_check = true config.action_dispatch.show_exceptions = true + config.action_dispatch.best_standards_support = true # Prepare dispatcher callbacks and run 'prepare' callbacks initializer "action_dispatch.prepare_dispatcher" do |app| ActionDispatch::Callbacks.to_prepare { app.routes_reloader.execute_if_updated } end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index c664fb0bc2..683dd72555 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' -require 'action_controller/polymorphic_routes' module ActionDispatch # = Routing @@ -31,7 +30,7 @@ module ActionDispatch # Think of creating routes as drawing a map for your requests. The map tells # them where to go based on some predefined pattern: # - # AppName::Application.routes.draw do |map| + # AppName::Application.routes.draw do # Pattern 1 tells some request to go to one place # Pattern 2 tell them to go to another # ... @@ -106,7 +105,7 @@ module ActionDispatch # You can specify a regular expression to define a format for a parameter. # # controller 'geocode' do - # match 'geocode/:postalcode' => :show', :constraints => { + # match 'geocode/:postalcode' => :show, :constraints => { # :postalcode => /\d{5}(-\d{4})?/ # } # @@ -114,13 +113,13 @@ module ActionDispatch # expression modifiers: # # controller 'geocode' do - # match 'geocode/:postalcode' => :show', :constraints => { + # match 'geocode/:postalcode' => :show, :constraints => { # :postalcode => /hx\d\d\s\d[a-z]{2}/i # } # end # # controller 'geocode' do - # match 'geocode/:postalcode' => :show', :constraints => { + # match 'geocode/:postalcode' => :show, :constraints => { # :postalcode => /# Postcode format # \d{5} #Prefix # (-\d{4})? #Suffix @@ -217,13 +216,14 @@ module ActionDispatch autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' autoload :UrlFor, 'action_dispatch/routing/url_for' + autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes' SEPARATORS = %w( / . ? ) #:nodoc: HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] #:nodoc: # A helper module to hold URL related helpers. module Helpers #:nodoc: - include ActionController::PolymorphicRoutes + include PolymorphicRoutes end end end diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index b3146a1c60..e04062ce8b 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/with_options' +require 'active_support/core_ext/object/try' module ActionDispatch module Routing @@ -500,7 +501,7 @@ module ActionDispatch end def add_conditions_for(conditions, method) - returning({:conditions => conditions.dup}) do |options| + {:conditions => conditions.dup}.tap do |options| options[:conditions][:method] = method unless method == :any end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 430f6fc5a3..c118c72440 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,3 +1,4 @@ +require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' @@ -35,11 +36,12 @@ module ActionDispatch end class Mapping #:nodoc: - IGNORE_OPTIONS = [:to, :as, :controller, :action, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] + IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] def initialize(set, scope, args) @set, @scope = set, scope @path, @options = extract_path_and_options(args) + normalize_options! end def to_route @@ -57,14 +59,6 @@ module ActionDispatch path = args.first end - if @scope[:module] && options[:to] && !options[:to].is_a?(Proc) - if options[:to].to_s.include?("#") - options[:to] = "#{@scope[:module]}/#{options[:to]}" - elsif @scope[:controller].nil? - options[:to] = "#{@scope[:module]}##{options[:to]}" - end - end - if path.match(':controller') raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] @@ -75,15 +69,19 @@ module ActionDispatch options.reverse_merge!(:controller => /.+?/) end - path = normalize_path(path) - path_without_format = path.sub(/\(\.:format\)$/, '') + [ normalize_path(path), options ] + end + + def normalize_options! + path_without_format = @path.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, options) - options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') - options[:as] ||= path_without_format[1..-1].gsub("/", "_") + if using_match_shorthand?(path_without_format, @options) + to_shorthand = @options[:to].blank? + @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') + @options[:as] ||= path_without_format[1..-1].gsub("/", "_") end - [ path, options ] + @options.merge!(default_controller_and_action(to_shorthand)) end # match "account" => "account#index" @@ -122,44 +120,43 @@ module ActionDispatch def defaults @defaults ||= (@options[:defaults] || {}).tap do |defaults| - defaults.merge!(default_controller_and_action) defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults] @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) } end end - def default_controller_and_action + def default_controller_and_action(to_shorthand=nil) if to.respond_to?(:call) { } else - defaults = case to - when String + if to.is_a?(String) controller, action = to.split('#') - { :controller => controller, :action => action } - when Symbol - { :action => to.to_s } - else - {} + elsif to.is_a?(Symbol) + action = to.to_s end - defaults[:controller] ||= default_controller - defaults[:action] ||= default_action + controller ||= default_controller + action ||= default_action - defaults.delete(:controller) if defaults[:controller].blank? || defaults[:controller].is_a?(Regexp) - defaults.delete(:action) if defaults[:action].blank? || defaults[:action].is_a?(Regexp) + unless controller.is_a?(Regexp) || to_shorthand + controller = [@scope[:module], controller].compact.join("/").presence + end - defaults[:controller] = defaults[:controller].to_s if defaults.key?(:controller) - defaults[:action] = defaults[:action].to_s if defaults.key?(:action) + controller = controller.to_s unless controller.is_a?(Regexp) + action = action.to_s unless action.is_a?(Regexp) - if defaults[:controller].blank? && segment_keys.exclude?("controller") + if controller.blank? && segment_keys.exclude?("controller") raise ArgumentError, "missing :controller" end - if defaults[:action].blank? && segment_keys.exclude?("action") + if action.blank? && segment_keys.exclude?("action") raise ArgumentError, "missing :action" end - defaults + { :controller => controller, :action => action }.tap do |hash| + hash.delete(:controller) if hash[:controller].blank? + hash.delete(:action) if hash[:action].blank? + end end end @@ -207,6 +204,8 @@ module ActionDispatch def default_action if @options[:action] @options[:action] + elsif @scope[:action] + @scope[:action] end end end @@ -279,7 +278,6 @@ module ActionDispatch path = args.shift || block path_proc = path.is_a?(Proc) ? path : proc { |params| path % params } status = options[:status] || 301 - body = 'Moved Permanently' lambda do |env| req = Request.new(env) @@ -292,11 +290,14 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.port == 80 + body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + headers = { 'Location' => uri.to_s, 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s } + [ status, headers, [body] ] end end @@ -424,7 +425,7 @@ module ActionDispatch end def merge_controller_scope(parent, child) - @scope[:module] ? "#{@scope[:module]}/#{child}" : child + child end def merge_path_names_scope(parent, child) diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb new file mode 100644 index 0000000000..31dba835ac --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -0,0 +1,186 @@ +module ActionDispatch + module Routing + # Polymorphic URL helpers are methods for smart resolution to a named route call when + # given an Active Record model instance. They are to be used in combination with + # ActionController::Resources. + # + # These methods are useful when you want to generate correct URL or path to a RESTful + # resource without having to know the exact type of the record in question. + # + # Nested resources and/or namespaces are also supported, as illustrated in the example: + # + # polymorphic_url([:admin, @article, @comment]) + # + # results in: + # + # admin_article_comment_url(@article, @comment) + # + # == Usage within the framework + # + # Polymorphic URL helpers are used in a number of places throughout the Rails framework: + # + # * <tt>url_for</tt>, so you can use it with a record as the argument, e.g. + # <tt>url_for(@article)</tt>; + # * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write + # <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form + # action; + # * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write + # <tt>redirect_to(post)</tt> in your controllers; + # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs + # for feed entries. + # + # == Prefixed polymorphic helpers + # + # In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a + # number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt> + # in options. Those are: + # + # * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt> + # * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt> + # + # Example usage: + # + # edit_polymorphic_path(@post) # => "/posts/1/edit" + # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" + module PolymorphicRoutes + # Constructs a call to a named RESTful route for the given record and returns the + # resulting URL string. For example: + # + # # calls post_url(post) + # polymorphic_url(post) # => "http://example.com/posts/1" + # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" + # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" + # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" + # polymorphic_url(Comment) # => "http://example.com/comments" + # + # ==== Options + # + # * <tt>:action</tt> - Specifies the action prefix for the named route: + # <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix. + # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>. + # Default is <tt>:url</tt>. + # + # ==== Examples + # + # # an Article record + # polymorphic_url(record) # same as article_url(record) + # + # # a Comment record + # polymorphic_url(record) # same as comment_url(record) + # + # # it recognizes new records and maps to the collection + # record = Comment.new + # polymorphic_url(record) # same as comments_url() + # + # # the class of a record will also map to the collection + # polymorphic_url(Comment) # same as comments_url() + # + def polymorphic_url(record_or_hash_or_array, options = {}) + if record_or_hash_or_array.kind_of?(Array) + record_or_hash_or_array = record_or_hash_or_array.compact + record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + end + + record = extract_record(record_or_hash_or_array) + record = record.to_model if record.respond_to?(:to_model) + + args = case record_or_hash_or_array + when Hash; [ record_or_hash_or_array ] + when Array; record_or_hash_or_array.dup + else [ record_or_hash_or_array ] + end + + inflection = if options[:action].to_s == "new" + args.pop + :singular + elsif (record.respond_to?(:persisted?) && !record.persisted?) + args.pop + :plural + elsif record.is_a?(Class) + args.pop + :plural + else + :singular + end + + args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} + named_route = build_named_route_call(record_or_hash_or_array, inflection, options) + + url_options = options.except(:action, :routing_type) + unless url_options.empty? + args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options + end + + send(named_route, *args) + end + + # Returns the path component of a URL for the given record. It uses + # <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>. + def polymorphic_path(record_or_hash_or_array, options = {}) + polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + end + + %w(edit new).each do |action| + module_eval <<-EOT, __FILE__, __LINE__ + 1 + def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}")) # options.merge(:action => "edit")) + end # end + # + def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) + end # end + EOT + end + + private + def action_prefix(options) + options[:action] ? "#{options[:action]}_" : '' + end + + def routing_type(options) + options[:routing_type] || :url + end + + def build_named_route_call(records, inflection, options = {}) + unless records.is_a?(Array) + record = extract_record(records) + route = '' + else + record = records.pop + route = records.inject("") do |string, parent| + if parent.is_a?(Symbol) || parent.is_a?(String) + string << "#{parent}_" + else + string << ActiveModel::Naming.plural(parent).singularize + string << "_" + end + end + end + + if record.is_a?(Symbol) || record.is_a?(String) + route << "#{record}_" + else + route << ActiveModel::Naming.plural(record) + route = route.singularize if inflection == :singular + route << "_" + route << "index_" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + end + + action_prefix(options) + route + routing_type(options).to_s + end + + def extract_record(record_or_hash_or_array) + case record_or_hash_or_array + when Array; record_or_hash_or_array.last + when Hash; record_or_hash_or_array[:id] + else record_or_hash_or_array + end + end + end + end +end + diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 1b1a221c60..d23b580d97 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,10 +1,8 @@ +require 'rack/mount' require 'forwardable' require 'active_support/core_ext/object/to_query' require 'action_dispatch/routing/deprecated_mapper' -$: << File.expand_path('../../vendor/rack-mount-0.6.6.pre', __FILE__) -require 'rack/mount' - module ActionDispatch module Routing class RouteSet #:nodoc: @@ -394,10 +392,9 @@ module ActionDispatch end def generate - error = ActionController::RoutingError.new("No route matches #{options.inspect}") path, params = @set.set.generate(:path_info, named_route, options, recall, opts) - raise error unless path + raise_routing_error unless path params.reject! {|k,v| !v } @@ -406,7 +403,7 @@ module ActionDispatch path << "?#{params.to_query}" if params.any? "#{script_name}#{path}" rescue Rack::Mount::RoutingError - raise error + raise_routing_error end def opts @@ -416,12 +413,17 @@ module ActionDispatch elsif value.is_a?(Array) value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') else - Rack::Mount::Utils.escape_uri(value.to_param) + return nil unless param = value.to_param + param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") end end {:parameterize => parameterize} end + def raise_routing_error + raise ActionController::RoutingError.new("No route matches #{options.inspect}") + end + def different_controller? return false unless current_controller controller.to_param != current_controller.to_param @@ -455,7 +457,7 @@ module ActionDispatch def url_for(options) finalize! - options = default_url_options.merge(options || {}) + options = (options || {}).reverse_merge!(default_url_options) handle_positional_args(options) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 980abd44df..ba93ff8630 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -20,7 +20,7 @@ module ActionDispatch # <%= link_to('Click here', :controller => 'users', # :action => 'new', :message => 'Welcome!') %> # - # #=> Generates a link to: /users/new?message=Welcome%21 + # # Generates a link to /users/new?message=Welcome%21 # # link_to, and all other functions that require URL generation functionality, # actually use ActionController::UrlFor under the hood. And in particular, @@ -82,6 +82,7 @@ module ActionDispatch # module UrlFor extend ActiveSupport::Concern + include PolymorphicRoutes included do # TODO: with_routing extends @controller with url_helpers, trickling down to including this module which overrides its default_url_options @@ -128,7 +129,7 @@ module ActionDispatch when String options when nil, Hash - _routes.url_for(url_options.merge((options || {}).symbolize_keys)) + _routes.url_for((options || {}).reverse_merge!(url_options).symbolize_keys) else polymorphic_url(options) end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 0e4a92048f..822150b768 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -1,7 +1,6 @@ module ActionDispatch module Assertions autoload :DomAssertions, 'action_dispatch/testing/assertions/dom' - autoload :ModelAssertions, 'action_dispatch/testing/assertions/model' autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response' autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing' autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector' @@ -11,7 +10,6 @@ module ActionDispatch included do include DomAssertions - include ModelAssertions include ResponseAssertions include RoutingAssertions include SelectorAssertions diff --git a/actionpack/lib/action_dispatch/testing/assertions/model.rb b/actionpack/lib/action_dispatch/testing/assertions/model.rb deleted file mode 100644 index 46714418c6..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/model.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActionDispatch - module Assertions - module ModelAssertions - # Ensures that the passed record is valid by Active Record standards and - # returns any error messages if it is not. - # - # ==== Examples - # - # # assert that a newly created record is valid - # model = Model.new - # assert_valid(model) - # - def assert_valid(record) - ::ActiveSupport::Deprecation.warn("assert_valid is deprecated. Use assert record.valid? instead", caller) - assert record.valid?, record.errors.full_messages.join("\n") - end - end - end -end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 64eb6d8de7..b52795c575 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -1,6 +1,7 @@ require 'stringio' require 'uri' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/object/try' require 'rack/test' require 'test/unit/assertions' @@ -319,7 +320,7 @@ module ActionDispatch reset! unless @integration_session # reset the html_document variable, but only for new get/post calls @html_document = nil unless %w(cookies assigns).include?(method) - returning @integration_session.__send__(method, *args) do + @integration_session.__send__(method, *args).tap do copy_session_variables! end end @@ -362,7 +363,7 @@ module ActionDispatch def method_missing(sym, *args, &block) reset! unless @integration_session if @integration_session.respond_to?(sym) - returning @integration_session.__send__(sym, *args, &block) do + @integration_session.__send__(sym, *args, &block).tap do copy_session_variables! end else diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount.rb deleted file mode 100644 index 9fbf707724..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'rack' - -module Rack #:nodoc: - # A stackable dynamic tree based Rack router. - # - # Rack::Mount supports Rack's Cascade style of trying several routes until - # it finds one that is not a 404. This allows multiple routes to be nested - # or stacked on top of each other. Since the application endpoint can - # trigger the router to continue matching, middleware can be used to add - # arbitrary conditions to any route. This allows you to route based on - # other request attributes, session information, or even data dynamically - # pulled from a database. - module Mount - autoload :CodeGeneration, 'rack/mount/code_generation' - autoload :GeneratableRegexp, 'rack/mount/generatable_regexp' - autoload :Multimap, 'rack/mount/multimap' - autoload :Prefix, 'rack/mount/prefix' - autoload :RegexpWithNamedGroups, 'rack/mount/regexp_with_named_groups' - autoload :Route, 'rack/mount/route' - autoload :RouteSet, 'rack/mount/route_set' - autoload :RoutingError, 'rack/mount/route_set' - autoload :Strexp, 'rack/mount/strexp' - autoload :Utils, 'rack/mount/utils' - autoload :Version, 'rack/mount/version' - - module Analysis #:nodoc: - autoload :Frequency, 'rack/mount/analysis/frequency' - autoload :Histogram, 'rack/mount/analysis/histogram' - autoload :Splitting, 'rack/mount/analysis/splitting' - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/frequency.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/frequency.rb deleted file mode 100644 index 671258f807..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/frequency.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'rack/mount/utils' - -module Rack::Mount - module Analysis - class Frequency #:nodoc: - def initialize(*keys) - clear - keys.each { |key| self << key } - end - - def clear - @raw_keys = [] - @key_frequency = Analysis::Histogram.new - self - end - - def <<(key) - raise ArgumentError unless key.is_a?(Hash) - @raw_keys << key - nil - end - - def possible_keys - @possible_keys ||= begin - @raw_keys.map do |key| - key.inject({}) { |requirements, (method, requirement)| - process_key(requirements, method, requirement) - requirements - } - end - end - end - - def process_key(requirements, method, requirement) - if requirement.is_a?(Regexp) - expression = Utils.parse_regexp(requirement) - - if expression.is_a?(Regin::Expression) && expression.anchored_to_line? - expression = Regin::Expression.new(expression.reject { |e| e.is_a?(Regin::Anchor) }) - return requirements[method] = expression.to_s if expression.literal? - end - end - - requirements[method] = requirement - end - - def report - @report ||= begin - possible_keys.each { |keys| keys.each_pair { |key, _| @key_frequency << key } } - return [] if @key_frequency.count <= 1 - @key_frequency.keys_in_upper_quartile - end - end - - def expire! - @possible_keys = @report = nil - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/histogram.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/histogram.rb deleted file mode 100644 index 20aaa132f9..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/histogram.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Rack::Mount - module Analysis - class Histogram < Hash #:nodoc: - attr_reader :count - - def initialize - @count = 0 - super(0) - expire_caches! - end - - def <<(value) - @count += 1 - self[value] += 1 if value - expire_caches! - self - end - - def sorted_by_frequency - sort_by { |_, value| value }.reverse! - end - - def max - @max ||= values.max || 0 - end - - def min - @min ||= values.min || 0 - end - - def mean - @mean ||= calculate_mean - end - - def standard_deviation - @standard_deviation ||= calculate_standard_deviation - end - - def upper_quartile_limit - @upper_quartile_limit ||= calculate_upper_quartile_limit - end - - def keys_in_upper_quartile - @keys_in_upper_quartile ||= compute_keys_in_upper_quartile - end - - private - def calculate_mean - count / size - end - - def calculate_variance - values.inject(0) { |sum, e| sum + (e - mean) ** 2 } / count.to_f - end - - def calculate_standard_deviation - Math.sqrt(calculate_variance) - end - - def calculate_upper_quartile_limit - mean + standard_deviation - end - - def compute_keys_in_upper_quartile - sorted_by_frequency.select { |_, value| value >= upper_quartile_limit }.map! { |key, _| key } - end - - def expire_caches! - @max = @min = @mean = @standard_deviation = nil - @keys_in_upper_quartile = nil - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/splitting.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/splitting.rb deleted file mode 100644 index 8a8c551302..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/analysis/splitting.rb +++ /dev/null @@ -1,159 +0,0 @@ -require 'rack/mount/utils' - -module Rack::Mount - module Analysis - class Splitting < Frequency - NULL = "\0".freeze - - class Key < Struct.new(:method, :index, :separators) - def self.split(value, separator_pattern) - keys = value.split(separator_pattern) - keys.shift if keys[0] == '' - keys << NULL - keys - end - - def call(cache, obj) - (cache[method] ||= self.class.split(obj.send(method), separators))[index] - end - - def call_source(cache, obj) - "(#{cache}[:#{method}] ||= Analysis::Splitting::Key.split(#{obj}.#{method}, #{separators.inspect}))[#{index}]" - end - - def inspect - "#{method}[#{index}]" - end - end - - def clear - @boundaries = {} - super - end - - def <<(key) - super - key.each_pair do |k, v| - analyze_capture_boundaries(v, @boundaries[k] ||= Histogram.new) - end - end - - def separators(key) - (@boundaries[key].keys_in_upper_quartile + ['/']).uniq - end - - def process_key(requirements, method, requirement) - separators = separators(method) - if requirement.is_a?(Regexp) && separators.any? - generate_split_keys(requirement, separators).each_with_index do |value, index| - requirements[Key.new(method, index, Regexp.union(*separators))] = value - end - else - super - end - end - - private - def analyze_capture_boundaries(regexp, boundaries) #:nodoc: - return boundaries unless regexp.is_a?(Regexp) - - parts = Utils.parse_regexp(regexp) - parts.each_with_index do |part, index| - if part.is_a?(Regin::Group) - if index > 0 - previous = parts[index-1] - if previous.is_a?(Regin::Character) && previous.literal? - boundaries << previous.to_s - end - end - - if inside = part.expression[0] - if inside.is_a?(Regin::Character) && inside.literal? - boundaries << inside.to_s - end - end - - if index < parts.length - following = parts[index+1] - if following.is_a?(Regin::Character) && following.literal? - boundaries << following.to_s - end - end - end - end - - boundaries - end - - def generate_split_keys(regexp, separators) #:nodoc: - segments = [] - buf = nil - parts = Utils.parse_regexp(regexp) - parts.each_with_index do |part, index| - case part - when Regin::Anchor - if part.value == '$' || part.value == '\Z' - segments << join_buffer(buf, regexp) if buf - segments << NULL - buf = nil - break - end - when Regin::CharacterClass - break if separators.any? { |s| part.include?(s) } - buf = nil - segments << part.to_regexp(true) - when Regin::Character - if separators.any? { |s| part.include?(s) } - segments << join_buffer(buf, regexp) if buf - peek = parts[index+1] - if peek.is_a?(Regin::Character) && separators.include?(peek.value) - segments << '' - end - buf = nil - else - buf ||= Regin::Expression.new([]) - buf += [part] - end - when Regin::Group - if part.quantifier == '?' - value = part.expression.first - if separators.any? { |s| value.include?(s) } - segments << join_buffer(buf, regexp) if buf - buf = nil - end - break - elsif part.quantifier == nil - break if separators.any? { |s| part.include?(s) } - buf = nil - segments << part.to_regexp(true) - else - break - end - else - break - end - - if index + 1 == parts.size - segments << join_buffer(buf, regexp) if buf - buf = nil - break - end - end - - while segments.length > 0 && (segments.last.nil? || segments.last == '') - segments.pop - end - - segments - end - - def join_buffer(parts, regexp) - if parts.literal? - parts.to_s - else - parts.to_regexp(true) - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/code_generation.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/code_generation.rb deleted file mode 100644 index 903c79fdc6..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/code_generation.rb +++ /dev/null @@ -1,113 +0,0 @@ -module Rack::Mount - module CodeGeneration #:nodoc: - def _expired_recognize(env) #:nodoc: - raise 'route set not finalized' - end - - def rehash - super - optimize_recognize! - end - - private - def expire! - if @optimized_recognize_defined - remove_metaclass_method :recognize - - class << self - alias_method :recognize, :_expired_recognize - end - - @optimized_recognize_defined = false - end - - super - end - - def optimize_container_iterator(container) - body = [] - - container.each_with_index { |route, i| - body << "route = self[#{i}]" - body << 'matches = {}' - body << 'params = route.defaults.dup' - - conditions = [] - route.conditions.each do |method, condition| - b = [] - if condition.is_a?(Regexp) - b << "if m = obj.#{method}.match(#{condition.inspect})" - b << "matches[:#{method}] = m" - if (named_captures = route.named_captures[method]) && named_captures.any? - b << 'captures = m.captures' - b << 'p = nil' - b << named_captures.map { |k, j| "params[#{k.inspect}] = p if p = captures[#{j}]" }.join('; ') - end - else - b << "if m = obj.#{method} == route.conditions[:#{method}]" - end - b << 'true' - b << 'end' - conditions << "(#{b.join('; ')})" - end - - body << <<-RUBY - if #{conditions.join(' && ')} - yield route, matches, params - end - RUBY - } - - container.instance_eval(<<-RUBY, __FILE__, __LINE__) - def optimized_each(obj) - #{body.join("\n")} - nil - end - RUBY - end - - def optimize_recognize! - keys = @recognition_keys.map { |key| - if key.respond_to?(:call_source) - key.call_source(:cache, :obj) - else - "obj.#{key}" - end - }.join(', ') - - @optimized_recognize_defined = true - - remove_metaclass_method :recognize - - instance_eval(<<-RUBY, __FILE__, __LINE__) - def recognize(obj) - cache = {} - container = @recognition_graph[#{keys}] - optimize_container_iterator(container) unless container.respond_to?(:optimized_each) - - if block_given? - container.optimized_each(obj) do |route, matches, params| - yield route, matches, params - end - else - container.optimized_each(obj) do |route, matches, params| - return route, matches, params - end - end - - nil - end - RUBY - end - - # method_defined? can't distinguish between instance - # and meta methods. So we have to rescue if the method - # has not been defined in the metaclass yet. - def remove_metaclass_method(symbol) - metaclass = class << self; self; end - metaclass.send(:remove_method, symbol) - rescue NameError => e - nil - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/generatable_regexp.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/generatable_regexp.rb deleted file mode 100644 index 47bbab3784..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/generatable_regexp.rb +++ /dev/null @@ -1,210 +0,0 @@ -require 'rack/mount/utils' - -module Rack::Mount - class GeneratableRegexp < Regexp #:nodoc: - class DynamicSegment #:nodoc: - attr_reader :name, :requirement - - def initialize(name, requirement) - @name, @requirement = name.to_sym, requirement - freeze - end - - def ==(obj) - @name == obj.name && @requirement == obj.requirement - end - - def =~(str) - @requirement =~ str - end - - def to_hash - { @name => @requirement } - end - - def inspect - "/(?<#{@name}>#{@requirement.source})/" - end - end - - module InstanceMethods - def self.extended(obj) - obj.segments - end - - def defaults=(defaults) - @required_captures = nil - @required_params = nil - @required_defaults = nil - @defaults = defaults - end - - def defaults - @defaults ||= {} - end - - def generatable? - segments.any? - end - - def generate(params = {}, recall = {}, options = {}) - return nil unless generatable? - - merged = recall.merge(params) - return nil unless required_params.all? { |p| merged.include?(p) } - return nil unless required_defaults.all? { |k, v| merged[k] == v } - - generate_from_segments(segments, params, merged, options) - end - - def segments - @segments ||= begin - defaults - segments = [] - catch(:halt) do - expression = Utils.parse_regexp(self) - segments = parse_segments(expression) - end - segments - end - end - - def captures - segments.flatten.find_all { |s| s.is_a?(DynamicSegment) } - end - - def required_captures - @required_captures ||= segments.find_all { |s| - s.is_a?(DynamicSegment) && !@defaults.include?(s.name) - }.freeze - end - - def required_params - @required_params ||= required_captures.map { |s| s.name }.freeze - end - - def required_defaults - @required_defaults ||= begin - required_defaults = @defaults.dup - captures.inject({}) { |h, s| h.merge!(s.to_hash) }.keys.each { |name| - required_defaults.delete(name) - } - required_defaults - end - end - - def freeze - segments - captures - required_captures - required_params - required_defaults - super - end - - private - def parse_segments(segments) - s = [] - segments.each_with_index do |part, index| - case part - when Regin::Anchor - # ignore - when Regin::Character - throw :halt unless part.literal? - - if s.last.is_a?(String) - s.last << part.value.dup - else - s << part.value.dup - end - when Regin::Group - if part.name - s << DynamicSegment.new(part.name, part.expression.to_regexp(true)) - else - s << parse_segments(part.expression) - end - when Regin::Expression - return parse_segments(part) - else - throw :halt - end - end - - s - end - - EMPTY_STRING = ''.freeze - - def generate_from_segments(segments, params, merged, options, optional = false) - if optional - return EMPTY_STRING if segments.all? { |s| s.is_a?(String) } - return EMPTY_STRING unless segments.flatten.any? { |s| - params.has_key?(s.name) if s.is_a?(DynamicSegment) - } - return EMPTY_STRING if segments.any? { |segment| - if segment.is_a?(DynamicSegment) - value = merged[segment.name] || @defaults[segment.name] - value = parameterize(segment.name, value, options) - - merged_value = parameterize(segment.name, merged[segment.name], options) - default_value = parameterize(segment.name, @defaults[segment.name], options) - - if value.nil? || segment !~ value - true - elsif merged_value == default_value - # Nasty control flow - return :clear_remaining_segments - else - false - end - end - } - end - - generated = segments.map do |segment| - case segment - when String - segment - when DynamicSegment - value = params[segment.name] || merged[segment.name] || @defaults[segment.name] - value = parameterize(segment.name, value, options) - if value && segment =~ value.to_s - value - else - return - end - when Array - value = generate_from_segments(segment, params, merged, options, true) - if value == :clear_remaining_segments - segment.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) } - EMPTY_STRING - elsif value.nil? - EMPTY_STRING - else - value - end - end - end - - # Delete any used items from the params - segments.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) } - - generated.join - end - - def parameterize(name, value, options) - if block = options[:parameterize] - block.call(name, value) - else - value - end - end - end - include InstanceMethods - - def initialize(regexp) - super - segments - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/multimap.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/multimap.rb deleted file mode 100644 index 0f8eaaec67..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/multimap.rb +++ /dev/null @@ -1,53 +0,0 @@ -begin - require 'nested_multimap' -rescue LoadError - $: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/multimap')) - require 'nested_multimap' -end - -module Rack::Mount - class Multimap < NestedMultimap #:nodoc: - def store(*args) - keys = args.dup - value = keys.pop - key = keys.shift - - raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value - - unless key.respond_to?(:=~) - raise ArgumentError, "unsupported key: #{args.first.inspect}" - end - - if key.is_a?(Regexp) - if keys.empty? - @hash.each_pair { |k, l| l << value if k =~ key } - self.default << value - else - @hash.each_pair { |k, _| - if k =~ key - args[0] = k - super(*args) - end - } - - self.default = self.class.new(default) unless default.is_a?(self.class) - default[*keys.dup] = value - end - else - super(*args) - end - end - alias_method :[]=, :store - - undef :index, :invert - - def height - containers_with_default.max { |a, b| a.length <=> b.length }.length - end - - def average_height - lengths = containers_with_default.map { |e| e.length } - lengths.inject(0) { |sum, len| sum += len }.to_f / lengths.size - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/prefix.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/prefix.rb deleted file mode 100644 index 58892e4c4f..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/prefix.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'rack/mount/utils' - -module Rack::Mount - class Prefix #:nodoc: - EMPTY_STRING = ''.freeze - PATH_INFO = 'PATH_INFO'.freeze - SCRIPT_NAME = 'SCRIPT_NAME'.freeze - SLASH = '/'.freeze - - KEY = 'rack.mount.prefix'.freeze - - def initialize(app, prefix = nil) - @app, @prefix = app, prefix.freeze - freeze - end - - def call(env) - if prefix = env[KEY] || @prefix - old_path_info = env[PATH_INFO].dup - old_script_name = env[SCRIPT_NAME].dup - - begin - env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO].sub(prefix, EMPTY_STRING)) - env[PATH_INFO] = EMPTY_STRING if env[PATH_INFO] == SLASH - env[SCRIPT_NAME] = Utils.normalize_path(env[SCRIPT_NAME].to_s + prefix) - @app.call(env) - ensure - env[PATH_INFO] = old_path_info - env[SCRIPT_NAME] = old_script_name - end - else - @app.call(env) - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/regexp_with_named_groups.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/regexp_with_named_groups.rb deleted file mode 100644 index c11292b2a2..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/regexp_with_named_groups.rb +++ /dev/null @@ -1,69 +0,0 @@ -module Rack::Mount - if Regin.regexp_supports_named_captures? - RegexpWithNamedGroups = Regexp - else - require 'strscan' - - # A wrapper that adds shim named capture support to older - # versions of Ruby. - # - # Because the named capture syntax causes a parse error, an - # alternate syntax is used to indicate named captures. - # - # Ruby 1.9+ named capture syntax: - # - # /(?<foo>[a-z]+)/ - # - # Ruby 1.8 shim syntax: - # - # /(?:<foo>[a-z]+)/ - class RegexpWithNamedGroups < Regexp - def self.new(regexp) #:nodoc: - if regexp.is_a?(RegexpWithNamedGroups) - regexp - else - super - end - end - - # Wraps Regexp with named capture support. - def initialize(regexp) - regexp = Regexp.compile(regexp) unless regexp.is_a?(Regexp) - source, options = regexp.source, regexp.options - @names, scanner = [], StringScanner.new(source) - - while scanner.skip_until(/\(/) - if scanner.scan(/\?:<([^>]+)>/) - @names << scanner[1] - elsif scanner.scan(/\?(i?m?x?\-?i?m?x?)?:/) - # ignore noncapture - else - @names << nil - end - end - source.gsub!(/\?:<([^>]+)>/, '') - - @names = [] unless @names.any? - @names.freeze - - super(source, options) - end - - def names - @names.dup - end - - def named_captures - named_captures = {} - names.each_with_index { |n, i| - named_captures[n] = [i+1] if n - } - named_captures - end - - def eql?(other) - super && @names.eql?(other.names) - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route.rb deleted file mode 100644 index 680c40f147..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route.rb +++ /dev/null @@ -1,130 +0,0 @@ -require 'rack/mount/generatable_regexp' -require 'rack/mount/regexp_with_named_groups' -require 'rack/mount/utils' - -module Rack::Mount - # Route is an internal class used to wrap a single route attributes. - # - # Plugins should not depend on any method on this class or instantiate - # new Route objects. Instead use the factory method, RouteSet#add_route - # to create new routes and add them to the set. - class Route - # Valid rack application to call if conditions are met - attr_reader :app - - # A hash of conditions to match against. Conditions may be expressed - # as strings or regexps to match against. - attr_reader :conditions - - # A hash of values that always gets merged into the parameters hash - attr_reader :defaults - - # Symbol identifier for the route used with named route generations - attr_reader :name - - attr_reader :named_captures - - def initialize(app, conditions, defaults, name) - unless app.respond_to?(:call) - raise ArgumentError, 'app must be a valid rack application' \ - ' and respond to call' - end - @app = app - - @name = name ? name.to_sym : nil - @defaults = (defaults || {}).freeze - - @conditions = {} - - conditions.each do |method, pattern| - next unless method && pattern - - pattern = Regexp.compile("\\A#{Regexp.escape(pattern)}\\Z") if pattern.is_a?(String) - - if pattern.is_a?(Regexp) - pattern = Utils.normalize_extended_expression(pattern) - pattern = RegexpWithNamedGroups.new(pattern) - pattern.extend(GeneratableRegexp::InstanceMethods) - pattern.defaults = @defaults - end - - @conditions[method] = pattern.freeze - end - - @named_captures = {} - @conditions.map { |method, condition| - next unless condition.respond_to?(:named_captures) - @named_captures[method] = condition.named_captures.inject({}) { |named_captures, (k, v)| - named_captures[k.to_sym] = v.last - 1 - named_captures - }.freeze - } - @named_captures.freeze - - @has_significant_params = @conditions.any? { |method, condition| - (condition.respond_to?(:required_params) && condition.required_params.any?) || - (condition.respond_to?(:required_defaults) && condition.required_defaults.any?) - } - - if @conditions.has_key?(:path_info) && - !Utils.regexp_anchored?(@conditions[:path_info]) - @prefix = true - @app = Prefix.new(@app) - else - @prefix = false - end - - @conditions.freeze - end - - def prefix? - @prefix - end - - - def generation_keys - @conditions.inject({}) { |keys, (method, condition)| - if condition.respond_to?(:required_defaults) - keys.merge!(condition.required_defaults) - else - keys - end - } - end - - def significant_params? - @has_significant_params - end - - def generate(method, params = {}, recall = {}, options = {}) - if method.nil? - result = @conditions.inject({}) { |h, (m, condition)| - if condition.respond_to?(:generate) - h[m] = condition.generate(params, recall, options) - end - h - } - return nil if result.values.compact.empty? - else - if condition = @conditions[method] - if condition.respond_to?(:generate) - result = condition.generate(params, recall, options) - end - end - end - - if result - @defaults.each do |key, value| - params.delete(key) if params[key] == value - end - end - - result - end - - - def inspect #:nodoc: - "#<#{self.class.name} @app=#{@app.inspect} @conditions=#{@conditions.inspect} @defaults=#{@defaults.inspect} @name=#{@name.inspect}>" - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route_set.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route_set.rb deleted file mode 100644 index 0e5a65a640..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/route_set.rb +++ /dev/null @@ -1,409 +0,0 @@ -require 'rack/mount/multimap' -require 'rack/mount/route' -require 'rack/mount/utils' - -module Rack::Mount - class RoutingError < StandardError; end - - class RouteSet - # Initialize a new RouteSet without optimizations - def self.new_without_optimizations(options = {}, &block) - new(options.merge(:_optimize => false), &block) - end - - # Basic RouteSet initializer. - # - # If a block is given, the set is yielded and finalized. - # - # See other aspects for other valid options: - # - <tt>Generation::RouteSet.new</tt> - # - <tt>Recognition::RouteSet.new</tt> - def initialize(options = {}, &block) - @parameters_key = options.delete(:parameters_key) || 'rack.routing_args' - @parameters_key.freeze - - @named_routes = {} - - @recognition_key_analyzer = Analysis::Splitting.new - @generation_key_analyzer = Analysis::Frequency.new - - @request_class = options.delete(:request_class) || Rack::Request - @valid_conditions = @request_class.public_instance_methods.map! { |m| m.to_sym } - - extend CodeGeneration unless options[:_optimize] == false - @optimized_recognize_defined = false - - @routes = [] - expire! - - if block_given? - yield self - rehash - end - end - - # Builder method to add a route to the set - # - # <tt>app</tt>:: A valid Rack app to call if the conditions are met. - # <tt>conditions</tt>:: A hash of conditions to match against. - # Conditions may be expressed as strings or - # regexps to match against. - # <tt>defaults</tt>:: A hash of values that always gets merged in - # <tt>name</tt>:: Symbol identifier for the route used with named - # route generations - def add_route(app, conditions = {}, defaults = {}, name = nil) - unless conditions.is_a?(Hash) - raise ArgumentError, 'conditions must be a Hash' - end - - unless conditions.all? { |method, pattern| - @valid_conditions.include?(method) - } - raise ArgumentError, 'conditions may only include ' + - @valid_conditions.inspect - end - - route = Route.new(app, conditions, defaults, name) - @routes << route - - @recognition_key_analyzer << route.conditions - - @named_routes[route.name] = route if route.name - @generation_key_analyzer << route.generation_keys - - expire! - route - end - - def recognize(obj) - raise 'route set not finalized' unless @recognition_graph - - cache = {} - keys = @recognition_keys.map { |key| - if key.respond_to?(:call) - key.call(cache, obj) - else - obj.send(key) - end - } - - @recognition_graph[*keys].each do |route| - matches = {} - params = route.defaults.dup - - if route.conditions.all? { |method, condition| - value = obj.send(method) - if condition.is_a?(Regexp) && (m = value.match(condition)) - matches[method] = m - captures = m.captures - route.named_captures[method].each do |k, i| - if v = captures[i] - params[k] = v - end - end - true - elsif value == condition - true - else - false - end - } - if block_given? - yield route, matches, params - else - return route, matches, params - end - end - end - - nil - end - - X_CASCADE = 'X-Cascade'.freeze - PASS = 'pass'.freeze - PATH_INFO = 'PATH_INFO'.freeze - - # Rack compatible recognition and dispatching method. Routes are - # tried until one returns a non-catch status code. If no routes - # match, the catch status code is returned. - # - # This method can only be invoked after the RouteSet has been - # finalized. - def call(env) - raise 'route set not finalized' unless @recognition_graph - - env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO]) - - request = nil - req = @request_class.new(env) - recognize(req) do |route, matches, params| - # TODO: We only want to unescape params from uri related methods - params.each { |k, v| params[k] = Utils.unescape_uri(v) if v.is_a?(String) } - - if route.prefix? - env[Prefix::KEY] = matches[:path_info].to_s - end - - env[@parameters_key] = params - result = route.app.call(env) - return result unless result[1][X_CASCADE] == PASS - end - - request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']] - end - - # Generates a url from Rack env and identifiers or significant keys. - # - # To generate a url by named route, pass the name in as a +Symbol+. - # url(env, :dashboard) # => "/dashboard" - # - # Additional parameters can be passed in as a hash - # url(env, :people, :id => "1") # => "/people/1" - # - # If no name route is given, it will fall back to a slower - # generation search. - # url(env, :controller => "people", :action => "show", :id => "1") - # # => "/people/1" - def url(env, *args) - named_route, params = nil, {} - - case args.length - when 2 - named_route, params = args[0], args[1].dup - when 1 - if args[0].is_a?(Hash) - params = args[0].dup - else - named_route = args[0] - end - else - raise ArgumentError - end - - only_path = params.delete(:only_path) - recall = env[@parameters_key] || {} - - unless result = generate(:all, named_route, params, recall, - :parameterize => lambda { |name, param| Utils.escape_uri(param) }) - return - end - - parts, params = result - return unless parts - - params.each do |k, v| - if v - params[k] = v - else - params.delete(k) - end - end - - req = stubbed_request_class.new(env) - req._stubbed_values = parts.merge(:query_string => Utils.build_nested_query(params)) - only_path ? req.fullpath : req.url - end - - def generate(method, *args) #:nodoc: - raise 'route set not finalized' unless @generation_graph - - method = nil if method == :all - named_route, params, recall, options = extract_params!(*args) - merged = recall.merge(params) - route = nil - - if named_route - if route = @named_routes[named_route.to_sym] - recall = route.defaults.merge(recall) - url = route.generate(method, params, recall, options) - [url, params] - else - raise RoutingError, "#{named_route} failed to generate from #{params.inspect}" - end - else - keys = @generation_keys.map { |key| - if k = merged[key] - k.to_s - else - nil - end - } - @generation_graph[*keys].each do |r| - next unless r.significant_params? - if url = r.generate(method, params, recall, options) - return [url, params] - end - end - - raise RoutingError, "No route matches #{params.inspect}" - end - end - - # Number of routes in the set - def length - @routes.length - end - - def rehash #:nodoc: - @recognition_keys = build_recognition_keys - @recognition_graph = build_recognition_graph - @generation_keys = build_generation_keys - @generation_graph = build_generation_graph - end - - # Finalizes the set and builds optimized data structures. You *must* - # freeze the set before you can use <tt>call</tt> and <tt>url</tt>. - # So remember to call freeze after you are done adding routes. - def freeze - unless frozen? - rehash - - @recognition_key_analyzer = nil - @generation_key_analyzer = nil - @valid_conditions = nil - - @routes.each { |route| route.freeze } - @routes.freeze - end - - super - end - - def marshal_dump #:nodoc: - hash = {} - - instance_variables_to_serialize.each do |ivar| - hash[ivar] = instance_variable_get(ivar) - end - - if graph = hash[:@recognition_graph] - hash[:@recognition_graph] = graph.dup - end - - hash - end - - def marshal_load(hash) #:nodoc: - hash.each do |ivar, value| - instance_variable_set(ivar, value) - end - end - - protected - def recognition_stats - { :keys => @recognition_keys, - :keys_size => @recognition_keys.size, - :graph_size => @recognition_graph.size, - :graph_height => @recognition_graph.height, - :graph_average_height => @recognition_graph.average_height } - end - - private - def expire! #:nodoc: - @recognition_keys = @recognition_graph = nil - @recognition_key_analyzer.expire! - - @generation_keys = @generation_graph = nil - @generation_key_analyzer.expire! - end - - def instance_variables_to_serialize - instance_variables.map { |ivar| ivar.to_sym } - [:@stubbed_request_class, :@optimized_recognize_defined] - end - - # An internal helper method for constructing a nested set from - # the linear route set. - # - # build_nested_route_set([:request_method, :path_info]) { |route, method| - # route.send(method) - # } - def build_nested_route_set(keys, &block) - graph = Multimap.new - @routes.each_with_index do |route, index| - catch(:skip) do - k = keys.map { |key| block.call(key, index) } - Utils.pop_trailing_nils!(k) - k.map! { |key| key || /.+/ } - graph[*k] = route - end - end - graph - end - - def build_recognition_graph - build_nested_route_set(@recognition_keys) { |k, i| - @recognition_key_analyzer.possible_keys[i][k] - } - end - - def build_recognition_keys - @recognition_key_analyzer.report - end - - def build_generation_graph - build_nested_route_set(@generation_keys) { |k, i| - throw :skip unless @routes[i].significant_params? - - if k = @generation_key_analyzer.possible_keys[i][k] - k.to_s - else - nil - end - } - end - - def build_generation_keys - @generation_key_analyzer.report - end - - def extract_params!(*args) - case args.length - when 4 - named_route, params, recall, options = args - when 3 - if args[0].is_a?(Hash) - params, recall, options = args - else - named_route, params, recall = args - end - when 2 - if args[0].is_a?(Hash) - params, recall = args - else - named_route, params = args - end - when 1 - if args[0].is_a?(Hash) - params = args[0] - else - named_route = args[0] - end - else - raise ArgumentError - end - - named_route ||= nil - params ||= {} - recall ||= {} - options ||= {} - - [named_route, params.dup, recall.dup, options.dup] - end - - def stubbed_request_class - @stubbed_request_class ||= begin - klass = Class.new(@request_class) - klass.public_instance_methods.each do |method| - next if method =~ /^__|object_id/ - klass.class_eval <<-RUBY - def #{method}(*args, &block) - @_stubbed_values[:#{method}] || super - end - RUBY - end - klass.class_eval { attr_accessor :_stubbed_values } - klass - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp.rb deleted file mode 100644 index d0d8797008..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'rack/mount/strexp/parser' - -module Rack::Mount - class Strexp - class << self - # Parses segmented string expression and converts it into a Regexp - # - # Strexp.compile('foo') - # # => %r{\Afoo\Z} - # - # Strexp.compile('foo/:bar', {}, ['/']) - # # => %r{\Afoo/(?<bar>[^/]+)\Z} - # - # Strexp.compile(':foo.example.com') - # # => %r{\A(?<foo>.+)\.example\.com\Z} - # - # Strexp.compile('foo/:bar', {:bar => /[a-z]+/}, ['/']) - # # => %r{\Afoo/(?<bar>[a-z]+)\Z} - # - # Strexp.compile('foo(.:extension)') - # # => %r{\Afoo(\.(?<extension>.+))?\Z} - # - # Strexp.compile('src/*files') - # # => %r{\Asrc/(?<files>.+)\Z} - def compile(str, requirements = {}, separators = [], anchor = true) - return Regexp.compile(str) if str.is_a?(Regexp) - - requirements = requirements ? requirements.dup : {} - normalize_requirements!(requirements, separators) - - parser = StrexpParser.new - parser.anchor = anchor - parser.requirements = requirements - - begin - re = parser.scan_str(str) - rescue Racc::ParseError => e - raise RegexpError, e.message - end - - Regexp.compile(re) - end - alias_method :new, :compile - - private - def normalize_requirements!(requirements, separators) - requirements.each do |key, value| - if value.is_a?(Regexp) - if regexp_has_modifiers?(value) - requirements[key] = value - else - requirements[key] = value.source - end - else - requirements[key] = Regexp.escape(value) - end - end - requirements.default ||= separators.any? ? - "[^#{separators.join}]+" : '.+' - requirements - end - - def regexp_has_modifiers?(regexp) - regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.rb deleted file mode 100644 index cfe05afc61..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.rb +++ /dev/null @@ -1,160 +0,0 @@ -# -# DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.6 -# from Racc grammer file "". -# - -require 'racc/parser.rb' - -require 'rack/mount/utils' -require 'rack/mount/strexp/tokenizer' - -module Rack - module Mount - class StrexpParser < Racc::Parser - - -if Regin.regexp_supports_named_captures? - REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze -else - REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze -end - -attr_accessor :anchor, :requirements -##### State transition tables begin ### - -racc_action_table = [ - 1, 2, 3, 9, 4, 1, 2, 3, 12, 4, - 1, 2, 3, 11, 4, 1, 2, 3, nil, 4 ] - -racc_action_check = [ - 0, 0, 0, 5, 0, 3, 3, 3, 9, 3, - 8, 8, 8, 8, 8, 6, 6, 6, nil, 6 ] - -racc_action_pointer = [ - -2, nil, nil, 3, nil, 3, 13, nil, 8, 8, - nil, nil, nil ] - -racc_action_default = [ - -8, -4, -5, -8, -7, -8, -1, -3, -8, -8, - -2, -6, 13 ] - -racc_goto_table = [ - 6, 5, 10, 8, 10 ] - -racc_goto_check = [ - 2, 1, 3, 2, 3 ] - -racc_goto_pointer = [ - nil, 1, 0, -4 ] - -racc_goto_default = [ - nil, nil, nil, 7 ] - -racc_reduce_table = [ - 0, 0, :racc_error, - 1, 8, :_reduce_1, - 2, 9, :_reduce_2, - 1, 9, :_reduce_none, - 1, 10, :_reduce_4, - 1, 10, :_reduce_5, - 3, 10, :_reduce_6, - 1, 10, :_reduce_7 ] - -racc_reduce_n = 8 - -racc_shift_n = 13 - -racc_token_table = { - false => 0, - :error => 1, - :PARAM => 2, - :GLOB => 3, - :LPAREN => 4, - :RPAREN => 5, - :CHAR => 6 } - -racc_nt_base = 7 - -racc_use_result_var = true - -Racc_arg = [ - racc_action_table, - racc_action_check, - racc_action_default, - racc_action_pointer, - racc_goto_table, - racc_goto_check, - racc_goto_default, - racc_goto_pointer, - racc_nt_base, - racc_reduce_table, - racc_token_table, - racc_shift_n, - racc_reduce_n, - racc_use_result_var ] - -Racc_token_to_s_table = [ - "$end", - "error", - "PARAM", - "GLOB", - "LPAREN", - "RPAREN", - "CHAR", - "$start", - "target", - "expr", - "token" ] - -Racc_debug_parser = false - -##### State transition tables end ##### - -# reduce 0 omitted - -def _reduce_1(val, _values, result) - result = anchor ? "\\A#{val.join}\\Z" : "\\A#{val.join}" - result -end - -def _reduce_2(val, _values, result) - result = val.join - result -end - -# reduce 3 omitted - -def _reduce_4(val, _values, result) - name = val[0].to_sym - requirement = requirements[name] - result = REGEXP_NAMED_CAPTURE % [name, requirement] - - result -end - -def _reduce_5(val, _values, result) - name = val[0].to_sym - requirement = requirements[name] - result = REGEXP_NAMED_CAPTURE % [name, '.+' || requirement] - - result -end - -def _reduce_6(val, _values, result) - result = "(?:#{val[1]})?" - result -end - -def _reduce_7(val, _values, result) - result = Regexp.escape(val[0]) - result -end - -def _reduce_none(val, _values, result) - val[0] -end - - end # class StrexpParser - end # module Mount -end # module Rack diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.y b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.y deleted file mode 100644 index ffbd9fae11..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/parser.y +++ /dev/null @@ -1,34 +0,0 @@ -class Rack::Mount::StrexpParser -rule - target: expr { result = anchor ? "\\A#{val.join}\\Z" : "\\A#{val.join}" } - - expr: expr token { result = val.join } - | token - - token: PARAM { - name = val[0].to_sym - requirement = requirements[name] - result = REGEXP_NAMED_CAPTURE % [name, requirement] - } - | GLOB { - name = val[0].to_sym - requirement = requirements[name] - result = REGEXP_NAMED_CAPTURE % [name, '.+' || requirement] - } - | LPAREN expr RPAREN { result = "(?:#{val[1]})?" } - | CHAR { result = Regexp.escape(val[0]) } -end - ----- header ---- -require 'rack/mount/utils' -require 'rack/mount/strexp/tokenizer' - ----- inner - -if Regin.regexp_supports_named_captures? - REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze -else - REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze -end - -attr_accessor :anchor, :requirements diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rb deleted file mode 100644 index 0ff7f67661..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rb +++ /dev/null @@ -1,83 +0,0 @@ -#-- -# DO NOT MODIFY!!!! -# This file is automatically generated by rex 1.0.5.beta1 -# from lexical definition file "lib/rack/mount/strexp/tokenizer.rex". -#++ - -require 'racc/parser' -class Rack::Mount::StrexpParser < Racc::Parser - require 'strscan' - - class ScanError < StandardError ; end - - attr_reader :lineno - attr_reader :filename - attr_accessor :state - - def scan_setup(str) - @ss = StringScanner.new(str) - @lineno = 1 - @state = nil - end - - def action - yield - end - - def scan_str(str) - scan_setup(str) - do_parse - end - alias :scan :scan_str - - def load_file( filename ) - @filename = filename - open(filename, "r") do |f| - scan_setup(f.read) - end - end - - def scan_file( filename ) - load_file(filename) - do_parse - end - - - def next_token - return if @ss.eos? - - text = @ss.peek(1) - @lineno += 1 if text == "\n" - token = case @state - when nil - case - when (text = @ss.scan(/\\(\(|\)|:|\*)/)) - action { [:CHAR, @ss[1]] } - - when (text = @ss.scan(/\:([a-zA-Z_]\w*)/)) - action { [:PARAM, @ss[1]] } - - when (text = @ss.scan(/\*([a-zA-Z_]\w*)/)) - action { [:GLOB, @ss[1]] } - - when (text = @ss.scan(/\(/)) - action { [:LPAREN, text] } - - when (text = @ss.scan(/\)/)) - action { [:RPAREN, text] } - - when (text = @ss.scan(/./)) - action { [:CHAR, text] } - - else - text = @ss.string[@ss.pos .. -1] - raise ScanError, "can not match: '" + text + "'" - end # if - - else - raise ScanError, "undefined state: '" + state.to_s + "'" - end # case state - token - end # def next_token - -end # class diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rex b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rex deleted file mode 100644 index 473bd096e1..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/strexp/tokenizer.rex +++ /dev/null @@ -1,12 +0,0 @@ -class Rack::Mount::StrexpParser -macro - RESERVED \(|\)|:|\* - ALPHA_U [a-zA-Z_] -rule - \\({RESERVED}) { [:CHAR, @ss[1]] } - \:({ALPHA_U}\w*) { [:PARAM, @ss[1]] } - \*({ALPHA_U}\w*) { [:GLOB, @ss[1]] } - \( { [:LPAREN, text] } - \) { [:RPAREN, text] } - . { [:CHAR, text] } -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/utils.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/utils.rb deleted file mode 100644 index aa23b1162f..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/utils.rb +++ /dev/null @@ -1,148 +0,0 @@ -begin - require 'regin' -rescue LoadError - $: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/regin')) - require 'regin' -end - -require 'uri' - -module Rack::Mount - # Private utility methods used throughout Rack::Mount. - #-- - # This module is a trash can. Try to move these functions into - # more appropriate contexts. - #++ - module Utils - # Normalizes URI path. - # - # Strips off trailing slash and ensures there is a leading slash. - # - # normalize_path("/foo") # => "/foo" - # normalize_path("/foo/") # => "/foo" - # normalize_path("foo") # => "/foo" - # normalize_path("") # => "/" - def normalize_path(path) - path = "/#{path}" - path.squeeze!('/') - path.sub!(%r{/+\Z}, '') - path = '/' if path == '' - path - end - module_function :normalize_path - - # Removes trailing nils from array. - # - # pop_trailing_nils!([1, 2, 3]) # => [1, 2, 3] - # pop_trailing_nils!([1, 2, 3, nil, nil]) # => [1, 2, 3] - # pop_trailing_nils!([nil]) # => [] - def pop_trailing_nils!(ary) - while ary.length > 0 && ary.last.nil? - ary.pop - end - ary - end - module_function :pop_trailing_nils! - - RESERVED_PCHAR = ':@&=+$,;%' - SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" - if RUBY_VERSION >= '1.9' - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze - else - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze - end - - Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI - - def escape_uri(uri) - Parser.escape(uri.to_s, UNSAFE_PCHAR) - end - module_function :escape_uri - - if ''.respond_to?(:force_encoding) - def unescape_uri(uri) - Parser.unescape(uri).force_encoding('utf-8') - end - else - def unescape_uri(uri) - URI.unescape(uri) - end - end - module_function :unescape_uri - - # Taken from Rack 1.1.x to build nested query strings - def build_nested_query(value, prefix = nil) #:nodoc: - case value - when Array - value.map { |v| - build_nested_query(v, "#{prefix}[]") - }.join("&") - when Hash - value.map { |k, v| - build_nested_query(v, prefix ? "#{prefix}[#{Rack::Utils.escape(k)}]" : Rack::Utils.escape(k)) - }.join("&") - when String - raise ArgumentError, "value must be a Hash" if prefix.nil? - "#{prefix}=#{Rack::Utils.escape(value)}" - else - prefix - end - end - module_function :build_nested_query - - # Determines whether the regexp must match the entire string. - # - # regexp_anchored?(/^foo$/) # => true - # regexp_anchored?(/foo/) # => false - # regexp_anchored?(/^foo/) # => false - # regexp_anchored?(/foo$/) # => false - def regexp_anchored?(regexp) - regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/m ? true : false - end - module_function :regexp_anchored? - - def normalize_extended_expression(regexp) - return regexp unless regexp.options & Regexp::EXTENDED != 0 - source = regexp.source - source.gsub!(/#.+$/, '') - source.gsub!(/\s+/, '') - source.gsub!(/\\\//, '/') - Regexp.compile(source) - end - module_function :normalize_extended_expression - - def parse_regexp(regexp) - cache = @@_parse_regexp_cache ||= {} - - if expression = cache[regexp] - return expression - end - - unless regexp.is_a?(RegexpWithNamedGroups) - regexp = RegexpWithNamedGroups.new(regexp) - end - - expression = Regin.parse(regexp) - - unless Regin.regexp_supports_named_captures? - tag_captures = Proc.new do |group| - case group - when Regin::Group - # TODO: dup instead of mutating - group.instance_variable_set('@name', regexp.names[group.index]) if group.index - tag_captures.call(group.expression) - when Regin::Expression - group.each { |child| tag_captures.call(child) } - end - end - tag_captures.call(expression) - end - - cache[regexp] = expression.freeze - expression - rescue Racc::ParseError, Regin::Parser::ScanError - [] - end - module_function :parse_regexp - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multimap.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multimap.rb deleted file mode 100644 index 0b49b49280..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multimap.rb +++ /dev/null @@ -1,569 +0,0 @@ -require 'forwardable' -require 'multiset' - -# Multimap is a generalization of a map or associative array -# abstract data type in which more than one value may be associated -# with and returned for a given key. -# -# == Example -# -# require 'multimap' -# map = Multimap.new -# map["a"] = 100 -# map["b"] = 200 -# map["a"] = 300 -# map["a"] # -> [100, 300] -# map["b"] # -> [200] -# map.keys # -> #<Multiset: {a, a, b}> -class Multimap - extend Forwardable - - include Enumerable - - # call-seq: - # Multimap[ [key =>|, value]* ] => multimap - # - # Creates a new multimap populated with the given objects. - # - # Multimap["a", 100, "b", 200] #=> {"a"=>[100], "b"=>[200]} - # Multimap["a" => 100, "b" => 200] #=> {"a"=>[100], "b"=>[200]} - def self.[](*args) - default = [] - - if args.size == 2 && args.last.is_a?(Hash) - default = args.shift - elsif !args.first.is_a?(Hash) && args.size % 2 == 1 - default = args.shift - end - - if args.size == 1 && args.first.is_a?(Hash) - args[0] = args.first.inject({}) { |hash, (key, value)| - unless value.is_a?(default.class) - value = (default.dup << value) - end - hash[key] = value - hash - } - else - index = 0 - args.map! { |value| - unless index % 2 == 0 || value.is_a?(default.class) - value = (default.dup << value) - end - index += 1 - value - } - end - - map = new - map.instance_variable_set(:@hash, Hash[*args]) - map.default = default - map - end - - # call-seq: - # Multimap.new => multimap - # Multimap.new(default) => multimap - # - # Returns a new, empty multimap. - # - # map = Multimap.new(Set.new) - # h["a"] = 100 - # h["b"] = 200 - # h["a"] #=> [100].to_set - # h["c"] #=> [].to_set - def initialize(default = []) - @hash = Hash.new(default) - end - - def initialize_copy(original) #:nodoc: - @hash = Hash.new(original.default.dup) - original._internal_hash.each_pair do |key, container| - @hash[key] = container.dup - end - end - - def_delegators :@hash, :clear, :default, :default=, :empty?, - :fetch, :has_key?, :key? - - # Retrieves the <i>value</i> object corresponding to the - # <i>*keys</i> object. - def [](key) - @hash[key] - end - - # call-seq: - # map[key] = value => value - # map.store(key, value) => value - # - # Associates the value given by <i>value</i> with the key - # given by <i>key</i>. Unlike a regular hash, multiple can be - # assoicated with the same value. - # - # map = Multimap["a" => 100, "b" => 200] - # map["a"] = 9 - # map["c"] = 4 - # map #=> {"a" => [100, 9], "b" => [200], "c" => [4]} - def store(key, value) - update_container(key) do |container| - container << value - container - end - end - alias_method :[]=, :store - - # call-seq: - # map.delete(key, value) => value - # map.delete(key) => value - # - # Deletes and returns a key-value pair from <i>map</i>. If only - # <i>key</i> is given, all the values matching that key will be - # deleted. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.delete("b", 300) #=> 300 - # map.delete("a") #=> [100] - def delete(key, value = nil) - if value - @hash[key].delete(value) - else - @hash.delete(key) - end - end - - # call-seq: - # map.each { |key, value| block } => map - # - # Calls <i>block</i> for each key/value pair in <i>map</i>, passing - # the key and value to the block as a two-element array. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.each { |key, value| puts "#{key} is #{value}" } - # - # <em>produces:</em> - # - # a is 100 - # b is 200 - # b is 300 - def each - each_pair do |key, value| - yield [key, value] - end - end - - # call-seq: - # map.each_association { |key, container| block } => map - # - # Calls <i>block</i> once for each key/container in <i>map</i>, passing - # the key and container to the block as parameters. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.each_association { |key, container| puts "#{key} is #{container}" } - # - # <em>produces:</em> - # - # a is [100] - # b is [200, 300] - def each_association(&block) - @hash.each_pair(&block) - end - - # call-seq: - # map.each_container { |container| block } => map - # - # Calls <i>block</i> for each container in <i>map</i>, passing the - # container as a parameter. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.each_container { |container| puts container } - # - # <em>produces:</em> - # - # [100] - # [200, 300] - def each_container - each_association do |_, container| - yield container - end - end - - # call-seq: - # map.each_key { |key| block } => map - # - # Calls <i>block</i> for each key in <i>hsh</i>, passing the key - # as a parameter. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.each_key { |key| puts key } - # - # <em>produces:</em> - # - # a - # b - # b - def each_key - each_pair do |key, _| - yield key - end - end - - # call-seq: - # map.each_pair { |key_value_array| block } => map - # - # Calls <i>block</i> for each key/value pair in <i>map</i>, - # passing the key and value as parameters. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.each_pair { |key, value| puts "#{key} is #{value}" } - # - # <em>produces:</em> - # - # a is 100 - # b is 200 - # b is 300 - def each_pair - each_association do |key, values| - values.each do |value| - yield key, value - end - end - end - - # call-seq: - # map.each_value { |value| block } => map - # - # Calls <i>block</i> for each key in <i>map</i>, passing the - # value as a parameter. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.each_value { |value| puts value } - # - # <em>produces:</em> - # - # 100 - # 200 - # 300 - def each_value - each_pair do |_, value| - yield value - end - end - - def ==(other) #:nodoc: - case other - when Multimap - @hash == other._internal_hash - else - @hash == other - end - end - - def eql?(other) #:nodoc: - case other - when Multimap - @hash.eql?(other._internal_hash) - else - @hash.eql?(other) - end - end - - def freeze #:nodoc: - each_container { |container| container.freeze } - default.freeze - super - end - - # call-seq: - # map.has_value?(value) => true or false - # map.value?(value) => true or false - # - # Returns <tt>true</tt> if the given value is present for any key - # in <i>map</i>. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.has_value?(300) #=> true - # map.has_value?(999) #=> false - def has_value?(value) - values.include?(value) - end - alias_method :value?, :has_value? - - # call-seq: - # map.index(value) => key - # - # Returns the key for a given value. If not found, returns - # <tt>nil</tt>. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.index(100) #=> "a" - # map.index(200) #=> "b" - # map.index(999) #=> nil - def index(value) - invert[value] - end - - # call-seq: - # map.delete_if {| key, value | block } -> map - # - # Deletes every key-value pair from <i>map</i> for which <i>block</i> - # evaluates to <code>true</code>. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.delete_if {|key, value| value >= 300 } - # #=> Multimap["a" => 100, "b" => 200] - # - def delete_if - each_association do |key, container| - container.delete_if do |value| - yield [key, value] - end - end - self - end - - # call-seq: - # map.reject {| key, value | block } -> map - # - # Same as <code>Multimap#delete_if</code>, but works on (and returns) a - # copy of the <i>map</i>. Equivalent to - # <code><i>map</i>.dup.delete_if</code>. - # - def reject(&block) - dup.delete_if(&block) - end - - # call-seq: - # map.reject! {| key, value | block } -> map or nil - # - # Equivalent to <code>Multimap#delete_if</code>, but returns - # <code>nil</code> if no changes were made. - # - def reject!(&block) - old_size = size - delete_if(&block) - old_size == size ? nil : self - end - - # call-seq: - # map.replace(other_map) => map - # - # Replaces the contents of <i>map</i> with the contents of - # <i>other_map</i>. - # - # map = Multimap["a" => 100, "b" => 200] - # map.replace({ "c" => 300, "d" => 400 }) - # #=> Multimap["c" => 300, "d" => 400] - def replace(other) - case other - when Array - @hash.replace(self.class[self.default, *other]) - when Hash - @hash.replace(self.class[self.default, other]) - when self.class - @hash.replace(other) - else - raise ArgumentError - end - end - - # call-seq: - # map.invert => multimap - # - # Returns a new multimap created by using <i>map</i>'s values as keys, - # and the keys as values. - # - # map = Multimap["n" => 100, "m" => 100, "d" => [200, 300]] - # map.invert #=> Multimap[100 => ["n", "m"], 200 => "d", 300 => "d"] - def invert - h = self.class.new(default.dup) - each_pair { |key, value| h[value] = key } - h - end - - # call-seq: - # map.keys => multiset - # - # Returns a new +Multiset+ populated with the keys from this hash. See also - # <tt>Multimap#values</tt> and <tt>Multimap#containers</tt>. - # - # map = Multimap["a" => 100, "b" => [200, 300], "c" => 400] - # map.keys #=> Multiset.new(["a", "b", "b", "c"]) - def keys - keys = Multiset.new - each_key { |key| keys << key } - keys - end - - # Returns true if the given key is present in Multimap. - def include?(key) - keys.include?(key) - end - alias_method :member?, :include? - - # call-seq: - # map.length => fixnum - # map.size => fixnum - # - # Returns the number of key-value pairs in the map. - # - # map = Multimap["a" => 100, "b" => [200, 300], "c" => 400] - # map.length #=> 4 - # map.delete("a") #=> 100 - # map.length #=> 3 - def size - values.size - end - alias_method :length, :size - - # call-seq: - # map.merge(other_map) => multimap - # - # Returns a new multimap containing the contents of <i>other_map</i> and - # the contents of <i>map</i>. - # - # map1 = Multimap["a" => 100, "b" => 200] - # map2 = Multimap["a" => 254, "c" => 300] - # map2.merge(map2) #=> Multimap["a" => 100, "b" => [200, 254], "c" => 300] - # map1 #=> Multimap["a" => 100, "b" => 200] - def merge(other) - dup.update(other) - end - - # call-seq: - # map.merge!(other_map) => multimap - # map.update(other_map) => multimap - # - # Adds each pair from <i>other_map</i> to <i>map</i>. - # - # map1 = Multimap["a" => 100, "b" => 200] - # map2 = Multimap["b" => 254, "c" => 300] - # - # map1.merge!(map2) - # #=> Multimap["a" => 100, "b" => [200, 254], "c" => 300] - def update(other) - case other - when self.class - other.each_pair { |key, value| store(key, value) } - when Hash - update(self.class[self.default, other]) - else - raise ArgumentError - end - self - end - alias_method :merge!, :update - - # call-seq: - # map.select { |key, value| block } => multimap - # - # Returns a new Multimap consisting of the pairs for which the - # block returns true. - # - # map = Multimap["a" => 100, "b" => 200, "c" => 300] - # map.select { |k,v| k > "a" } #=> Multimap["b" => 200, "c" => 300] - # map.select { |k,v| v < 200 } #=> Multimap["a" => 100] - def select - inject(self.class.new) { |map, (key, value)| - map[key] = value if yield([key, value]) - map - } - end - - # call-seq: - # map.to_a => array - # - # Converts <i>map</i> to a nested array of [<i>key, - # value</i>] arrays. - # - # map = Multimap["a" => 100, "b" => [200, 300], "c" => 400] - # map.to_a #=> [["a", 100], ["b", 200], ["b", 300], ["c", 400]] - def to_a - ary = [] - each_pair do |key, value| - ary << [key, value] - end - ary - end - - # call-seq: - # map.to_hash => hash - # - # Converts <i>map</i> to a basic hash. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.to_hash #=> { "a" => [100], "b" => [200, 300] } - def to_hash - @hash.dup - end - - # call-seq: - # map.containers => array - # - # Returns a new array populated with the containers from <i>map</i>. See - # also <tt>Multimap#keys</tt> and <tt>Multimap#values</tt>. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.containers #=> [[100], [200, 300]] - def containers - containers = [] - each_container { |container| containers << container } - containers - end - - # call-seq: - # map.values => array - # - # Returns a new array populated with the values from <i>map</i>. See - # also <tt>Multimap#keys</tt> and <tt>Multimap#containers</tt>. - # - # map = Multimap["a" => 100, "b" => [200, 300]] - # map.values #=> [100, 200, 300] - def values - values = [] - each_value { |value| values << value } - values - end - - # Return an array containing the values associated with the given keys. - def values_at(*keys) - @hash.values_at(*keys) - end - - def marshal_dump #:nodoc: - @hash - end - - def marshal_load(hash) #:nodoc: - @hash = hash - end - - def to_yaml(opts = {}) #:nodoc: - YAML::quick_emit(self, opts) do |out| - out.map(taguri, to_yaml_style) do |map| - @hash.each do |k, v| - map.add(k, v) - end - map.add('__default__', @hash.default) - end - end - end - - def yaml_initialize(tag, val) #:nodoc: - default = val.delete('__default__') - @hash = val - @hash.default = default - self - end - - protected - def _internal_hash #:nodoc: - @hash - end - - def update_container(key) #:nodoc: - container = @hash[key] - container = container.dup if container.equal?(default) - container = yield(container) - @hash[key] = container - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multiset.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multiset.rb deleted file mode 100644 index 119bf12646..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/multiset.rb +++ /dev/null @@ -1,185 +0,0 @@ -require 'set' - -# Multiset implements a collection of unordered values and -# allows duplicates. -# -# == Example -# -# require 'multiset' -# s1 = Multiset.new [1, 2] # -> #<Multiset: {1, 2}> -# s1.add(2) # -> #<Multiset: {1, 2, 2}> -# s1.merge([2, 6]) # -> #<Multiset: {1, 2, 2, 2, 3}> -# s1.multiplicity(2) # -> 3 -# s1.multiplicity(3) # -> 1 -class Multiset < Set - def initialize(*args, &block) #:nodoc: - @hash = Hash.new(0) - super - end - - # Returns the number of times an element belongs to the multiset. - def multiplicity(e) - @hash[e] - end - - # Returns the total number of elements in a multiset, including - # repeated memberships - def cardinality - @hash.inject(0) { |s, (e, m)| s += m } - end - alias_method :size, :cardinality - alias_method :length, :cardinality - - # Converts the set to an array. The order of elements is uncertain. - def to_a - inject([]) { |ary, (key, _)| ary << key } - end - - # Returns true if the set is a superset of the given set. - def superset?(set) - set.is_a?(self.class) or raise ArgumentError, "value must be a set" - return false if cardinality < set.cardinality - set.all? { |o| set.multiplicity(o) <= multiplicity(o) } - end - - # Returns true if the set is a proper superset of the given set. - def proper_superset?(set) - set.is_a?(self.class) or raise ArgumentError, "value must be a set" - return false if cardinality <= set.cardinality - set.all? { |o| set.multiplicity(o) <= multiplicity(o) } - end - - # Returns true if the set is a subset of the given set. - def subset?(set) - set.is_a?(self.class) or raise ArgumentError, "value must be a set" - return false if set.cardinality < cardinality - all? { |o| multiplicity(o) <= set.multiplicity(o) } - end - - # Returns true if the set is a proper subset of the given set. - def proper_subset?(set) - set.is_a?(self.class) or raise ArgumentError, "value must be a set" - return false if set.cardinality <= cardinality - all? { |o| multiplicity(o) <= set.multiplicity(o) } - end - - # Calls the given block once for each element in the set, passing - # the element as parameter. Returns an enumerator if no block is - # given. - def each - @hash.each_pair do |key, multiplicity| - multiplicity.times do - yield(key) - end - end - self - end - - # Adds the given object to the set and returns self. Use +merge+ to - # add many elements at once. - def add(o) - @hash[o] ||= 0 - @hash[o] += 1 - self - end - alias << add - - undef :add? - - # Deletes all the identical object from the set and returns self. - # If +n+ is given, it will remove that amount of identical objects - # from the set. Use +subtract+ to delete many different items at - # once. - def delete(o, n = nil) - if n - @hash[o] ||= 0 - @hash[o] -= n if @hash[o] > 0 - @hash.delete(o) if @hash[o] == 0 - else - @hash.delete(o) - end - self - end - - undef :delete? - - # Deletes every element of the set for which block evaluates to - # true, and returns self. - def delete_if - each { |o| delete(o) if yield(o) } - self - end - - # Merges the elements of the given enumerable object to the set and - # returns self. - def merge(enum) - enum.each { |o| add(o) } - self - end - - # Deletes every element that appears in the given enumerable object - # and returns self. - def subtract(enum) - enum.each { |o| delete(o, 1) } - self - end - - # Returns a new set containing elements common to the set and the - # given enumerable object. - def &(enum) - s = dup - n = self.class.new - enum.each { |o| - if s.include?(o) - s.delete(o, 1) - n.add(o) - end - } - n - end - alias intersection & - - # Returns a new set containing elements exclusive between the set - # and the given enumerable object. (set ^ enum) is equivalent to - # ((set | enum) - (set & enum)). - def ^(enum) - n = self.class.new(enum) - each { |o| n.include?(o) ? n.delete(o, 1) : n.add(o) } - n - end - - # Returns true if two sets are equal. Two multisets are equal if - # they have the same cardinalities and each element has the same - # multiplicity in both sets. The equality of each element inside - # the multiset is defined according to Object#eql?. - def eql?(set) - return true if equal?(set) - set = self.class.new(set) unless set.is_a?(self.class) - return false unless cardinality == set.cardinality - superset?(set) && subset?(set) - end - alias_method :==, :eql? - - def marshal_dump #:nodoc: - @hash - end - - def marshal_load(hash) #:nodoc: - @hash = hash - end - - def to_yaml(opts = {}) #:nodoc: - YAML::quick_emit(self, opts) do |out| - out.map(taguri, to_yaml_style) do |map| - @hash.each do |k, v| - map.add(k, v) - end - end - end - end - - def yaml_initialize(tag, val) #:nodoc: - @hash = val - self - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/nested_multimap.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/nested_multimap.rb deleted file mode 100644 index 4eb088b91a..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/multimap/nested_multimap.rb +++ /dev/null @@ -1,158 +0,0 @@ -require 'multimap' - -# NestedMultimap allows values to be assoicated with a nested -# set of keys. -class NestedMultimap < Multimap - # call-seq: - # multimap[*keys] = value => value - # multimap.store(*keys, value) => value - # - # Associates the value given by <i>value</i> with multiple key - # given by <i>keys</i>. - # - # map = NestedMultimap.new - # map["a"] = 100 - # map["a", "b"] = 101 - # map["a"] = 102 - # map #=> {"a"=>{"b"=>[100, 101, 102], default => [100, 102]}} - def store(*args) - keys = args - value = args.pop - - raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value - - if keys.length > 1 - update_container(keys.shift) do |container| - container = self.class.new(container) unless container.is_a?(self.class) - container[*keys] = value - container - end - elsif keys.length == 1 - super(keys.first, value) - else - self << value - end - end - alias_method :[]=, :store - - # call-seq: - # multimap << obj => multimap - # - # Pushes the given object on to the end of all the containers. - # - # map = NestedMultimap["a" => [100], "b" => [200, 300]] - # map << 300 - # map["a"] #=> [100, 300] - # map["c"] #=> [300] - def <<(value) - @hash.each_value { |container| container << value } - self.default << value - self - end - - # call-seq: - # multimap[*keys] => value - # multimap[key1, key2, key3] => value - # - # Retrieves the <i>value</i> object corresponding to the - # <i>*keys</i> object. - def [](*keys) - i, l, r, k = 0, keys.length, self, self.class - while r.is_a?(k) - r = i < l ? r._internal_hash[keys[i]] : r.default - i += 1 - end - r - end - - # call-seq: - # multimap.each_association { |key, container| block } => multimap - # - # Calls <i>block</i> once for each key/container in <i>map</i>, passing - # the key and container to the block as parameters. - # - # map = NestedMultimap.new - # map["a"] = 100 - # map["a", "b"] = 101 - # map["a"] = 102 - # map["c"] = 200 - # map.each_association { |key, container| puts "#{key} is #{container}" } - # - # <em>produces:</em> - # - # ["a", "b"] is [100, 101, 102] - # "c" is [200] - def each_association - super() do |key, container| - if container.respond_to?(:each_association) - container.each_association do |nested_key, value| - yield [key, nested_key].flatten, value - end - else - yield key, container - end - end - end - - # call-seq: - # multimap.each_container_with_default { |container| block } => map - # - # Calls <i>block</i> for every container in <i>map</i> including - # the default, passing the container as a parameter. - # - # map = NestedMultimap.new - # map["a"] = 100 - # map["a", "b"] = 101 - # map["a"] = 102 - # map.each_container_with_default { |container| puts container } - # - # <em>produces:</em> - # - # [100, 101, 102] - # [100, 102] - # [] - def each_container_with_default(&block) - @hash.each_value do |container| - iterate_over_container(container, &block) - end - iterate_over_container(default, &block) - self - end - - # call-seq: - # multimap.containers_with_default => array - # - # Returns a new array populated with all the containers from - # <i>map</i> including the default. - # - # map = NestedMultimap.new - # map["a"] = 100 - # map["a", "b"] = 101 - # map["a"] = 102 - # map.containers_with_default #=> [[100, 101, 102], [100, 102], []] - def containers_with_default - containers = [] - each_container_with_default { |container| containers << container } - containers - end - - def inspect #:nodoc: - super.gsub(/\}$/, ", default => #{default.inspect}}") - end - - private - def iterate_over_container(container) - if container.respond_to?(:each_container_with_default) - container.each_container_with_default do |value| - yield value - end - else - yield container - end - end -end - -begin - require 'nested_multimap_ext' -rescue LoadError -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin.rb deleted file mode 100644 index d38922bcc6..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Regin - autoload :Alternation, 'regin/alternation' - autoload :Anchor, 'regin/anchor' - autoload :Atom, 'regin/atom' - autoload :Character, 'regin/character' - autoload :CharacterClass, 'regin/character_class' - autoload :Collection, 'regin/collection' - autoload :Expression, 'regin/expression' - autoload :Group, 'regin/group' - autoload :Options, 'regin/options' - autoload :Parser, 'regin/parser' - - class << self - begin - eval('foo = /(?<foo>.*)/').named_captures - - # Returns true if the interpreter is using the Oniguruma Regexp lib - # and supports named captures. - # - # /(?<foo>bar)/ - def regexp_supports_named_captures? - true - end - rescue SyntaxError, NoMethodError - def regexp_supports_named_captures? #:nodoc: - false - end - end - - # Parses Regexp and returns a Expression data structure. - def parse(regexp) - Parser.parse_regexp(regexp) - end - - # Recompiles Regexp by parsing it and turning it back into a Regexp. - # - # (In the future Regin will perform some Regexp optimizations - # such as removing unnecessary captures and options) - def compile(source) - regexp = Regexp.compile(source) - expression = parse(regexp) - Regexp.compile(expression.to_s(true), expression.flags) - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/alternation.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/alternation.rb deleted file mode 100644 index ce4f52bfdb..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/alternation.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Regin - class Alternation < Collection - def initialize(*args) - args, options = extract_options(args) - - if args.length == 1 && args.first.instance_of?(Array) - super(args.first) - else - super(args) - end - - if options.key?(:ignorecase) - @array.map! { |e| e.dup(:ignorecase => options[:ignorecase]) } - end - end - - # Returns true if expression could be treated as a literal string. - # - # Alternation groups are never literal. - def literal? - false - end - - def flags - 0 - end - - def dup(options = {}) - self.class.new(to_a, options) - end - - def to_s(parent = false) - map { |e| e.to_s(parent) }.join('|') - end - - def inspect #:nodoc: - to_s.inspect - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/anchor.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/anchor.rb deleted file mode 100644 index 05520dd5e0..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/anchor.rb +++ /dev/null @@ -1,4 +0,0 @@ -module Regin - class Anchor < Atom - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/atom.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/atom.rb deleted file mode 100644 index eb1923a5a1..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/atom.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Regin - class Atom - attr_reader :value, :ignorecase - - def initialize(value, options = {}) - @value = value - @ignorecase = options[:ignorecase] - end - - def option_names - %w( ignorecase ) - end - - # Returns true if expression could be treated as a literal string. - def literal? - false - end - - def casefold? - ignorecase ? true : false - end - - def dup(options = {}) - original_options = option_names.inject({}) do |h, m| - h[m.to_sym] = send(m) - h - end - self.class.new(value, original_options.merge(options)) - end - - def to_s(parent = false) - "#{value}" - end - - def inspect #:nodoc: - "#<#{self.class.to_s.sub('Regin::', '')} #{to_s.inspect}>" - end - - def ==(other) #:nodoc: - case other - when String - other == to_s - else - eql?(other) - end - end - - def eql?(other) #:nodoc: - other.instance_of?(self.class) && - self.value.eql?(other.value) && - (!!self.ignorecase).eql?(!!other.ignorecase) - end - - def freeze #:nodoc: - value.freeze - super - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character.rb deleted file mode 100644 index 12a9199d2a..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Regin - class Character < Atom - attr_reader :quantifier - - def initialize(value, options = {}) - @quantifier = options[:quantifier] - super - end - - def option_names - %w( quantifier ) + super - end - - # Returns true if expression could be treated as a literal string. - # - # A Character is literal is there is no quantifier attached to it. - def literal? - quantifier.nil? && !ignorecase - end - - def to_s(parent = false) - if !parent && ignorecase - "(?i-mx:#{value})#{quantifier}" - else - "#{value}#{quantifier}" - end - end - - def to_regexp(anchored = false) - re = to_s(true) - re = "\\A#{re}\\Z" if anchored - Regexp.compile(re, ignorecase) - end - - def match(char) - to_regexp(true).match(char) - end - - def include?(char) - if ignorecase - value.downcase == char.downcase - else - value == char - end - end - - def eql?(other) #:nodoc: - super && quantifier.eql?(other.quantifier) - end - - def freeze #:nodoc: - quantifier.freeze if quantifier - super - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character_class.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character_class.rb deleted file mode 100644 index caed5ef9d0..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/character_class.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Regin - class CharacterClass < Character - def initialize(value, options = {}) - @negate = options[:negate] - super - end - - def option_names - %w( negate ) + super - end - - attr_reader :negate - - def negated? - negate ? true : false - end - - # Returns true if expression could be treated as a literal string. - # - # A CharacterClass is never literal. - def literal? - false - end - - def bracketed? - value != '.' && value !~ /^\\[dDsSwW]$/ - end - - def to_s(parent = false) - if bracketed? - if !parent && ignorecase - "(?i-mx:[#{negate && '^'}#{value}])#{quantifier}" - else - "[#{negate && '^'}#{value}]#{quantifier}" - end - else - super - end - end - - def include?(char) - re = quantifier ? to_s.sub(/#{Regexp.escape(quantifier)}$/, '') : to_s - Regexp.compile("\\A#{re}\\Z").match(char) - end - - def eql?(other) #:nodoc: - super && negate == other.negate - end - - def freeze #:nodoc: - negate.freeze if negate - super - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/collection.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/collection.rb deleted file mode 100644 index b60353268a..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/collection.rb +++ /dev/null @@ -1,83 +0,0 @@ -module Regin - class Collection - include Enumerable - - def initialize(*args) - @array = Array.new(*args) - end - - def each - @array.each{ |item| yield item } - end - - def [](i) - @array[i] - end - - def length - @array.length - end - alias_method :size, :length - - def first - @array.first - end - - def last - @array.last - end - - def +(other) - ary = other.is_a?(self.class) ? other.internal_array : other - self.class.new(@array + ary) - end - - def to_regexp(anchored = false) - re = to_s(true) - re = "\\A#{re}\\Z" if anchored - Regexp.compile(re, flags) - end - - def match(char) - to_regexp.match(char) - end - - def include?(char) - any? { |e| e.include?(char) } - end - - def ==(other) #:nodoc: - case other - when String - other == to_s - when Array - other == @array - else - eql?(other) - end - end - - def eql?(other) #:nodoc: - other.instance_of?(self.class) && @array.eql?(other.internal_array) - end - - def freeze #:nodoc: - each { |e| e.freeze } - @array.freeze - super - end - - protected - def internal_array #:nodoc: - @array - end - - def extract_options(args) - if args.last.is_a?(Hash) - return args[0..-2], args.last - else - return args, {} - end - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/expression.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/expression.rb deleted file mode 100644 index 18e4965097..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/expression.rb +++ /dev/null @@ -1,126 +0,0 @@ -module Regin - class Expression < Collection - attr_reader :ignorecase, :multiline, :extended - - def initialize(*args) - args, options = extract_options(args) - - @multiline = @ignorecase = @extended = nil - - if args.length == 1 && args.first.instance_of?(Array) - super(args.first) - else - args = args.map { |e| e.instance_of?(String) ? Character.new(e) : e } - super(args) - end - - self.multiline = options[:multiline] if options.key?(:multiline) - self.ignorecase = options[:ignorecase] if options.key?(:ignorecase) - self.extended = options[:extended] if options.key?(:extended) - end - - # Returns true if expression could be treated as a literal string. - # - # A Expression is literal if all its elements are literal. - def literal? - !ignorecase && all? { |e| e.literal? } - end - - def anchored? - anchored_to_start? && anchored_to_end? - end - - def anchored_to_start? - first.is_a?(Anchor) && first == '\A' - end - - def anchored_to_end? - last.is_a?(Anchor) && last == '\Z' - end - - def anchored_to_line? - anchored_to_start_of_line? && anchored_to_end_of_line? - end - - def anchored_to_start_of_line? - anchored_to_start? || (first.is_a?(Anchor) && first == '^') - end - - def anchored_to_end_of_line? - anchored_to_end? || (last.is_a?(Anchor) && last == '$') - end - - def options? - options.any?(true) - end - - def flags - options.to_i - end - - def +(other) - ary = other.is_a?(self.class) ? other.internal_array : other - ary = @array + ary + [options.to_h(true)] - self.class.new(*ary) - end - - def dup(options = {}) - expression = super() - expression.multiline = options[:multiline] if options.key?(:multiline) - expression.ignorecase = options[:ignorecase] if options.key?(:ignorecase) - expression.extended = options[:extended] if options.key?(:extended) - expression - end - - def to_s(parent = false) - if parent || !options? - map { |e| e.to_s(parent) }.join - else - with, without = [], [] - multiline ? (with << 'm') : (without << 'm') - ignorecase ? (with << 'i') : (without << 'i') - extended ? (with << 'x') : (without << 'x') - - with = with.join - without = without.any? ? "-#{without.join}" : '' - - "(?#{with}#{without}:#{map { |e| e.to_s(true) }.join})" - end - end - - def inspect #:nodoc: - "#<Expression #{to_s.inspect}>" - end - - def casefold? - ignorecase - end - - def eql?(other) #:nodoc: - super && - !!self.multiline == !!other.multiline && - !!self.ignorecase == !!other.ignorecase && - !!self.extended == !!other.extended - end - - protected - def options - Options.new(multiline, ignorecase, extended) - end - - def multiline=(multiline) - @multiline = multiline - end - - def ignorecase=(ignorecase) - if @ignorecase.nil? - @array.map! { |e| e.dup(:ignorecase => ignorecase) } - @ignorecase = ignorecase - end - end - - def extended=(extended) - @extended = extended - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/group.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/group.rb deleted file mode 100644 index d682148bd9..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/group.rb +++ /dev/null @@ -1,90 +0,0 @@ -module Regin - class Group - attr_reader :expression, :quantifier, :capture, :index, :name - - def initialize(expression, options = {}) - @quantifier = @index = @name = nil - @capture = true - @expression = expression.dup(options) - - @quantifier = options[:quantifier] if options.key?(:quantifier) - @capture = options[:capture] if options.key?(:capture) - @index = options[:index] if options.key?(:index) - @name = options[:name] if options.key?(:name) - end - - def option_names - %w( quantifier capture index name ) - end - - # Returns true if expression could be treated as a literal string. - # - # A Group is literal if its expression is literal and it has no quantifier. - def literal? - quantifier.nil? && expression.literal? - end - - def to_s(parent = false) - if !expression.options? - "(#{capture ? '' : '?:'}#{expression.to_s(parent)})#{quantifier}" - elsif capture == false - "#{expression.to_s}#{quantifier}" - else - "(#{expression.to_s})#{quantifier}" - end - end - - def to_regexp(anchored = false) - re = to_s - re = "\\A#{re}\\Z" if anchored - Regexp.compile(re) - end - - def dup(options = {}) - original_options = option_names.inject({}) do |h, m| - h[m.to_sym] = send(m) - h - end - self.class.new(expression, original_options.merge(options)) - end - - def inspect #:nodoc: - to_s.inspect - end - - def match(char) - to_regexp.match(char) - end - - def include?(char) - expression.include?(char) - end - - def capture? - capture - end - - def ==(other) #:nodoc: - case other - when String - other == to_s - else - eql?(other) - end - end - - def eql?(other) #:nodoc: - other.is_a?(self.class) && - self.expression == other.expression && - self.quantifier == other.quantifier && - self.capture == other.capture && - self.index == other.index && - self.name == other.name - end - - def freeze #:nodoc: - expression.freeze if expression - super - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/options.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/options.rb deleted file mode 100644 index 03ba29d9a5..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/options.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Regin - class Options - def self.from_int(flags) - multiline = flags & Regexp::MULTILINE != 0 - ignorecase = flags & Regexp::IGNORECASE != 0 - extended = flags & Regexp::EXTENDED != 0 - - new(multiline, ignorecase, extended) - end - - attr_reader :multiline, :ignorecase, :extended - - def initialize(*args) - if args.first.is_a?(Hash) - @multiline = args[0][:multiline] - @ignorecase = args[0][:ignorecase] - @extended = args[0][:extended] - else - @multiline = args[0] - @ignorecase = args[1] - @extended = args[2] - end - end - - def any?(explicit = false) - if explicit - !multiline.nil? || !ignorecase.nil? || !extended.nil? - else - multiline || ignorecase || extended - end - end - - def to_h(explicit = false) - if explicit - options = {} - options[:multiline] = multiline unless multiline.nil? - options[:ignorecase] = ignorecase unless ignorecase.nil? - options[:extended] = extended unless extended.nil? - options - else - { :multiline => multiline, - :ignorecase => ignorecase, - :extended => extended } - end - end - - def to_i - flag = 0 - flag |= Regexp::MULTILINE if multiline - flag |= Regexp::IGNORECASE if ignorecase - flag |= Regexp::EXTENDED if extended - flag - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/parser.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/parser.rb deleted file mode 100644 index 0bb9b87e9c..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/parser.rb +++ /dev/null @@ -1,415 +0,0 @@ -# -# DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.6 -# from Racc grammer file "". -# - -require 'racc/parser.rb' -module Regin - class Parser < Racc::Parser #:nodoc: all - -def self.parse_regexp(regexp) - options = Options.from_int(regexp.options) - - parser = new - parser.options_stack << options.to_h - - expression = parser.scan_str(regexp.source) - expression = expression.dup(options.to_h) if options.any? - expression -end - -attr_accessor :options_stack - -def initialize - @capture_index = 0 - @capture_index_stack = [] - @options_stack = [] -end - -##### State transition tables begin ### - -racc_action_table = [ - 2, 18, 19, 19, 8, 10, 11, 13, 48, 19, - 2, 45, 3, 5, 8, 10, 11, 13, 64, 47, - 2, 55, 3, 5, 8, 10, 11, 13, 29, 19, - 2, 16, 3, 5, 8, 10, 11, 13, 61, 19, - 2, 63, 3, 5, 8, 10, 11, 13, 60, 36, - 2, 34, 3, 5, 8, 10, 11, 13, 28, 49, - 2, nil, 3, 5, 8, 10, 11, 13, nil, nil, - 2, nil, 3, 5, 8, 10, 11, 13, nil, 26, - 42, 43, 3, 5, 37, 38, 40, 21, 44, 37, - 38, 40, 22, 23, 24, 14, nil, 15, 31, 32, - 16, nil, 33, 46, 32, nil, nil, 33, 51, 37, - 38, 40, 58, 37, 38, 40, 37, 38, 40, 37, - 38, 40, 37, 38, 40, 37, 38, 40, 37, 38, - 40 ] - -racc_action_check = [ - 0, 4, 27, 4, 0, 0, 0, 0, 36, 56, - 49, 27, 0, 0, 49, 49, 49, 49, 56, 36, - 43, 48, 49, 49, 43, 43, 43, 43, 15, 53, - 6, 15, 43, 43, 6, 6, 6, 6, 53, 52, - 42, 55, 6, 6, 42, 42, 42, 42, 52, 24, - 35, 18, 42, 42, 35, 35, 35, 35, 14, 39, - 19, nil, 35, 35, 19, 19, 19, 19, nil, nil, - 13, nil, 19, 19, 13, 13, 13, 13, nil, 13, - 26, 26, 13, 13, 54, 54, 54, 9, 26, 26, - 26, 26, 9, 9, 9, 2, nil, 2, 17, 17, - 2, nil, 17, 30, 30, nil, nil, 30, 41, 41, - 41, 41, 50, 50, 50, 50, 44, 44, 44, 59, - 59, 59, 58, 58, 58, 51, 51, 51, 62, 62, - 62 ] - -racc_action_pointer = [ - -3, nil, 91, nil, 1, nil, 27, nil, nil, 75, - nil, nil, nil, 67, 53, 22, nil, 93, 51, 57, - nil, nil, nil, nil, 40, nil, 67, 0, nil, nil, - 98, nil, nil, nil, nil, 47, -1, nil, nil, 46, - nil, 87, 37, 17, 94, nil, nil, nil, 12, 7, - 91, 103, 37, 27, 62, 21, 7, nil, 100, 97, - nil, nil, 106, nil, nil, nil, nil, nil ] - -racc_action_default = [ - -37, -13, -37, -19, -37, -20, -2, -4, -11, -6, - -12, -14, -7, -37, -37, -29, -28, -37, -37, -37, - -3, -23, -21, -22, -37, -5, -37, -37, -8, -29, - -37, -9, -27, -26, 68, -1, -37, -34, -35, -37, - -36, -37, -37, -37, -37, -15, -10, -25, -37, -37, - -37, -37, -37, -37, -37, -37, -37, -33, -37, -37, - -17, -18, -37, -24, -16, -32, -31, -30 ] - -racc_goto_table = [ - 4, 41, 20, 35, 17, 39, 25, nil, nil, nil, - nil, nil, nil, 27, nil, nil, 50, 30, nil, 54, - nil, nil, nil, nil, nil, 57, 59, nil, nil, 62, - nil, 20, nil, 65, 66, nil, nil, 67, nil, nil, - nil, nil, 52, 53, nil, nil, nil, nil, nil, 56 ] - -racc_goto_check = [ - 1, 10, 3, 2, 7, 9, 5, nil, nil, nil, - nil, nil, nil, 1, nil, nil, 10, 7, nil, 10, - nil, nil, nil, nil, nil, 10, 10, nil, nil, 10, - nil, 3, nil, 10, 10, nil, nil, 10, nil, nil, - nil, nil, 1, 1, nil, nil, nil, nil, nil, 1 ] - -racc_goto_pointer = [ - nil, 0, -16, -4, nil, -3, nil, 2, nil, -21, - -25 ] - -racc_goto_default = [ - nil, nil, 6, 7, 9, nil, 12, nil, 1, nil, - nil ] - -racc_reduce_table = [ - 0, 0, :racc_error, - 3, 26, :_reduce_1, - 1, 26, :_reduce_2, - 2, 27, :_reduce_3, - 1, 27, :_reduce_4, - 2, 28, :_reduce_5, - 1, 28, :_reduce_none, - 1, 29, :_reduce_none, - 3, 29, :_reduce_8, - 3, 29, :_reduce_9, - 4, 29, :_reduce_10, - 1, 29, :_reduce_11, - 1, 29, :_reduce_12, - 1, 29, :_reduce_13, - 1, 29, :_reduce_14, - 3, 31, :_reduce_15, - 6, 31, :_reduce_16, - 5, 31, :_reduce_17, - 5, 31, :_reduce_18, - 1, 33, :_reduce_none, - 1, 33, :_reduce_none, - 1, 30, :_reduce_none, - 1, 30, :_reduce_none, - 1, 30, :_reduce_none, - 5, 30, :_reduce_24, - 3, 30, :_reduce_25, - 2, 32, :_reduce_26, - 2, 32, :_reduce_27, - 1, 32, :_reduce_none, - 1, 32, :_reduce_none, - 4, 34, :_reduce_30, - 4, 34, :_reduce_31, - 4, 34, :_reduce_32, - 3, 34, :_reduce_33, - 1, 35, :_reduce_34, - 1, 35, :_reduce_35, - 1, 35, :_reduce_36 ] - -racc_reduce_n = 37 - -racc_shift_n = 68 - -racc_token_table = { - false => 0, - :error => 1, - :BAR => 2, - :LBRACK => 3, - :CTYPE => 4, - :RBRACK => 5, - :NEGATE => 6, - :CCLASS => 7, - :DOT => 8, - :CHAR => 9, - :LPAREN => 10, - :RPAREN => 11, - :QMARK => 12, - :COLON => 13, - :NAME => 14, - :L_ANCHOR => 15, - :R_ANCHOR => 16, - :STAR => 17, - :PLUS => 18, - :LCURLY => 19, - :RCURLY => 20, - :MINUS => 21, - :MULTILINE => 22, - :IGNORECASE => 23, - :EXTENDED => 24 } - -racc_nt_base = 25 - -racc_use_result_var = true - -Racc_arg = [ - racc_action_table, - racc_action_check, - racc_action_default, - racc_action_pointer, - racc_goto_table, - racc_goto_check, - racc_goto_default, - racc_goto_pointer, - racc_nt_base, - racc_reduce_table, - racc_token_table, - racc_shift_n, - racc_reduce_n, - racc_use_result_var ] - -Racc_token_to_s_table = [ - "$end", - "error", - "BAR", - "LBRACK", - "CTYPE", - "RBRACK", - "NEGATE", - "CCLASS", - "DOT", - "CHAR", - "LPAREN", - "RPAREN", - "QMARK", - "COLON", - "NAME", - "L_ANCHOR", - "R_ANCHOR", - "STAR", - "PLUS", - "LCURLY", - "RCURLY", - "MINUS", - "MULTILINE", - "IGNORECASE", - "EXTENDED", - "$start", - "expression", - "subexpression", - "quantified_atom", - "atom", - "quantifier", - "group", - "bracket_expression", - "anchor", - "options", - "modifier" ] - -Racc_debug_parser = false - -##### State transition tables end ##### - -# reduce 0 omitted - -def _reduce_1(val, _values, result) - # TODO remove this conditional by breaking - # it into another production - if val[0][0].is_a?(Regin::Alternation) - alt = val[0][0] + [Expression.new(val[2])] - else - alt = Alternation.new(val[0], Expression.new(val[2])) - end - result = Expression.new(alt) - - result -end - -def _reduce_2(val, _values, result) - result = Expression.new(val[0]) - result -end - -def _reduce_3(val, _values, result) - result = val[0] + [val[1]] - result -end - -def _reduce_4(val, _values, result) - result = [val[0]] - result -end - -def _reduce_5(val, _values, result) - result = val[0].dup(:quantifier => val[1]) - result -end - -# reduce 6 omitted - -# reduce 7 omitted - -def _reduce_8(val, _values, result) - result = CharacterClass.new(val[1]) - result -end - -def _reduce_9(val, _values, result) - result = CharacterClass.new(val[1]) - result -end - -def _reduce_10(val, _values, result) - result = CharacterClass.new(val[2], :negate => true) - result -end - -def _reduce_11(val, _values, result) - result = CharacterClass.new(val[0]) - result -end - -def _reduce_12(val, _values, result) - result = CharacterClass.new('.') - result -end - -def _reduce_13(val, _values, result) - result = Anchor.new(val[0]) - result -end - -def _reduce_14(val, _values, result) - result = Character.new(val[0]) - result -end - -def _reduce_15(val, _values, result) - result = Group.new(val[1], :index => @capture_index_stack.pop) - - result -end - -def _reduce_16(val, _values, result) - result = Group.new(val[4], val[2].merge(:capture => false)) - @options_stack.pop - - result -end - -def _reduce_17(val, _values, result) - result = Group.new(val[3], :capture => false); - - result -end - -def _reduce_18(val, _values, result) - result = Group.new(val[3], :name => val[2], :index => @capture_index_stack.pop); - - result -end - -# reduce 19 omitted - -# reduce 20 omitted - -# reduce 21 omitted - -# reduce 22 omitted - -# reduce 23 omitted - -def _reduce_24(val, _values, result) - result = val.join - result -end - -def _reduce_25(val, _values, result) - result = val.join - result -end - -def _reduce_26(val, _values, result) - result = val.join - result -end - -def _reduce_27(val, _values, result) - result = val.join - result -end - -# reduce 28 omitted - -# reduce 29 omitted - -def _reduce_30(val, _values, result) - @options_stack << result = { val[1] => false, val[2] => false, val[3] => false } - - result -end - -def _reduce_31(val, _values, result) - @options_stack << result = { val[0] => true, val[2] => false, val[3] => false } - - result -end - -def _reduce_32(val, _values, result) - @options_stack << result = { val[0] => true, val[1] => true, val[3] => false } - - result -end - -def _reduce_33(val, _values, result) - @options_stack << result = { val[0] => true, val[1] => true, val[2] => true } - - result -end - -def _reduce_34(val, _values, result) - result = :multiline - result -end - -def _reduce_35(val, _values, result) - result = :ignorecase - result -end - -def _reduce_36(val, _values, result) - result = :extended - result -end - -def _reduce_none(val, _values, result) - val[0] -end - - end # class Parser -end # module Regin - -require 'regin/tokenizer' diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/tokenizer.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/tokenizer.rb deleted file mode 100644 index 59e4ffb611..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/tokenizer.rb +++ /dev/null @@ -1,213 +0,0 @@ -#-- -# DO NOT MODIFY!!!! -# This file is automatically generated by rex 1.0.5.beta1 -# from lexical definition file "lib/regin/tokenizer.rex". -#++ - -require 'racc/parser' -class Regin::Parser < Racc::Parser - require 'strscan' - - class ScanError < StandardError ; end - - attr_reader :lineno - attr_reader :filename - attr_accessor :state - - def scan_setup(str) - @ss = StringScanner.new(str) - @lineno = 1 - @state = nil - end - - def action - yield - end - - def scan_str(str) - scan_setup(str) - do_parse - end - alias :scan :scan_str - - def load_file( filename ) - @filename = filename - open(filename, "r") do |f| - scan_setup(f.read) - end - end - - def scan_file( filename ) - load_file(filename) - do_parse - end - - - def next_token - return if @ss.eos? - - text = @ss.peek(1) - @lineno += 1 if text == "\n" - token = case @state - when nil - case - when (text = @ss.scan(/\\[dDsSwW]/)) - action { [:CCLASS, text] } - - when (text = @ss.scan(/\^|\\A/)) - action { [:L_ANCHOR, text] } - - when (text = @ss.scan(/\$|\\Z/)) - action { [:R_ANCHOR, text] } - - when (text = @ss.scan(/<(\w+)>/)) - action { [:NAME, @ss[1]] } - - when (text = @ss.scan(/\(/)) - action { - @capture_index_stack << @capture_index - @capture_index += 1 - @state = :OPTIONS if @ss.peek(1) == '?'; - [:LPAREN, text] - } - - - when (text = @ss.scan(/\)/)) - action { [:RPAREN, text] } - - when (text = @ss.scan(/\[/)) - action { @state = :CCLASS; [:LBRACK, text] } - - when (text = @ss.scan(/\{/)) - action { [:LCURLY, text] } - - when (text = @ss.scan(/\}/)) - action { [:RCURLY, text] } - - when (text = @ss.scan(/\|/)) - action { [:BAR, text] } - - when (text = @ss.scan(/\./)) - action { [:DOT, text] } - - when (text = @ss.scan(/\?/)) - action { [:QMARK, text] } - - when (text = @ss.scan(/\+(?:\?)/)) - action { [:PLUS, text] } - - when (text = @ss.scan(/\*(?:\?)/)) - action { [:STAR, text] } - - when (text = @ss.scan(/\#/)) - action { - if @options_stack[-1][:extended] - @state = :COMMENT; - next_token - else - [:CHAR, text] - end - } - - - when (text = @ss.scan(/\s|\n/)) - action { - if @options_stack[-1][:extended] - next_token - else - [:CHAR, text] - end - } - - - when (text = @ss.scan(/\\(.)/)) - action { [:CHAR, @ss[1]] } - - when (text = @ss.scan(/./)) - action { [:CHAR, text] } - - else - text = @ss.string[@ss.pos .. -1] - raise ScanError, "can not match: '" + text + "'" - end # if - - when :CCLASS - case - when (text = @ss.scan(/\]/)) - action { @state = nil; [:RBRACK, text] } - - when (text = @ss.scan(/\^/)) - action { [:NEGATE, text] } - - when (text = @ss.scan(/:(alnum|alpha|ascii|blank|cntrl|digit|graph|lower|print|punct|space|upper|word|xdigit):/)) - action { [:CTYPE, text] } - - when (text = @ss.scan(/\\-/)) - action { [:CHAR, text] } - - when (text = @ss.scan(/\\(.)/)) - action { [:CHAR, @ss[1]] } - - when (text = @ss.scan(/./)) - action { [:CHAR, text] } - - else - text = @ss.string[@ss.pos .. -1] - raise ScanError, "can not match: '" + text + "'" - end # if - - when :OPTIONS - case - when (text = @ss.scan(/\?/)) - action { - @state = nil unless @ss.peek(1) =~ /-|m|i|x|:/ - [:QMARK, text] - } - - - when (text = @ss.scan(/\-/)) - action { [:MINUS, text] } - - when (text = @ss.scan(/m/)) - action { [:MULTILINE, text] } - - when (text = @ss.scan(/i/)) - action { [:IGNORECASE, text] } - - when (text = @ss.scan(/x/)) - action { [:EXTENDED, text] } - - when (text = @ss.scan(/\:/)) - action { - @capture_index_stack.pop - @capture_index -= 1 - @state = nil; - [:COLON, text] - } - - - else - text = @ss.string[@ss.pos .. -1] - raise ScanError, "can not match: '" + text + "'" - end # if - - when :COMMENT - case - when (text = @ss.scan(/\n/)) - action { @state = nil; next_token } - - when (text = @ss.scan(/./)) - action { next_token } - - else - text = @ss.string[@ss.pos .. -1] - raise ScanError, "can not match: '" + text + "'" - end # if - - else - raise ScanError, "undefined state: '" + state.to_s + "'" - end # case state - token - end # def next_token - -end # class diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/version.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/version.rb deleted file mode 100644 index 7ad2a5a25e..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/vendor/regin/regin/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Regin - Version = '0.3.3' -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/version.rb b/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/version.rb deleted file mode 100644 index a3688b102e..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-mount-0.6.6.pre/rack/mount/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Rack - module Mount - Version = '0.6.6.pre' - end -end diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 532d060c06..7eaf7d0534 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -3,7 +3,7 @@ module ActionPack MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 9f56cca869..c0d7423682 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -23,6 +23,7 @@ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) + require 'active_support/ruby/shim' require 'active_support/core_ext/class/attribute_accessors' diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index ba3bdd0d18..b7ffa345cc 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -20,7 +20,6 @@ module ActionView #:nodoc: autoload :NumberHelper autoload :PrototypeHelper autoload :RawOutputHelper - autoload :RecordIdentificationHelper autoload :RecordTagHelper autoload :SanitizeHelper autoload :ScriptaculousHelper @@ -51,7 +50,6 @@ module ActionView #:nodoc: include NumberHelper include PrototypeHelper include RawOutputHelper - include RecordIdentificationHelper include RecordTagHelper include SanitizeHelper include ScriptaculousHelper diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index cb5a1404ff..8e7cf2e701 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -10,7 +10,7 @@ module ActionView # Full usage example: # # config/routes.rb: - # Basecamp::Application.routes.draw do |map| + # Basecamp::Application.routes.draw do # resources :posts # root :to => "posts#index" # end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index f9105ca364..89e95e8694 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -165,7 +165,7 @@ module ActionView def with_output_buffer(buf = nil) #:nodoc: unless buf buf = ActionView::OutputBuffer.new - buf.force_encoding(output_buffer.encoding) if output_buffer && buf.respond_to?(:force_encoding) + buf.force_encoding(output_buffer.encoding) if output_buffer.respond_to?(:encoding) && buf.respond_to?(:force_encoding) end self.output_buffer, old_buffer = buf, output_buffer yield diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index f097b9a5a3..8050669adb 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -800,7 +800,8 @@ module ActionView start = options.delete(:start) || 0 stop = options.delete(:end) || 59 step = options.delete(:step) || 1 - leading_zeros = options.delete(:leading_zeros).nil? ? true : false + options.reverse_merge!({:leading_zeros => true}) + leading_zeros = options.delete(:leading_zeros) select_options = [] start.step(stop, step) do |i| diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 6302491c2a..ebe055bebd 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -150,10 +150,6 @@ module ActionView # here a named route directly as well. Defaults to the current action. # * <tt>:html</tt> - Optional HTML attributes for the form tag. # - # Worth noting is that the +form_for+ tag is called in a ERb evaluation - # block, not an ERb output block. So that's <tt><% %></tt>, not - # <tt><%= %></tt>. - # # Also note that +form_for+ doesn't create an exclusive scope. It's still # possible to use both the stand-alone FormHelper methods and methods # from FormTagHelper. For example: @@ -219,7 +215,7 @@ module ActionView # ... # <% end %> # - # If your resource has associations defined, for example, you want to add comments + # If your resource has associations defined, for example, you want to add comments # to the post given that the routes are set correctly: # # <%= form_for([@document, @comment]) do |f| %> @@ -302,12 +298,12 @@ module ActionView object_name = record_or_name_or_array when Array object = record_or_name_or_array.last - object_name = options[:as] || ActionController::RecordIdentifier.singular_class_name(object) + object_name = options[:as] || ActiveModel::Naming.singular(object) apply_form_for_options!(record_or_name_or_array, options) args.unshift object else object = record_or_name_or_array - object_name = options[:as] || ActionController::RecordIdentifier.singular_class_name(object) + object_name = options[:as] || ActiveModel::Naming.singular(object) apply_form_for_options!([object], options) args.unshift object end @@ -533,7 +529,7 @@ module ActionView object = args.first else object = record_or_name_or_array - object_name = ActionController::RecordIdentifier.singular_class_name(object) + object_name = ActiveModel::Naming.singular(object) end builder = options[:builder] || ActionView::Base.default_form_builder @@ -587,8 +583,9 @@ module ActionView # 'Accept <a href="/terms">Terms</a>.' # end def label(object_name, method, content_or_options = nil, options = nil, &block) - if block_given? - options = content_or_options if content_or_options.is_a?(Hash) + content_is_options = content_or_options.is_a?(Hash) + if content_is_options || block_given? + options = content_or_options if content_is_options text = nil else text = content_or_options @@ -676,7 +673,7 @@ module ActionView # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> # def file_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options) + InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options.update({:size => nil})) end # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) @@ -1009,9 +1006,9 @@ module ActionView def value_before_type_cast(object, method_name) unless object.nil? - object.respond_to?(method_name + "_before_type_cast") ? - object.send(method_name + "_before_type_cast") : - object.send(method_name) + object.respond_to?(method_name) ? + object.send(method_name) : + object.send(method_name + "_before_type_cast") end end @@ -1156,11 +1153,11 @@ module ActionView end when Array object = record_or_name_or_array.last - name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" + name = "#{object_name}#{index}[#{ActiveModel::Naming.singular(object)}]" args.unshift(object) else object = record_or_name_or_array - name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" + name = "#{object_name}#{index}[#{ActiveModel::Naming.singular(object)}]" args.unshift(object) end diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 6f9d14de8b..ee34452769 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -447,7 +447,7 @@ module ActionView # wrap the output in an appropriate <tt><select></tt> tag. def grouped_options_for_select(grouped_options, selected_key = nil, prompt = nil) body = '' - body << content_tag(:option, prompt, :value => "") if prompt + body << content_tag(:option, prompt, { :value => "" }, true) if prompt grouped_options = grouped_options.sort if grouped_options.is_a?(Hash) @@ -593,11 +593,11 @@ module ActionView private def add_options(option_tags, options, value = nil) if options[:include_blank] - option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags + option_tags = "<option value=\"\">#{html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags end if value.blank? && options[:prompt] prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select') - option_tags = "<option value=\"\">#{prompt}</option>\n" + option_tags + option_tags = "<option value=\"\">#{html_escape(prompt)}</option>\n" + option_tags end option_tags.html_safe end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 4c1b751160..1ea870426a 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -1,6 +1,5 @@ require 'cgi' require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/object/returning' require 'active_support/core_ext/object/blank' module ActionView @@ -527,7 +526,7 @@ module ActionView private def html_options_for_form(url_for_options, options, *parameters_for_url) - returning options.stringify_keys do |html_options| + options.stringify_keys.tap do |html_options| html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") # The following URL is unescaped, this is just a hash of options, and it is the # responsability of the caller to escape all the values. @@ -539,7 +538,7 @@ module ActionView def extra_tags_for_form(html_options) snowman_tag = tag(:input, :type => "hidden", - :name => "_snowman", :value => "☃".html_safe) + :name => "_e", :value => "☃".html_safe) method = html_options.delete("method").to_s diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 37e5d91d8b..f11027bc93 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -186,14 +186,7 @@ module ActionView # number_with_delimiter(12345678.05, :locale => :fr) # => 12 345 678,05 # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",") # # => 98 765 432,98 - # - # You can still use <tt>number_with_delimiter</tt> with the old API that accepts the - # +delimiter+ as its optional second and the +separator+ as its - # optional third parameter: - # number_with_delimiter(12345678, " ") # => 12 345 678 - # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05 - def number_with_delimiter(number, *args) - options = args.extract_options! + def number_with_delimiter(number, options = {}) options.symbolize_keys! begin @@ -207,14 +200,6 @@ module ActionView end defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - - unless args.empty? - ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' + - 'instead of separate delimiter and precision arguments.', caller) - options[:delimiter] ||= args[0] if args[0] - options[:separator] ||= args[1] if args[1] - end - options = options.reverse_merge(defaults) parts = number.to_s.split('.') @@ -249,13 +234,7 @@ module ActionView # number_with_precision(389.32314, :precision => 4, :significant => true) # => 389.3 # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') # # => 1.111,23 - # - # You can still use <tt>number_with_precision</tt> with the old API that accepts the - # +precision+ as its optional second parameter: - # number_with_precision(111.2345, 2) # => 111.23 - def number_with_precision(number, *args) - - options = args.extract_options! + def number_with_precision(number, options = {}) options.symbolize_keys! number = begin @@ -272,13 +251,6 @@ module ActionView precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], :default => {}) defaults = defaults.merge(precision_defaults) - #Backwards compatibility - unless args.empty? - ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' + - 'instead of a separate precision argument.', caller) - options[:precision] ||= args[0] if args[0] - end - options = options.reverse_merge(defaults) # Allow the user to unset default values: Eg.: :significant => false precision = options.delete :precision significant = options.delete :significant @@ -337,13 +309,7 @@ module ActionView # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" # number_to_human_size(524288000, :precision=>5) # => "500 MB" - # - # You can still use <tt>number_to_human_size</tt> with the old API that accepts the - # +precision+ as its optional second parameter: - # number_to_human_size(1234567, 1) # => 1 MB - # number_to_human_size(483989, 2) # => 470 KB - def number_to_human_size(number, *args) - options = args.extract_options! + def number_to_human_size(number, options = {}) options.symbolize_keys! number = begin @@ -359,13 +325,7 @@ module ActionView defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) defaults = defaults.merge(human) - - unless args.empty? - ActiveSupport::Deprecation.warn('number_to_human_size takes an option hash ' + - 'instead of a separate precision argument.', caller) - options[:precision] ||= args[0] if args[0] - end - + options = options.reverse_merge(defaults) #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 5c0ff5d59c..99f9363a9a 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -102,7 +102,7 @@ module ActionView :form, :with, :update, :script, :type ]).merge(CALLBACKS) # Returns the JavaScript needed for a remote function. - # Takes the same arguments as link_to_remote. + # See the link_to_remote documentation at http://github.com/rails/prototype_legacy_helper as it takes the same arguments. # # Example: # # Generates: <select id="options" onchange="new Ajax.Updater('options', @@ -139,7 +139,7 @@ module ActionView function = "if (#{options[:condition]}) { #{function}; }" if options[:condition] function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm] - return function + return function.html_safe end # All the methods were moved to GeneratorMethods so that diff --git a/actionpack/lib/action_view/helpers/record_identification_helper.rb b/actionpack/lib/action_view/helpers/record_identification_helper.rb deleted file mode 100644 index 372f1cb8aa..0000000000 --- a/actionpack/lib/action_view/helpers/record_identification_helper.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ActionView - # = Action View Record Identification Helpers - # - # See ActionController::RecordIdentifier for documentation on these methods. - module Helpers - module RecordIdentificationHelper - # See ActionController::RecordIdentifier.partial_path -- this is just a delegate to that for convenient access in the view. - def partial_path(*args, &block) - ActionController::RecordIdentifier.partial_path(*args, &block) - end - - # See ActionController::RecordIdentifier.dom_class -- this is just a delegate to that for convenient access in the view. - def dom_class(*args, &block) - ActionController::RecordIdentifier.dom_class(*args, &block) - end - - # See ActionController::RecordIdentifier.dom_id -- this is just a delegate to that for convenient access in the view. - def dom_id(*args, &block) - ActionController::RecordIdentifier.dom_id(*args, &block) - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 7433f08777..e4a9210cde 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -1,7 +1,11 @@ +require 'action_controller/record_identifier' + module ActionView # = Action View Record Tag Helpers module Helpers module RecordTagHelper + include ActionController::RecordIdentifier + # Produces a wrapper DIV element with id and class parameters that # relate to the specified Active Record object. Usage example: # diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index b47818a22a..d82005fa24 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/object/try' require 'action_controller/vendor/html-scanner' require 'action_view/helpers/tag_helper' @@ -7,6 +8,7 @@ module ActionView # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements. # These helper methods extend Action View making them callable within your template files. module SanitizeHelper + extend ActiveSupport::Concern # This +sanitize+ helper will html encode all tags and strip all attributes that # aren't specifically allowed. # @@ -32,13 +34,13 @@ module ActionView # # Add table tags to the default allowed tags # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' # end # # Remove tags to the default allowed tags # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.after_initialize do # ActionView::Base.sanitized_allowed_tags.delete 'div' # end @@ -46,7 +48,7 @@ module ActionView # # Change allowed default attributes # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style' # end # @@ -143,7 +145,7 @@ module ActionView # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with # any object that responds to +sanitize+. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.full_sanitizer = MySpecialSanitizer.new # end # @@ -154,7 +156,7 @@ module ActionView # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with # any object that responds to +sanitize+. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.link_sanitizer = MySpecialSanitizer.new # end # @@ -165,7 +167,7 @@ module ActionView # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+. # Replace with any object that responds to +sanitize+. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.white_list_sanitizer = MySpecialSanitizer.new # end # @@ -175,7 +177,7 @@ module ActionView # Adds valid HTML attributes that the +sanitize+ helper checks for URIs. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target' # end # @@ -185,7 +187,7 @@ module ActionView # Adds to the Set of 'bad' tags for the +sanitize+ helper. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_bad_tags = 'embed', 'object' # end # @@ -195,7 +197,7 @@ module ActionView # Adds to the Set of allowed tags for the +sanitize+ helper. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' # end # @@ -205,7 +207,7 @@ module ActionView # Adds to the Set of allowed HTML attributes for the +sanitize+ helper. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc' # end # @@ -215,7 +217,7 @@ module ActionView # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_css_properties = 'expression' # end # @@ -225,7 +227,7 @@ module ActionView # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_css_keywords = 'expression' # end # @@ -235,7 +237,7 @@ module ActionView # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_shorthand_css_properties = 'expression' # end # @@ -245,7 +247,7 @@ module ActionView # Adds to the Set of allowed protocols for the +sanitize+ helper. # - # Rails::Initializer.run do |config| + # class Application < Rails::Application # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed' # end # diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 0be8a2c36e..c1de5c8cb3 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -10,6 +10,9 @@ module ActionView # your views. These helper methods extend Action View making them callable # within your template files. module TextHelper + extend ActiveSupport::Concern + + include SanitizeHelper # The preferred method of outputting text in your views is to use the # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods # do not operate as expected in an eRuby code block. If you absolutely must @@ -61,27 +64,8 @@ module ActionView # # truncate("<p>Once upon a time in a world far far away</p>") # # => "<p>Once upon a time in a wo..." - # - # You can still use <tt>truncate</tt> with the old API that accepts the - # +length+ as its optional second and the +ellipsis+ as its - # optional third parameter: - # truncate("Once upon a time in a world far far away", 14) - # # => "Once upon a..." - # - # truncate("And they found that many people were sleeping better.", 25, "... (continued)") - # # => "And they f... (continued)" - def truncate(text, *args) - options = args.extract_options! - unless args.empty? - ActiveSupport::Deprecation.warn('truncate takes an option hash instead of separate ' + - 'length and omission arguments', caller) - - options[:length] = args[0] || 30 - options[:omission] = args[1] || "..." - end - + def truncate(text, options = {}) options.reverse_merge!(:length => 30) - text.truncate(options.delete(:length), options) if text end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 0e3cc54fd1..a5c6718c58 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -95,7 +95,7 @@ module ActionView when String options when Hash - options = { :only_path => options[:host].nil? }.update(options.symbolize_keys) + options = options.symbolize_keys.reverse_merge!(:only_path => options[:host].nil?) super when :back controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' @@ -398,7 +398,7 @@ module ActionView def link_to_unless(condition, name, options = {}, html_options = {}, &block) if condition if block_given? - block.arity <= 1 ? yield(name) : yield(name, options, html_options) + block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block) else name end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 56aebca957..137281e5e9 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -37,7 +37,7 @@ module ActionView include ActionController::TemplateAssertions include ActionView::Context - include ActionController::PolymorphicRoutes + include ActionDispatch::Routing::PolymorphicRoutes include ActionController::RecordIdentifier include AbstractController::Helpers diff --git a/actionpack/test/abstract/abstract_controller_test.rb b/actionpack/test/abstract/abstract_controller_test.rb index 3b5013a47a..19855490b4 100644 --- a/actionpack/test/abstract/abstract_controller_test.rb +++ b/actionpack/test/abstract/abstract_controller_test.rb @@ -250,5 +250,19 @@ module AbstractController end end + class Me6 < AbstractController::Base + self.action_methods + + def index + end + end + + class TestActionMethodsReloading < ActiveSupport::TestCase + + test "action_methods should be reloaded after defining a new method" do + assert_equal ["index"], Me6.action_methods + end + end + end end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 765e111226..53cdd358b4 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -482,21 +482,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_redirected_to :controller => 'admin/user' end - def test_assert_valid - get :get_valid_record - assert_deprecated { assert_valid assigns('record') } - end - - def test_assert_valid_failing - get :get_invalid_record - - begin - assert_deprecated { assert_valid assigns('record') } - assert false - rescue ActiveSupport::TestCase::Assertion => e - end - end - def test_assert_response_uses_exception_message @controller = AssertResponseWithUnexpectedErrorController.new get :index diff --git a/actionpack/test/controller/record_identifier_test.rb b/actionpack/test/controller/record_identifier_test.rb index 6a84475758..835a0e970b 100644 --- a/actionpack/test/controller/record_identifier_test.rb +++ b/actionpack/test/controller/record_identifier_test.rb @@ -26,20 +26,6 @@ class Sheep end end -class Comment::Nested < Comment; end - -class Test::Unit::TestCase - protected - def comments_url - 'http://www.example.com/comments' - end - - def comment_url(comment) - "http://www.example.com/comments/#{comment.id}" - end -end - - class RecordIdentifierTest < Test::Unit::TestCase include ActionController::RecordIdentifier @@ -76,30 +62,4 @@ class RecordIdentifierTest < Test::Unit::TestCase def test_dom_class_with_prefix assert_equal "custom_prefix_#{@singular}", dom_class(@record, :custom_prefix) end - - def test_singular_class_name - assert_equal @singular, singular_class_name(@record) - end - - def test_singular_class_name_for_class - assert_equal @singular, singular_class_name(@klass) - end - - def test_plural_class_name - assert_equal @plural, plural_class_name(@record) - end - - def test_plural_class_name_for_class - assert_equal @plural, plural_class_name(@klass) - end - - def test_uncountable - assert_equal true, uncountable?(@uncountable) - assert_equal false, uncountable?(@klass) - end - - private - def method_missing(method, *args) - RecordIdentifier.send(method, *args) - end end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 0f64b77647..a24de62b19 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -79,6 +79,14 @@ class RescueController < ActionController::Base render :text => 'no way' end + rescue_from ActionView::TemplateError do + render :text => 'action_view templater error' + end + + rescue_from IOError do + render :text => 'io error' + end + before_filter(:only => :before_filter_raises) { raise 'umm nice' } def before_filter_raises @@ -141,6 +149,14 @@ class RescueController < ActionController::Base def missing_template end + + def io_error_in_view + raise ActionView::TemplateError.new(nil, {}, IOError.new('this is io error')) + end + + def zero_division_error_in_view + raise ActionView::TemplateError.new(nil, {}, ZeroDivisionError.new('this is zero division error')) + end protected def deny_access @@ -228,6 +244,17 @@ class ControllerInheritanceRescueControllerTest < ActionController::TestCase end class RescueControllerTest < ActionController::TestCase + + def test_io_error_in_view + get :io_error_in_view + assert_equal 'io error', @response.body + end + + def test_zero_division_error_in_view + get :zero_division_error_in_view + assert_equal 'action_view templater error', @response.body + end + def test_rescue_handler get :not_authorized assert_response :forbidden diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index a9d1c55c05..6c8f470fba 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/object/try' require 'abstract_unit' class ResourcesController < ActionController::Base diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index f9fc7a0976..13c9d9ee38 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'controller/fake_controllers' +require 'active_support/ordered_hash' class TestTest < ActionController::TestCase class TestController < ActionController::Base @@ -137,14 +138,14 @@ XML end def test_raw_post_handling - params = {:page => {:name => 'page name'}, 'some key' => 123} + params = ActiveSupport::OrderedHash[:page, {:name => 'page name'}, 'some key', 123] post :render_raw_post, params.dup assert_equal params.to_query, @response.body end def test_body_stream - params = { :page => { :name => 'page name' }, 'some key' => 123 } + params = ActiveSupport::OrderedHash[:page, { :name => 'page name' }, 'some key', 123] post :render_body, params.dup @@ -461,6 +462,13 @@ XML def test_assert_routing_in_module assert_routing 'admin/user', :controller => 'admin/user', :action => 'index' end + + def test_assert_routing_with_glob + with_routing do |set| + set.draw { |map| match('*path' => "pages#show") } + assert_routing('/company/about', { :controller => 'pages', :action => 'show', :path => 'company/about' }) + end + end def test_params_passing get :test_params, :page => {:name => "Page name", :month => '4', :year => '2004', :day => '6'} diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 170c5b8565..6a1a4f556f 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -66,6 +66,16 @@ class MiddlewareStackTest < ActiveSupport::TestCase assert_equal BazMiddleware, @stack[0].klass end + test "raise an error on invalid index" do + assert_raise RuntimeError do + @stack.insert("HiyaMiddleware", BazMiddleware) + end + + assert_raise RuntimeError do + @stack.insert_after("HiyaMiddleware", BazMiddleware) + end + end + test "lazy evaluates middleware class" do assert_difference "@stack.size" do @stack.use "MiddlewareStackTest::BazMiddleware" diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index e5ee412021..c8947aac80 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -392,19 +392,19 @@ class RequestTest < ActiveSupport::TestCase [{'baz'=>[{'foo'=>'baz'}, "1"]}, {'baz'=>[{'foo'=>'[FILTERED]'}, "1"]}, [/foo/]]] test_hashes.each do |before_filter, after_filter, filter_words| - request = stub_request('action_dispatch.parameter_filter' => filter_words) - assert_equal after_filter, request.send(:process_parameter_filter, before_filter) + parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) + assert_equal after_filter, parameter_filter.filter(before_filter) filter_words << 'blah' filter_words << lambda { |key, value| value.reverse! if key =~ /bargain/ } - request = stub_request('action_dispatch.parameter_filter' => filter_words) + parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) before_filter['barg'] = {'bargain'=>'gain', 'blah'=>'bar', 'bar'=>{'bargain'=>{'blah'=>'foo'}}} after_filter['barg'] = {'bargain'=>'niag', 'blah'=>'[FILTERED]', 'bar'=>{'bargain'=>{'blah'=>'[FILTERED]'}}} - assert_equal after_filter, request.send(:process_parameter_filter, before_filter) + assert_equal after_filter, parameter_filter.filter(before_filter) end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 2a014bf976..3f090b7254 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1,3 +1,4 @@ +require 'erb' require 'abstract_unit' require 'controller/fake_controllers' @@ -56,7 +57,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match 'account/proc/:name', :to => redirect {|params| "/#{params[:name].pluralize}" } match 'account/proc_req' => redirect {|params, req| "/#{req.method}" } - match 'account/google' => redirect('http://www.google.com/') + match 'account/google' => redirect('http://www.google.com/', :status => 302) match 'openid/login', :via => [:get, :post], :to => "openid#login" @@ -245,7 +246,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest namespace :account do match 'shorthand' - match 'description', :to => "description", :as => "description" + match 'description', :to => :description, :as => "description" + match ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback resource :subscription, :credit, :credit_card root :to => "account#index" @@ -500,9 +502,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_login_redirect with_test_routes do get '/account/login' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/login', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com/login' end end @@ -510,18 +510,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest with_test_routes do assert_equal '/account/logout', logout_redirect_path get '/account/logout' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/logout', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com/logout' end end def test_namespace_redirect with_test_routes do get '/private' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/private/index', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com/private/index' end end @@ -585,27 +581,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_redirect_modulo with_test_routes do get '/account/modulo/name' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/names', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com/names' end end def test_redirect_proc with_test_routes do get '/account/proc/person' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/people', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com/people' end end def test_redirect_proc_with_request with_test_routes do get '/account/proc_req' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/GET', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com/GET' end end @@ -1159,7 +1149,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_convention_match_with_no_scope + def test_match_shorthand_with_no_scope with_test_routes do assert_equal '/account/overview', account_overview_path get '/account/overview' @@ -1167,7 +1157,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_convention_match_inside_namespace + def test_match_shorthand_inside_namespace with_test_routes do assert_equal '/account/shorthand', account_shorthand_path get '/account/shorthand' @@ -1175,6 +1165,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_scoped_controller_with_namespace_and_action + with_test_routes do + assert_equal '/account/twitter/callback', account_callback_path("twitter") + get '/account/twitter/callback' + assert_equal 'account/callbacks#twitter', @response.body + + get '/account/whatever/callback' + assert_equal 'Not Found', @response.body + end + end + def test_convention_match_nested_and_with_leading_slash with_test_routes do assert_equal '/account/nested/overview', account_nested_overview_path @@ -1191,12 +1192,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_redirect_with_complete_url + def test_redirect_with_complete_url_and_status with_test_routes do get '/account/google' - assert_equal 301, @response.status - assert_equal 'http://www.google.com/', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.google.com/', 302 end end @@ -1204,9 +1203,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest previous_host, self.host = self.host, 'www.example.com:3000' with_test_routes do get '/account/login' - assert_equal 301, @response.status - assert_equal 'http://www.example.com:3000/login', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + verify_redirect 'http://www.example.com:3000/login' end ensure self.host = previous_host @@ -1887,8 +1884,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - private - def with_test_routes - yield - end +private + def with_test_routes + yield + end + + def verify_redirect(url, status=301) + assert_equal status, @response.status + assert_equal url, @response.headers['Location'] + assert_equal expected_redirect_body(url), @response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{ERB::Util.h(url)}">redirected</a>.</body></html>) + end end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index f0e01bfff0..3864821ef0 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -44,7 +44,12 @@ class CookieStoreTest < ActionController::IntegrationTest session[:foo] = 'bye!' * 1024 head :ok end - + + def change_session_id + request.session_options[:id] = nil + get_session_id + end + def rescue_action(e) raise end end @@ -212,6 +217,19 @@ class CookieStoreTest < ActionController::IntegrationTest end end + def test_setting_session_id_to_nil_is_respected + with_test_route_set do + cookies[SessionKey] = SignedBar + + get "/get_session_id" + sid = response.body + assert_equal sid.size, 36 + + get "/change_session_id" + assert_not_equal sid, response.body + end + end + def test_session_store_with_expire_after with_test_route_set(:expire_after => 5.hours) do # First request accesses the session diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb index 9f3d68a639..f7c42c7f22 100644 --- a/actionpack/test/template/capture_helper_test.rb +++ b/actionpack/test/template/capture_helper_test.rb @@ -114,7 +114,7 @@ class CaptureHelperTest < ActionView::TestCase end def view_with_controller - returning(TestController.new.view_context) do |view| + TestController.new.view_context.tap do |view| view.output_buffer = ActionView::OutputBuffer.new end end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 4b9e41803f..be66710ae5 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -4,6 +4,16 @@ require 'controller/fake_models' class FormHelperTest < ActionView::TestCase tests ActionView::Helpers::FormHelper + class Developer + def name_before_type_cast + "David" + end + + def name + "Santiago" + end + end + def form_for(*) @output_buffer = super end @@ -110,6 +120,13 @@ class FormHelperTest < ActionView::TestCase I18n.locale = old_locale end + def test_label_with_locales_and_options + old_locale, I18n.locale = I18n.locale, :label + assert_dom_equal('<label for="post_body" class="post_body">Write entire text here</label>', label(:post, :body, :class => 'post_body')) + ensure + I18n.locale = old_locale + end + def test_label_with_for_attribute_as_symbol assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for")) end @@ -200,6 +217,11 @@ class FormHelperTest < ActionView::TestCase assert_equal object_name, "post[]" end + def test_file_field_has_no_size + expected = '<input id="user_avatar" name="user[avatar]" type="file" />' + assert_dom_equal expected, file_field("user", "avatar") + end + def test_hidden_field assert_dom_equal '<input id="post_title" name="post[title]" type="hidden" value="Hello World" />', hidden_field("post", "title") @@ -228,6 +250,13 @@ class FormHelperTest < ActionView::TestCase text_field("user", "email", :type => "email") end + def test_text_field_from_a_user_defined_method + @developer = Developer.new + assert_dom_equal( + '<input id="developer_name" name="developer[name]" size="30" type="text" value="Santiago" />', text_field("developer", "name") + ) + end + def test_check_box assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', @@ -598,7 +627,7 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_symbol_object_name form_for(@post, :as => "other_name", :html => { :id => 'create-post' }) do |f| - concat f.label(:title) + concat f.label(:title, :class => 'post_title') concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) @@ -606,7 +635,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "other_name_edit", :method => "put") do - "<label for='other_name_title'>Title</label>" + + "<label for='other_name_title' class='post_title'>Title</label>" + "<input name='other_name[title]' size='30' id='other_name_title' value='Hello World' type='text' />" + "<textarea name='other_name[body]' id='other_name_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='other_name[secret]' value='0' type='hidden' />" + @@ -1484,7 +1513,7 @@ class FormHelperTest < ActionView::TestCase def snowman(method = nil) txt = %{<div style="margin:0;padding:0;display:inline">} - txt << %{<input name="_snowman" type="hidden" value="☃" />} + txt << %{<input name="_e" type="hidden" value="☃" />} txt << %{<input name="_method" type="hidden" value="#{method}" />} if method txt << %{</div>} end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 65b5f5ccc1..d14e5020c7 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -210,6 +210,12 @@ class FormOptionsHelperTest < ActionView::TestCase assert grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]]).html_safe? end + def test_grouped_options_for_select_with_prompt_returns_html_escaped_string + assert_dom_equal( + "<option value=\"\"><Choose One></option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, '<Choose One>')) + end + def test_optgroups_with_with_options_with_hash assert_dom_equal( "<optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup><optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup>", @@ -367,6 +373,15 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_blank_as_string_escaped + @post = Post.new + @post.category = "<mus>" + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"><None></option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :include_blank => '<None>') + ) + end + def test_select_with_default_prompt @post = Post.new @post.category = "" @@ -394,6 +409,14 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_given_prompt_escaped + @post = Post.new + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"><The prompt></option>\n<option value=\"abe\">abe</option>\n<option value=\"<mus>\"><mus></option>\n<option value=\"hest\">hest</option></select>", + select("post", "category", %w( abe <mus> hest), :prompt => '<The prompt>') + ) + end + def test_select_with_prompt_and_blank @post = Post.new @post.category = "" diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 8a0f352bc0..6c85952d40 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -12,7 +12,7 @@ class FormTagHelperTest < ActionView::TestCase method = options[:method] txt = %{<div style="margin:0;padding:0;display:inline">} - txt << %{<input name="_snowman" type="hidden" value="☃" />} + txt << %{<input name="_e" type="hidden" value="☃" />} txt << %{<input name="_method" type="hidden" value="#{method}" />} if method txt << %{</div>} end diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb index c5c2a6b952..a8ca19931b 100644 --- a/actionpack/test/template/javascript_helper_test.rb +++ b/actionpack/test/template/javascript_helper_test.rb @@ -89,6 +89,15 @@ class JavaScriptHelperTest < ActionView::TestCase link_to_function("Greeting", "alert('Hello world!')", :href => 'http://example.com/') end + def test_link_to_function_with_inner_block_does_not_raise_exception + html = link_to_function( "Greet me!" ) do |page| + page.replace_html 'header', (content_tag :h1 do + 'Greetings' + end) + end + assert_dom_equal %(<a href="#" onclick="Element.update("header", "\\u003Ch1\\u003EGreetings\\u003C/h1\\u003E");; return false;">Greet me!</a>), html + end + def test_javascript_tag self.output_buffer = 'foo' diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index 14e81fc9dc..7f787b7b00 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -82,16 +82,6 @@ class NumberHelperTest < ActionView::TestCase assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :delimiter => '.', :separator => ',') end - def test_number_with_delimiter_old_api - silence_deprecation_warnings - assert_equal '12 345 678', number_with_delimiter(12345678, " ") - assert_equal '12-345-678.05', number_with_delimiter(12345678.05, '-') - assert_equal '12.345.678,05', number_with_delimiter(12345678.05, '.', ',') - assert_equal '12,345,678.05', number_with_delimiter(12345678.05, ',', '.') - assert_equal '12 345 678-05', number_with_delimiter(12345678.05, ',', '.', :delimiter => ' ', :separator => '-') - restore_deprecation_warnings - end - def test_number_with_precision assert_equal("111.235", number_with_precision(111.2346)) assert_equal("31.83", number_with_precision(31.825, :precision => 2)) @@ -146,15 +136,6 @@ class NumberHelperTest < ActionView::TestCase assert_equal "12", number_with_precision("12.3", :precision => 0, :significant => true ) end - def test_number_with_precision_old_api - silence_deprecation_warnings - assert_equal("31.8250", number_with_precision(31.825, 4)) - assert_equal("111.235", number_with_precision(111.2346, 3)) - assert_equal("111.00", number_with_precision(111, 2)) - assert_equal("111.000", number_with_precision(111, 2, :precision =>3)) - restore_deprecation_warnings - end - def test_number_to_human_size assert_equal '0 Bytes', number_to_human_size(0) assert_equal '1 Byte', number_to_human_size(1) @@ -202,14 +183,6 @@ class NumberHelperTest < ActionView::TestCase assert_equal '1.000,1 TB', number_to_human_size(terabytes(1000.1), :precision => 5, :delimiter => '.', :separator => ',') end - def test_number_to_human_size_old_api - silence_deprecation_warnings - assert_equal '1.3143 KB', number_to_human_size(kilobytes(1.3143), 4, :significant => false) - assert_equal '10.45 KB', number_to_human_size(kilobytes(10.453), 4) - assert_equal '10 KB', number_to_human_size(kilobytes(10.453), 4, :precision => 2) - restore_deprecation_warnings - end - def test_number_to_human assert_equal '123', number_to_human(123) assert_equal '1.23 Thousand', number_to_human(1234) diff --git a/actionpack/test/template/prototype_helper_test.rb b/actionpack/test/template/prototype_helper_test.rb index 0ff37f44c2..036a44730c 100644 --- a/actionpack/test/template/prototype_helper_test.rb +++ b/actionpack/test/template/prototype_helper_test.rb @@ -104,6 +104,12 @@ class PrototypeHelperTest < PrototypeHelperBaseTest assert_equal javascript_tag(create_generator(&block).to_s, {:defer => 'true'}), update_page_tag({:defer => 'true'}, &block) end + def test_remote_function + res = remote_function(:url => authors_path, :with => "'author[name]='+$F('author_name')+'&author[dob]='+$F('author_dob')") + assert_equal "new Ajax.Request('/authors', {asynchronous:true, evalScripts:true, parameters:'author[name]='+$F('author_name')+'&author[dob]='+$F('author_dob')})", res + assert res.html_safe? + end + protected def author_path(record) "/authors/#{record.id}" diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 048f96c9a9..d59bbec4a9 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -1,6 +1,6 @@ # encoding: utf-8 require 'abstract_unit' -require 'active_support/ordered_options' +require 'active_support/ordered_hash' require 'controller/fake_controllers' class UrlHelperTest < ActiveSupport::TestCase @@ -31,17 +31,13 @@ class UrlHelperTest < ActiveSupport::TestCase {} end - def abcd(hash = {}) - hash_for(:a => :b, :c => :d).merge(hash) - end - - def hash_for(opts = {}) - {:controller => "foo", :action => "bar"}.merge(opts) + def hash_for(opts = []) + ActiveSupport::OrderedHash[*([:controller, "foo", :action, "bar"].concat(opts))] end alias url_hash hash_for def test_url_for_does_not_escape_urls - assert_equal "/?a=b&c=d", url_for(abcd) + assert_equal "/?a=b&c=d", url_for(hash_for([:a, :b, :c, :d])) end def test_url_for_with_back @@ -128,7 +124,7 @@ class UrlHelperTest < ActiveSupport::TestCase end def test_link_tag_with_host_option - hash = hash_for(:host => "www.example.com") + hash = hash_for([:host, "www.example.com"]) expected = %q{<a href="http://www.example.com/">Test Link</a>} assert_dom_equal(expected, link_to('Test Link', hash)) end @@ -294,7 +290,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_current_page_with_params_that_match @request = request_for_url("/?order=desc&page=1") - assert current_page?(hash_for(:order => "desc", :page => "1")) + assert current_page?(hash_for([:order, "desc", :page, "1"])) assert current_page?("http://www.example.com/?order=desc&page=1") end @@ -316,20 +312,20 @@ class UrlHelperTest < ActiveSupport::TestCase @request = request_for_url("/?order=desc&page=1") assert_equal "Showing", - link_to_unless_current("Showing", hash_for(:order=>'desc', :page=>'1')) + link_to_unless_current("Showing", hash_for([:order, 'desc', :page, '1'])) assert_equal "Showing", link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=1") @request = request_for_url("/?order=desc") assert_equal %{<a href="/?order=asc">Showing</a>}, - link_to_unless_current("Showing", hash_for(:order => :asc)) + link_to_unless_current("Showing", hash_for([:order, :asc])) assert_equal %{<a href="http://www.example.com/?order=asc">Showing</a>}, link_to_unless_current("Showing", "http://www.example.com/?order=asc") @request = request_for_url("/?order=desc") assert_equal %{<a href="/?order=desc&page=2\">Showing</a>}, - link_to_unless_current("Showing", hash_for(:order => "desc", :page => 2)) + link_to_unless_current("Showing", hash_for([:order, "desc", :page, 2])) assert_equal %{<a href="http://www.example.com/?order=desc&page=2">Showing</a>}, link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=2") diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index a5e7f300d9..8374853231 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis] + + *Rails 3.0.0 [beta 4] (June 8th, 2010)* * JSON supports a custom root option: to_json(:root => 'custom') #4515 [Jatinder Singh] diff --git a/activemodel/README b/activemodel/README.rdoc index 6f162ef408..89cacbcab4 100644 --- a/activemodel/README +++ b/activemodel/README.rdoc @@ -1,21 +1,21 @@ -= Active Model - defined interfaces for Rails - -Prior to Rails 3.0, if a plugin or gem developer wanted to be able to have -an object interact with Action Pack helpers, it was required to either -copy chunks of code from Rails, or monkey patch entire helpers to make them -handle objects that did not look like Active Record. This generated code -duplication and fragile applications that broke on upgrades. - -Active Model is a solution for this problem. - -Active Model provides a known set of interfaces that your objects can implement -to then present a common interface to the Action Pack helpers. You can include -functionality from the following modules: - -* Adding attribute magic to your objects - - Add prefixes and suffixes to defined attribute methods... - += Active Model -- model interfaces for Rails + +Active Model provides a known set of interfaces for usage in model classes. +They allow for Action Pack helpers to interact with non-ActiveRecord models, +for example. Active Model also helps building custom ORMs for use outside of +the Rails framework. + +Prior to Rails 3.0, if a plugin or gem developer wanted to have an object +interact with Action Pack helpers, it was required to either copy chunks of +code from Rails, or monkey patch entire helpers to make them handle objects +that did not exacly conform to the Active Record interface. This would result +in code duplication and fragile applications that broke on upgrades. + +Active Model solves this. You can include functionality from the following +modules: + +* Add attribute magic to objects + class Person include ActiveModel::AttributeMethods @@ -23,17 +23,18 @@ functionality from the following modules: define_attribute_methods [:name, :age] attr_accessor :name, :age - + def clear_attribute(attr) send("#{attr}=", nil) end end - ...gives you clear_name, clear_age. + person.clear_name + person.clear_age {Learn more}[link:classes/ActiveModel/AttributeMethods.html] -* Adding callbacks to your objects +* Callbacks for certain operations class Person extend ActiveModel::Callbacks @@ -45,26 +46,16 @@ functionality from the following modules: end end end - - ...gives you before_create, around_create and after_create class methods that - wrap your create method. - + + This generates +before_create+, +around_create+ and +after_create+ + class methods that wrap your create method. + {Learn more}[link:classes/ActiveModel/CallBacks.html] -* For classes that already look like an Active Record object +* Tracking value changes - class Person - include ActiveModel::Conversion - end - - ...returns the class itself when sent :to_model - - {Learn more}[link:classes/ActiveModel/Conversion.html] + The ActiveModel::Dirty module allows for tracking attribute changes: -* Tracking changes in your object - - Provides all the value tracking features implemented by ActiveRecord... - person = Person.new person.name # => nil person.changed? # => false @@ -75,14 +66,14 @@ functionality from the following modules: person.name = 'robert' person.save person.previous_changes # => {'name' => ['bob, 'robert']} - + {Learn more}[link:classes/ActiveModel/Dirty.html] -* Adding +errors+ support to your object +* Adding +errors+ interface to objects - Provides the error messages to allow your object to interact with Action Pack - helpers seamlessly... - + Exposing error messages allows objects to interact with Action Pack + helpers seamlessly. + class Person def initialize @@ -102,51 +93,38 @@ functionality from the following modules: end - ... gives you... - person.errors.full_messages # => ["Name Can not be nil"] + person.errors.full_messages # => ["Name Can not be nil"] {Learn more}[link:classes/ActiveModel/Errors.html] -* Testing the compliance of your object +* Model name introspection - Use ActiveModel::Lint to test the compliance of your object to the - basic ActiveModel API... - - {Learn more}[link:classes/ActiveModel/Lint/Tests.html] - -* Providing a human face to your object - - ActiveModel::Naming provides your model with the model_name convention - and a human_name attribute... - class NamedPerson extend ActiveModel::Naming end - ...gives you... - NamedPerson.model_name #=> "NamedPerson" NamedPerson.model_name.human #=> "Named person" {Learn more}[link:classes/ActiveModel/Naming.html] -* Adding observer support to your objects +* Observer support - ActiveModel::Observers allows your object to implement the Observer - pattern in a Rails App and take advantage of all the standard observer - functions. + ActiveModel::Observers allows your object to implement the Observer + pattern in a Rails App and take advantage of all the standard observer + functions. {Learn more}[link:classes/ActiveModel/Observer.html] -* Making your object serializable +* Making objects serializable - ActiveModel::Serialization provides a standard interface for your object - to provide to_json or to_xml serialization... - + ActiveModel::Serialization provides a standard interface for your object + to provide +to_json+ or +to_xml+ serialization. + s = SerialPerson.new s.serializable_hash # => {"name"=>nil} s.to_json # => "{\"name\":null}" @@ -154,36 +132,36 @@ functionality from the following modules: {Learn more}[link:classes/ActiveModel/Serialization.html] -* Integrating with Rail's internationalization (i18n) handling through - ActiveModel::Translations... +* Internationalization (i18n) support class Person extend ActiveModel::Translation end + + Person.human_attribute_name('my_attribute') + #=> "My attribute" {Learn more}[link:classes/ActiveModel/Translation.html] -* Providing a full Validation stack for your objects... +* Validation support class Person include ActiveModel::Validations attr_accessor :first_name, :last_name - validates_each :first_name, :last_name do |record, attr, value| record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z end end - person = Person.new person.first_name = 'zoolander' - person.valid? #=> false + person.valid? #=> false {Learn more}[link:classes/ActiveModel/Validations.html] -* Make custom validators +* Custom validators class Person include ActiveModel::Validations @@ -196,7 +174,7 @@ functionality from the following modules: record.errors[:name] = "must exist" if record.name.blank? end end - + p = ValidatorPerson.new p.valid? #=> false p.errors.full_messages #=> ["Name must exist"] diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 1dba664539..3fffc0d021 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,6 +1,6 @@ dir = File.dirname(__FILE__) -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake/testtask' @@ -23,16 +23,16 @@ namespace :test do end -require 'rake/rdoctask' +require 'rdoc/task' # Generate the RDoc documentation -Rake::RDocTask.new do |rdoc| +RDoc::Task.new do |rdoc| rdoc.rdoc_dir = "doc" rdoc.title = "Active Model" - rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.options << '-f' << 'horo' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include("README", "CHANGELOG") + rdoc.options << '--main' << 'README.rdoc' + rdoc.rdoc_files.include("README.rdoc", "CHANGELOG") rdoc.rdoc_files.include("lib/**/*.rb") end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 6bd6fe49ff..c483ecbc3c 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'activemodel' - s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' s.has_rdoc = true diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 817640b178..a43436e008 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -283,7 +283,7 @@ module ActiveModel @attribute_methods_generated = true end - # Removes all the preiously dynamically defined methods from the class + # Removes all the previously dynamically defined methods from the class def undefine_attribute_methods generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index e7aad17021..8c10c54b54 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -19,7 +19,7 @@ module ActiveModel # # define_model_callbacks :create, :update # - # This will provide all three standard callbacks (before, around and after) around + # This will provide all three standard callbacks (before, around and after) for # both the :create and :update methods. To implement, you need to wrap the methods # you want callbacks on in a block so that the callbacks get a chance to fire: # diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 2a1650faa9..d2bd160dc7 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -17,9 +17,9 @@ module ActiveModel # end # # cm = ContactMessage.new - # cm.to_model == self #=> true - # cm.to_key #=> nil - # cm.to_param #=> nil + # cm.to_model == self # => true + # cm.to_key # => nil + # cm.to_param # => nil # module Conversion # If your object is already designed to implement all of the Active Model diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 4c80863e3a..2516377afd 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -37,12 +37,13 @@ module ActiveModel # end # # def name=(val) - # name_will_change! + # name_will_change! unless val == @name # @name = val # end # # def save # @previously_changed = changes + # @changed_attributes.clear # end # # end @@ -77,13 +78,10 @@ module ActiveModel # person.changed # => ['name'] # person.changes # => { 'name' => ['Bill', 'Bob'] } # - # Resetting an attribute returns it to its original state: - # person.reset_name! # => 'Bill' - # person.changed? # => false - # person.name_changed? # => false - # person.name # => 'Bill' + # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> + # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to + # in-place attributes. # - # Before modifying an attribute in-place: # person.name_will_change! # person.name << 'y' # person.name_change # => ['Bill', 'Billy'] diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 482b3dac47..272ddb1554 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -37,11 +37,11 @@ module ActiveModel # send(attr) # end # - # def ErrorsPerson.human_attribute_name(attr, options = {}) + # def Person.human_attribute_name(attr, options = {}) # attr # end # - # def ErrorsPerson.lookup_ancestors + # def Person.lookup_ancestors # [self] # end # @@ -83,20 +83,16 @@ module ActiveModel # When passed a symbol or a name of a method, returns an array of errors # for the method. # - # p.errors[:name] #=> ["can not be nil"] - # p.errors['name'] #=> ["can not be nil"] + # p.errors[:name] # => ["can not be nil"] + # p.errors['name'] # => ["can not be nil"] def [](attribute) - if errors = get(attribute.to_sym) - errors - else - set(attribute.to_sym, []) - end + get(attribute.to_sym) || set(attribute.to_sym, []) end # Adds to the supplied attribute the supplied error message. # # p.errors[:name] = "must be set" - # p.errors[:name] #=> ['must be set'] + # p.errors[:name] # => ['must be set'] def []=(attribute, error) self[attribute.to_sym] << error end @@ -124,9 +120,9 @@ module ActiveModel # Returns the number of error messages. # # p.errors.add(:name, "can't be blank") - # p.errors.size #=> 1 + # p.errors.size # => 1 # p.errors.add(:name, "must be specified") - # p.errors.size #=> 2 + # p.errors.size # => 2 def size values.flatten.size end @@ -135,16 +131,16 @@ module ActiveModel # # p.errors.add(:name, "can't be blank") # p.errors.add(:name, "must be specified") - # p.errors.to_a #=> ["name can't be blank", "name must be specified"] + # p.errors.to_a # => ["name can't be blank", "name must be specified"] def to_a full_messages end # Returns the number of error messages. # p.errors.add(:name, "can't be blank") - # p.errors.count #=> 1 + # p.errors.count # => 1 # p.errors.add(:name, "must be specified") - # p.errors.count #=> 2 + # p.errors.count # => 2 def count to_a.size end @@ -158,8 +154,8 @@ module ActiveModel # # p.errors.add(:name, "can't be blank") # p.errors.add(:name, "must be specified") - # p.errors.to_xml #=> Produces: - # + # p.errors.to_xml + # # => # # <?xml version=\"1.0\" encoding=\"UTF-8\"?> # # <errors> # # <error>name can't be blank</error> @@ -169,9 +165,9 @@ module ActiveModel to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) end - # Returns an array as JSON representation for this object. + # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. def as_json(options=nil) - to_a + self end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb index 7c48472799..9fcb94d48a 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -1,3 +1,4 @@ +require 'set' require 'active_model/mass_assignment_security/sanitizer' module ActiveModel @@ -36,4 +37,4 @@ module ActiveModel end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index ca1e9f0ee8..b74d669f0a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -17,7 +17,10 @@ module ActiveModel end # Transform the model name into a more humane format, using I18n. By default, - # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post"). + # it will underscore then humanize the class name + # + # BlogPost.model_name.human # => "Blog post" + # # Specify +options+ with additional translating options. def human(options={}) return @human unless @klass.respond_to?(:lookup_ancestors) && @@ -45,8 +48,8 @@ module ActiveModel # extend ActiveModel::Naming # end # - # BookCover.model_name #=> "BookCover" - # BookCover.model_name.human #=> "Book cover" + # BookCover.model_name # => "BookCover" + # BookCover.model_name.human # => "Book cover" # # Providing the functionality that ActiveModel::Naming provides in your object # is required to pass the Active Model Lint test. So either extending the provided @@ -57,6 +60,35 @@ module ActiveModel def model_name @_model_name ||= ActiveModel::Name.new(self) end + + # Returns the plural class name of a record or class. Examples: + # + # ActiveModel::Naming.plural(post) # => "posts" + # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people" + def self.plural(record_or_class) + model_name_from_record_or_class(record_or_class).plural + end + + # Returns the singular class name of a record or class. Examples: + # + # ActiveModel::Naming.singular(post) # => "post" + # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person" + def self.singular(record_or_class) + model_name_from_record_or_class(record_or_class).singular + end + + # Identifies whether the class name of a record or class is uncountable. Examples: + # + # ActiveModel::Naming.uncountable?(Sheep) # => true + # ActiveModel::Naming.uncountable?(Post) => false + def self.uncountable?(record_or_class) + plural(record_or_class) == singular(record_or_class) + end + + private + def self.model_name_from_record_or_class(record_or_class) + (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name + end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 5670ec74cb..e675937f4d 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -61,6 +61,8 @@ module ActiveModel # person.serializable_hash # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... + # + # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . module Serialization def serializable_hash(options = nil) options ||= {} diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 918cd0ab76..e1dbc522de 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -19,8 +19,8 @@ module ActiveModel # passed through +options+. # # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the - # top-level behavior of to_json. It is true by default. When it is <tt>true</tt>, - # to_json will emit a single root node named after the object's type. For example: + # top-level behavior of +to_json+. If true (the default) +to_json+ will + # emit a single root node named after the object's type. For example: # # konata = User.find(1) # konata.to_json @@ -32,11 +32,11 @@ module ActiveModel # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. + # The remainder of the examples in this section assume +include_root_in_json+ + # is false. # - # Without any +options+, the returned JSON string will include all - # the model's attributes. For example: + # Without any +options+, the returned JSON string will include all the model's + # attributes. For example: # # konata = User.find(1) # konata.to_json @@ -52,14 +52,14 @@ module ActiveModel # konata.to_json(:except => [ :id, :created_at, :age ]) # # => {"name": "Konata Izumi", "awesome": true} # - # To include any methods on the model, use <tt>:methods</tt>. + # To include the result of some method calls on the model use <tt>:methods</tt>: # # konata.to_json(:methods => :permalink) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "permalink": "1-konata-izumi"} # - # To include associations, use <tt>:include</tt>. + # To include associations use <tt>:include</tt>: # # konata.to_json(:include => :posts) # # => {"id": 1, "name": "Konata Izumi", "age": 16, @@ -67,7 +67,7 @@ module ActiveModel # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, # {"id": 2, author_id: 1, "title": "So I was thinking"}]} # - # 2nd level and higher order associations work as well: + # Second level and higher order associations work as well: # # konata.to_json(:include => { :posts => { # :include => { :comments => { diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 0554677296..0facbd6ce1 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -14,7 +14,7 @@ module ActiveModel # end # # TranslatedPerson.human_attribute_name('my_attribute') - # #=> "My attribute" + # # => "My attribute" # # This also provides the required class methods for hooking into the # Rails internationalization API, including being able to define a diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 5779ba3b29..3407c59e7a 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -24,20 +24,16 @@ module ActiveModel # end # # Which provides you with the full standard validation stack that you - # know from ActiveRecord. + # know from Active Record: # # person = Person.new - # person.valid? - # #=> true - # person.invalid? - # #=> false + # person.valid? # => true + # person.invalid? # => false + # # person.first_name = 'zoolander' - # person.valid? - # #=> false - # person.invalid? - # #=> true - # person.errors - # #=> #<OrderedHash {:first_name=>["starts with z."]}> + # person.valid? # => false + # person.invalid? # => true + # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}> # # Note that ActiveModel::Validations automatically adds an +errors+ method # to your instances initialized with a new ActiveModel::Errors object, so @@ -122,11 +118,13 @@ module ActiveModel # end # def validate(*args, &block) - options = args.last - if options.is_a?(Hash) && options.key?(:on) + options = args.extract_options! + if options.key?(:on) + options = options.dup options[:if] = Array.wrap(options[:if]) options[:if] << "validation_context == :#{options[:on]}" end + args << options set_callback(:validate, *args, &block) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index c8a77ad666..a7af4f2b4d 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -40,8 +40,6 @@ module ActiveModel CHECKS.each do |key, validity_check| next unless check_value = options[key] - default_message = options[MESSAGES[key]] - options[:message] ||= default_message if default_message valid_value = if key == :maximum value.nil? || value.size.send(validity_check, check_value) @@ -51,8 +49,13 @@ module ActiveModel next if valid_value - record.errors.add(attribute, MESSAGES[key], - options.except(*RESERVED_OPTIONS).merge!(:count => check_value)) + errors_options = options.except(*RESERVED_OPTIONS) + errors_options[:count] = check_value + + default_message = options[MESSAGES[key]] + errors_options[:message] ||= default_message if default_message + + record.errors.add(attribute, MESSAGES[key], errors_options) end end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 0674640925..3260e6bc5a 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -40,7 +40,7 @@ module ActiveModel # validates :email, :presence => true, :email => true # end # - # Validator classes my also exist within the class being validated + # Validator classes may also exist within the class being validated # allowing custom modules of validators to be included as needed e.g. # # class Film diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 689c617177..163124d531 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -102,8 +102,8 @@ module ActiveModel #:nodoc: # # == Examples # - # PresenceValidator.kind #=> :presence - # UniquenessValidator.kind #=> :uniqueness + # PresenceValidator.kind # => :presence + # UniquenessValidator.kind # => :uniqueness # def self.kind @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? @@ -111,7 +111,7 @@ module ActiveModel #:nodoc: # Accepts options that will be made available through the +options+ reader. def initialize(options) - @options = options + @options = options.freeze end # Return the kind for this validator. diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index c36fc8b1f7..f2f4b15520 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -3,7 +3,7 @@ module ActiveModel MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index e1a35be384..858ae9cb69 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -3,10 +3,11 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods [:name] + define_attribute_methods [:name, :color] def initialize @name = nil + @color = nil end def name @@ -17,13 +18,92 @@ class DirtyTest < ActiveModel::TestCase name_will_change! @name = val end + + def color + @color + end + + def color=(val) + color_will_change! unless val == @color + @color = val + end + + def save + @previously_changed = changes + @changed_attributes.clear + end + end + + setup do + @model = DirtyModel.new + end + + test "setting attribute will result in change" do + assert !@model.changed? + assert !@model.name_changed? + @model.name = "Ringo" + assert @model.changed? + assert @model.name_changed? + end + + test "list of changed attributes" do + assert_equal [], @model.changed + @model.name = "Paul" + assert_equal ['name'], @model.changed + end + + test "changes to attribute values" do + assert !@model.changes['name'] + @model.name = "John" + assert_equal [nil, "John"], @model.changes['name'] end test "changes accessible through both strings and symbols" do - model = DirtyModel.new - model.name = "David" - assert_not_nil model.changes[:name] - assert_not_nil model.changes['name'] + @model.name = "David" + assert_not_nil @model.changes[:name] + assert_not_nil @model.changes['name'] + end + + test "attribute mutation" do + @model.instance_variable_set("@name", "Yam") + assert !@model.name_changed? + @model.name.replace("Hadad") + assert !@model.name_changed? + @model.name_will_change! + @model.name.replace("Baal") + assert @model.name_changed? + end + + test "resetting attribute" do + @model.name = "Bob" + @model.reset_name! + assert_nil @model.name + #assert !@model.name_changed #Doesn't work yet + end + + test "setting color to same value should not result in change being recorded" do + @model.color = "red" + assert @model.color_changed? + @model.save + assert !@model.color_changed? + assert !@model.changed? + @model.color = "red" + assert !@model.color_changed? + assert !@model.changed? + end + + test "saving should reset model's changed status" do + @model.name = "Alf" + assert @model.changed? + @model.save + assert !@model.changed? + assert !@model.name_changed? + end + + test "saving should preserve previous changes" do + @model.name = "Jericho Cane" + @model.save + assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb new file mode 100644 index 0000000000..79b45bb298 --- /dev/null +++ b/activemodel/test/cases/errors_test.rb @@ -0,0 +1,65 @@ +require "cases/helper" + +class ErrorsTest < ActiveModel::TestCase + class Person + extend ActiveModel::Naming + def initialize + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :name + attr_reader :errors + + def validate! + errors.add(:name, "can not be nil") if name == nil + end + + def read_attribute_for_validation(attr) + send(attr) + end + + def self.human_attribute_name(attr, options = {}) + attr + end + + def self.lookup_ancestors + [self] + end + + end + + test "method validate! should work" do + person = Person.new + person.validate! + assert_equal ["name can not be nil"], person.errors.full_messages + assert_equal ["can not be nil"], person.errors[:name] + + end + + test 'should be able to assign error' do + person = Person.new + person.errors[:name] = 'should not be nil' + assert_equal ["should not be nil"], person.errors[:name] + end + + test 'should be able to add an error on an attribute' do + person = Person.new + person.errors.add(:name, "can not be blank") + assert_equal ["can not be blank"], person.errors[:name] + end + + test 'should respond to size' do + person = Person.new + person.errors.add(:name, "can not be blank") + assert_equal 1, person.errors.size + end + + test 'to_a should return an array' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a + + end + +end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 367207aab3..015153ec7c 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -31,7 +31,7 @@ class SanitizerTest < ActiveModel::TestCase log = StringIO.new @sanitizer.logger = Logger.new(log) @sanitizer.sanitize(original_attributes) - assert (log.string =~ /admin/), "Should log removed attributes: #{log.string}" + assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}") end end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index 0f7a38b0bc..c25b0fdf00 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -35,10 +35,10 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections' - assert (TightPerson.protected_attributes - TightPerson.attributes_protected_by_default).blank? + assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes - assert (TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank? + assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes end @@ -49,4 +49,4 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal sanitized, { } end -end
\ No newline at end of file +end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index dc39b84ed8..5a8bff378a 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -1,4 +1,6 @@ require 'cases/helper' +require 'models/contact' +require 'models/sheep' require 'models/track_back' class NamingTest < ActiveModel::TestCase @@ -26,3 +28,40 @@ class NamingTest < ActiveModel::TestCase assert_equal 'post/track_backs/track_back', @model_name.partial_path end end + +class NamingHelpersTest < Test::Unit::TestCase + def setup + @klass = Contact + @record = @klass.new + @singular = 'contact' + @plural = 'contacts' + @uncountable = Sheep + end + + def test_singular + assert_equal @singular, singular(@record) + end + + def test_singular_for_class + assert_equal @singular, singular(@klass) + end + + def test_plural + assert_equal @plural, plural(@record) + end + + def test_plural_for_class + assert_equal @plural, plural(@klass) + end + + def test_uncountable + assert uncountable?(@uncountable), "Expected 'sheep' to be uncoutable" + assert !uncountable?(@klass), "Expected 'contact' to be countable" + end + + private + def method_missing(method, *args) + ActiveModel::Naming.send(method, *args) + end +end + diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializeration/json_serialization_test.rb index 04b50e5bb8..1ac991a8f1 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializeration/json_serialization_test.rb @@ -89,7 +89,7 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "methds are called on object" do + test "methods are called on object" do # Define methods on fixture. def @contact.label; "Has cheezburger"; end def @contact.favorite_quote; "Constraints are liberating"; end @@ -102,4 +102,18 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"label":"Has cheezburger"}, methods_json assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + + test "should return OrderedHash for errors" do + car = Automobile.new + + # run the validation + car.valid? + + hash = ActiveSupport::OrderedHash.new + hash[:make] = "can't be blank" + hash[:model] = "is too short (minimum is 2 characters)" + assert_equal hash.to_json, car.errors.to_json + end + + end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 012c5a2f37..1e6180a938 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -229,6 +229,20 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["hoo 5"], t.errors["title"] end + def test_validates_length_of_custom_errors_for_both_too_short_and_too_long + Topic.validates_length_of :title, :minimum => 3, :maximum => 5, :too_short => 'too short', :too_long => 'too long' + + t = Topic.new(:title => 'a') + assert t.invalid? + assert t.errors[:title].any? + assert_equal ['too short'], t.errors['title'] + + t = Topic.new(:title => 'aaaaaa') + assert t.invalid? + assert t.errors[:title].any? + assert_equal ['too long'], t.errors['title'] + end + def test_validates_length_of_custom_errors_for_is_with_message Topic.validates_length_of( :title, :is=>5, :message=>"boo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index e94d8ce88c..8d6bdeb6a5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -170,9 +170,11 @@ class ValidationsTest < ActiveModel::TestCase assert_match %r{<errors>}, xml assert_match %r{<error>Title can't be blank</error>}, xml assert_match %r{<error>Content can't be blank</error>}, xml - - json = t.errors.to_json - assert_equal t.errors.to_a.to_json, json + + hash = ActiveSupport::OrderedHash.new + hash[:title] = "can't be blank" + hash[:content] = "can't be blank" + assert_equal t.errors.to_json, hash.to_json end def test_validation_order diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index 605e435f39..f4f3078473 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -1,4 +1,5 @@ class Contact + extend ActiveModel::Naming include ActiveModel::Conversion attr_accessor :id, :name, :age, :created_at, :awesome, :preferences diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb new file mode 100644 index 0000000000..175dbe6477 --- /dev/null +++ b/activemodel/test/models/sheep.rb @@ -0,0 +1,4 @@ +class Sheep + extend ActiveModel::Naming +end +
\ No newline at end of file diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index a1a82fdff5..20b2286fc0 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,4 +1,6 @@ -*Rails 3.0.0 [RC1] (unreleased)* +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* Changed update_attribute to not run callbacks and update the record directly in the database [Neeraj Singh] * Add scoping and unscoped as the syntax to replace the old with_scope and with_exclusive_scope [José Valim] diff --git a/activerecord/README b/activerecord/README deleted file mode 100644 index d68eb28a64..0000000000 --- a/activerecord/README +++ /dev/null @@ -1,351 +0,0 @@ -= Active Record -- Object-relation mapping put on rails - -Active Record connects business objects and database tables to create a persistable -domain model where logic and data are presented in one wrapping. It's an implementation -of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html] -by the same name as described by Martin Fowler: - - "An object that wraps a row in a database table or view, encapsulates - the database access, and adds domain logic on that data." - -Active Record's main contribution to the pattern is to relieve the original of two stunting problems: -lack of associations and inheritance. By adding a simple domain language-like set of macros to describe -the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the -gap of functionality between the data mapper and active record approach. - -A short rundown of the major features: - -* Automated mapping between classes and tables, attributes and columns. - - class Product < ActiveRecord::Base; end - - ...is automatically mapped to the table named "products", such as: - - CREATE TABLE products ( - id int(11) NOT NULL auto_increment, - name varchar(255), - PRIMARY KEY (id) - ); - - ...which again gives Product#name and Product#name=(new_name) - - {Learn more}[link:classes/ActiveRecord/Base.html] - - -* Associations between objects controlled by simple meta-programming macros. - - class Firm < ActiveRecord::Base - has_many :clients - has_one :account - belongs_to :conglomorate - end - - {Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html] - - -* Aggregations of value objects controlled by simple meta-programming macros. - - class Account < ActiveRecord::Base - composed_of :balance, :class_name => "Money", - :mapping => %w(balance amount) - composed_of :address, - :mapping => [%w(address_street street), %w(address_city city)] - end - - {Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html] - - -* Validation rules that can differ for new or existing objects. - - class Account < ActiveRecord::Base - validates_presence_of :subdomain, :name, :email_address, :password - validates_uniqueness_of :subdomain - validates_acceptance_of :terms_of_service, :on => :create - validates_confirmation_of :password, :email_address, :on => :create - end - - {Learn more}[link:classes/ActiveRecord/Validations.html] - -* Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc). - - class Person < ActiveRecord::Base - def before_destroy # is called just before Person#destroy - CreditCard.find(credit_card_id).destroy - end - end - - class Account < ActiveRecord::Base - after_find :eager_load, 'self.class.announce(#{id})' - end - - {Learn more}[link:classes/ActiveRecord/Callbacks.html] - - -* Observers for the entire lifecycle - - class CommentObserver < ActiveRecord::Observer - def after_create(comment) # is called just after Comment#save - Notifications.deliver_new_comment("david@loudthinking.com", comment) - end - end - - {Learn more}[link:classes/ActiveRecord/Observer.html] - - -* Inheritance hierarchies - - class Company < ActiveRecord::Base; end - class Firm < Company; end - class Client < Company; end - class PriorityClient < Client; end - - {Learn more}[link:classes/ActiveRecord/Base.html] - - -* Transactions - - # Database transaction - Account.transaction do - david.withdrawal(100) - mary.deposit(100) - end - - {Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html] - - -* Reflections on columns, associations, and aggregations - - reflection = Firm.reflect_on_association(:clients) - reflection.klass # => Client (class) - Firm.columns # Returns an array of column descriptors for the firms table - - {Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html] - - -* Direct manipulation (instead of service invocation) - - So instead of (Hibernate[http://www.hibernate.org/] example): - - long pkId = 1234; - DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) ); - // something interesting involving a cat... - sess.save(cat); - sess.flush(); // force the SQL INSERT - - Active Record lets you: - - pkId = 1234 - cat = Cat.find(pkId) - # something even more interesting involving the same cat... - cat.save - - {Learn more}[link:classes/ActiveRecord/Base.html] - - -* Database abstraction through simple adapters (~100 lines) with a shared connector - - ActiveRecord::Base.establish_connection(:adapter => "sqlite", :database => "dbfile") - - ActiveRecord::Base.establish_connection( - :adapter => "mysql", - :host => "localhost", - :username => "me", - :password => "secret", - :database => "activerecord" - ) - - {Learn more}[link:classes/ActiveRecord/Base.html#M000081] and read about the built-in support for - MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html], PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], SQLite[link:classes/ActiveRecord/ConnectionAdapters/SQLiteAdapter.html], Oracle[link:classes/ActiveRecord/ConnectionAdapters/OracleAdapter.html], SQLServer[link:classes/ActiveRecord/ConnectionAdapters/SQLServerAdapter.html], and DB2[link:classes/ActiveRecord/ConnectionAdapters/DB2Adapter.html]. - - -* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc] - - ActiveRecord::Base.logger = Logger.new(STDOUT) - ActiveRecord::Base.logger = Log4r::Logger.new("Application Log") - - -* Database agnostic schema management with Migrations - - class AddSystemSettings < ActiveRecord::Migration - def self.up - create_table :system_settings do |t| - t.string :name - t.string :label - t.text :value - t.string :type - t.integer :position - end - - SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1 - end - - def self.down - drop_table :system_settings - end - end - - {Learn more}[link:classes/ActiveRecord/Migration.html] - -== Simple example (1/2): Defining tables and classes (using MySQL) - -Data definitions are specified only in the database. Active Record queries the database for -the column names (that then serves to determine which attributes are valid) on regular -object instantiation through the new constructor and relies on the column names in the rows -with the finders. - - # CREATE TABLE companies ( - # id int(11) unsigned NOT NULL auto_increment, - # client_of int(11), - # name varchar(255), - # type varchar(100), - # PRIMARY KEY (id) - # ) - -Active Record automatically links the "Company" object to the "companies" table - - class Company < ActiveRecord::Base - has_many :people, :class_name => "Person" - end - - class Firm < Company - has_many :clients - - def people_with_all_clients - clients.inject([]) { |people, client| people + client.people } - end - end - -The foreign_key is only necessary because we didn't use "firm_id" in the data definition - - class Client < Company - belongs_to :firm, :foreign_key => "client_of" - end - - # CREATE TABLE people ( - # id int(11) unsigned NOT NULL auto_increment, - # name text, - # company_id text, - # PRIMARY KEY (id) - # ) - -Active Record will also automatically link the "Person" object to the "people" table - - class Person < ActiveRecord::Base - belongs_to :company - end - -== Simple example (2/2): Using the domain - -Picking a database connection for all the Active Records - - ActiveRecord::Base.establish_connection( - :adapter => "mysql", - :host => "localhost", - :username => "me", - :password => "secret", - :database => "activerecord" - ) - -Create some fixtures - - firm = Firm.new("name" => "Next Angle") - # SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm") - firm.save - - client = Client.new("name" => "37signals", "client_of" => firm.id) - # SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm") - client.save - -Lots of different finders - - # SQL: SELECT * FROM companies WHERE id = 1 - next_angle = Company.find(1) - - # SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm' - next_angle = Firm.find(1) - - # SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle' - next_angle = Company.find(:first, :conditions => "name = 'Next Angle'") - - next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first - -The supertype, Company, will return subtype instances - - Firm === next_angle - -All the dynamic methods added by the has_many macro - - next_angle.clients.empty? # true - next_angle.clients.size # total number of clients - all_clients = next_angle.clients - -Constrained finds makes access security easier when ID comes from a web-app - - # SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2 - thirty_seven_signals = next_angle.clients.find(2) - -Bi-directional associations thanks to the "belongs_to" macro - - thirty_seven_signals.firm.nil? # true - - -== Philosophy - -Active Record attempts to provide a coherent wrapper as a solution for the inconvenience that is -object-relational mapping. The prime directive for this mapping has been to minimize -the amount of code needed to build a real-world domain model. This is made possible -by relying on a number of conventions that make it easy for Active Record to infer -complex relations and structures from a minimal amount of explicit direction. - -Convention over Configuration: -* No XML-files! -* Lots of reflection and run-time extension -* Magic is not inherently a bad word - -Admit the Database: -* Lets you drop down to SQL for odd cases and performance -* Doesn't attempt to duplicate or replace data definitions - - -== Download - -The latest version of Active Record can be found at - -* http://rubyforge.org/project/showfiles.php?group_id=182 - -Documentation can be found at - -* http://ar.rubyonrails.com - - -== Installation - -The prefered method of installing Active Record is through its GEM file. You'll need to have -RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have, -then use: - - % [sudo] gem install activerecord-1.10.0.gem - -You can also install Active Record the old-fashioned way with the following command: - - % [sudo] ruby install.rb - -from its distribution directory. - - -== License - -Active Record is released under the MIT license. - - -== Support - -The Active Record homepage is http://www.rubyonrails.com. You can find the Active Record -RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says: - - Feel free to submit commits or feature requests. If you send a patch, - remember to update the corresponding unit tests. If fact, I prefer - new feature to be submitted in the form of new unit tests. - -For other information, feel free to ask on the rubyonrails-talk -(http://groups.google.com/group/rubyonrails-talk) mailing list. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc new file mode 100644 index 0000000000..8dbd6c82b5 --- /dev/null +++ b/activerecord/README.rdoc @@ -0,0 +1,222 @@ += Active Record -- Object-relational mapping put on rails + +Active Record connects classes to relational database tables to establish an +almost zero-configuration persistence layer for applications. The library +provides a base class that, when subclassed, sets up a mapping between the new +class and an existing table in the database. In context of an application, +these classes are commonly referred to as *models*. Models can also be +connected to other models; this is done by defining *associations*. + +Active Record relies heavily on naming in that it uses class and association +names to establish mappings between respective database tables and foreign key +columns. Although these mappings can be defined explicitly, it's recommended +to follow naming conventions, especially when getting started with the +library. + +A short rundown of some of the major features: + +* Automated mapping between classes and tables, attributes and columns. + + class Product < ActiveRecord::Base + end + + The Product class is automatically mapped to the table named "products", + which might look like this: + + CREATE TABLE products ( + id int(11) NOT NULL auto_increment, + name varchar(255), + PRIMARY KEY (id) + ); + + This would also define the following accessors: `Product#name` and + `Product#name=(new_name)` + + {Learn more}[link:classes/ActiveRecord/Base.html] + + +* Associations between objects defined by simple class methods. + + class Firm < ActiveRecord::Base + has_many :clients + has_one :account + belongs_to :conglomerate + end + + {Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html] + + +* Aggregations of value objects. + + class Account < ActiveRecord::Base + composed_of :balance, :class_name => "Money", + :mapping => %w(balance amount) + composed_of :address, + :mapping => [%w(address_street street), %w(address_city city)] + end + + {Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html] + + +* Validation rules that can differ for new or existing objects. + + class Account < ActiveRecord::Base + validates_presence_of :subdomain, :name, :email_address, :password + validates_uniqueness_of :subdomain + validates_acceptance_of :terms_of_service, :on => :create + validates_confirmation_of :password, :email_address, :on => :create + end + + {Learn more}[link:classes/ActiveRecord/Validations.html] + + +* Callbacks available for the entire lifecycle (instantiation, saving, destroying, validating, etc.) + + class Person < ActiveRecord::Base + before_destroy :invalidate_payment_plan + # the `invalidate_payment_plan` method gets called just before Person#destroy + end + + {Learn more}[link:classes/ActiveRecord/Callbacks.html] + + +* Observers that react to changes in a model + + class CommentObserver < ActiveRecord::Observer + def after_create(comment) # is called just after Comment#save + Notifications.deliver_new_comment("david@loudthinking.com", comment) + end + end + + {Learn more}[link:classes/ActiveRecord/Observer.html] + + +* Inheritance hierarchies + + class Company < ActiveRecord::Base; end + class Firm < Company; end + class Client < Company; end + class PriorityClient < Client; end + + {Learn more}[link:classes/ActiveRecord/Base.html] + + +* Transactions + + # Database transaction + Account.transaction do + david.withdrawal(100) + mary.deposit(100) + end + + {Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html] + + +* Reflections on columns, associations, and aggregations + + reflection = Firm.reflect_on_association(:clients) + reflection.klass # => Client (class) + Firm.columns # Returns an array of column descriptors for the firms table + + {Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html] + + +* Database abstraction through simple adapters + + # connect to SQLite3 + ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "dbfile.sqlite3") + + # connect to MySQL with authentication + ActiveRecord::Base.establish_connection( + :adapter => "mysql", + :host => "localhost", + :username => "me", + :password => "secret", + :database => "activerecord" + ) + + {Learn more}[link:classes/ActiveRecord/Base.html] and read about the built-in support for + MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html], + PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], and + SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. + + +* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc] + + ActiveRecord::Base.logger = Logger.new(STDOUT) + ActiveRecord::Base.logger = Log4r::Logger.new("Application Log") + + +* Database agnostic schema management with Migrations + + class AddSystemSettings < ActiveRecord::Migration + def self.up + create_table :system_settings do |t| + t.string :name + t.string :label + t.text :value + t.string :type + t.integer :position + end + + SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1 + end + + def self.down + drop_table :system_settings + end + end + + {Learn more}[link:classes/ActiveRecord/Migration.html] + + +== Philosophy + +Active Record is an implementation of the object-relational mapping (ORM) +pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html] by the same +name described by Martin Fowler: + + "An object that wraps a row in a database table or view, + encapsulates the database access, and adds domain logic on that data." + +Active Record attempts to provide a coherent wrapper as a solution for the inconvenience that is +object-relational mapping. The prime directive for this mapping has been to minimize +the amount of code needed to build a real-world domain model. This is made possible +by relying on a number of conventions that make it easy for Active Record to infer +complex relations and structures from a minimal amount of explicit direction. + +Convention over Configuration: +* No XML-files! +* Lots of reflection and run-time extension +* Magic is not inherently a bad word + +Admit the Database: +* Lets you drop down to SQL for odd cases and performance +* Doesn't attempt to duplicate or replace data definitions + + +== Download and installation + +The latest version of Active Record can be installed with Rubygems: + + % [sudo] gem install activerecord + +Source code can be downloaded as part of the Rails project on GitHub + +* http://github.com/rails/rails/tree/master/activerecord/ + + +== License + +Active Record is released under the MIT license. + + +== Support + +API documentation is at + +* http://api.rubyonrails.com + +Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: + +* https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 392b717e0a..c1e90cc099 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,8 +1,8 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -24,14 +24,14 @@ def run_without_aborting(*tasks) abort "Errors running #{errors.join(', ')}" if errors.any? end -desc 'Run mysql, sqlite, and postgresql tests by default' +desc 'Run mysql, mysql2, sqlite, and postgresql tests by default' task :default => :test -desc 'Run mysql, sqlite, and postgresql tests' +desc 'Run mysql, mysql2, sqlite, and postgresql tests' task :test do tasks = defined?(JRUBY_VERSION) ? %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) : - %w(test_mysql test_sqlite3 test_postgresql) + %w(test_mysql test_mysql2 test_sqlite3 test_postgresql) run_without_aborting(*tasks) end @@ -39,15 +39,15 @@ namespace :test do task :isolated do tasks = defined?(JRUBY_VERSION) ? %w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) : - %w(isolated_test_mysql isolated_test_sqlite3 isolated_test_postgresql) + %w(isolated_test_mysql isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql) run_without_aborting(*tasks) end end -%w( mysql postgresql sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| +%w( mysql mysql2 postgresql sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| Rake::TestTask.new("test_#{adapter}") { |t| connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}" - adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/] + adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] t.libs << "test" << connection_path t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { |x| x =~ /\/adapters\// @@ -59,7 +59,7 @@ end task "isolated_test_#{adapter}" do connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}" - adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/] + adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] puts [adapter, adapter_short, connection_path].inspect ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) (Dir["test/cases/**/*_test.rb"].reject { @@ -166,13 +166,13 @@ task :rebuild_frontbase_databases => 'frontbase:rebuild_databases' # Generate the RDoc documentation -Rake::RDocTask.new { |rdoc| +RDoc::Task.new { |rdoc| rdoc.rdoc_dir = 'doc' rdoc.title = "Active Record -- Object-relation mapping put on rails" - rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.options << '-f' << 'horo' + rdoc.options << '--main' << 'README.rdoc' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') + rdoc.rdoc_files.include('README.rdoc', 'RUNNING_UNIT_TESTS', 'CHANGELOG') rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.exclude('lib/active_record/vendor/*') rdoc.rdoc_files.include('dev-utils/*.rb') @@ -224,9 +224,3 @@ task :release => :package do Rake::Gemcutter::Tasks.new(spec).define Rake::Task['gem:push'].invoke end - -desc "Publish the API documentation" -task :pdoc => [:rdoc] do - require 'rake/contrib/sshpublisher' - Rake::SshDirPublisher.new("rails@api.rubyonrails.org", "public_html/ar", "doc").upload -end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 5aea992801..67d521d56b 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -14,15 +14,15 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'activerecord' - s.files = Dir['CHANGELOG', 'README', 'examples/**/*', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*'] s.require_path = 'lib' s.has_rdoc = true - s.extra_rdoc_files = %w( README ) - s.rdoc_options.concat ['--main', 'README'] + s.extra_rdoc_files = %w( README.rdoc ) + s.rdoc_options.concat ['--main', 'README.rdoc'] s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) s.add_dependency('arel', '~> 0.4.0') - s.add_dependency('tzinfo', '~> 0.3.16') + s.add_dependency('tzinfo', '~> 0.3.22') end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index f7d358337c..a985cfcb66 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -58,7 +58,7 @@ end sqlfile = File.expand_path("../performance.sql", __FILE__) if File.exists?(sqlfile) - mysql_bin = %w[mysql mysql5].select { |bin| `which #{bin}`.length > 0 } + mysql_bin = %w[mysql mysql5].detect { |bin| `which #{bin}`.length > 0 } `#{mysql_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} < #{sqlfile}` else puts 'Generating data...' diff --git a/activerecord/install.rb b/activerecord/install.rb deleted file mode 100644 index c87398b1f4..0000000000 --- a/activerecord/install.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rbconfig' -require 'find' -require 'ftools' - -include Config - -# this was adapted from rdoc's install.rb by ways of Log4r - -$sitedir = CONFIG["sitelibdir"] -unless $sitedir - version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] - $libdir = File.join(CONFIG["libdir"], "ruby", version) - $sitedir = $:.find {|x| x =~ /site_ruby/ } - if !$sitedir - $sitedir = File.join($libdir, "site_ruby") - elsif $sitedir !~ Regexp.quote(version) - $sitedir = File.join($sitedir, version) - end -end - -# the actual gruntwork -Dir.chdir("lib") - -Find.find("active_record", "active_record.rb") { |f| - if f[-3..-1] == ".rb" - File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) - else - File::makedirs(File.join($sitedir, *f.split(/\//))) - end -} diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index c45400d3d9..83a9ab46c5 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -9,11 +9,13 @@ module ActiveRecord end unless self.new_record? end - # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes - # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is] - # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the - # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) - # and how it can be turned back into attributes (when the entity is saved to the database). Example: + # Active Record implements aggregation through a macro-like class method called +composed_of+ + # for representing attributes as value objects. It expresses relationships like "Account [is] + # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call + # to the macro adds a description of how the value objects are created from the attributes of + # the entity object (when the entity is initialized either as a new object or from finding an + # existing object) and how it can be turned back into attributes (when the entity is saved to + # the database). # # class Customer < ActiveRecord::Base # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) @@ -68,9 +70,10 @@ module ActiveRecord # end # end # - # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the - # composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our - # +balance+ attribute. You interact with the value objects just like you would any other attribute, though: + # Now it's possible to access attributes from the database through the value objects instead. If + # you choose to name the composition the same as the attribute's name, it will be the only way to + # access that attribute. That's the case with our +balance+ attribute. You interact with the value + # objects just like you would any other attribute, though: # # customer.balance = Money.new(20) # sets the Money value object and the attribute # customer.balance # => Money value object @@ -79,8 +82,8 @@ module ActiveRecord # customer.balance == Money.new(20) # => true # customer.balance < Money.new(5) # => false # - # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will - # determine the order of the parameters. Example: + # Value objects can also be composed of multiple attributes, such as the case of Address. The order + # of the mappings will determine the order of the parameters. # # customer.address_street = "Hyancintvej" # customer.address_city = "Copenhagen" @@ -91,38 +94,43 @@ module ActiveRecord # # == Writing value objects # - # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing - # $5. Two Money objects both representing $5 should be equal (through methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking - # makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as Customer can - # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or - # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects. + # Value objects are immutable and interchangeable objects that represent a given value, such as + # a Money object representing $5. Two Money objects both representing $5 should be equal (through + # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is + # unlike entity objects where equality is determined by identity. An entity class such as Customer can + # easily have two different objects that both have an address on Hyancintvej. Entity identity is + # determined by object or relational unique identifiers (such as primary keys). Normal + # ActiveRecord::Base classes are entity objects. # - # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after - # creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchange_to method that - # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been - # changed through means other than the writer method. + # It's also important to treat the value objects as immutable. Don't allow the Money object to have + # its amount changed after creation. Create a new Money object with the new value instead. This + # is exemplified by the Money#exchange_to method that returns a new value object instead of changing + # its own values. Active Record won't persist value objects that have been changed through means + # other than the writer method. # - # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to - # change it afterwards will result in a ActiveSupport::FrozenObjectError. + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value + # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError. # - # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects - # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable + # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not + # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable # # == Custom constructors and converters # - # By default value objects are initialized by calling the <tt>new</tt> constructor of the value class passing each of the - # mapped attributes, in the order specified by the <tt>:mapping</tt> option, as arguments. If the value class doesn't support - # this convention then +composed_of+ allows a custom constructor to be specified. + # By default value objects are initialized by calling the <tt>new</tt> constructor of the value + # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> + # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows + # a custom constructor to be specified. # - # When a new value is assigned to the value object the default assumption is that the new value is an instance of the value - # class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if - # necessary. + # When a new value is assigned to the value object the default assumption is that the new value + # is an instance of the value class. Specifying a custom converter allows the new value to be automatically + # converted to an instance of value class if necessary. # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the - # NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it - # expects a CIDR address string as a parameter. New values can be assigned to the value object using either another - # NetAddr::CIDR object, a string or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to - # meet these requirements: + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that + # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor + # for the value class is called +create+ and it expects a CIDR address string as a parameter. New + # values can be assigned to the value object using either another NetAddr::CIDR object, a string + # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet + # these requirements: # # class NetworkResource < ActiveRecord::Base # composed_of :cidr, @@ -149,9 +157,9 @@ module ActiveRecord # # == Finding records by a value object # - # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance - # of the value object in the conditions hash. The following example finds all customers with +balance_amount+ equal to 20 and - # +balance_currency+ equal to "USD": + # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database + # by specifying an instance of the value object in the conditions hash. The following example + # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": # # Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")}) # @@ -160,23 +168,28 @@ module ActiveRecord # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. # # Options are: - # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name can't be inferred - # from the part id. So <tt>composed_of :address</tt> will by default be linked to the Address class, but - # if the real class name is CompanyAddress, you'll have to specify it with this option. - # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value object. Each mapping - # is represented as an array where the first item is the name of the entity attribute and the second item is the - # name the attribute in the value object. The order in which mappings are defined determine the order in which - # attributes are sent to the value class constructor. + # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name + # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked + # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it + # with this option. + # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value + # object. Each mapping is represented as an array where the first item is the name of the + # entity attribute and the second item is the name the attribute in the value object. The + # order in which mappings are defined determine the order in which attributes are sent to the + # value class constructor. # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped - # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes. + # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all + # mapped attributes. # This defaults to +false+. - # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that is called to - # initialize the value object. The constructor is passed all of the mapped attributes, in the order that they - # are defined in the <tt>:mapping option</tt>, as arguments and uses them to instantiate a <tt>:class_name</tt> object. + # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that + # is called to initialize the value object. The constructor is passed all of the mapped attributes, + # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them + # to instantiate a <tt>:class_name</tt> object. # The default is <tt>:new</tt>. - # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> or a Proc that is - # called when a new value is assigned to the value object. The converter is passed the single value that is used - # in the assignment and is only called if the new value is not an instance of <tt>:class_name</tt>. + # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> + # or a Proc that is called when a new value is assigned to the value object. The converter is + # passed the single value that is used in the assignment and is only called if the new value is + # not an instance of <tt>:class_name</tt>. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index cbec5789fd..0f0fdc2e21 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -9,8 +9,8 @@ module ActiveRecord # Implements the details of eager loading of Active Record associations. # Application developers should not use this module directly. # - # ActiveRecord::Base is extended with this module. The source code in - # ActiveRecord::Base references methods defined in this module. + # <tt>ActiveRecord::Base</tt> is extended with this module. The source code in + # <tt>ActiveRecord::Base</tt> references methods defined in this module. # # Note that 'eager loading' and 'preloading' are actually the same thing. # However, there are two different eager loading strategies. @@ -55,7 +55,7 @@ module ActiveRecord # == Parameters # +records+ is an array of ActiveRecord::Base. This array needs not be flat, # i.e. +records+ itself may also contain arrays of records. In any case, - # +preload_associations+ will preload the associations all records by + # +preload_associations+ will preload the all associations records by # flattening +records+. # # +associations+ specifies one or more associations that you want to @@ -110,15 +110,15 @@ module ActiveRecord def preload_one_association(records, association, preload_options={}) class_to_reflection = {} # Not all records have the same class, so group then preload - # group on the reflection itself so that if various subclass share the same association then we do not split them - # unnecessarily - records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records| + # group on the reflection itself so that if various subclass share the same association then + # we do not split them unnecessarily + records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records| raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus, # the following could call 'preload_belongs_to_association', # 'preload_has_many_association', etc. - send("preload_#{reflection.macro}_association", records, reflection, preload_options) + send("preload_#{reflection.macro}_association", _records, reflection, preload_options) end end @@ -149,7 +149,8 @@ module ActiveRecord seen_keys = {} associated_records.each do |associated_record| #this is a has_one or belongs_to: there should only be one record. - #Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please + #Unfortunately we can't (in portable way) ask the database for + #'all records where foo_id in (x,y,z), but please # only one row per distinct foo_id' so this where we enforce that next if seen_keys[associated_record[key].to_s] seen_keys[associated_record[key].to_s] = true @@ -304,7 +305,8 @@ module ActiveRecord polymorph_type = options[:foreign_type] klasses_and_ids = {} - # Construct a mapping from klass to a list of ids to load and a mapping of those ids back to their parent_records + # Construct a mapping from klass to a list of ids to load and a mapping of those ids back + # to their parent_records records.each do |record| if klass = record.send(polymorph_type) klass_id = record.send(primary_key_name) @@ -378,7 +380,7 @@ module ActiveRecord :order => preload_options[:order] || options[:order] } - reflection.klass.unscoped.apply_finder_options(find_options).to_a + reflection.klass.scoped.apply_finder_options(find_options).to_a end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 65daa8ffbe..73c0900c8b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/conversions' +require 'active_support/core_ext/module/remove_method' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -113,7 +114,7 @@ module ActiveRecord autoload :HasOneAssociation, 'active_record/associations/has_one_association' autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' - # Clears out the association cache + # Clears out the association cache. def clear_association_cache #:nodoc: self.class.reflect_on_all_associations.to_a.each do |assoc| instance_variable_set "@#{assoc.name}", nil @@ -121,7 +122,7 @@ module ActiveRecord end private - # Gets the specified association instance if it responds to :loaded?, nil otherwise. + # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) ivar = "@#{name}" if instance_variable_defined?(ivar) @@ -135,10 +136,12 @@ module ActiveRecord instance_variable_set("@#{name}", association) end - # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like - # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are - # specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt> - # methods. Example: + # Associations are a set of macro-like class methods for tying objects together through + # foreign keys. They express relationships like "Project has one Project Manager" + # or "Project belongs to a Portfolio". Each macro adds a number of methods to the + # class which are specialized according to the collection or association symbol and the + # options hash. It works much the same way as Ruby's own <tt>attr*</tt> + # methods. # # class Project < ActiveRecord::Base # belongs_to :portfolio @@ -147,7 +150,8 @@ module ActiveRecord # has_and_belongs_to_many :categories # end # - # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships: + # The project class now has the following methods (and more) to ease the traversal and + # manipulation of its relationships: # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> @@ -158,8 +162,9 @@ module ActiveRecord # # === A word of warning # - # Don't create associations that have the same name as instance methods of ActiveRecord::Base. Since the association - # adds a method with that name to its model, it will override the inherited method and break things. + # Don't create associations that have the same name as instance methods of + # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # its model, it will override the inherited method and break things. # For instance, +attributes+ and +connection+ would be bad choices for association names. # # == Auto-generated methods @@ -269,8 +274,8 @@ module ActiveRecord # # == Is it a +belongs_to+ or +has_one+ association? # - # Both express a 1-1 relationship. The difference is mostly where to place the foreign key, which goes on the table for the class - # declaring the +belongs_to+ relationship. Example: + # Both express a 1-1 relationship. The difference is mostly where to place the foreign + # key, which goes on the table for the class declaring the +belongs_to+ relationship. # # class User < ActiveRecord::Base # # I reference an account. @@ -299,36 +304,44 @@ module ActiveRecord # # == Unsaved objects and associations # - # You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be - # aware of, mostly involving the saving of associated objects. + # You can manipulate objects and associations before they are saved to the database, but + # there is some special behavior you should be aware of, mostly involving the saving of + # associated objects. # - # Unless you set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>, + # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>, # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it # to +true+ will _always_ save the members, whereas setting it to +false+ will # _never_ save the members. # # === One-to-one associations # - # * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in - # order to update their primary keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). - # * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns +false+ and the assignment - # is cancelled. - # * If you wish to assign an object to a +has_one+ association without saving it, use the <tt>association.build</tt> method (documented below). - # * Assigning an object to a +belongs_to+ association does not save the object, since the foreign key field belongs on the parent. It - # does not save the parent either. + # * Assigning an object to a +has_one+ association automatically saves that object and + # the object being replaced (if there is one), in order to update their primary + # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). + # * If either of these saves fail (due to one of the objects being invalid) the assignment + # statement returns +false+ and the assignment is cancelled. + # * If you wish to assign an object to a +has_one+ association without saving it, + # use the <tt>association.build</tt> method (documented below). + # * Assigning an object to a +belongs_to+ association does not save the object, since + # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # - # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically saves that object, except if the parent object - # (the owner of the collection) is not yet stored in the database. - # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) fails, then <tt>push</tt> returns +false+. - # * You can add an object to a collection without automatically saving it by using the <tt>collection.build</tt> method (documented below). - # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically saved when the parent is saved. + # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. + # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) + # fails, then <tt>push</tt> returns +false+. + # * You can add an object to a collection without automatically saving it by using the + # <tt>collection.build</tt> method (documented below). + # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically + # saved when the parent is saved. # # === Association callbacks # - # Similar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get - # triggered when you add an object to or remove an object from an association collection. Example: + # Similar to the normal callbacks that hook into the lifecycle of an Active Record object, + # you can also define callbacks that get triggered when you add an object to or remove an + # object from an association collection. # # class Project # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity @@ -341,19 +354,21 @@ module ActiveRecord # It's possible to stack callbacks by passing them as an array. Example: # # class Project - # has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] + # has_and_belongs_to_many :developers, + # :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] # end # # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. # - # Should any of the +before_add+ callbacks throw an exception, the object does not get added to the collection. Same with - # the +before_remove+ callbacks; if an exception is thrown the object doesn't get removed. + # Should any of the +before_add+ callbacks throw an exception, the object does not get + # added to the collection. Same with the +before_remove+ callbacks; if an exception is + # thrown the object doesn't get removed. # # === Association extensions # - # The proxy objects that control the access to associations can be extended through anonymous modules. This is especially - # beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association. - # Example: + # The proxy objects that control the access to associations can be extended through anonymous + # modules. This is especially beneficial for adding new finders, creators, and other + # factory-type methods that are only used as part of this association. # # class Account < ActiveRecord::Base # has_many :people do @@ -368,7 +383,8 @@ module ActiveRecord # person.first_name # => "David" # person.last_name # => "Heinemeier Hansson" # - # If you need to share the same extensions between many associations, you can use a named extension module. Example: + # If you need to share the same extensions between many associations, you can use a named + # extension module. # # module FindOrCreateByNameExtension # def find_or_create_by_name(name) @@ -385,9 +401,10 @@ module ActiveRecord # has_many :people, :extend => FindOrCreateByNameExtension # end # - # If you need to use multiple named extension modules, you can specify an array of modules with the <tt>:extend</tt> option. - # In the case of name conflicts between methods in the modules, methods in modules later in the array supercede - # those earlier in the array. Example: + # If you need to use multiple named extension modules, you can specify an array of modules + # with the <tt>:extend</tt> option. + # In the case of name conflicts between methods in the modules, methods in modules later + # in the array supercede those earlier in the array. # # class Account < ActiveRecord::Base # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] @@ -398,12 +415,14 @@ module ActiveRecord # # * +proxy_owner+ - Returns the object the association is part of. # * +proxy_reflection+ - Returns the reflection object that describes the association. - # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. + # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or + # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. # # === Association Join Models # - # Has Many associations can be configured with the <tt>:through</tt> option to use an explicit join model to retrieve the data. This - # operates similarly to a +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, + # Has Many associations can be configured with the <tt>:through</tt> option to use an + # explicit join model to retrieve the data. This operates similarly to a + # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, # callbacks, and extra attributes on the join model. Consider the following schema: # # class Author < ActiveRecord::Base @@ -417,7 +436,7 @@ module ActiveRecord # end # # @author = Author.find :first - # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to. + # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to # @author.books # selects all books by using the Authorship join model # # You can also go through a +has_many+ association on the join model: @@ -438,7 +457,7 @@ module ActiveRecord # # @firm = Firm.find :first # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm - # @firm.invoices # selects all invoices by going through the Client join model. + # @firm.invoices # selects all invoices by going through the Client join model # # Similarly you can go through a +has_one+ association on the join model: # @@ -460,16 +479,18 @@ module ActiveRecord # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # - # An important caveat with going through +has_one+ or +has_many+ associations on the join model is that these associations are - # *read-only*. For example, the following would not work following the previous example: + # An important caveat with going through +has_one+ or +has_many+ associations on the + # join model is that these associations are *read-only*. For example, the following + # would not work following the previous example: # - # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around. + # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around # @group.avatars.delete(@group.avatars.last) # so would this # # === Polymorphic Associations # - # Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they - # specify an interface that a +has_many+ association must adhere to. + # Polymorphic associations on models are not restricted on what types of models they + # can be associated with. Rather, they specify an interface that a +has_many+ association + # must adhere to. # # class Asset < ActiveRecord::Base # belongs_to :attachable, :polymorphic => true @@ -481,13 +502,16 @@ module ActiveRecord # # @asset.attachable = @post # - # This works by using a type column in addition to a foreign key to specify the associated record. In the Asset example, you'd need - # an +attachable_id+ integer column and an +attachable_type+ string column. + # This works by using a type column in addition to a foreign key to specify the associated + # record. In the Asset example, you'd need an +attachable_id+ integer column and an + # +attachable_type+ string column. # - # Using polymorphic associations in combination with single table inheritance (STI) is a little tricky. In order - # for the associations to work as expected, ensure that you store the base model for the STI models in the - # type column of the polymorphic association. To continue with the asset example above, suppose there are guest posts - # and member posts that use the posts table for STI. In this case, there must be a +type+ column in the posts table. + # Using polymorphic associations in combination with single table inheritance (STI) is + # a little tricky. In order for the associations to work as expected, ensure that you + # store the base model for the STI models in the type column of the polymorphic + # association. To continue with the asset example above, suppose there are guest posts + # and member posts that use the posts table for STI. In this case, there must be a +type+ + # column in the posts table. # # class Asset < ActiveRecord::Base # belongs_to :attachable, :polymorphic => true @@ -510,9 +534,10 @@ module ActiveRecord # # == Caching # - # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically - # instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without - # worrying too much about performance at the first go. Example: + # All of the methods are built on a simple caching principle that will keep the result + # of the last query around unless specifically instructed not to. The cache is even + # shared across methods to make it even cheaper to use the macro-added methods without + # worrying too much about performance at the first go. # # project.milestones # fetches milestones from the database # project.milestones.size # uses the milestone cache @@ -522,9 +547,10 @@ module ActiveRecord # # == Eager loading of associations # - # Eager loading is a way to find objects of a certain class and a number of named associations. This is - # one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each need to display their author - # triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 2. Example: + # Eager loading is a way to find objects of a certain class and a number of named associations. + # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # posts that each need to display their author triggers 101 database queries. Through the + # use of eager loading, the 101 queries can be reduced to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -539,44 +565,55 @@ module ActiveRecord # puts "Last comment on: " + post.comments.first.created_on # end # - # To iterate over these one hundred posts, we'll generate 201 database queries. Let's first just optimize it for retrieving the author: + # To iterate over these one hundred posts, we'll generate 201 database queries. Let's + # first just optimize it for retrieving the author: # # for post in Post.find(:all, :include => :author) # - # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> symbol. After loading the posts, find - # will collect the +author_id+ from each one and load all the referenced authors with one query. Doing so will cut down the number of queries from 201 to 102. + # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> + # symbol. After loading the posts, find will collect the +author_id+ from each one and load + # all the referenced authors with one query. Doing so will cut down the number of queries + # from 201 to 102. # # We can improve upon the situation further by referencing both associations in the finder with: # # for post in Post.find(:all, :include => [ :author, :comments ]) # - # This will load all comments with a single query. This reduces the total number of queries to 3. More generally the number of queries - # will be 1 plus the number of associations named (except if some of the associations are polymorphic +belongs_to+ - see below). + # This will load all comments with a single query. This reduces the total number of queries + # to 3. More generally the number of queries will be 1 plus the number of associations + # named (except if some of the associations are polymorphic +belongs_to+ - see below). # # To include a deep hierarchy of associations, use a hash: # # for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ]) # - # That'll grab not only all the comments but all their authors and gravatar pictures. You can mix and match - # symbols, arrays and hashes in any combination to describe the associations you want to load. + # That'll grab not only all the comments but all their authors and gravatar pictures. + # You can mix and match symbols, arrays and hashes in any combination to describe the + # associations you want to load. # - # All of this power shouldn't fool you into thinking that you can pull out huge amounts of data with no performance penalty just because you've reduced - # the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no - # catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above. + # All of this power shouldn't fool you into thinking that you can pull out huge amounts + # of data with no performance penalty just because you've reduced the number of queries. + # The database still needs to send all the data to Active Record and it still needs to + # be processed. So it's no catch-all for performance problems, but it's a great way to + # cut down on the number of queries in a situation as the one described above. # - # Since only one table is loaded at a time, conditions or orders cannot reference tables other than the main one. If this is the case - # Active Record falls back to the previously used LEFT OUTER JOIN based strategy. For example + # Since only one table is loaded at a time, conditions or orders cannot reference tables + # other than the main one. If this is the case Active Record falls back to the previously + # used LEFT OUTER JOIN based strategy. For example # # Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true]) # - # This will result in a single SQL query with joins along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and - # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions like this can have unintended consequences. - # In the above example posts with no approved comments are not returned at all, because the conditions apply to the SQL statement as a whole - # and not just to the association. You must disambiguate column references for this fallback to happen, for example + # This will result in a single SQL query with joins along the lines of: + # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and + # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions + # like this can have unintended consequences. + # In the above example posts with no approved comments are not returned at all, because + # the conditions apply to the SQL statement as a whole and not just to the association. + # You must disambiguate column references for this fallback to happen, for example # <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not. # - # If you do want eager load only some members of an association it is usually more natural to <tt>:include</tt> an association - # which has conditions defined on it: + # If you do want eager load only some members of an association it is usually more natural + # to <tt>:include</tt> an association which has conditions defined on it: # # class Post < ActiveRecord::Base # has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true] @@ -584,9 +621,11 @@ module ActiveRecord # # Post.find(:all, :include => :approved_comments) # - # This will load posts and eager load the +approved_comments+ association, which contains only those comments that have been approved. + # This will load posts and eager load the +approved_comments+ association, which contains + # only those comments that have been approved. # - # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, returning all the associated objects: + # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, + # returning all the associated objects: # # class Picture < ActiveRecord::Base # has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10 @@ -594,8 +633,8 @@ module ActiveRecord # # Picture.find(:first, :include => :most_recent_comments).most_recent_comments # => returns all associated comments. # - # When eager loaded, conditions are interpolated in the context of the model class, not the model instance. Conditions are lazily interpolated - # before the actual model exists. + # When eager loaded, conditions are interpolated in the context of the model class, not + # the model instance. Conditions are lazily interpolated before the actual model exists. # # Eager loading is supported with polymorphic associations. # @@ -607,17 +646,21 @@ module ActiveRecord # # Address.find(:all, :include => :addressable) # - # This will execute one query to load the addresses and load the addressables with one query per addressable type. - # For example if all the addressables are either of class Person or Company then a total of 3 queries will be executed. The list of - # addressable types to load is determined on the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. The reason is that the parent - # model's type is a column value so its corresponding table name cannot be put in the +FROM+/+JOIN+ clauses of that query. + # This will execute one query to load the addresses and load the addressables with one + # query per addressable type. + # For example if all the addressables are either of class Person or Company then a total + # of 3 queries will be executed. The list of addressable types to load is determined on + # the back of the addresses loaded. This is not supported if Active Record has to fallback + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. + # The reason is that the parent model's type is a column value so its corresponding table + # name cannot be put in the +FROM+/+JOIN+ clauses of that query. # # == Table Aliasing # - # Active Record uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once, - # the standard table name is used. The second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. Indexes are appended - # for any more successive uses of the table name. + # Active Record uses table aliasing in the case that a table is referenced multiple times + # in a join. If a table is referenced only once, the standard table name is used. The + # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. + # Indexes are appended for any more successive uses of the table name. # # Post.find :all, :joins => :comments # # => SELECT ... FROM posts INNER JOIN comments ON ... @@ -650,7 +693,8 @@ module ActiveRecord # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2 # - # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table names will take precedence over the eager associations: + # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table + # names will take precedence over the eager associations: # # Post.find :all, :joins => :comments, :joins => "inner join comments ..." # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ... @@ -659,7 +703,8 @@ module ActiveRecord # INNER JOIN comments special_comments_posts ... # INNER JOIN comments ... # - # Table aliases are automatically truncated according to the maximum length of table identifiers according to the specific database. + # Table aliases are automatically truncated according to the maximum length of table identifiers + # according to the specific database. # # == Modules # @@ -675,9 +720,10 @@ module ActiveRecord # end # end # - # When <tt>Firm#clients</tt> is called, it will in turn call <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. - # If you want to associate with a class in another module scope, this can be done by specifying the complete class name. - # Example: + # When <tt>Firm#clients</tt> is called, it will in turn call + # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. + # If you want to associate with a class in another module scope, this can be done by + # specifying the complete class name. # # module MyApplication # module Business @@ -693,8 +739,8 @@ module ActiveRecord # # == Bi-directional associations # - # When you specify an association there is usually an association on the associated model that specifies the same - # relationship in reverse. For example, with the following models: + # When you specify an association there is usually an association on the associated model + # that specifies the same relationship in reverse. For example, with the following models: # # class Dungeon < ActiveRecord::Base # has_many :traps @@ -709,9 +755,11 @@ module ActiveRecord # belongs_to :dungeon # end # - # The +traps+ association on +Dungeon+ and the the +dungeon+ association on +Trap+ are the inverse of each other and the - # inverse of the +dungeon+ association on +EvilWizard+ is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record doesn't know anything about these inverse relationships and so no object loading optimisation is possible. For example: + # The +traps+ association on +Dungeon+ and the the +dungeon+ association on +Trap+ are + # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ + # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, + # Active Record doesn't know anything about these inverse relationships and so no object + # loading optimisation is possible. For example: # # d = Dungeon.first # t = d.traps.first @@ -719,9 +767,11 @@ module ActiveRecord # d.level = 10 # d.level == t.dungeon.level # => false # - # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to the same object data from the database, but are - # actually different in-memory copies of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell - # Active Record about inverse relationships and it will optimise object loading. For example, if we changed our model definitions to: + # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to + # the same object data from the database, but are actually different in-memory copies + # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell + # Active Record about inverse relationships and it will optimise object loading. For + # example, if we changed our model definitions to: # # class Dungeon < ActiveRecord::Base # has_many :traps, :inverse_of => :dungeon @@ -736,8 +786,8 @@ module ActiveRecord # belongs_to :dungeon, :inverse_of => :evil_wizard # end # - # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same in-memory instance and our final <tt>d.level == t.dungeon.level</tt> - # will return +true+. + # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same + # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+. # # There are limitations to <tt>:inverse_of</tt> support: # @@ -747,13 +797,13 @@ module ActiveRecord # # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt> # - # If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll - # get an <tt>ActiveRecord::AssociationTypeMismatch</tt>. + # If you attempt to assign an object to an association that doesn't match the inferred + # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>. # # == Options # - # All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones - # possible. + # All of the association macros can be specialized through options. This makes cases + # more complex than the simple and guessable ones possible. module ClassMethods # Specifies a one-to-many association. The following methods for retrieval and query of # collections of associated objects will be added: @@ -827,20 +877,22 @@ module ActiveRecord # === Supported options # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but - # if the real class name is SpecialProduct, you'll have to specify it with this option. + # from the association name. So <tt>has_many :products</tt> will by default be linked + # to the Product class, but if the real class name is SpecialProduct, you'll have to + # specify it with this option. # [:conditions] # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash - # is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> - # or <tt>@blog.posts.build</tt>. + # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from + # the association are scoped if a hash is used. + # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published + # posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>. # [:order] # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt>. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id" - # as the default <tt>:foreign_key</tt>. + # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ + # association will use "person_id" as the default <tt>:foreign_key</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:dependent] @@ -854,10 +906,12 @@ module ActiveRecord # # [:finder_sql] # Specify a complete SQL statement to fetch the association. This is a good way to go for complex - # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added. + # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ + # is _not_ added. # [:counter_sql] # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is - # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by + # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. # [:extend] # Specify a named module for extending the proxy. See "Association extensions". # [:include] @@ -865,25 +919,31 @@ module ActiveRecord # [:group] # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. + # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> + # returns. Uses the <tt>HAVING</tt> SQL-clause. # [:limit] # An integer determining the limit on the number of rows that should be returned. # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # An integer determining the offset from where the rows should be fetched. So at 5, + # it would skip the first 4 rows. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join - # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you, for example, want to do a join but not include the joined columns. Do not forget + # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] - # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> - # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt> - # <tt>has_one</tt> or <tt>has_many</tt> association on the join model. The collection of join models can be managed via the collection - # API. For example, new join models are created for newly associated objects, and if some are gone their rows are deleted (directly, + # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt> + # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You + # can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, <tt>has_one</tt> + # or <tt>has_many</tt> association on the join model. The collection of join models + # can be managed via the collection API. For example, new join models are created for + # newly associated objects, and if some are gone their rows are deleted (directly, # no destroy callbacks are triggered). # [:source] - # Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be - # inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or + # Specifies the source association name used by <tt>has_many :through</tt> queries. + # Only use it if the name cannot be inferred from the association. + # <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. # [:source_type] # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source @@ -895,12 +955,14 @@ module ActiveRecord # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. # [:autosave] - # If true, always save the associated objects or destroy them if marked for destruction, when saving the parent object. + # If true, always save the associated objects or destroy them if marked for destruction, + # when saving the parent object. # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_many</tt> - # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. + # Specifies the name of the <tt>belongs_to</tt> association on the associated object + # that is the inverse of this <tt>has_many</tt> association. Does not work in combination + # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: @@ -974,19 +1036,20 @@ module ActiveRecord # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash - # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create an enabled account with <tt>@company.create_account</tt> - # or <tt>@company.build_account</tt>. + # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create + # an enabled account with <tt>@company.create_account</tt> or <tt>@company.build_account</tt>. # [:order] # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt>. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to - # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated - # object's foreign key is set to +NULL+. Also, association is assigned. + # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. + # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+. + # Also, association is assigned. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association will use "person_id" - # as the default <tt>:foreign_key</tt>. + # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association + # will use "person_id" as the default <tt>:foreign_key</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:include] @@ -994,15 +1057,18 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join - # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, + # you want to do a join but not include the joined columns. Do not forget to include the + # primary and foreign keys, otherwise it will raise an error. # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> - # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a - # <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model. + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> + # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You + # can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt> + # association on the join model. # [:source] - # Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be - # inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a + # Specifies the source association name used by <tt>has_one :through</tt> queries. + # Only use it if the name cannot be inferred from the association. + # <tt>has_one :favorite, :through => :favorites</tt> will look for a # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source @@ -1012,17 +1078,19 @@ module ActiveRecord # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. - # If false, never save or destroy the associated object. + # If true, always save the associated object or destroy it if marked for destruction, + # when saving the parent object. If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_one</tt> - # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options. + # Specifies the name of the <tt>belongs_to</tt> association on the associated object + # that is the inverse of this <tt>has_one</tt> association. Does not work in combination + # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card - # has_one :credit_card, :dependent => :nullify # updates the associated records foreign key value to NULL rather than destroying it + # has_one :credit_card, :dependent => :nullify # updates the associated records foreign + # # key value to NULL rather than destroying it # has_one :last_comment, :class_name => "Comment", :order => "posted_on" # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" # has_one :attachment, :as => :attachable @@ -1084,27 +1152,34 @@ module ActiveRecord # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>authorized = 1</tt>. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join - # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed + # if, for example, you want to do a join but not include the joined columns. Do not + # forget to include the primary and foreign keys, otherwise it will raise an error. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use - # "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt> - # will use a foreign key of "favorite_person_id". + # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> + # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, + # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key + # of "favorite_person_id". # [:primary_key] - # Specify the method that returns the primary key of associated object used for the association. By default this is id. + # Specify the method that returns the primary key of associated object used for the association. + # By default this is id. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to - # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when - # <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave + # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. + # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with + # a <tt>has_many</tt> relationship on another class because of the potential to leave # orphaned records behind. # [:counter_cache] # Caches the number of belonging objects on the associate class through the use of +increment_counter+ - # and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's - # destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) - # is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing - # a column name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) - # Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+. + # and +decrement_counter+. The counter cache is incremented when an object of this + # class is created and decremented when it's destroyed. This requires that a column + # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) + # is used on the associate class (such as a Post class). You can also specify a custom counter + # cache column by providing a column name instead of a +true+/+false+ value to this + # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) + # Note: Specifying a counter cache will add it to that model's list of readonly attributes + # using +attr_readonly+. # [:include] # Specify second-order associations that should be eager loaded when this object is loaded. # [:polymorphic] @@ -1116,15 +1191,18 @@ module ActiveRecord # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. + # If true, always save the associated object or destroy it if marked for destruction, when + # saving the parent object. # If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or - # destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute. + # If true, the associated object will be touched (the updated_at/on attributes set to now) + # when this record is either saved or destroyed. If you specify a symbol, that attribute + # will be updated with the current time instead of the updated_at/on attribute. # [:inverse_of] - # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated object that is the inverse of this <tt>belongs_to</tt> - # association. Does not work in combination with the <tt>:polymorphic</tt> options. + # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated + # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in + # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: @@ -1158,9 +1236,10 @@ module ActiveRecord # Specifies a many-to-many relationship with another class. This associates two classes via an # intermediate join table. Unless the join table is explicitly specified as an option, it is # guessed using the lexical order of the class names. So a join between Developer and Project - # will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence - # is calculated using the <tt><</tt> operator for String. This means that if the strings are of different lengths, - # and the strings are equal when compared up to the shortest length, then the longer string is considered of higher + # will give the default join table name of "developers_projects" because "D" outranks "P". + # Note that this precedence is calculated using the <tt><</tt> operator for String. This + # means that if the strings are of different lengths, and the strings are equal when compared + # up to the shortest length, then the longer string is considered of higher # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the @@ -1182,9 +1261,10 @@ module ActiveRecord # end # end # - # Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through - # +has_and_belongs_to_many+ associations. Records returned from join tables with additional attributes will be marked as - # readonly (because we can't save changes to the additional attributes). It's strongly recommended that you upgrade any + # Deprecated: Any additional fields added to the join table will be placed as attributes when + # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join + # tables with additional attributes will be marked as readonly (because we can't save changes + # to the additional attributes). It's strongly recommended that you upgrade any # associations with attributes to a real join model (see introduction). # # Adds the following methods for retrieval and query: @@ -1224,7 +1304,8 @@ module ActiveRecord # with +attributes+ and linked to this object through the join table, but has not yet been saved. # [collection.create(attributes = {})] # Returns a new object of the collection type that has been instantiated - # with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation). + # with +attributes+, linked to this object through the join table, and that has already been + # saved (if it passed the validation). # # (+collection+ is replaced with the symbol passed as the first argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.) @@ -1259,8 +1340,9 @@ module ActiveRecord # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association - # to Project will use "person_id" as the default <tt>:foreign_key</tt>. + # of this class in lower-case and "_id" suffixed. So a Person class that makes + # a +has_and_belongs_to_many+ association to Project will use "person_id" as the + # default <tt>:foreign_key</tt>. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. @@ -1268,7 +1350,8 @@ module ActiveRecord # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used. + # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are + # scoped if a hash is used. # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> # or <tt>@blog.posts.build</tt>. # [:order] @@ -1280,7 +1363,8 @@ module ActiveRecord # Overwrite the default generated SQL statement used to fetch the association with a manual statement # [:counter_sql] # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is - # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by + # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. # [:delete_sql] # Overwrite the default generated SQL statement used to remove links between the associated # classes with a manual statement. @@ -1294,20 +1378,24 @@ module ActiveRecord # [:group] # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. + # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. + # Uses the <tt>HAVING</tt> SQL-clause. # [:limit] # An integer determining the limit on the number of rows that should be returned. # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # An integer determining the offset from where the rows should be fetched. So at 5, + # it would skip the first 4 rows. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join - # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, + # you want to do a join but not include the joined columns. Do not forget to include the primary + # and foreign keys, otherwise it will raise an error. # [:readonly] # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. # [:autosave] - # If true, always save the associated objects or destroy them if marked for destruction, when saving the parent object. + # If true, always save the associated objects or destroy them if marked for destruction, when + # saving the parent object. # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # @@ -1354,7 +1442,7 @@ module ActiveRecord end def association_accessor_methods(reflection, association_proxy_class) - define_method(reflection.name) do |*params| + redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = association_instance_get(reflection.name) @@ -1371,12 +1459,12 @@ module ActiveRecord association.target.nil? ? nil : association end - define_method("loaded_#{reflection.name}?") do + redefine_method("loaded_#{reflection.name}?") do association = association_instance_get(reflection.name) association && association.loaded? end - - define_method("#{reflection.name}=") do |new_value| + + redefine_method("#{reflection.name}=") do |new_value| association = association_instance_get(reflection.name) if association.nil? || association.target != new_value @@ -1386,8 +1474,8 @@ module ActiveRecord association.replace(new_value) association_instance_set(reflection.name, new_value.nil? ? nil : association) end - - define_method("set_#{reflection.name}_target") do |target| + + redefine_method("set_#{reflection.name}_target") do |target| return if target.nil? and association_proxy_class == BelongsToAssociation association = association_proxy_class.new(self, reflection) association.target = target @@ -1396,7 +1484,7 @@ module ActiveRecord end def collection_reader_method(reflection, association_proxy_class) - define_method(reflection.name) do |*params| + redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = association_instance_get(reflection.name) @@ -1409,8 +1497,8 @@ module ActiveRecord association end - - define_method("#{reflection.name.to_s.singularize}_ids") do + + redefine_method("#{reflection.name.to_s.singularize}_ids") do if send(reflection.name).loaded? || reflection.options[:finder_sql] send(reflection.name).map(&:id) else @@ -1430,22 +1518,24 @@ module ActiveRecord collection_reader_method(reflection, association_proxy_class) if writer - define_method("#{reflection.name}=") do |new_value| + redefine_method("#{reflection.name}=") do |new_value| # Loads proxy class instance (defined in collection_reader_method) if not already loaded association = send(reflection.name) association.replace(new_value) association end - define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| - ids = (new_value || []).reject { |nid| nid.blank? }.map(&:to_i) + redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| + pk_column = reflection.primary_key_column + ids = (new_value || []).reject { |nid| nid.blank? } + ids.map!{ |i| pk_column.type_cast(i) } send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids)) end end end def association_constructor_method(constructor, reflection, association_proxy_class) - define_method("#{constructor}_#{reflection.name}") do |*params| + redefine_method("#{constructor}_#{reflection.name}") do |*params| attributees = params.first unless params.empty? replace_existing = params[1].nil? ? true : params[1] association = association_instance_get(reflection.name) @@ -1486,8 +1576,8 @@ module ActiveRecord end def add_touch_callbacks(reflection, touch_attribute) - method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do + method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}" + redefine_method(method_name) do association = send(reflection.name) if touch_attribute == true @@ -1497,20 +1587,18 @@ module ActiveRecord end end after_save(method_name) + after_touch(method_name) after_destroy(method_name) end # Creates before_destroy callback methods that nullify, delete or destroy # has_many associated objects, according to the defined :dependent rule. - # If the association is marked as :dependent => :restrict, create a callback - # that prevents deleting entirely. # - # See HasManyAssociation#delete_records. Dependent associations - # delete children, otherwise foreign key is set to NULL. - # See HasManyAssociation#delete_records. Dependent associations - # delete children if the option is set to :destroy or :delete_all, set the - # foreign key to NULL if the option is set to :nullify, and do not touch the - # child records if the option is set to :restrict. + # See HasManyAssociation#delete_records for more information. In general + # - delete children if the option is set to :destroy or :delete_all + # - set the foreign key to NULL if the option is set to :nullify + # - do not delete the parent record if there is any child record if the + # option is set to :restrict # # The +extra_conditions+ parameter, which is not used within the main # Active Record codebase, is meant to allow plugins to define extra @@ -1761,7 +1849,7 @@ module ActiveRecord def graft(*associations) associations.each do |association| join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self), association.join_class) + build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class) end self end @@ -1801,9 +1889,7 @@ module ActiveRecord case associations when Symbol, String reflection = base.reflections[associations] - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end + remove_uniq_by_reflection(reflection, records) when Array associations.each do |association| remove_duplicate_results!(base, records, association) @@ -1811,6 +1897,7 @@ module ActiveRecord when Hash associations.keys.each do |name| reflection = base.reflections[name] + remove_uniq_by_reflection(reflection, records) parent_records = [] records.each do |record| @@ -1829,6 +1916,7 @@ module ActiveRecord end protected + def build(associations, parent = nil, join_class = Arel::InnerJoin) parent ||= @joins.last case associations @@ -1851,6 +1939,12 @@ module ActiveRecord end end + def remove_uniq_by_reflection(reflection, records) + if reflection && reflection.collection? + records.each { |record| record.send(reflection.name).target.uniq! } + end + end + def build_join_association(reflection, parent) JoinAssociation.new(reflection, self, parent) end @@ -1965,7 +2059,7 @@ module ActiveRecord end class JoinAssociation < JoinBase # :nodoc: - attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name + attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection def initialize(reflection, join_dependency, parent = nil) @@ -1982,6 +2076,7 @@ module ActiveRecord @parent_table_name = parent.active_record.table_name @aliased_table_name = aliased_table_name_for(table_name) @join = nil + @join_class = Arel::InnerJoin if reflection.macro == :has_and_belongs_to_many @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") @@ -2004,10 +2099,6 @@ module ActiveRecord end end - def join_class - @join_class ||= Arel::InnerJoin - end - def with_join_class(join_class) @join_class = join_class self diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 615b7d2719..b5159eead3 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -183,10 +183,13 @@ module ActiveRecord # descendant's +construct_sql+ method will have set :counter_sql automatically. # Otherwise, construct options and pass them with scope to the target class's +count+. def count(column_name = nil, options = {}) - if @reflection.options[:counter_sql] + column_name, options = nil, column_name if column_name.is_a?(Hash) + + if @reflection.options[:counter_sql] && !options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + elsif @reflection.options[:counter_sql] @reflection.klass.count_by_sql(@counter_sql) else - column_name, options = nil, column_name if column_name.is_a?(Hash) if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. @@ -215,9 +218,9 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - remove_records(records) do |records, old_records| + remove_records(records) do |_records, old_records| delete_records(old_records) if old_records.any? - records.each { |record| @target.delete(record) } + _records.each { |record| @target.delete(record) } end end @@ -228,7 +231,7 @@ module ActiveRecord # ignoring the +:dependent+ option. def destroy(*records) records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)} - remove_records(records) do |records, old_records| + remove_records(records) do |_records, old_records| old_records.each { |record| record.destroy } end @@ -393,11 +396,12 @@ module ActiveRecord if @target.is_a?(Array) && @target.any? @target = find_target.map do |f| i = @target.index(f) - t = @target.delete_at(i) if i - if t && t.changed? - t + if i + @target.delete_at(i).tap do |t| + keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) + t.attributes = f.attributes.except(*keys) + end else - f.mark_for_destruction if t && t.marked_for_destruction? f end end + @target @@ -415,15 +419,10 @@ module ActiveRecord end def method_missing(method, *args) - case method.to_s - when 'find_or_create' - return find(:first, :conditions => args.first) || create(args.first) - when /^find_or_create_by_(.*)$/ - rest = $1 - return send("find_by_#{rest}", *args) || - method_missing("create_by_#{rest}", *args) - when /^create_by_(.*)$/ - return create Hash[$1.split('_and_').zip(args)] + match = DynamicFinderMatch.match(method) + if match && match.creator? + attributes = match.attribute_names + return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) end if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) @@ -479,7 +478,11 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? @target ||= [] unless loaded? - @target << record unless @reflection.options[:uniq] && @target.include?(record) + if index = @target.index(record) + @target[index] = record + else + @target << record + end callback(:after_add, record) set_inverse_instance(record, @owner) record diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c2a6495db5..4558872a2b 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -22,7 +22,7 @@ module ActiveRecord else raise_on_type_mismatch(record) - if counter_cache_name && !@owner.new_record? + if counter_cache_name && !@owner.new_record? && record.id != @owner[@reflection.primary_key_name] @reflection.klass.increment_counter(counter_cache_name, record.id) @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index c989c3536d..bec123e7a2 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -45,17 +45,23 @@ module ActiveRecord if @reflection.options[:insert_sql] @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) else - relation = Arel::Table.new(@reflection.options[:join_table]) + relation = Arel::Table.new(@reflection.options[:join_table]) + timestamps = record_timestamp_columns(record) + timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? + attributes = columns.inject({}) do |attrs, column| - case column.name.to_s + name = column.name + case name.to_s when @reflection.primary_key_name.to_s - attrs[relation[column.name]] = owner_quoted_id + attrs[relation[name]] = @owner.id when @reflection.association_foreign_key.to_s - attrs[relation[column.name]] = record.quoted_id + attrs[relation[name]] = record.id + when *timestamps + attrs[relation[name]] = timezone else - if record.has_attribute?(column.name) - value = @owner.send(:quote_value, record[column.name], column) - attrs[relation[column.name]] = value unless value.nil? + if record.has_attribute?(name) + value = @owner.send(:quote_value, record[name], column) + attrs[relation[name]] = value unless value.nil? end end attrs @@ -100,9 +106,10 @@ module ActiveRecord :limit => @reflection.options[:limit] } } end - # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select - # clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has - # an id column. This will then overwrite the id column of the records coming back. + # Join tables with additional columns on top of the two foreign keys must be considered + # ambiguous unless a select clause has been explicitly defined. Otherwise you can get + # broken records back, if, for example, the join column also has an id column. This will + # then overwrite the id column of the records coming back. def finding_with_ambiguous_select?(select_clause) !select_clause && columns.size != 2 end @@ -117,6 +124,14 @@ module ActiveRecord build_record(attributes, &block) end end + + def record_timestamp_columns(record) + if record.record_timestamps + record.send(:all_timestamp_attributes).map(&:to_s) + else + [] + end + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index d74fb7c702..c33bc6aa47 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -24,7 +24,7 @@ module ActiveRecord # If the association has a counter cache it gets that value. Otherwise # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if # there's one. Some configuration options like :group make it impossible - # to do a SQL count, in those cases the array count will be used. + # to do an SQL count, in those cases the array count will be used. # # That does not depend on whether the collection has already been loaded # or not. The +size+ method is the one that takes the loaded flag into diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 17f850756f..608b1c741a 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -24,9 +24,10 @@ module ActiveRecord end end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and - # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero, - # and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length. + # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been + # loaded and calling collection.size if it has. If it's more likely than not that the collection does + # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer + # SELECT query if you use #length. def size return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? return @target.size if loaded? diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 22e1033a9d..cabb33c4a8 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -35,7 +35,7 @@ module ActiveRecord @owner.class.base_class.name.to_s, reflection.klass.columns_hash["#{as}_type"]) } elsif reflection.macro == :belongs_to - { reflection.klass.primary_key => @owner[reflection.primary_key_name] } + { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) } else { reflection.primary_key_name => owner_quoted_id } end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 783d61383b..8f0aacba42 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -14,7 +14,8 @@ module ActiveRecord module ClassMethods protected # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. + # This enhanced read method automatically converts the UTC time stored in the database to the time + # zone stored in Time.zone. def define_method_attribute(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index e31acac050..7a2de3bf80 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -14,8 +14,8 @@ module ActiveRecord end end - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float - # columns are turned into +nil+. + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings + # for fixnum and float columns are turned into +nil+. def write_attribute(attr_name, value) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 7517896235..2c7afe3c9f 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -4,14 +4,13 @@ module ActiveRecord # = Active Record Autosave Association # # AutosaveAssociation is a module that takes care of automatically saving - # your associations when the parent is saved. In addition to saving, it - # also destroys any associations that were marked for destruction. + # associacted records when parent is saved. In addition to saving, it + # also destroys any associated records that were marked for destruction. # (See mark_for_destruction and marked_for_destruction?) # # Saving of the parent, its associations, and the destruction of marked # associations, all happen inside 1 transaction. This should never leave the - # database in an inconsistent state after, for instance, mass assigning - # attributes and saving them. + # database in an inconsistent state. # # If validations for any of the associations fail, their error messages will # be applied to the parent. @@ -21,8 +20,6 @@ module ActiveRecord # # === One-to-one Example # - # Consider a Post model with one Author: - # # class Post # has_one :author, :autosave => true # end @@ -155,11 +152,12 @@ module ActiveRecord CODE end - # Adds a validate and save callback for the association as specified by + # Adds validation and save callbacks for the association as specified by # the +reflection+. # - # For performance reasons, we don't check whether to validate at runtime, - # but instead only define the method and callback when needed. However, + # For performance reasons, we don't check whether to validate at runtime. + # However the validation and callback methods are lazy and those methods + # get created when they are invoked for the very first time. However, # this can change, for instance, when using nested attributes, which is # called _after_ the association has been defined. Since we don't want # the callbacks to get defined multiple times, there are guards that @@ -197,14 +195,15 @@ module ActiveRecord end end - # Reloads the attributes of the object as usual and removes a mark for destruction. + # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag. def reload(options = nil) @marked_for_destruction = false super end # Marks this record to be destroyed as part of the parents save transaction. - # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called. + # This does _not_ actually destroy the record instantly, rather child record will be destroyed + # when <tt>parent.save</tt> is called. # # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. def mark_for_destruction @@ -249,7 +248,7 @@ module ActiveRecord end # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is - # turned on for the association specified by +reflection+. + # turned on for the association. def validate_single_association(reflection) if (association = association_instance_get(reflection.name)) && !association.target.nil? association_valid?(reflection, association) @@ -357,14 +356,9 @@ module ActiveRecord end end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled - # on the association. + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. # - # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + # In addition, it will destroy the association if it was marked for destruction. def save_belongs_to_association(reflection) if (association = association_instance_get(reflection.name)) && !association.destroyed? autosave = reflection.options[:autosave] @@ -377,10 +371,6 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) self[reflection.primary_key_name] = association_id - # TODO: Removing this code doesn't seem to matter... - if reflection.options[:polymorphic] - self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s - end end saved if autosave @@ -388,4 +378,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c78060c956..8da4fbcba7 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -26,17 +26,19 @@ require 'active_record/log_subscriber' module ActiveRecord #:nodoc: # = Active Record # - # Active Record objects don't specify their attributes directly, but rather infer them from the table definition with - # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change - # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain + # Active Record objects don't specify their attributes directly, but rather infer them from + # the table definition with which they're linked. Adding, removing, and changing attributes + # and their type is done directly in the database. Any change is instantly reflected in the + # Active Record objects. The mapping that binds a given Active Record class to a certain # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones. # # See the mapping rules in table_name and the full example in link:files/README.html for more insight. # # == Creation # - # Active Records accept constructor parameters either in a hash or as a block. The hash method is especially useful when - # you're receiving the data from somewhere else, like an HTTP request. It works like this: + # Active Records accept constructor parameters either in a hash or as a block. The hash + # method is especially useful when you're receiving the data from somewhere else, like an + # HTTP request. It works like this: # # user = User.new(:name => "David", :occupation => "Code Artist") # user.name # => "David" @@ -75,14 +77,17 @@ module ActiveRecord #:nodoc: # end # end # - # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection - # attacks if the <tt>user_name</tt> and +password+ parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and - # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query, - # which will ensure that an attacker can't escape the query and fake the login (or worse). + # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query + # and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+ + # parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and + # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ + # before inserting them in the query, which will ensure that an attacker can't escape the + # query and fake the login (or worse). # - # When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth - # question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing - # the question marks with symbols and supplying a hash with values for the matching symbol keys: + # When using multiple parameters in the conditions, it can easily become hard to read exactly + # what the fourth or fifth question mark is supposed to represent. In those cases, you can + # resort to named bind variables instead. That's done by replacing the question marks with + # symbols and supplying a hash with values for the matching symbol keys: # # Company.where( # "id = :id AND name = :name AND division = :division AND created_at > :accounting_date", @@ -103,18 +108,19 @@ module ActiveRecord #:nodoc: # # Student.where(:grade => [9,11,12]) # - # When joining tables, nested hashes or keys written in the form 'table_name.column_name' can be used to qualify the table name of a - # particular condition. For instance: + # When joining tables, nested hashes or keys written in the form 'table_name.column_name' + # can be used to qualify the table name of a particular condition. For instance: # # Student.joins(:schools).where(:schools => { :type => 'public' }) # Student.joins(:schools).where('schools.type' => 'public' ) # # == Overwriting default accessors # - # All column values are automatically available through basic accessors on the Active Record object, but sometimes you - # want to specialize this behavior. This can be done by overwriting the default accessors (using the same - # name as the attribute) and calling <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually change things. - # Example: + # All column values are automatically available through basic accessors on the Active Record + # object, but sometimes you want to specialize this behavior. This can be done by overwriting + # the default accessors (using the same name as the attribute) and calling + # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually + # change things. # # class Song < ActiveRecord::Base # # Uses an integer of seconds to hold the length of the song @@ -128,8 +134,8 @@ module ActiveRecord #:nodoc: # end # end # - # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> instead of <tt>write_attribute(:attribute, value)</tt> and - # <tt>read_attribute(:attribute)</tt> as a shorter form. + # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> + # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. # # == Attribute query methods # @@ -147,34 +153,43 @@ module ActiveRecord #:nodoc: # # == Accessing attributes before they have been typecasted # - # Sometimes you want to be able to read the raw attribute data without having the column-determined typecast run its course first. - # That can be done by using the <tt><attribute>_before_type_cast</tt> accessors that all attributes have. For example, if your Account model - # has a <tt>balance</tt> attribute, you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>. + # Sometimes you want to be able to read the raw attribute data without having the column-determined + # typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt> + # accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute, + # you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>. # - # This is especially useful in validation situations where the user might supply a string for an integer field and you want to display - # the original string back in an error message. Accessing the attribute normally would typecast the string to 0, which isn't what you - # want. + # This is especially useful in validation situations where the user might supply a string for an + # integer field and you want to display the original string back in an error message. Accessing the + # attribute normally would typecast the string to 0, which isn't what you want. # # == Dynamic attribute-based finders # - # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by - # appending the name of an attribute to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt>, so you get finders like <tt>Person.find_by_user_name</tt>, - # <tt>Person.find_all_by_last_name</tt>, and <tt>Payment.find_by_transaction_id</tt>. So instead of writing + # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects + # by simple queries without turning to SQL. They work by appending the name of an attribute + # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders + # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and + # <tt>Payment.find_by_transaction_id</tt>. Instead of writing # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. - # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do <tt>Person.find_all_by_last_name(last_name)</tt>. + # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do + # <tt>Person.find_all_by_last_name(last_name)</tt>. # - # It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like - # <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing - # <tt>Person.where(:user_name => user_name, :password => password).first</tt>, you just do - # <tt>Person.find_by_user_name_and_password(user_name, password)</tt>. + # It's also possible to use multiple attributes in the same find by separating them with "_and_". + # + # Person.where(:user_name => user_name, :password => password).first + # Person.find_by_user_name_and_password #with dynamic finder + # + # Person.where(:user_name => user_name, :password => password, :gender => 'male').first + # Payment.find_by_user_name_and_password_and_gender # - # It's even possible to call these dynamic finder methods on relations and named scopes. For example : + # It's even possible to call these dynamic finder methods on relations and named scopes. # # Payment.order("created_on").find_all_by_amount(50) # Payment.pending.find_last_by_amount(100) # - # The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with - # <tt>find_or_create_by_</tt> and will return the object if it already exists and otherwise creates it, then returns it. Protected attributes won't be set unless they are given in a block. For example: + # The same dynamic finder style can be used to create the object if it doesn't already exist. + # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if + # it already exists and otherwise creates it, then returns it. Protected attributes won't be set + # unless they are given in a block. # # # No 'Summer' tag exists # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") @@ -185,23 +200,33 @@ module ActiveRecord #:nodoc: # # Now 'Bob' exist and is an 'admin' # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } # - # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be set unless they are given in a block. For example: + # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without + # saving it first. Protected attributes won't be set unless they are given in a block. # # # No 'Winter' tag exists # winter = Tag.find_or_initialize_by_name("Winter") # winter.new_record? # true # # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of - # a list of parameters. For example: + # a list of parameters. # # Tag.find_or_create_by_name(:name => "rails", :creator => current_user) # - # That will either find an existing tag named "rails", or create a new one while setting the user that created it. + # That will either find an existing tag named "rails", or create a new one while setting the + # user that created it. + # + # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about + # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique + # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit. + # + # User.scoped_by_user_name('David') # # == Saving arrays, hashes, and other non-mappable objects in text columns # - # Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+. - # This makes it possible to store arrays, hashes, and other non-mappable objects without doing any additional work. Example: + # Active Record can serialize any object in text columns using YAML. To do so, you must + # specify this with a call to the class method +serialize+. + # This makes it possible to store arrays, hashes, and other non-mappable objects without doing + # any additional work. # # class User < ActiveRecord::Base # serialize :preferences @@ -210,8 +235,8 @@ module ActiveRecord #:nodoc: # user = User.create(:preferences => { "background" => "black", "display" => large }) # User.find(user.id).preferences # => { "background" => "black", "display" => large } # - # You can also specify a class option as the second parameter that'll raise an exception if a serialized object is retrieved as a - # descendant of a class not in the hierarchy. Example: + # You can also specify a class option as the second parameter that'll raise an exception + # if a serialized object is retrieved as a descendant of a class not in the hierarchy. # # class User < ActiveRecord::Base # serialize :preferences, Hash @@ -222,52 +247,63 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by default is named "type" (can be changed - # by overwriting <tt>Base.inheritance_column</tt>). This means that an inheritance looking like this: + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: # # class Company < ActiveRecord::Base; end # class Firm < Company; end # class Client < Company; end # class PriorityClient < Client; end # - # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in the companies table with type = "Firm". You can then - # fetch this row again using <tt>Company.where(:name => '37signals').first</tt> and it will return a Firm object. + # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(:name => '37signals').first</tt> and it will return a Firm object. # - # If you don't have a type column defined in your table, single-table inheritance won't be triggered. In that case, it'll work just - # like normal subclasses with no special magic for differentiating between them or reloading the right type with find. + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. # # Note, all the attributes for all the cases are kept in the same table. Read more: # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html # # == Connection to multiple databases in different models # - # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection. - # All classes inheriting from ActiveRecord::Base will use this connection. But you can also set a class-specific connection. - # For example, if Course is an ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt> + # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved + # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this + # connection. But you can also set a class-specific connection. For example, if Course is an + # ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt> # and Course and all of its subclasses will use this connection instead. # - # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is a Hash indexed by the class. If a connection is - # requested, the retrieve_connection method will go up the class-hierarchy until a connection is found in the connection pool. + # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is + # a Hash indexed by the class. If a connection is requested, the retrieve_connection method + # will go up the class-hierarchy until a connection is found in the connection pool. # # == Exceptions # # * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record. # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an # <tt>:adapter</tt> key. - # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a non-existent adapter + # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a + # non-existent adapter # (or a bad spelling of an existing one). - # * AssociationTypeMismatch - The object assigned to the association wasn't of the type specified in the association definition. + # * AssociationTypeMismatch - The object assigned to the association wasn't of the type + # specified in the association definition. # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. - # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> before querying. + # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> + # before querying. # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal # nothing was found, please check its documentation for further details. # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the - # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of AttributeAssignmentError + # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of + # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * AttributeAssignmentError - An error occurred while doing a mass assignment through the <tt>attributes=</tt> method. - # You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. + # * AttributeAssignmentError - An error occurred while doing a mass assignment through the + # <tt>attributes=</tt> method. + # You can inspect the +attribute+ property of the exception object to determine which attribute + # triggered the error. # # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all @@ -275,8 +311,9 @@ module ActiveRecord #:nodoc: class Base ## # :singleton-method: - # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed - # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+. + # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, + # which is then passed on to any new database connections made and which can be retrieved on both + # a class and instance level by calling +logger+. cattr_accessor :logger, :instance_writer => false class << self @@ -323,21 +360,24 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and - # :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as - # the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember + # Accessor for the prefix type that will be prepended to every primary key column name. + # The options are :table_name and :table_name_with_underscore. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember # that this is a global setting for all Active Records. cattr_accessor :primary_key_prefix_type, :instance_writer => false @@primary_key_prefix_type = nil ## # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all - # table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace - # for tables in a shared database. By default, the prefix is the empty string. + # Accessor for the name of the prefix string to prepend to every table name. So if set + # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", + # etc. This is a convenient way of creating a namespace for tables in a shared database. + # By default, the prefix is the empty string. # - # If you are organising your models within modules you can add a prefix to the models within a namespace by defining - # a singleton method in the parent module called table_name_prefix which returns your chosen prefix. + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. class_attribute :table_name_prefix, :instance_writer => false self.table_name_prefix = "" @@ -358,8 +398,8 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database. - # This is set to :local by default. + # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling + # dates and times from the database. This is set to :local by default. cattr_accessor :default_timezone, :instance_writer => false @@default_timezone = :local @@ -398,7 +438,7 @@ module ActiveRecord #:nodoc: delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped + delegate :select, :group, :order, :reorder, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will @@ -476,7 +516,8 @@ module ActiveRecord #:nodoc: connection.select_value(sql, "#{name} Count").to_i end - # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards. + # Attributes listed as readonly will be used to create a new record but update operations will + # ignore these fields. def attr_readonly(*attributes) write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || [])) end @@ -505,15 +546,18 @@ module ActiveRecord #:nodoc: serialized_attributes[attr_name.to_s] = class_name end - # Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values. + # Returns a hash of all the attributes that have been specified for serialization as + # keys and their class restriction as values. def serialized_attributes read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {}) end - # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending - # directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used - # to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class - # in Active Support, which knows almost all common English inflections. You can add new inflections in config/initializers/inflections.rb. + # Guesses the table name (in forced lower-case) based on the name of the class in the + # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy + # looks like: Reply < Message < ActiveRecord::Base, then Message is used + # to guess the table name even when called on Reply. The rules used to do the guess + # are handled by the Inflector class in Active Support, which knows almost all common + # English inflections. You can add new inflections in config/initializers/inflections.rb. # # Nested classes are given table names prefixed by the singular form of # the parent's table name. Enclosing modules are not considered. @@ -561,8 +605,8 @@ module ActiveRecord #:nodoc: (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end - # Defines the column name for use with single table inheritance - # -- can be set in subclasses like so: self.inheritance_column = "type_id" + # Defines the column name for use with single table inheritance. Use + # <tt>set_inheritance_column</tt> to set a different value. def inheritance_column @inheritance_column ||= "type".freeze end @@ -579,8 +623,8 @@ module ActiveRecord #:nodoc: default end - # Sets the table name to use to the given value, or (if the value - # is nil or false) to the value returned by the given block. + # Sets the table name. If the value is nil or false then the value returned by the given + # block is used. # # class Project < ActiveRecord::Base # set_table_name "project" @@ -803,7 +847,7 @@ module ActiveRecord #:nodoc: end def arel_table - @arel_table ||= Arel::Table.new(table_name, :engine => arel_engine) + @arel_table ||= Arel::Table.new(table_name, arel_engine) end def arel_engine @@ -923,15 +967,15 @@ module ActiveRecord #:nodoc: end end - # Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt> - # that are turned into <tt>where(:user_name => user_name).first</tt> and <tt>where(:user_name => user_name, :password => :password).first</tt> - # respectively. Also works for <tt>all</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>where(:amount => 50).all</tt>. + # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and + # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders + # section at the top of this file for more detailed information. # - # It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+ - # is actually <tt>find_all_by_amount(amount, options)</tt>. + # It's even possible to use all the additional parameters to +find+. For example, the + # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. # - # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future - # attempts to use it do not run through method_missing. + # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it + # is first invoked, so that future attempts to use it do not run through method_missing. def method_missing(method_id, *arguments, &block) if match = DynamicFinderMatch.match(method_id) attribute_names = match.attribute_names @@ -991,8 +1035,8 @@ module ActiveRecord #:nodoc: end protected - # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. - # method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while + # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be + # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while # <tt>:create</tt> parameters are an attributes hash. # # class Article < ActiveRecord::Base @@ -1030,15 +1074,14 @@ module ActiveRecord #:nodoc: # class Article < ActiveRecord::Base # def self.find_with_exclusive_scope # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) + # with_exclusive_scope(:find => limit(10)) do # all # => SELECT * from articles LIMIT 10 # end # end # end # end # - # *Note*: the +:find+ scope also has effect on update and deletion methods, - # like +update_all+ and +delete_all+. + # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. def with_scope(method_scoping = {}, action = :merge, &block) method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) @@ -1255,6 +1298,8 @@ MSG replace_named_bind_variables(statement, values.first) elsif statement.include?('?') replace_bind_variables(statement, values) + elsif statement.blank? + statement else statement % values.collect { |value| connection.quote_string(value.to_s) } end @@ -1355,7 +1400,7 @@ MSG # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is # application specific and is therefore left to the application to implement according to its need. def initialize_copy(other) - callback(:after_initialize) if respond_to_without_attributes?(:after_initialize) + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) cloned_attributes.delete(self.class.primary_key) @@ -1471,7 +1516,7 @@ MSG # user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false) # user.is_admin? # => true def attributes=(new_attributes, guard_protected_attributes = true) - return unless new_attributes.is_a? Hash + return unless new_attributes.is_a?(Hash) attributes = new_attributes.stringify_keys multi_parameter_attributes = [] @@ -1605,10 +1650,11 @@ MSG private - # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to do Reply.new without having to - # set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. No such attribute would be set for objects of the - # Message class in that example. + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # No such attribute would be set for objects of the Message class in that example. def ensure_proper_type unless self.class.descends_from_active_record? write_attribute(self.class.inheritance_column, self.class.sti_name) @@ -1657,8 +1703,9 @@ MSG # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float, - # s for String, and a for Array. If all the values for a given attribute are empty, the attribute will be set to nil. + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, + # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the + # attribute will be set to nil. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 637dac450b..aa92bf999f 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -26,8 +26,8 @@ module ActiveRecord # <tt>after_rollback</tt>. # # That's a total of ten callbacks, which gives you immense power to react and prepare for each state in the - # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, except that each - # <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback. + # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, + # except that each <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback. # # Examples: # class CreditCard < ActiveRecord::Base @@ -55,9 +55,9 @@ module ActiveRecord # # == Inheritable callback queues # - # Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros. - # Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance - # hierarchy. Example: + # Besides the overwritable callback methods, it's also possible to register callbacks through the + # use of the callback macros. Their main advantage is that the macros add behavior into a callback + # queue that is kept intact down through an inheritance hierarchy. # # class Topic < ActiveRecord::Base # before_destroy :destroy_author @@ -67,9 +67,9 @@ module ActiveRecord # before_destroy :destroy_readers # end # - # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is run, both +destroy_author+ and - # +destroy_readers+ are called. Contrast this to the situation where we've implemented the save behavior through overwriteable - # methods: + # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is + # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation + # where the +before_destroy+ methis is overriden: # # class Topic < ActiveRecord::Base # def before_destroy() destroy_author end @@ -79,20 +79,21 @@ module ActiveRecord # def before_destroy() destroy_readers end # end # - # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. So, use the callback macros when - # you want to ensure that a certain callback is called for the entire hierarchy, and use the regular overwriteable methods - # when you want to leave it up to each descendant to decide whether they want to call +super+ and trigger the inherited callbacks. + # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. + # So, use the callback macros when you want to ensure that a certain callback is called for the entire + # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant + # to decide whether they want to call +super+ and trigger the inherited callbacks. # - # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the - # associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't - # be inherited. + # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the + # callbacks before specifying the associations. Otherwise, you might trigger the loading of a + # child before the parent has registered the callbacks and they won't be inherited. # # == Types of callbacks # # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects, - # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the - # recommended approaches, inline methods using a proc are sometimes appropriate (such as for creating mix-ins), and inline - # eval methods are deprecated. + # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects + # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for + # creating mix-ins), and inline eval methods are deprecated. # # The method reference callbacks work by specifying a protected or private method available in the object, like this: # @@ -169,15 +170,15 @@ module ActiveRecord # end # end # - # The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string", - # which will then be evaluated within the binding of the callback. Example: + # The callback macros usually accept a symbol for the method they're supposed to run, but you can also + # pass a "method string", which will then be evaluated within the binding of the callback. Example: # # class Topic < ActiveRecord::Base # before_destroy 'self.class.delete_all "parent_id = #{id}"' # end # - # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback is triggered. Also note that these - # inline callbacks can be stacked just like the regular ones: + # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback + # is triggered. Also note that these inline callbacks can be stacked just like the regular ones: # # class Topic < ActiveRecord::Base # before_destroy 'self.class.delete_all "parent_id = #{id}"', @@ -186,22 +187,24 @@ module ActiveRecord # # == The +after_find+ and +after_initialize+ exceptions # - # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, such as <tt>Base.find(:all)</tt>, we've had - # to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, +after_find+ and - # +after_initialize+ will only be run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the + # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, + # such as <tt>Base.find(:all)</tt>, we've had to implement a simple performance constraint (50% more speed + # on a simple test case). Unlike all the other callbacks, +after_find+ and +after_initialize+ will only be + # run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the # callback types will be called. # # == <tt>before_validation*</tt> returning statements # - # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Base#save</tt> will return +false+. - # If Base#save! is called it will raise a ActiveRecord::RecordInvalid exception. - # Nothing will be appended to the errors object. + # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be + # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a + # ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object. # # == Canceling callbacks # - # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns - # +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks - # defined as methods on the model, which are called last. + # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are + # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled. + # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as + # methods on the model, which are called last. # # == Transactions # @@ -217,7 +220,8 @@ module ActiveRecord # # == Debugging callbacks # - # To list the methods and procs registered with a particular callback, append <tt>_callback_chain</tt> to the callback name that you wish to list and send that to your class from the Rails console: + # To list the methods and procs registered with a particular callback, append <tt>_callback_chain</tt> to + # the callback name that you wish to list and send that to your class from the Rails console: # # >> Topic.after_save_callback_chain # => [#<ActiveSupport::Callbacks::Callback:0x3f6a448 @@ -228,7 +232,7 @@ module ActiveRecord extend ActiveSupport::Concern CALLBACKS = [ - :after_initialize, :after_find, :before_validation, :after_validation, + :after_initialize, :after_find, :after_touch, :before_validation, :after_validation, :before_save, :around_save, :after_save, :before_create, :around_create, :after_create, :before_update, :around_update, :after_update, :before_destroy, :around_destroy, :after_destroy @@ -238,7 +242,7 @@ module ActiveRecord extend ActiveModel::Callbacks include ActiveModel::Validations::Callbacks - define_model_callbacks :initialize, :find, :only => :after + define_model_callbacks :initialize, :find, :touch, :only => :after define_model_callbacks :save, :create, :update, :destroy end @@ -256,6 +260,10 @@ module ActiveRecord _run_destroy_callbacks { super } end + def touch(*) #:nodoc: + _run_touch_callbacks { super } + end + def deprecated_callback_method(symbol) #:nodoc: if respond_to?(symbol, true) ActiveSupport::Deprecation.warn("Overwriting #{symbol} in your models has been deprecated, please use Base##{symbol} :method_name instead") diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index c2d79a421d..02a8f4e214 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -103,8 +103,8 @@ module ActiveRecord # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. - def release_connection - conn = @reserved_connections.delete(current_connection_id) + def release_connection(with_id = current_connection_id) + conn = @reserved_connections.delete(with_id) checkin conn if conn end @@ -112,10 +112,11 @@ module ActiveRecord # exists checkout a connection, yield it to the block, and checkin the # connection when finished. def with_connection - fresh_connection = true unless @reserved_connections[current_connection_id] + connection_id = current_connection_id + fresh_connection = true unless @reserved_connections[connection_id] yield connection ensure - release_connection if fresh_connection + release_connection(connection_id) if fresh_connection end # Returns true if a connection has already been opened. @@ -161,8 +162,13 @@ module ActiveRecord # Return any checked-out connections back to the pool by threads that # are no longer alive. def clear_stale_cached_connections! - remove_stale_cached_threads!(@reserved_connections) do |name, conn| - checkin conn + keys = @reserved_connections.keys - Thread.list.find_all { |t| + t.alive? + }.map { |thread| thread.object_id } + + keys.each do |key| + checkin @reserved_connections[key] + @reserved_connections.delete(key) end end @@ -232,20 +238,6 @@ module ActiveRecord Thread.current.object_id end - # Remove stale threads from the cache. - def remove_stale_cached_threads!(cache, &block) - keys = Set.new(cache.keys) - - Thread.list.each do |thread| - keys.delete(thread.object_id) if thread.alive? - end - keys.each do |key| - next unless cache.has_key?(key) - block.call(key, cache[key]) - cache.delete(key) - end - end - def checkout_new_connection c = new_connection @connections << c @@ -290,14 +282,12 @@ module ActiveRecord # ActiveRecord::Base.connection_handler. Active Record models use this to # determine that connection pool that they should use. class ConnectionHandler + attr_reader :connection_pools + def initialize(pools = {}) @connection_pools = pools end - def connection_pools - @connection_pools ||= {} - end - def establish_connection(name, spec) @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec) end @@ -345,9 +335,11 @@ module ActiveRecord # re-establishing the connection. def remove_connection(klass) pool = @connection_pools[klass.name] + return nil unless pool + @connection_pools.delete_if { |key, value| value == pool } - pool.disconnect! if pool - pool.spec.config if pool + pool.disconnect! + pool.spec.config end def retrieve_connection_pool(klass) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 23c42d670b..8e74eff0ab 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -66,15 +66,9 @@ module ActiveRecord unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end begin - require 'rubygems' - gem "activerecord-#{spec[:adapter]}-adapter" require "active_record/connection_adapters/#{spec[:adapter]}_adapter" rescue LoadError - begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})" - end + raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})" end adapter_method = "#{spec[:adapter]}_connection" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 4118ea7b31..a130c330dd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -42,7 +42,7 @@ module ActiveRecord 65535 end - # the maximum length of a SQL query + # the maximum length of an SQL query def sql_query_length 1048575 end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d7b5bf8e31..e2b3773a99 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -30,7 +30,7 @@ module ActiveRecord if value.acts_like?(:date) || value.acts_like?(:time) "'#{quoted_date(value)}'" else - "'#{quote_string(value.to_yaml)}'" + "'#{quote_string(value.to_s)}'" end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 7691b6a788..9118ceb33c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -23,7 +23,8 @@ module ActiveRecord # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in <tt>company_name varchar(60)</tt>. + # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in + # <tt>company_name varchar(60)</tt>. # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type = nil, null = true) @@ -359,7 +360,8 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns. + # Requests a maximum column length. This is number of characters for <tt>:string</tt> and + # <tt>:text</tt> columns and number of bytes for :binary and :integer columns. # * <tt>:default</tt> - # The column's default value. Use nil for NULL. # * <tt>:null</tt> - @@ -462,8 +464,8 @@ module ActiveRecord # TableDefinition#timestamps that'll add created_at and +updated_at+ as datetimes. # # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type - # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of options, these will be - # used when creating the <tt>_type</tt> column. So what can be written like this: + # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of + # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id @@ -535,7 +537,7 @@ module ActiveRecord end end - # Represents a SQL table in an abstract way for updating a table. + # Represents an SQL table in an abstract way for updating a table. # Also see TableDefinition and SchemaStatements#create_table # # Available transformations are: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index ffc3847a31..7dee68502f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -327,6 +327,8 @@ module ActiveRecord # # Note: SQLite doesn't support index length def add_index(table_name, column_name, options = {}) + options[:name] = options[:name].to_s if options.key?(:name) + column_names = Array.wrap(column_name) index_name = index_name(table_name, :column => column_names) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index be8d1bd76b..d8c92d0ad3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -36,14 +36,12 @@ module ActiveRecord define_callbacks :checkout, :checkin - @@row_even = true - def initialize(connection, logger = nil) #:nodoc: @active = nil @connection, @logger = connection, logger - @runtime = 0 @query_cache_enabled = false @query_cache = {} + @instrumenter = ActiveSupport::Notifications.instrumenter end # Returns the human-readable name of the adapter. Use mixed case - one @@ -92,11 +90,6 @@ module ActiveRecord false end - def reset_runtime #:nodoc: - rt, @runtime = @runtime, 0 - rt - end - # QUOTING ================================================== # Override to return the quoted table name. Defaults to column quoting. @@ -199,12 +192,10 @@ module ActiveRecord def log(sql, name) name ||= "SQL" - result = nil - ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => name, :connection_id => self.object_id) do - @runtime += Benchmark.ms { result = yield } + @instrumenter.instrument("sql.active_record", + :sql => sql, :name => name, :connection_id => object_id) do + yield end - result rescue Exception => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.debug message if @logger diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb new file mode 100644 index 0000000000..568759775b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -0,0 +1,639 @@ +# encoding: utf-8 + +require 'mysql2' unless defined? Mysql2 + +module ActiveRecord + class Base + def self.mysql2_connection(config) + config[:username] = 'root' if config[:username].nil? + client = Mysql2::Client.new(config.symbolize_keys) + options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] + ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + end + end + + module ConnectionAdapters + class Mysql2Column < Column + BOOL = "tinyint(1)" + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? nil : '' + else + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + elsif missing_default_forged_as_empty_string?(default) + nil + else + super + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime then Time + when :date then Date + when :timestamp then Time + when :time then Time + when :text, :string then String + when :binary then String + when :boolean then Object + end + end + + def type_cast(value) + return nil if value.nil? + case type + when :string then value + when :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f # returns self if it's already a Float + when :decimal then self.class.value_to_decimal(value) + when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value) + when :time then value.class == Time ? value : self.class.string_to_dummy_time(value) + when :date then value.class == Date ? value : self.class.string_to_date(value) + when :binary then value + when :boolean then self.class.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + case type + when :string then nil + when :text then nil + when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0" + when :float then "#{var_name}.to_f" + when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})" + when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})" + when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})" + when :binary then nil + when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" + else nil + end + end + + private + def simplified_type(field_type) + return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) + return :string if field_type =~ /enum/i or field_type =~ /set/i + return :integer if field_type =~ /year/i + return :binary if field_type =~ /bit/i + super + end + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + super # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + class Mysql2Adapter < AbstractAdapter + cattr_accessor :emulate_booleans + self.emulate_booleans = true + + ADAPTER_NAME = 'Mysql2' + PRIMARY = "PRIMARY" + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" ] + + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + NATIVE_DATABASE_TYPES = { + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int", :limit => 4 }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "tinyint", :limit => 1 } + } + + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @quoted_column_names, @quoted_table_names = {}, {} + configure_connection + end + + def adapter_name + ADAPTER_NAME + end + + def supports_migrations? + true + end + + def supports_primary_key? + true + end + + def supports_savepoints? + true + end + + def native_database_types + NATIVE_DATABASE_TYPES + end + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + elsif value.kind_of?(BigDecimal) + value.to_s("F") + else + super + end + end + + def quote_column_name(name) #:nodoc: + @quoted_column_names[name] ||= "`#{name}`" + end + + def quote_table_name(name) #:nodoc: + @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + end + + def quote_string(string) + @connection.escape(string) + end + + def quoted_true + QUOTED_TRUE + end + + def quoted_false + QUOTED_FALSE + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # CONNECTION MANAGEMENT ==================================== + + def active? + return false unless @connection + @connection.query 'select 1' + true + rescue Mysql2::Error + false + end + + def reconnect! + disconnect! + connect + end + + # this is set to true in 2.3, but we don't want it to be + def requires_reloading? + false + end + + def disconnect! + unless @connection.nil? + @connection.close + @connection = nil + end + end + + def reset! + disconnect! + connect + end + + # DATABASE STATEMENTS ====================================== + + # FIXME: re-enable the following once a "better" query_cache solution is in core + # + # The overrides below perform much better than the originals in AbstractAdapter + # because we're able to take advantage of mysql2's lazy-loading capabilities + # + # # Returns a record hash with the column names as keys and column values + # # as values. + # def select_one(sql, name = nil) + # result = execute(sql, name) + # result.each(:as => :hash) do |r| + # return r + # end + # end + # + # # Returns a single value from a record + # def select_value(sql, name = nil) + # result = execute(sql, name) + # if first = result.first + # first.first + # end + # end + # + # # Returns an array of the values of the first column in a select: + # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] + # def select_values(sql, name = nil) + # execute(sql, name).map { |row| row.first } + # end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. + def select_rows(sql, name = nil) + execute(sql, name).to_a + end + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + if name == :skip_logging + @connection.query(sql) + else + log(sql, name) { @connection.query(sql) } + end + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + super + id_value || @connection.last_id + end + alias :create :insert_sql + + def update_sql(sql, name = nil) + super + @connection.affected_rows + end + + def begin_db_transaction + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def add_limit_offset!(sql, options) + limit, offset = options[:limit], options[:offset] + if limit && offset + sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" + elsif limit + sql << " LIMIT #{sanitize_limit(limit)}" + elsif offset + sql << " OFFSET #{offset.to_i}" + end + sql + end + + # SCHEMA STATEMENTS ======================================== + + def structure_dump + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).inject("") do |structure, table| + table.delete('Table_type') + structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + end + end + + def recreate_database(name, options = {}) + drop_database(name) + create_database(name, options) + end + + # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. + # Charset defaults to utf8. + # + # Example: + # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'matt_development' + # create_database 'matt_development', :charset => :big5 + def create_database(name, options = {}) + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_value 'SELECT DATABASE() as db' + end + + # Returns the database character set. + def charset + show_variable 'character_set_database' + end + + # Returns the database collation strategy. + def collation + show_variable 'collation_database' + end + + def tables(name = nil) + tables = [] + execute("SHOW TABLES", name).each do |field| + tables << field.first + end + tables + end + + def drop_table(table_name, options = {}) + super(table_name, options) + end + + def indexes(table_name, name = nil) + indexes = [] + current_index = nil + result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) + result.each(:symbolize_keys => true, :as => :hash) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == PRIMARY # skip the primary key + current_index = row[:Key_name] + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, []) + end + + indexes.last.columns << row[:Column_name] + end + indexes + end + + def columns(table_name, name = nil) + sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + columns = [] + result = execute(sql, :skip_logging) + result.each(:symbolize_keys => true, :as => :hash) { |field| + columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") + } + columns + end + + def create_table(table_name, options = {}) + super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + end + + def rename_table(table_name, new_name) + execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + end + + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + execute(add_column_sql) + end + + def change_column_default(table_name, column_name, default) + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) + options = {} + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + execute(rename_column_sql) + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + end + + def show_variable(name) + variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables.first['Value'] unless variables.empty? + end + + def pk_and_sequence_for(table) + keys = [] + result = execute("describe #{quote_table_name(table)}") + result.each(:symbolize_keys => true, :as => :hash) do |row| + keys << row[:Field] if row[:Key] == "PRI" + end + keys.length == 1 ? [keys.first, nil] : nil + end + + # Returns just a table's primary key + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def case_sensitive_equality_operator + "= BINARY" + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + protected + def quoted_columns_for_index(column_names, options = {}) + length = options[:length] if options.is_a?(Hash) + + quoted_column_names = case length + when Hash + column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } + when Fixnum + column_names.map {|name| "#{quote_column_name(name)}(#{length})"} + else + column_names.map {|name| quote_column_name(name) } + end + end + + def translate_exception(exception, message) + return super unless exception.respond_to?(:error_number) + + case exception.error_number + when 1062 + RecordNotUnique.new(message, exception) + when 1452 + InvalidForeignKey.new(message, exception) + else + super + end + end + + private + def connect + @connection = Mysql2::Client.new(@config) + configure_connection + end + + def configure_connection + @connection.query_options.merge!(:as => :array) + encoding = @config[:encoding] + execute("SET NAMES '#{encoding}'", :skip_logging) if encoding + + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + end + + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select(sql, name = nil) + execute(sql, name).each(:as => :hash) + end + + def supports_views? + version[0] >= 5 + end + + def version + @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index aa3626a37e..ba0051de05 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -125,7 +125,7 @@ module ActiveRecord # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> # as boolean. If you wish to disable this emulation (which was the default # behavior in versions 0.13.1 and earlier) you can add the following line - # to your environment.rb file: + # to your application.rb file: # # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false cattr_accessor :emulate_booleans @@ -278,7 +278,8 @@ module ActiveRecord rows end - # Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it. + # Executes an SQL query and returns a MySQL::Result object. Note that you have to free + # the Result object after you're done using it. def execute(sql, name = nil) #:nodoc: if name == :skip_logging @connection.query(sql) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2fe2ae7136..6fae899e87 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -183,10 +183,14 @@ module ActiveRecord # * <tt>:username</tt> - Defaults to nothing. # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option. - # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO <encoding></tt> call on the connection. - # * <tt>:min_messages</tt> - An optional client min messages that is used in a <tt>SET client_min_messages TO <min_messages></tt> call on the connection. - # * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods. + # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given + # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option. + # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO + # <encoding></tt> call on the connection. + # * <tt>:min_messages</tt> - An optional client min messages that is used in a + # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. + # * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; + # otherwise, use blocking query methods. class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = 'PostgreSQL'.freeze @@ -218,6 +222,9 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil + @table_alias_length = nil + @postgresql_version = nil + connect @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] end @@ -308,14 +315,16 @@ module ActiveRecord # Quotes PostgreSQL-specific data types for SQL input. def quote(value, column = nil) #:nodoc: - if value.kind_of?(String) && column && column.type == :binary + return super unless column + + if value.kind_of?(String) && column.type == :binary "'#{escape_bytea(value)}'" - elsif value.kind_of?(String) && column && column.sql_type == 'xml' + elsif value.kind_of?(String) && column.sql_type == 'xml' "xml '#{quote_string(value)}'" - elsif value.kind_of?(Numeric) && column && column.sql_type == 'money' + elsif value.kind_of?(Numeric) && column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value.to_s}'" - elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/ + "'#{value}'" + elsif value.kind_of?(String) && column.sql_type =~ /^bit/ case value when /^[01]*$/ "B'#{value}'" # Bit-string notation @@ -370,7 +379,7 @@ module ActiveRecord def supports_disable_referential_integrity?() #:nodoc: version = query("SHOW server_version")[0][0].split('.') - (version[0].to_i >= 8 && version[1].to_i >= 1) ? true : false + version[0].to_i >= 8 && version[1].to_i >= 1 rescue return false end @@ -431,17 +440,37 @@ module ActiveRecord def result_as_array(res) #:nodoc: # check if we have any binary column and if they need escaping unescape_col = [] - for j in 0...res.nfields do - # unescape string passed BYTEA field (OID == 17) - unescape_col << ( res.ftype(j)==17 ) + res.nfields.times do |j| + unescape_col << res.ftype(j) end ary = [] - for i in 0...res.ntuples do + res.ntuples.times do |i| ary << [] - for j in 0...res.nfields do + res.nfields.times do |j| data = res.getvalue(i,j) - data = unescape_bytea(data) if unescape_col[j] and data.is_a?(String) + case unescape_col[j] + + # unescape string passed BYTEA field (OID == 17) + when BYTEA_COLUMN_TYPE_OID + data = unescape_bytea(data) if String === data + + # If this is a money type column and there are any currency symbols, + # then strip them off. Indeed it would be prettier to do this in + # PostgreSQLColumn.string_to_decimal but would break form input + # fields that call value_before_type_cast. + when MONEY_COLUMN_TYPE_OID + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + case data + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + data.gsub!(/[^-\d\.]/, '') + when /^-?\D+[\d\.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + end ary[i] << data end end @@ -828,11 +857,12 @@ module ActiveRecord # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) return super unless type.to_s == 'integer' + return 'integer' unless limit case limit - when 1..2; 'smallint' - when 3..4, nil; 'integer' - when 5..8; 'bigint' + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") end end @@ -889,6 +919,8 @@ module ActiveRecord private # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: + # The internal PostgreSQL identifier of the BYTEA data type. + BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. @@ -941,51 +973,17 @@ module ActiveRecord # conversions that are required to be performed here instead of in PostgreSQLColumn. def select(sql, name = nil) fields, rows = select_raw(sql, name) - result = [] - for row in rows - row_hash = {} - fields.each_with_index do |f, i| - row_hash[f] = row[i] - end - result << row_hash + rows.map do |row| + Hash[*fields.zip(row).flatten] end - result end def select_raw(sql, name = nil) res = execute(sql, name) results = result_as_array(res) - fields = [] - rows = [] - if res.ntuples > 0 - fields = res.fields - results.each do |row| - hashed_row = {} - row.each_index do |cell_index| - # If this is a money type column and there are any currency symbols, - # then strip them off. Indeed it would be prettier to do this in - # PostgreSQLColumn.string_to_decimal but would break form input - # fields that call value_before_type_cast. - if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - case column = row[cell_index] - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - row[cell_index] = column.gsub(/[^-\d\.]/, '') - when /^-?\D+[\d\.]+,\d{2}$/ # (2) - row[cell_index] = column.gsub(/[^-\d,]/, '').sub(/,/, '.') - end - end - - hashed_row[fields[cell_index]] = column - end - rows << row - end - end + fields = res.fields res.clear - return fields, rows + return fields, results end # Returns the list of a table's column names, data types, and default values. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 117cf447df..82ad0a3b8e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -29,8 +29,8 @@ module ActiveRecord end end - # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and - # from http://rubyforge.org/projects/sqlite-ruby/). + # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby + # drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/). # # Options: # @@ -190,16 +190,21 @@ module ActiveRecord def indexes(table_name, name = nil) #:nodoc: execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| - index = IndexDefinition.new(table_name, row['name']) - index.unique = row['unique'].to_i != 0 - index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] } - index + IndexDefinition.new( + table_name, + row['name'], + row['unique'].to_i != 0, + execute("PRAGMA index_info('#{row['name']}')").map { |col| + col['name'] + }) end end def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find {|field| field['pk'].to_i == 1} - column ? column['name'] : nil + column = table_structure(table_name).find { |field| + field['pk'].to_i == 1 + } + column && column['name'] end def remove_index!(table_name, index_name) #:nodoc: @@ -278,10 +283,8 @@ module ActiveRecord def select(sql, name = nil) #:nodoc: execute(sql, name).map do |row| record = {} - row.each_key do |key| - if key.is_a?(String) - record[key.sub(/^"?\w+"?\./, '')] = row[key] - end + row.each do |key, value| + record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String) end record end @@ -378,9 +381,9 @@ module ActiveRecord def default_primary_key_type if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze + 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' else - 'INTEGER PRIMARY KEY NOT NULL'.freeze + 'INTEGER PRIMARY KEY NOT NULL' end end diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index b39b291352..0dc965bd26 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -2,8 +2,8 @@ module ActiveRecord # = Active Record Dynamic Finder Match # - # Provides dynamic attribute-based finders such as <tt>find_by_country</tt> - # if, for example, the <tt>Person</tt> has an attribute with that name. + # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info + # class DynamicFinderMatch def self.match(method) df_match = self.new(method) @@ -42,6 +42,10 @@ module ActiveRecord @finder == :first && !@instantiator.nil? end + def creator? + @finder == :first && @instantiator == :create + end + def bang? @bang end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 7aa725d095..e9ac5516ec 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -30,7 +30,8 @@ module ActiveRecord class SerializationTypeMismatch < ActiveRecordError end - # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> misses adapter field). + # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> + # misses adapter field). class AdapterNotSpecified < ActiveRecordError end @@ -38,7 +39,8 @@ module ActiveRecord class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for example when <tt>connection=</tt> is given a nil object). + # Raised when connection to the database could not been established (for example when <tt>connection=</tt> + # is given a nil object). class ConnectionNotEstablished < ActiveRecordError end @@ -51,7 +53,8 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError end - # Raised when SQL statement cannot be executed by the database (for example, it's often the case for MySQL when Ruby driver used is too old). + # Raised when SQL statement cannot be executed by the database (for example, it's often the case for + # MySQL when Ruby driver used is too old). class StatementInvalid < ActiveRecordError end @@ -78,7 +81,8 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end - # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, when using +find+ method) + # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, + # when using +find+ method) # does not match number of expected variables. # # For example, in @@ -165,4 +169,4 @@ module ActiveRecord @errors = errors end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 82270c56b3..e44102b538 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -39,9 +39,10 @@ end # This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures # in a non-verbose, human-readable format. It ships with Ruby 1.8.1+. # -# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed -# by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just -# put your files in <tt><your-rails-app>/test/fixtures/</tt>). The fixture file ends with the <tt>.yml</tt> file extension (Rails example: +# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed +# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is +# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). +# The fixture file ends with the <tt>.yml</tt> file extension (Rails example: # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this: # # rubyonrails: @@ -58,7 +59,8 @@ end # indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing # pleasure. # -# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. See http://yaml.org/type/omap.html +# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. +# See http://yaml.org/type/omap.html # for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table. # This is commonly needed for tree structures. Example: # @@ -79,7 +81,8 @@ end # (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>). # # The format of this type of fixture file is much more compact than the others, but also a little harder to read by us -# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the file is then comprised +# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the +# file is then comprised # of the actual data (1 per line). Here's an example: # # id, name, url @@ -99,15 +102,16 @@ end # # == Single-file fixtures # -# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats. -# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory -# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just -# put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> -- +# This type of fixture was the original format for Active Record that has since been deprecated in +# favor of the YAML and CSV formats. +# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) +# to the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically +# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> -- # like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model). # # Each text file placed in this directory represents a "record". Usually these types of fixtures are named without -# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. Here's what the -# above example might look like: +# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. +# Here's what the above example might look like: # # web_sites/google # web_sites/yahoo.txt @@ -133,7 +137,8 @@ end # end # end # -# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, so this test will succeed. +# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, +# so this test will succeed. # The testing environment will automatically load the all fixtures into the database before each test. # To ensure consistent data, the environment deletes the fixtures before running the load. # @@ -182,13 +187,15 @@ end # This will create 1000 very simple YAML fixtures. # # Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. -# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable -# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application -# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell. +# This is however a feature to be used with some caution. The point of fixtures are that they're +# stable units of predictable sample data. If you feel that you need to inject dynamic values, then +# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values +# in fixtures are to be considered a code smell. # # = Transactional fixtures # -# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case. +# TestCases can use begin+rollback to isolate their changes to the database instead of having to +# delete+insert for every test case. # # class FooTest < ActiveSupport::TestCase # self.use_transactional_fixtures = true @@ -205,15 +212,18 @@ end # end # # If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, -# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes. +# then you may omit all fixtures declarations in your test cases since all the data's already there +# and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide -# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+) +# access to fixture data for every table that has been loaded through fixtures (depending on the +# value of +use_instantiated_fixtures+) # # When *not* to use transactional fixtures: # -# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit, -# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify +# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until +# all parent transactions commit, particularly, the fixtures transaction which is begun in setup +# and rolled back in teardown. Thus, you won't be able to verify # the results of your transaction until Active Record supports nested transactions or savepoints (in progress). # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. # Use InnoDB, MaxDB, or NDB instead. @@ -664,14 +674,13 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) end def has_primary_key_column? - @has_primary_key_column ||= model_class && primary_key_name && - model_class.columns.find { |c| c.name == primary_key_name } + @has_primary_key_column ||= primary_key_name && + model_class.columns.any? { |c| c.name == primary_key_name } end def timestamp_column_names - @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name| - column_names.include?(name) - end + @timestamp_column_names ||= + %w(created_at created_on updated_at updated_on) & column_names end def inheritance_column_name @@ -872,7 +881,7 @@ module ActiveRecord table_names.each do |table_name| table_name = table_name.to_s.tr('./', '_') - define_method(table_name) do |*fixtures| + redefine_method(table_name) do |*fixtures| force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload @fixture_cache[table_name] ||= {} diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index ceb0902fde..b6f87a57b8 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -124,6 +124,7 @@ module ActiveRecord end end + @destroyed = true freeze end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 71065f9908..c7ae12977a 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -1,19 +1,35 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber + def self.runtime=(value) + Thread.current["active_record_sql_runtime"] = value + end + + def self.runtime + Thread.current["active_record_sql_runtime"] ||= 0 + end + + def self.reset_runtime + rt, self.runtime = runtime, 0 + rt + end + def initialize super @odd_or_even = false end def sql(event) + self.class.runtime += event.duration + return unless logger.debug? + name = '%s (%.1fms)' % [event.payload[:name], event.duration] sql = event.payload[:sql].squeeze(' ') if odd? - name = color(name, :cyan, true) + name = color(name, CYAN, true) sql = color(sql, nil, true) else - name = color(name, :magenta, true) + name = color(name, MAGENTA, true) end debug " #{name} #{sql}" diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4c5e1ae218..5e272f0ba4 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -284,7 +284,7 @@ module ActiveRecord # # config.active_record.timestamped_migrations = false # - # In environment.rb. + # In application.rb. # class Migration @@verbose = true diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 849ec9c884..0e560418dc 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -26,7 +26,7 @@ module ActiveRecord # You can define a \scope that applies to all finders using # ActiveRecord::Base.default_scope. def scoped(options = nil) - if options.present? + if options scoped.apply_finder_options(options) else current_scoped_methods ? relation.merge(current_scoped_methods) : relation.clone @@ -48,18 +48,21 @@ module ActiveRecord # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. # - # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object - # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, - # <tt>Shirt.red.where(:size => 'small')</tt>. Also, just as with the association objects, named \scopes act like an Array, - # implementing Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> + # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it + # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, + # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. + # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; + # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> # all behave as if Shirt.red really was an Array. # - # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments - # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. + # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce + # all shirts that are both red and dry clean only. + # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. Similarly with + # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which the \scopes were defined. But they are also available to - # <tt>has_many</tt> associations. If, + # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which + # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, # # class Person < ActiveRecord::Base # has_many :shirts @@ -105,7 +108,7 @@ module ActiveRecord extension ? relation.extending(extension) : relation end - singleton_class.send :define_method, name, &scopes[name] + singleton_class.send(:redefine_method, name, &scopes[name]) end def named_scope(*args, &block) diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index cf8c5aaf84..e652296e2c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -78,7 +78,7 @@ module ActiveRecord # member.avatar_attributes = { :id => '2', :_destroy => '1' } # member.avatar.marked_for_destruction? # => true # member.save - # member.reload.avatar #=> nil + # member.reload.avatar # => nil # # Note that the model will _not_ be destroyed until the parent is saved. # @@ -180,7 +180,7 @@ module ActiveRecord # # member.attributes = params['member'] # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true - # member.posts.length #=> 2 + # member.posts.length # => 2 # member.save # member.reload.posts.length # => 1 # diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index d2ed643f35..78bac55bf2 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -67,8 +67,8 @@ module ActiveRecord # # == Configuration # - # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration setting in your - # <tt>config/environment.rb</tt> file. + # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration + # setting in your <tt>config/application.rb</tt> file. # # config.active_record.observers = :comment_observer, :signup_observer # diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 828a8b41b6..71b46beaef 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -60,7 +60,7 @@ module ActiveRecord # reflect that no changes should be made (since they can't be # persisted). Returns the frozen instance. # - # The row is simply removed with a SQL +DELETE+ statement on the + # The row is simply removed with an SQL +DELETE+ statement on the # record's primary key, and no callbacks are executed. # # To enforce the object's +before_destroy+ and +after_destroy+ @@ -91,8 +91,8 @@ module ActiveRecord # like render <tt>:partial => @client.becomes(Company)</tt> to render that # instance using the companies/company partial instead of clients/client. # - # Note: The new instance will share a link to the same attributes as the original class. So any change to the attributes in either - # instance will affect the other. + # Note: The new instance will share a link to the same attributes as the original class. + # So any change to the attributes in either instance will affect the other. def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) @@ -102,35 +102,57 @@ module ActiveRecord became end - # Updates a single attribute and saves the record without going through the normal validation procedure - # or callbacks. This is especially useful for boolean flags on existing records. + # Updates a single attribute and saves the record. + # This is especially useful for boolean flags on existing records. Also note that + # + # * The attribute being updated must be a column name. + # * Validation is skipped. + # * No callbacks are invoked. + # * updated_at/updated_on column is updated if that column is available. + # * Does not work on associations. + # * Does not work on attr_accessor attributes. + # * Does not work on new record. <tt>record.new_record?</tt> should return false for this method to work. + # * Updates only the attribute that is input to the method. If there are other changed attributes then + # those attributes are left alone. In that case even after this method has done its work <tt>record.changed?</tt> + # will return true. + # def update_attribute(name, value) - send("#{name}=", value) - hash = { name => read_attribute(name) } + raise ActiveRecordError, "#{name.to_s} is marked as readonly" if self.class.readonly_attributes.include? name.to_s + + changes = record_update_timestamps || {} - if record_update_timestamps - timestamp_attributes_for_update_in_model.each do |column| - hash[column] = read_attribute(column) - end + if name + name = name.to_s + send("#{name}=", value) + changes[name] = read_attribute(name) end - @changed_attributes.delete(name.to_s) + @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key - self.class.update_all(hash, { primary_key => self[primary_key] }) == 1 + self.class.update_all(changes, { primary_key => self[primary_key] }) == 1 end - # Updates all the attributes from the passed-in Hash and saves the record. - # If the object is invalid, the saving will fail and false will be returned. + # Updates the attributes of the model from the passed-in hash and saves the + # record, all wrapped in a transaction. If the object is invalid, the saving + # will fail and false will be returned. def update_attributes(attributes) - self.attributes = attributes - save + # The following transaction covers any possible database side-effects of the + # attributes assignment. For example, setting the IDs of a child collection. + with_transaction_returning_status do + self.attributes = attributes + save + end end - # Updates an object just like Base.update_attributes but calls save! instead - # of save so an exception is raised if the record is invalid. + # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead + # of +save+, so an exception is raised if the record is invalid. def update_attributes!(attributes) - self.attributes = attributes - save! + # The following transaction covers any possible database side-effects of the + # attributes assignment. For example, setting the IDs of a child collection. + with_transaction_returning_status do + self.attributes = attributes + save! + end end # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). @@ -196,6 +218,19 @@ module ActiveRecord self end + # Saves the record with the updated_at/on attributes set to the current time. + # Please note that no validation is performed and no callbacks are executed. + # If an attribute name is passed, that attribute is updated along with + # updated_at/on attributes. + # + # Examples: + # + # product.touch # updates updated_at/on + # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on + def touch(attribute = nil) + update_attribute(attribute, current_time_from_proper_timezone) + end + private def create_or_update raise ReadOnlyRecord if readonly? diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 2808e199fe..78fdb77216 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,16 +16,18 @@ module ActiveRecord config.generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" - - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::QueryCache" rake_tasks do load "active_record/railties/databases.rake" end + # When loading console, force ActiveRecord to be loaded to avoid cross + # references when loading a constant for the first time. + console do + ActiveRecord::Base + end + initializer "active_record.initialize_timezone" do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true @@ -72,6 +74,13 @@ module ActiveRecord end end + initializer "active_record.add_concurrency_middleware" do |app| + if app.config.allow_concurrency + app.config.middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::ConnectionAdapters::ConnectionManagement" + end + end + config.after_initialize do ActiveSupport.on_load(:active_record) do instantiate_observers diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index aed1c59b00..bc6ca936c0 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -11,9 +11,9 @@ module ActiveRecord def cleanup_view_runtime if ActiveRecord::Base.connected? - db_rt_before_render = ActiveRecord::Base.connection.reset_runtime + db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime runtime = super - db_rt_after_render = ActiveRecord::Base.connection.reset_runtime + db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = db_rt_before_render + db_rt_after_render runtime - db_rt_after_render else diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 5024787c3c..ae605d3e7a 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -274,7 +274,7 @@ namespace :db do task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ] desc 'Load the seed data from db/seeds.rb' - task :seed => :environment do + task :seed => 'db:abort_if_pending_migrations' do seed_file = File.join(Rails.root, 'db', 'seeds.rb') load(seed_file) if File.exist?(seed_file) end @@ -339,7 +339,7 @@ namespace :db do end namespace :structure do - desc "Dump the database structure to a SQL file" + desc "Dump the database structure to an SQL file" task :dump => :environment do abcs = ActiveRecord::Base.configurations case abcs[Rails.env]["adapter"] diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a82e5d7ed1..7f47a812eb 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -3,14 +3,14 @@ module ActiveRecord module Reflection # :nodoc: extend ActiveSupport::Concern - # Reflection allows you to interrogate Active Record classes and objects + # Reflection enables to interrogate Active Record classes and objects # about their associations and aggregations. This information can, - # for example, be used in a form builder that took an Active Record object - # and created input fields for all of the attributes depending on their type - # and displayed the associations to other objects. + # for example, be used in a form builder that takes an Active Record object + # and creates input fields for all of the attributes depending on their type + # and displays the associations to other objects. # - # You can find the interface for the AggregateReflection and AssociationReflection - # classes in the abstract MacroReflection class. + # MacroReflection class has info for AggregateReflection and AssociationReflection + # classes. module ClassMethods def create_reflection(macro, name, options, active_record) case macro @@ -24,7 +24,7 @@ module ActiveRecord reflection end - # Returns a hash containing all AssociationReflection objects for the current class + # Returns a hash containing all AssociationReflection objects for the current class. # Example: # # Invoice.reflections @@ -39,9 +39,9 @@ module ActiveRecord reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) } end - # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example: + # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). # - # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection + # Account.reflect_on_aggregation(:balance) #=> the balance AggregateReflection # def reflect_on_aggregation(aggregation) reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil @@ -50,7 +50,7 @@ module ActiveRecord # Returns an array of AssociationReflection objects for all the # associations in the class. If you only want to reflect on a certain # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, - # <tt>:belongs_to</tt>) for that as the first parameter. + # <tt>:belongs_to</tt>) as the first parameter. # # Example: # @@ -62,9 +62,9 @@ module ActiveRecord macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections end - # Returns the AssociationReflection object for the named +association+ (use the symbol). Example: + # Returns the AssociationReflection object for the +association+ (use the symbol). # - # Account.reflect_on_association(:owner) # returns the owner AssociationReflection + # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) @@ -78,8 +78,7 @@ module ActiveRecord end - # Abstract base class for AggregateReflection and AssociationReflection that - # describes the interface available for both of those classes. Objects of + # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection attr_reader :active_record @@ -88,34 +87,36 @@ module ActiveRecord @macro, @name, @options, @active_record = macro, name, options, active_record end - # Returns the name of the macro. For example, <tt>composed_of :balance, - # :class_name => 'Money'</tt> will return <tt>:balance</tt> or for - # <tt>has_many :clients</tt> it will return <tt>:clients</tt>. - def name - @name - end + # Returns the name of the macro. + # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> + # <tt>has_many :clients</tt> returns <tt>:clients</tt> + attr_reader :name - # Returns the macro type. For example, - # <tt>composed_of :balance, :class_name => 'Money'</tt> will return <tt>:composed_of</tt> - # or for <tt>has_many :clients</tt> will return <tt>:has_many</tt>. - def macro - @macro - end + # Returns the macro type. + # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt> + # <tt>has_many :clients</tt> returns <tt>:has_many</tt> + attr_reader :macro - # Returns the hash of options used for the macro. For example, it would return <tt>{ :class_name => "Money" }</tt> for - # <tt>composed_of :balance, :class_name => 'Money'</tt> or +{}+ for <tt>has_many :clients</tt>. - def options - @options - end + # Returns the hash of options used for the macro. + # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt> + # <tt>has_many :clients</tt> returns +{}+ + attr_reader :options - # Returns the class for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money - # class and <tt>has_many :clients</tt> returns the Client class. + # Returns the class for the macro. + # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class + # <tt>has_many :clients</tt> returns the Client class def klass @klass ||= class_name.constantize end - # Returns the class name for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt> - # and <tt>has_many :clients</tt> returns <tt>'Client'</tt>. + # Returns the class name for the macro. + # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> def class_name @class_name ||= options[:class_name] || derive_class_name end @@ -130,11 +131,6 @@ module ActiveRecord @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] end - # Returns +true+ if +self+ is a +belongs_to+ reflection. - def belongs_to? - macro == :belongs_to - end - private def derive_class_name name.to_s.camelize @@ -150,7 +146,7 @@ module ActiveRecord # Holds all the meta-data about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: - # Returns the target association's class: + # Returns the target association's class. # # class Author < ActiveRecord::Base # has_many :books @@ -159,7 +155,7 @@ module ActiveRecord # Author.reflect_on_association(:books).klass # # => Book # - # <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate + # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass @@ -206,6 +202,10 @@ module ActiveRecord @primary_key_name ||= options[:foreign_key] || derive_primary_key_name end + def primary_key_column + @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key } + end + def association_foreign_key @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key end @@ -270,7 +270,7 @@ module ActiveRecord end # Returns whether or not this association reflection is for a collection - # association. Returns +true+ if the +macro+ is one of +has_many+ or + # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? @collection @@ -280,7 +280,7 @@ module ActiveRecord # the parent's validation. # # Unless you explicitly disable validation with - # <tt>:validate => false</tt>, it will take place when: + # <tt>:validate => false</tt>, validation will take place when: # # * you explicitly enable validation; <tt>:validate => true</tt> # * you use autosave; <tt>:autosave => true</tt> @@ -300,6 +300,11 @@ module ActiveRecord dependent_conditions end + # Returns +true+ if +self+ is a +belongs_to+ reflection. + def belongs_to? + macro == :belongs_to + end + private def derive_class_name class_name = name.to_s.camelize @@ -324,8 +329,6 @@ module ActiveRecord # Gets the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # - # (The <tt>:tags</tt> association on Tagging below.) - # # class Post < ActiveRecord::Base # has_many :taggings # has_many :tags, :through => :taggings @@ -336,7 +339,7 @@ module ActiveRecord end # Returns the AssociationReflection object specified in the <tt>:through</tt> option - # of a HasManyThrough or HasOneThrough association. Example: + # of a HasManyThrough or HasOneThrough association. # # class Post < ActiveRecord::Base # has_many :taggings diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index d9fc1b4940..30be723291 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,17 +10,18 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches - delegate :to_xml, :to_json, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a delegate :insert, :to => :arel - attr_reader :table, :klass + attr_reader :table, :klass, :loaded attr_accessor :extensions + alias :loaded? :loaded def initialize(klass, table) @klass, @table = klass, table @implicit_readonly = nil - @loaded = nil + @loaded = false SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} @@ -66,7 +67,8 @@ module ActiveRecord preload += @includes_values unless eager_loading? preload.each {|associations| @klass.send(:preload_associations, @records, associations) } - # @readonly_value is true only if set explicitly. @implicit_readonly is true if there are JOINS and no explicit SELECT. + # @readonly_value is true only if set explicitly. @implicit_readonly is true if there + # are JOINS and no explicit SELECT. readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value @records.each { |record| record.readonly! } if readonly @@ -74,6 +76,8 @@ module ActiveRecord @records end + def as_json(options = nil) to_a end #:nodoc: + # Returns size of the records. def size loaded? ? @records.length : count @@ -96,7 +100,7 @@ module ActiveRecord if block_given? to_a.many? { |*block_args| yield(*block_args) } else - @limit_value.present? ? to_a.many? : size > 1 + @limit_value ? to_a.many? : size > 1 end end @@ -105,7 +109,7 @@ module ActiveRecord # ==== Example # # Comment.where(:post_id => 1).scoping do - # Comment.first #=> SELECT * FROM comments WHERE post_id = 1 + # Comment.first # SELECT * FROM comments WHERE post_id = 1 # end # # Please check unscoped if you want to remove all previous scopes (including @@ -127,7 +131,8 @@ module ActiveRecord # ==== Parameters # # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. - # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro. + # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. + # See conditions in the intro. # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # # ==== Examples @@ -141,7 +146,7 @@ module ActiveRecord # # Update all avatars migrated more than a week ago # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] # - # # Update all books that match our conditions, but limit it to 5 ordered by date + # # Update all books that match conditions, but limit it to 5 ordered by date # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 def update_all(updates, conditions = nil, options = {}) if conditions || options.present? @@ -162,14 +167,14 @@ module ActiveRecord # ==== Parameters # # * +id+ - This should be the id or an array of ids to be updated. - # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes. + # * +attributes+ - This should be a hash of attributes or an array of hashes. # # ==== Examples # - # # Updating one record: + # # Updates one record # Person.update(15, :user_name => 'Samuel', :group => 'expert') # - # # Updating multiple records: + # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) def update(id, attributes) @@ -290,10 +295,6 @@ module ActiveRecord where(@klass.primary_key => id_or_array).delete_all end - def loaded? - @loaded - end - def reload reset to_a # force reload @@ -317,12 +318,14 @@ module ActiveRecord def scope_for_create @scope_for_create ||= begin - @create_with_value || @where_values.inject({}) do |hash, where| - if where.is_a?(Arel::Predicates::Equality) - hash[where.operand1.name] = where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2 - end - hash - end + @create_with_value || Hash[ + @where_values.find_all { |w| + w.respond_to?(:operator) && w.operator == :== + }.map { |where| + [where.operand1.name, + where.operand2.respond_to?(:value) ? + where.operand2.value : where.operand2] + }] end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 412be895c4..d7494ebb5a 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -50,9 +50,9 @@ module ActiveRecord def find_in_batches(options = {}) relation = self - if orders.present? || taken.present? - ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") - end + if orders.present? || taken.present? + ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") + end if (finder_options = options.except(:start, :batch_size)).present? raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present? @@ -73,7 +73,7 @@ module ActiveRecord break if records.size < batch_size if primary_key_offset = records.last.id - records = relation.where(primary_key.gt(primary_key_offset)).all + records = relation.where(primary_key.gt(primary_key_offset)).to_a else raise "Primary key not included in the custom select clause" end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 44baeb6c84..a679c444cf 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,30 +1,38 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/try' module ActiveRecord module Calculations # Count operates using three different approaches. # # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. - # * Count using column: By passing a column name to count, it will return a count of all the rows for the model with supplied column present + # * Count using column: By passing a column name to count, it will return a count of all the + # rows for the model with supplied column present # * Count using options will find the row count matched by the options used. # # The third approach, count using options, accepts an option hash as the only parameter. The options are: # - # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base. + # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. + # See conditions in the intro to ActiveRecord::Base. # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed) - # or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s). - # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # or named associations in the same form used for the <tt>:include</tt> option, which will + # perform an INNER JOIN on the associated table(s). + # If the value is a string, then the records will be returned read-only since they will have + # attributes that do not correspond to the table's columns. # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer - # to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting. + # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. + # The symbols named refer to already defined associations. When using named associations, count + # returns the number of DISTINCT items for the model you're counting. # See eager loading under Associations. # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not + # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, + # want to do a join but not # include the joined columns. - # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name - # of a database view). + # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as + # SELECT COUNT(DISTINCT posts.id) ... + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an + # alternate table name (or even the name of a database view). # # Examples for counting all: # Person.count # returns the total count of all people @@ -34,12 +42,19 @@ module ActiveRecord # # Examples for count with options: # Person.count(:conditions => "age > 26") - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. + # + # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) + # + # # finds the number of rows matching the conditions and joins. + # Person.count(:conditions => "age > 26 AND job.salary > 60000", + # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") + # # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') # - # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead. + # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. + # Use Person.count instead. def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) calculate(:count, column_name, options) @@ -80,13 +95,15 @@ module ActiveRecord calculate(:sum, column_name, options) end - # This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts. - # Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. + # This calculates aggregate values in the given column. Methods for count, sum, average, + # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, + # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. # # There are two basic forms of output: - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else. - # * Grouped values: This returns an ordered hash of the values and groups them by the <tt>:group</tt> option. It takes either a column name, or the name - # of a belongs_to association. + # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float + # for AVG, and the given column's type for everything else. + # * Grouped values: This returns an ordered hash of the values and groups them by the + # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. # # values = Person.maximum(:age, :group => 'last_name') # puts values["Drake"] @@ -102,21 +119,30 @@ module ActiveRecord # end # # Options: - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. - # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). - # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. + # See conditions in the intro to ActiveRecord::Base. + # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, + # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. + # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". + # (Rarely needed). + # The records will be returned read-only since they will have attributes that do not correspond to the + # table's columns. # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not - # include the joined columns. - # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... + # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example + # want to do a join, but not include the joined columns. + # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as + # SELECT COUNT(DISTINCT posts.id) ... # # Examples: # Person.calculate(:count, :all) # The same as Person.count # Person.average(:age) # SELECT AVG(age) FROM people... - # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' - # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors + # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for + # # everyone with a last name other than 'Drake' + # + # # Selects the minimum age for any family without any minors + # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) + # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) if options.except(:distinct).present? diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3bf4c5bdd1..b34c11973b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -21,23 +21,28 @@ module ActiveRecord # # ==== Parameters # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, + # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. + # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a + # <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4. + # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, + # it would skip rows 0 through 4. # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), - # named associations in the same form used for the <tt>:include</tt> option, which will perform an <tt>INNER JOIN</tt> on the associated table(s), + # named associations in the same form used for the <tt>:include</tt> option, which will perform an + # <tt>INNER JOIN</tt> on the associated table(s), # or an array containing a mixture of both strings and named associations. - # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # If the value is a string, then the records will be returned read-only since they will + # have attributes that do not correspond to the table's columns. # Pass <tt>:readonly => false</tt> to override. # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer # to already defined associations. See eager loading under Associations. - # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, for example, want to do a join but not - # include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name - # of a database view). + # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, + # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed + # to an alternate table name (or even the name of a database view). # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE". @@ -164,6 +169,8 @@ module ActiveRecord # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? def exists?(id = nil) + id = id.id if ActiveRecord::Base === id + case id when Array, Hash where(id).exists? @@ -279,6 +286,8 @@ module ActiveRecord end def find_one(id) + id = id.id if ActiveRecord::Base === id + record = where(primary_key.eq(id)).first unless record diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4692271266..e71f1cca72 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -11,91 +11,91 @@ module ActiveRecord def includes(*args) args.reject! { |a| a.blank? } - clone.tap { |r| r.includes_values += args if args.present? } + clone.tap {|r| r.includes_values += args if args.present? } end def eager_load(*args) - clone.tap { |r| r.eager_load_values += args if args.present? } + clone.tap {|r| r.eager_load_values += args if args.present? } end def preload(*args) - clone.tap { |r| r.preload_values += args if args.present? } + clone.tap {|r| r.preload_values += args if args.present? } end def select(*args) if block_given? - to_a.select { |*block_args| yield(*block_args) } + to_a.select {|*block_args| yield(*block_args) } else - clone.tap { |r| r.select_values += args if args.present? } + clone.tap {|r| r.select_values += args if args.present? } end end def group(*args) - clone.tap { |r| r.group_values += args if args.present? } + clone.tap {|r| r.group_values += args.flatten if args.present? } end def order(*args) - clone.tap { |r| r.order_values += args if args.present? } + clone.tap {|r| r.order_values += args if args.present? } end def reorder(*args) - clone.tap { |r| r.order_values = args if args.present? } + clone.tap {|r| r.order_values = args if args.present? } end def joins(*args) args.flatten! - clone.tap { |r| r.joins_values += args if args.present? } + clone.tap {|r| r.joins_values += args if args.present? } end - def where(*args) - value = build_where(*args) - clone.tap { |r| r.where_values += Array.wrap(value) if value.present? } + def where(opts, *rest) + value = build_where(opts, rest) + value ? clone.tap {|r| r.where_values += Array.wrap(value) } : clone end def having(*args) value = build_where(*args) - clone.tap { |r| r.having_values += Array.wrap(value) if value.present? } + clone.tap {|r| r.having_values += Array.wrap(value) if value.present? } end def limit(value = true) - clone.tap { |r| r.limit_value = value } + clone.tap {|r| r.limit_value = value } end def offset(value = true) - clone.tap { |r| r.offset_value = value } + clone.tap {|r| r.offset_value = value } end def lock(locks = true) case locks when String, TrueClass, NilClass - clone.tap { |r| r.lock_value = locks || true } + clone.tap {|r| r.lock_value = locks || true } else - clone.tap { |r| r.lock_value = false } + clone.tap {|r| r.lock_value = false } end end def readonly(value = true) - clone.tap { |r| r.readonly_value = value } + clone.tap {|r| r.readonly_value = value } end def create_with(value = true) - clone.tap { |r| r.create_with_value = value } + clone.tap {|r| r.create_with_value = value } end def from(value = true) - clone.tap { |r| r.from_value = value } + clone.tap {|r| r.from_value = value } end def extending(*modules, &block) modules << Module.new(&block) if block_given? - clone.tap { |r| r.send(:apply_modules, *modules) } + clone.tap {|r| r.send(:apply_modules, *modules) } end def reverse_order order_clause = arel.send(:order_clauses).join(', ') relation = except(:order) - if order_clause.present? + unless order_clauses.blank? relation.order(reverse_sql_order(order_clause)) else relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC") @@ -129,7 +129,7 @@ module ActiveRecord def build_arel arel = table - arel = build_joins(arel, @joins_values) if @joins_values.present? + arel = build_joins(arel, @joins_values) unless @joins_values.empty? @where_values.uniq.each do |where| next if where.blank? @@ -143,36 +143,27 @@ module ActiveRecord end end - arel = arel.having(*@having_values.uniq.select{|h| h.present?}) if @having_values.present? + arel = arel.having(*@having_values.uniq.select{|h| h.present?}) unless @having_values.empty? - arel = arel.take(@limit_value) if @limit_value.present? - arel = arel.skip(@offset_value) if @offset_value.present? + arel = arel.take(@limit_value) if @limit_value + arel = arel.skip(@offset_value) if @offset_value - arel = arel.group(*@group_values.uniq.select{|g| g.present?}) if @group_values.present? + arel = arel.group(*@group_values.uniq.select{|g| g.present?}) unless @group_values.empty? - arel = arel.order(*@order_values.uniq.select{|o| o.present?}) if @order_values.present? + arel = arel.order(*@order_values.uniq.select{|o| o.present?}) unless @order_values.empty? arel = build_select(arel, @select_values.uniq) - arel = arel.from(@from_value) if @from_value.present? - - case @lock_value - when TrueClass - arel = arel.lock - when String - arel = arel.lock(@lock_value) - end if @lock_value.present? + arel = arel.from(@from_value) if @from_value + arel = arel.lock(@lock_value) if @lock_value arel end - def build_where(*args) - return if args.blank? - - opts = args.first + def build_where(opts, other = []) case opts when String, Array - @klass.send(:sanitize_sql, args.size > 1 ? args : opts) + @klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other)) when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) PredicateBuilder.new(table.engine).build_from_hash(attributes, table) @@ -230,7 +221,7 @@ module ActiveRecord @implicit_readonly = false # TODO: fix this ugly hack, we should refactor the callers to get an ARel compatible array. # Before this change we were passing to ARel the last element only, and ARel is capable of handling an array - if selects.all? { |s| s.is_a?(String) || !s.is_a?(Arel::Expression) } && !(selects.last =~ /^COUNT\(/) + if selects.all? {|s| s.is_a?(String) || !s.is_a?(Arel::Expression) } && !(selects.last =~ /^COUNT\(/) arel.project(*selects) else arel.project(selects.last) @@ -247,7 +238,7 @@ module ActiveRecord end def reverse_sql_order(order_query) - order_query.to_s.split(/,/).each { |s| + order_query.split(',').each { |s| if s.match(/\s(asc|ASC)$/) s.gsub!(/\s(asc|ASC)$/, ' DESC') elsif s.match(/\s(desc|DESC)$/) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index e2783087ec..c1bc3214ea 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Schema - # + # # Allows programmers to programmatically define a schema in a portable # DSL. This means you can define tables, indexes, etc. without using SQL # directly, so your applications can more easily support multiple diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index a4757773d8..e9af20e1b6 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -8,13 +8,13 @@ module ActiveRecord # output format (i.e., ActiveRecord::Schema). class SchemaDumper #:nodoc: private_class_method :new - + ## # :singleton-method: - # A list of tables which should not be dumped to the schema. + # A list of tables which should not be dumped to the schema. # Acceptable values are strings as well as regexp. # This setting is only used if ActiveRecord::Base.schema_format == :ruby - cattr_accessor :ignore_tables + cattr_accessor :ignore_tables @@ignore_tables = [] def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT) @@ -71,7 +71,7 @@ HEADER else raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' end - end + end table(tbl, stream) end end @@ -87,7 +87,7 @@ HEADER elsif @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end - + tbl.print " create_table #{table.inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' @@ -105,7 +105,7 @@ HEADER next if column.name == pk spec = {} spec[:name] = column.name.inspect - + # AR has an optimisation which handles zero-scale decimals as integers. This # code ensures that the dumper still dumps the column as a decimal. spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) } @@ -148,7 +148,7 @@ HEADER tbl.puts " end" tbl.puts - + indexes(table, tbl) tbl.rewind @@ -158,7 +158,7 @@ HEADER stream.puts "# #{e.message}" stream.puts end - + stream end @@ -172,7 +172,7 @@ HEADER value.inspect end end - + def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index b88d550086..becde0fbfd 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -16,7 +16,7 @@ module ActiveRecord # ActionController::SessionOverflowError will be raised. # # You may configure the table name, primary key, and data column. - # For example, at the end of <tt>config/environment.rb</tt>: + # For example, at the end of <tt>config/application.rb</tt>: # # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' # ActiveRecord::SessionStore::Session.primary_key = 'session_id' @@ -49,8 +49,34 @@ module ActiveRecord # The example SqlBypass class is a generic SQL session store. You may # use it as a basis for high-performance database-specific stores. class SessionStore < ActionDispatch::Session::AbstractStore + module ClassMethods # :nodoc: + def marshal(data) + ActiveSupport::Base64.encode64(Marshal.dump(data)) if data + end + + def unmarshal(data) + Marshal.load(ActiveSupport::Base64.decode64(data)) if data + end + + def drop_table! + connection.execute "DROP TABLE #{table_name}" + end + + def create_table! + connection.execute <<-end_sql + CREATE TABLE #{table_name} ( + id #{connection.type_to_sql(:primary_key)}, + #{connection.quote_column_name(session_id_column)} VARCHAR(255) UNIQUE, + #{connection.quote_column_name(data_column_name)} TEXT + ) + end_sql + end + end + # The default Active Record class. class Session < ActiveRecord::Base + extend ClassMethods + ## # :singleton-method: # Customizable data column name. Defaults to 'data'. @@ -62,7 +88,7 @@ module ActiveRecord class << self def data_column_size_limit - @data_column_size_limit ||= columns_hash[@@data_column_name].limit + @data_column_size_limit ||= columns_hash[data_column_name].limit end # Hook to set up sessid compatibility. @@ -71,29 +97,11 @@ module ActiveRecord find_by_session_id(session_id) end - def marshal(data) - ActiveSupport::Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(ActiveSupport::Base64.decode64(data)) if data - end - - def create_table! - connection.execute <<-end_sql - CREATE TABLE #{table_name} ( - id INTEGER PRIMARY KEY, - #{connection.quote_column_name('session_id')} TEXT UNIQUE, - #{connection.quote_column_name(@@data_column_name)} TEXT(255) - ) - end_sql - end - - def drop_table! - connection.execute "DROP TABLE #{table_name}" - end - private + def session_id_column + 'session_id' + end + # Compatibility with tables using sessid instead of session_id. def setup_sessid_compatibility! # Reset column info since it may be stale. @@ -106,6 +114,8 @@ module ActiveRecord define_method(:session_id) { sessid } define_method(:session_id=) { |session_id| self.sessid = session_id } else + class << self; remove_method :find_by_session_id; end + def self.find_by_session_id(session_id) find :first, :conditions => {:session_id=>session_id} end @@ -113,6 +123,11 @@ module ActiveRecord end end + def initialize(attributes = nil) + @data = nil + super + end + # Lazy-unmarshal session state. def data @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} @@ -122,22 +137,22 @@ module ActiveRecord # Has the session been loaded yet? def loaded? - !!@data + @data end private def marshal_data! - return false if !loaded? - write_attribute(@@data_column_name, self.class.marshal(self.data)) + return false unless loaded? + write_attribute(@@data_column_name, self.class.marshal(data)) end # Ensures that the data about to be stored in the database is not # larger than the data storage column. Raises # ActionController::SessionOverflowError. def raise_on_session_data_overflow! - return false if !loaded? + return false unless loaded? limit = self.class.data_column_size_limit - if loaded? and limit and read_attribute(@@data_column_name).size > limit + if limit and read_attribute(@@data_column_name).size > limit raise ActionController::SessionOverflowError end end @@ -162,6 +177,8 @@ module ActiveRecord # binary session data in a +text+ column. For higher performance, # store in a +blob+ column instead and forgo the Base64 encoding. class SqlBypass + extend ClassMethods + ## # :singleton-method: # Use the ActiveRecord::Base.connection by default. @@ -186,6 +203,8 @@ module ActiveRecord @@data_column = 'data' class << self + alias :data_column_name :data_column + def connection @@connection ||= ActiveRecord::Base.connection end @@ -196,43 +215,21 @@ module ActiveRecord new(:session_id => session_id, :marshaled_data => record['data']) end end - - def marshal(data) - ActiveSupport::Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(ActiveSupport::Base64.decode64(data)) if data - end - - def create_table! - @@connection.execute <<-end_sql - CREATE TABLE #{table_name} ( - id INTEGER PRIMARY KEY, - #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE, - #{@@connection.quote_column_name(data_column)} TEXT - ) - end_sql - end - - def drop_table! - @@connection.execute "DROP TABLE #{table_name}" - end end - attr_reader :session_id + attr_reader :session_id, :new_record + alias :new_record? :new_record + attr_writer :data # Look for normal and marshaled data, self.find_by_session_id's way of # telling us to postpone unmarshaling until the data is requested. # We need to handle a normal data attribute in case of a new record. def initialize(attributes) - @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data] - @new_record = @marshaled_data.nil? - end - - def new_record? - @new_record + @session_id = attributes[:session_id] + @data = attributes[:data] + @marshaled_data = attributes[:marshaled_data] + @new_record = @marshaled_data.nil? end # Lazy-unmarshal session state. @@ -248,39 +245,41 @@ module ActiveRecord end def loaded? - !!@data + @data end def save - return false if !loaded? + return false unless loaded? marshaled_data = self.class.marshal(data) + connect = connection if @new_record @new_record = false - @@connection.update <<-end_sql, 'Create session' - INSERT INTO #{@@table_name} ( - #{@@connection.quote_column_name(@@session_id_column)}, - #{@@connection.quote_column_name(@@data_column)} ) + connect.update <<-end_sql, 'Create session' + INSERT INTO #{table_name} ( + #{connect.quote_column_name(session_id_column)}, + #{connect.quote_column_name(data_column)} ) VALUES ( - #{@@connection.quote(session_id)}, - #{@@connection.quote(marshaled_data)} ) + #{connect.quote(session_id)}, + #{connect.quote(marshaled_data)} ) end_sql else - @@connection.update <<-end_sql, 'Update session' - UPDATE #{@@table_name} - SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)} - WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} + connect.update <<-end_sql, 'Update session' + UPDATE #{table_name} + SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)} + WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} end_sql end end def destroy - unless @new_record - @@connection.delete <<-end_sql, 'Destroy session' - DELETE FROM #{@@table_name} - WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} - end_sql - end + return if @new_record + + connect = connection + connect.delete <<-end_sql, 'Destroy session' + DELETE FROM #{table_name} + WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} + end_sql end end @@ -289,7 +288,7 @@ module ActiveRecord cattr_accessor :session_class self.session_class = Session - SESSION_RECORD_KEY = 'rack.session.record'.freeze + SESSION_RECORD_KEY = 'rack.session.record' private def get_session(env, sid) @@ -317,7 +316,7 @@ module ActiveRecord sid end - + def destroy(env) if sid = current_session_id(env) Base.silence do diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 1075a60f07..5531d12a41 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -12,6 +12,19 @@ module ActiveRecord # Timestamps are in the local timezone by default but you can use UTC by setting: # # <tt>ActiveRecord::Base.default_timezone = :utc</tt> + # + # == Time Zone aware attributes + # + # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. + # + # ActiveRecord::Base.time_zone_aware_attributes = true + # + # This feature can easily be turned off by assigning value <tt>false</tt> . + # + # If your attributes are time zone aware and you desire to skip time zone conversion for certain + # attributes then you can do following: + # + # Topic.skip_time_zone_conversion_for_attributes = [:written_on] module Timestamp extend ActiveSupport::Concern @@ -19,35 +32,16 @@ module ActiveRecord class_inheritable_accessor :record_timestamps, :instance_writer => false self.record_timestamps = true end - - # Saves the record with the updated_at/on attributes set to the current time. - # If the save fails because of validation errors, an - # ActiveRecord::RecordInvalid exception is raised. If an attribute name is passed, - # that attribute is used for the touch instead of the updated_at/on attributes. - # - # Examples: - # - # product.touch # updates updated_at - # product.touch(:designed_at) # updates the designed_at attribute - def touch(attribute = nil) - current_time = current_time_from_proper_timezone - - if attribute - write_attribute(attribute, current_time) - else - timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } - end - - save! - end private + def create #:nodoc: if record_timestamps current_time = current_time_from_proper_timezone - write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil? - write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil? + timestamp_attributes_for_create.each do |column| + write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil? + end timestamp_attributes_for_update_in_model.each do |column| write_attribute(column.to_s, current_time) if self.send(column).nil? @@ -58,22 +52,33 @@ module ActiveRecord end def update(*args) #:nodoc: - record_update_timestamps + record_update_timestamps if !partial_updates? || changed? super end - def record_update_timestamps - if record_timestamps && (!partial_updates? || changed?) - current_time = current_time_from_proper_timezone - timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } - true - else - false + def record_update_timestamps #:nodoc: + return unless record_timestamps + current_time = current_time_from_proper_timezone + timestamp_attributes_for_update_in_model.inject({}) do |hash, column| + hash[column.to_s] = write_attribute(column.to_s, current_time) + hash end end def timestamp_attributes_for_update_in_model #:nodoc: - [:updated_at, :updated_on].select { |elem| respond_to?(elem) } + timestamp_attributes_for_update.select { |elem| respond_to?(elem) } + end + + def timestamp_attributes_for_update #:nodoc: + [:updated_at, :updated_on] + end + + def timestamp_attributes_for_create #:nodoc: + [:created_at, :created_on] + end + + def all_timestamp_attributes #:nodoc: + timestamp_attributes_for_update + timestamp_attributes_for_create end def current_time_from_proper_timezone #:nodoc: diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 0b0f5682aa..15b587de45 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -27,8 +27,9 @@ module ActiveRecord # # this would specify a circular dependency and cause infinite recursion. # - # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association - # is both present and guaranteed to be valid, you also need to use +validates_presence_of+. + # NOTE: This validation will not fail if the association hasn't been assigned. If you want to + # ensure that the association is both present and guaranteed to be valid, you also need to + # use +validates_presence_of+. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid") @@ -44,4 +45,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 1c9ecc7b1b..bf863c7063 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -78,22 +78,25 @@ module ActiveRecord end module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user + # Validates whether the value of the specified attributes are unique across the system. + # Useful for making sure that only one user # can be named "davidhh". # # class Person < ActiveRecord::Base # validates_uniqueness_of :user_name, :scope => :account_id # end # - # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, - # making sure that a teacher can only be on the schedule once per semester for a particular class. + # It can also validate whether the value of the specified attributes are unique based on multiple + # scope parameters. For example, making sure that a teacher can only be on the schedule once + # per semester for a particular class. # # class TeacherSchedule < ActiveRecord::Base # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] # end # - # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified - # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. + # When the record is created, a check is performed to make sure that no record exists in the database + # with the given value for the specified attribute (that maps to a column). When the record is updated, + # the same check is made but disregarding the record itself. # # Configuration options: # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). @@ -102,11 +105,12 @@ module ActiveRecord # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). + # The method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should + # return or evaluate to a true or false value. # # === Concurrency and integrity # diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index d18fed0131..a467ffa960 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -3,7 +3,7 @@ module ActiveRecord MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 6e6645511c..509baacaef 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -42,7 +42,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal "DROP TABLE `people`", drop_table(:people) end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def test_create_mysql_database_with_encoding assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) @@ -101,6 +101,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase #we need to actually modify some data, so we make execute point to the original method ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do alias_method :execute_with_stub, :execute + remove_method :execute alias_method :execute, :execute_without_stub end yield diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb new file mode 100644 index 0000000000..a83399d0cd --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -0,0 +1,125 @@ +require "cases/helper" + +class ActiveSchemaTest < ActiveRecord::TestCase + def setup + ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + alias_method :execute_without_stub, :execute + remove_method :execute + def execute(sql, name = nil) return sql end + end + end + + def teardown + ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + remove_method :execute + alias_method :execute, :execute_without_stub + end + end + + def test_add_index + # add_index calls index_name_exists? which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:define_method, :index_name_exists?) do |*| + false + end + expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)" + assert_equal expected, add_index(:people, :last_name, :length => nil) + + expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))" + assert_equal expected, add_index(:people, :last_name, :length => 10) + + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))" + assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15) + + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)" + assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15}) + + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))" + assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) + ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:remove_method, :index_name_exists?) + end + + def test_drop_table + assert_equal "DROP TABLE `people`", drop_table(:people) + end + + if current_adapter?(:Mysql2Adapter) + def test_create_mysql_database_with_encoding + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) + assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) + end + + def test_recreate_mysql_database_with_encoding + create_database(:luca, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) + end + end + + def test_add_column + assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string) + end + + def test_add_column_with_limit + assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32) + end + + def test_drop_table_with_specific_database + assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people') + end + + def test_add_timestamps + with_real_execute do + begin + ActiveRecord::Base.connection.create_table :delete_me do |t| + end + ActiveRecord::Base.connection.add_timestamps :delete_me + assert column_present?('delete_me', 'updated_at', 'datetime') + assert column_present?('delete_me', 'created_at', 'datetime') + ensure + ActiveRecord::Base.connection.drop_table :delete_me rescue nil + end + end + end + + def test_remove_timestamps + with_real_execute do + begin + ActiveRecord::Base.connection.create_table :delete_me do |t| + t.timestamps + end + ActiveRecord::Base.connection.remove_timestamps :delete_me + assert !column_present?('delete_me', 'updated_at', 'datetime') + assert !column_present?('delete_me', 'created_at', 'datetime') + ensure + ActiveRecord::Base.connection.drop_table :delete_me rescue nil + end + end + end + + private + def with_real_execute + #we need to actually modify some data, so we make execute point to the original method + ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + alias_method :execute_with_stub, :execute + remove_method :execute + alias_method :execute, :execute_without_stub + end + yield + ensure + #before finishing, we restore the alias to the mock-up method + ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + remove_method :execute + alias_method :execute, :execute_with_stub + end + end + + + def method_missing(method_symbol, *arguments) + ActiveRecord::Base.connection.send(method_symbol, *arguments) + end + + def column_present?(table_name, column_name, type) + results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'") + results.first && results.first['Type'] == type + end +end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb new file mode 100644 index 0000000000..b973da621b --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" + +class MysqlConnectionTest < ActiveRecord::TestCase + def setup + super + @connection = ActiveRecord::Base.connection + end + + def test_no_automatic_reconnection_after_timeout + assert @connection.active? + @connection.update('set @@wait_timeout=1') + sleep 2 + assert !@connection.active? + end + + def test_successful_reconnection_after_timeout_with_manual_reconnect + assert @connection.active? + @connection.update('set @@wait_timeout=1') + sleep 2 + @connection.reconnect! + assert @connection.active? + end + + def test_successful_reconnection_after_timeout_with_verify + assert @connection.active? + @connection.update('set @@wait_timeout=1') + sleep 2 + @connection.verify! + assert @connection.active? + end + + private + + def run_without_connection + original_connection = ActiveRecord::Base.remove_connection + begin + yield original_connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb new file mode 100644 index 0000000000..90d8b0d923 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -0,0 +1,176 @@ +require "cases/helper" + +class Group < ActiveRecord::Base + Group.table_name = 'group' + belongs_to :select, :class_name => 'Select' + has_one :values +end + +class Select < ActiveRecord::Base + Select.table_name = 'select' + has_many :groups +end + +class Values < ActiveRecord::Base + Values.table_name = 'values' +end + +class Distinct < ActiveRecord::Base + Distinct.table_name = 'distinct' + has_and_belongs_to_many :selects + has_many :values, :through => :groups +end + +# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with +# reserved word names (ie: group, order, values, etc...) +class MysqlReservedWordTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + + # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() + # will fail with these table names if these test cases fail + + create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int', + 'select'=>'id int auto_increment primary key', + 'values'=>'id int auto_increment primary key, group_id int', + 'distinct'=>'id int auto_increment primary key', + 'distincts_selects'=>'distinct_id int, select_id int' + end + + def teardown + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + end + + # create tables with reserved-word names and columns + def test_create_tables + assert_nothing_raised { + @connection.create_table :order do |t| + t.column :group, :string + end + } + end + + # rename tables with reserved-word names + def test_rename_tables + assert_nothing_raised { @connection.rename_table(:group, :order) } + end + + # alter column with a reserved-word name in a table with a reserved-word name + def test_change_columns + assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') } + #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter + assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) } + assert_nothing_raised { @connection.rename_column(:group, :order, :values) } + end + + # dump structure of table with reserved word name + def test_structure_dump + assert_nothing_raised { @connection.structure_dump } + end + + # introspect table with reserved word name + def test_introspect + assert_nothing_raised { @connection.columns(:group) } + assert_nothing_raised { @connection.indexes(:group) } + end + + #fixtures + self.use_instantiated_fixtures = true + self.use_transactional_fixtures = false + + #fixtures :group + + def test_fixtures + f = create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + + assert_nothing_raised { + f.each do |x| + x.delete_existing_fixtures + end + } + + assert_nothing_raised { + f.each do |x| + x.insert_fixtures + end + } + end + + #activerecord model class with reserved-word table name + def test_activerecord_model + create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + x = nil + assert_nothing_raised { x = Group.new } + x.order = 'x' + assert_nothing_raised { x.save } + x.order = 'y' + assert_nothing_raised { x.save } + assert_nothing_raised { y = Group.find_by_order('y') } + assert_nothing_raised { y = Group.find(1) } + x = Group.find(1) + end + + # has_one association with reserved-word table name + def test_has_one_associations + create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + v = nil + assert_nothing_raised { v = Group.find(1).values } + assert_equal 2, v.id + end + + # belongs_to association with reserved-word table name + def test_belongs_to_associations + create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + gs = nil + assert_nothing_raised { gs = Select.find(2).groups } + assert_equal gs.length, 2 + assert(gs.collect{|x| x.id}.sort == [2, 3]) + end + + # has_and_belongs_to_many with reserved-word table name + def test_has_and_belongs_to_many + create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + s = nil + assert_nothing_raised { s = Distinct.find(1).selects } + assert_equal s.length, 2 + assert(s.collect{|x|x.id}.sort == [1, 2]) + end + + # activerecord model introspection with reserved-word table and column names + def test_activerecord_introspection + assert_nothing_raised { Group.table_exists? } + assert_nothing_raised { Group.columns } + end + + # Calculations + def test_calculations_work_with_reserved_words + assert_nothing_raised { Group.count } + end + + def test_associations_work_with_reserved_words + assert_nothing_raised { Select.find(:all, :include => [:groups]) } + end + + #the following functions were added to DRY test cases + + private + # custom fixture loader, uses Fixtures#create_fixtures and appends base_path to the current file's path + def create_test_fixtures(*fixture_names) + Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + end + + # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name + def drop_tables_directly(table_names, connection = @connection) + table_names.each do |name| + connection.execute("DROP TABLE IF EXISTS `#{name}`") + end + end + + # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns + def create_tables_directly (tables, connection = @connection) + tables.each do |table_name, column_properties| + connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") + end + end + +end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index f106e14319..e4746d4aa3 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlActiveSchemaTest < Test::Unit::TestCase +class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def setup ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do alias_method :real_execute, :execute diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb new file mode 100644 index 0000000000..7b72151b57 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -0,0 +1,17 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapterTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def test_table_alias_length + assert_nothing_raised do + @connection.table_alias_length + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb index 2505372b7e..ce0b2f5f5b 100644 --- a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb @@ -103,17 +103,107 @@ module ActiveRecord end end + def test_columns + columns = @ctx.columns('items').sort_by { |x| x.name } + assert_equal 2, columns.length + assert_equal %w{ id number }.sort, columns.map { |x| x.name } + assert_equal [nil, nil], columns.map { |x| x.default } + assert_equal [true, true], columns.map { |x| x.null } + end + + def test_columns_with_default + @ctx.execute <<-eosql + CREATE TABLE columns_with_default ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer default 10 + ) + eosql + column = @ctx.columns('columns_with_default').find { |x| + x.name == 'number' + } + assert_equal 10, column.default + end + + def test_columns_with_not_null + @ctx.execute <<-eosql + CREATE TABLE columns_with_default ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer not null + ) + eosql + column = @ctx.columns('columns_with_default').find { |x| + x.name == 'number' + } + assert !column.null, "column should not be null" + end + + def test_indexes_logs + intercept_logs_on @ctx + assert_difference('@ctx.logged.length') do + @ctx.indexes('items') + end + assert_match(/items/, @ctx.logged.last.first) + end + + def test_no_indexes + assert_equal [], @ctx.indexes('items') + end + + def test_index + @ctx.add_index 'items', 'id', :unique => true, :name => 'fun' + index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } + + assert_equal 'items', index.table + assert index.unique, 'index is unique' + assert_equal ['id'], index.columns + end + + def test_non_unique_index + @ctx.add_index 'items', 'id', :name => 'fun' + index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } + assert !index.unique, 'index is not unique' + end + + def test_compound_index + @ctx.add_index 'items', %w{ id number }, :name => 'fun' + index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } + assert_equal %w{ id number }.sort, index.columns.sort + end + + def test_primary_key + assert_equal 'id', @ctx.primary_key('items') + + @ctx.execute <<-eosql + CREATE TABLE foos ( + internet integer PRIMARY KEY AUTOINCREMENT, + number integer not null + ) + eosql + assert_equal 'internet', @ctx.primary_key('foos') + end + + def test_no_primary_key + @ctx.execute 'CREATE TABLE failboat (number integer not null)' + assert_nil @ctx.primary_key('failboat') + end + + private + def assert_logged logs + intercept_logs_on @ctx + yield + assert_equal logs, @ctx.logged + end + + def intercept_logs_on ctx @ctx.extend(Module.new { - attr_reader :logged + attr_accessor :logged def log sql, name - @logged ||= [] @logged << [sql, name] yield end }) - yield - assert_equal logs, @ctx.logged + @ctx.logged = [] end end end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 74588b4f47..9e285e57dc 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -125,7 +125,7 @@ class OverridingAggregationsTest < ActiveRecord::TestCase class Name; end class DifferentName; end - class Person < ActiveRecord::Base + class Person < ActiveRecord::Base composed_of :composed_of, :mapping => %w(person_first_name first_name) end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 665c387d5d..588adc38e3 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -28,7 +28,7 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal 7, ActiveRecord::Migrator::current_version end - def test_schema_raises_an_error_for_invalid_column_ntype + def test_schema_raises_an_error_for_invalid_column_type assert_raise NoMethodError do ActiveRecord::Schema.define(:version => 8) do create_table :vegetables do |t| diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index fb1e6e7e70..a1ce9b1689 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -5,8 +5,6 @@ require 'models/company' require 'models/topic' require 'models/reply' require 'models/computer' -require 'models/customer' -require 'models/order' require 'models/post' require 'models/author' require 'models/tag' @@ -34,7 +32,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_with_primary_key_joins_on_correct_column sql = Client.joins(:firm_with_primary_key).to_sql - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql) assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql) elsif current_adapter?(:OracleAdapter) @@ -217,6 +215,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase r1.topic = Topic.find(t2.id) + assert_no_queries do + r1.topic = t2 + end + assert r1.save assert_equal 0, Topic.find(t1.id).replies.size assert_equal 1, Topic.find(t2.id).replies.size diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 91b1af125e..15537d6940 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -1,8 +1,6 @@ require "cases/helper" require 'models/post' -require 'models/comment' require 'models/author' -require 'models/category' require 'models/project' require 'models/developer' diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 9c5dcc2ad9..b93e49613d 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -2,7 +2,6 @@ require "cases/helper" require 'models/post' require 'models/comment' require 'models/author' -require 'models/category' require 'models/categorization' require 'models/company' require 'models/topic' @@ -46,6 +45,13 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').first end + def test_eager_association_loading_with_join_for_count + authors = Author.joins(:special_posts).includes([:posts, :categorizations]) + + assert_nothing_raised { authors.count } + assert_queries(3) { authors.all } + end + def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") assert_equal 2, authors.size diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index b11969a841..ed7d9a782c 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -2,20 +2,14 @@ require "cases/helper" require 'models/developer' require 'models/project' require 'models/company' -require 'models/topic' -require 'models/reply' -require 'models/computer' require 'models/customer' require 'models/order' require 'models/categorization' require 'models/category' require 'models/post' require 'models/author' -require 'models/comment' require 'models/tag' require 'models/tagging' -require 'models/person' -require 'models/reader' require 'models/parrot' require 'models/pirate' require 'models/treasure' @@ -24,6 +18,8 @@ require 'models/club' require 'models/member' require 'models/membership' require 'models/sponsor' +require 'models/country' +require 'models/treaty' require 'active_support/core_ext/string/conversions' class ProjectWithAfterCreateHook < ActiveRecord::Base @@ -83,6 +79,60 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings + def setup_data_for_habtm_case + ActiveRecord::Base.connection.execute('delete from countries_treaties') + + country = Country.new(:name => 'India') + country.country_id = 'c1' + country.save! + + treaty = Treaty.new(:name => 'peace') + treaty.treaty_id = 't1' + country.treaties << treaty + end + + def test_should_property_quote_string_primary_keys + setup_data_for_habtm_case + + con = ActiveRecord::Base.connection + sql = 'select * from countries_treaties' + record = con.select_rows(sql).last + assert_equal 'c1', record[0] + assert_equal 't1', record[1] + end + + def test_should_record_timestamp_for_join_table + setup_data_for_habtm_case + + con = ActiveRecord::Base.connection + sql = 'select * from countries_treaties' + record = con.select_rows(sql).last + assert_not_nil record[2] + assert_not_nil record[3] + if current_adapter?(:Mysql2Adapter) + assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[2].to_s(:db) + assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[3].to_s(:db) + else + assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[2] + assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[3] + end + end + + def test_should_record_timestamp_for_join_table_only_if_timestamp_should_be_recorded + begin + Treaty.record_timestamps = false + setup_data_for_habtm_case + + con = ActiveRecord::Base.connection + sql = 'select * from countries_treaties' + record = con.select_rows(sql).last + assert_nil record[2] + assert_nil record[3] + ensure + Treaty.record_timestamps = true + end + end + def test_has_and_belongs_to_many david = Developer.find(1) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a52cedd8c2..ac2021c369 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -11,53 +11,50 @@ require 'models/comment' require 'models/person' require 'models/reader' require 'models/tagging' +require 'models/invoice' +require 'models/line_item' -class HasManyAssociationsTest < ActiveRecord::TestCase - fixtures :accounts, :categories, :companies, :developers, :projects, - :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings - - def setup - Client.destroyed_client_ids.clear +class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" end + def test_should_fail + assert_raise(ArgumentError) do + Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) + end + end +end - def test_create_by - person = Person.create! :first_name => 'tenderlove' - post = Post.find :first - - assert_equal [], person.readers - assert_nil person.readers.find_by_post_id post.id - - reader = person.readers.create_by_post_id post.id - - assert_equal 1, person.readers.count - assert_equal 1, person.readers.length - assert_equal post, person.readers.first.post - assert_equal person, person.readers.first.person +class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items" end + def test_should_fail + assert_raise(ArgumentError) do + Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) + end + end +end - def test_create_by_multi - person = Person.create! :first_name => 'tenderlove' - post = Post.find :first - assert_equal [], person.readers - reader = person.readers.create_by_post_id_and_skimmer post.id, false +class HasManyAssociationsTest < ActiveRecord::TestCase + fixtures :accounts, :categories, :companies, :developers, :projects, + :developers_projects, :topics, :authors, :comments, + :people, :posts, :readers, :taggings - assert_equal 1, person.readers.count - assert_equal 1, person.readers.length - assert_equal post, person.readers.first.post - assert_equal person, person.readers.first.person + def setup + Client.destroyed_client_ids.clear end - def test_find_or_create_by - person = Person.create! :first_name => 'tenderlove' - post = Post.find :first + def test_create_resets_cached_counters + person = Person.create!(:first_name => 'tenderlove') + post = Post.first assert_equal [], person.readers - assert_nil person.readers.find_by_post_id post.id + assert_nil person.readers.find_by_post_id(post.id) - reader = person.readers.find_or_create_by_post_id post.id + reader = person.readers.create(:post_id => post.id) assert_equal 1, person.readers.count assert_equal 1, person.readers.length @@ -65,16 +62,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal person, person.readers.first.person end - def test_find_or_create + def test_find_or_create_by_resets_cached_counters person = Person.create! :first_name => 'tenderlove' - post = Post.find :first + post = Post.first assert_equal [], person.readers - assert_nil person.readers.find(:first, :conditions => { - :post_id => post.id - }) + assert_nil person.readers.find_by_post_id(post.id) - reader = person.readers.find_or_create :post_id => post.id + reader = person.readers.find_or_create_by_post_id(post.id) assert_equal 1, person.readers.count assert_equal 1, person.readers.length @@ -82,7 +77,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal person, person.readers.first.person end - def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.each {|f| } end @@ -173,6 +167,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).readonly_clients.find(:all).each { |c| assert c.readonly? } end + def test_dynamic_find_or_create_from_two_attributes_using_an_association + author = authors(:david) + number_of_posts = Post.count + another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") + assert_equal number_of_posts + 1, Post.count + assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") + assert !another.new_record? + end + def test_cant_save_has_many_readonly_association authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } authors(:david).readonly_comments.each { |c| assert c.readonly? } @@ -549,7 +552,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert the_client.new_record? end - def test_find_or_create + def test_find_or_create_updates_size number_of_clients = companies(:first_firm).clients.size the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client") assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index e4dd810732..0eaadac5ae 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -14,9 +14,14 @@ require 'models/toy' require 'models/contract' require 'models/company' require 'models/developer' +require 'models/subscriber' +require 'models/book' +require 'models/subscription' class HasManyThroughAssociationsTest < ActiveRecord::TestCase - fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys, :jobs, :references, :companies + fixtures :posts, :readers, :people, :comments, :authors, + :owners, :pets, :toys, :jobs, :references, :companies, + :subscribers, :books, :subscriptions, :developers # Dummies to force column loads so query counts are clean. def setup @@ -383,4 +388,37 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lambda { authors(:david).very_special_comments.delete(authors(:david).very_special_comments.first) }, ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } end + + def test_collection_singular_ids_getter_with_string_primary_keys + book = books(:awdr) + assert_equal 2, book.subscriber_ids.size + assert_equal [subscribers(:first).nick, subscribers(:second).nick].sort, book.subscriber_ids.sort + end + + def test_collection_singular_ids_setter + company = companies(:rails_core) + dev = Developer.find(:first) + + company.developer_ids = [dev.id] + assert_equal [dev], company.developers + end + + def test_collection_singular_ids_setter_with_string_primary_keys + assert_nothing_raised do + book = books(:awdr) + book.subscriber_ids = [subscribers(:second).nick] + assert_equal [subscribers(:second)], book.subscribers(true) + + book.subscriber_ids = [] + assert_equal [], book.subscribers(true) + end + + end + + def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set + company = companies(:rails_core) + ids = [Developer.find(:first).id, -9999] + assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} + end + end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 178c57435b..3fcd150422 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -6,9 +6,12 @@ require 'models/membership' require 'models/sponsor' require 'models/organization' require 'models/member_detail' +require 'models/minivan' +require 'models/dashboard' +require 'models/speedometer' class HasOneThroughAssociationsTest < ActiveRecord::TestCase - fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations + fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, :dashboards, :speedometers def setup @member = members(:groucho) @@ -202,4 +205,11 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase Club.find(@club.id, :include => :sponsored_member).save! end end + + def test_value_is_properly_quoted + minivan = Minivan.find('m1') + assert_nothing_raised do + minivan.dashboard + end + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 34d24a2948..fa5c2e49df 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -412,7 +412,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase i = interests(:trainspotting) m = i.man assert_not_nil m.interests - iz = m.interests.detect {|iz| iz.id == i.id} + iz = m.interests.detect { |_iz| _iz.id == i.id} assert_not_nil iz assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" i.topic = 'Eating cheese with a spoon' @@ -516,7 +516,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase i = interests(:llama_wrangling) m = i.polymorphic_man assert_not_nil m.polymorphic_interests - iz = m.polymorphic_interests.detect {|iz| iz.id == i.id} + iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id} assert_not_nil iz assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" i.topic = 'Eating cheese with a spoon' diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 4ae776c35a..b31611e27a 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -2,11 +2,6 @@ require "cases/helper" require 'models/developer' require 'models/project' require 'models/company' -require 'models/topic' -require 'models/reply' -require 'models/computer' -require 'models/customer' -require 'models/order' require 'models/categorization' require 'models/category' require 'models/post' @@ -17,18 +12,66 @@ require 'models/tagging' require 'models/person' require 'models/reader' require 'models/parrot' -require 'models/pirate' -require 'models/treasure' -require 'models/price_estimate' -require 'models/club' -require 'models/member' -require 'models/membership' -require 'models/sponsor' +require 'models/ship_part' +require 'models/ship' +require 'models/liquid' +require 'models/molecule' +require 'models/electron' class AssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :developers_projects, :computers, :people, :readers + def test_eager_loading_should_not_change_count_of_children + liquid = Liquid.create(:name => 'salty') + molecule = liquid.molecules.create(:name => 'molecule_1') + molecule.electrons.create(:name => 'electron_1') + molecule.electrons.create(:name => 'electron_2') + + liquids = Liquid.includes(:molecules => :electrons).where('molecules.id is not null') + assert_equal 1, liquids[0].molecules.length + end + + def test_clear_association_cache_stored + firm = Firm.find(1) + assert_kind_of Firm, firm + + firm.clear_association_cache + assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort + end + + def test_clear_association_cache_new_record + firm = Firm.new + client_stored = Client.find(3) + client_new = Client.new + client_new.name = "The Joneses" + clients = [ client_stored, client_new ] + + firm.clients << clients + assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set + + firm.clear_association_cache + assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set + end + + def test_loading_the_association_target_should_keep_child_records_marked_for_destruction + ship = Ship.create!(:name => "The good ship Dollypop") + part = ship.parts.create!(:name => "Mast") + part.mark_for_destruction + ship.parts.send(:load_target) + assert ship.parts[0].marked_for_destruction? + end + + def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction + ship = Ship.create!(:name => "The good ship Dollypop") + part = ship.parts.create!(:name => "Mast") + part.mark_for_destruction + ShipPart.find(part.id).update_attribute(:name, 'Deck') + ship.parts.send(:load_target) + assert_equal 'Deck', ship.parts[0].name + end + + def test_include_with_order_works assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)} assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)} diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index d59fa0a632..2c069cd8a5 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,9 +1,15 @@ require "cases/helper" -require 'models/topic' require 'models/minimalistic' +require 'models/developer' +require 'models/auto_id' +require 'models/computer' +require 'models/topic' +require 'models/company' +require 'models/category' +require 'models/reply' class AttributeMethodsTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :developers, :companies, :computers def setup @old_matchers = ActiveRecord::Base.send(:attribute_method_matchers).dup @@ -16,6 +22,276 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end + def test_attribute_present + t = Topic.new + t.title = "hello there!" + t.written_on = Time.now + assert t.attribute_present?("title") + assert t.attribute_present?("written_on") + assert !t.attribute_present?("content") + end + + def test_attribute_keys_on_new_instance + t = Topic.new + assert_equal nil, t.title, "The topics table has a title column, so it should be nil" + assert_raise(NoMethodError) { t.title2 } + end + + def test_boolean_attributes + assert ! Topic.find(1).approved? + assert Topic.find(2).approved? + end + + def test_set_attributes + topic = Topic.find(1) + topic.attributes = { "title" => "Budget", "author_name" => "Jason" } + topic.save + assert_equal("Budget", topic.title) + assert_equal("Jason", topic.author_name) + assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address) + end + + def test_set_attributes_without_hash + topic = Topic.new + assert_nothing_raised { topic.attributes = '' } + end + + def test_integers_as_nil + test = AutoId.create('value' => '') + assert_nil AutoId.find(test.id).value + end + + def test_set_attributes_with_block + topic = Topic.new do |t| + t.title = "Budget" + t.author_name = "Jason" + end + + assert_equal("Budget", topic.title) + assert_equal("Jason", topic.author_name) + end + + def test_respond_to? + topic = Topic.find(1) + assert_respond_to topic, "title" + assert_respond_to topic, "title?" + assert_respond_to topic, "title=" + assert_respond_to topic, :title + assert_respond_to topic, :title? + assert_respond_to topic, :title= + assert_respond_to topic, "author_name" + assert_respond_to topic, "attribute_names" + assert !topic.respond_to?("nothingness") + assert !topic.respond_to?(:nothingness) + end + + def test_array_content + topic = Topic.new + topic.content = %w( one two three ) + topic.save + + assert_equal(%w( one two three ), Topic.find(topic.id).content) + end + + def test_read_attributes_before_type_cast + category = Category.new({:name=>"Test categoty", :type => nil}) + category_attrs = {"name"=>"Test categoty", "type" => nil, "categorizations_count" => nil} + assert_equal category_attrs , category.attributes_before_type_cast + end + + if current_adapter?(:MysqlAdapter) + def test_read_attributes_before_type_cast_on_boolean + bool = Booleantest.create({ "value" => false }) + assert_equal "0", bool.reload.attributes_before_type_cast["value"] + end + end + + unless current_adapter?(:Mysql2Adapter) + def test_read_attributes_before_type_cast_on_datetime + developer = Developer.find(:first) + # Oracle adapter returns Time before type cast + unless current_adapter?(:OracleAdapter) + assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"] + else + assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s(:db) + + developer.created_at = "345643456" + assert_equal developer.created_at_before_type_cast, "345643456" + assert_equal developer.created_at, nil + + developer.created_at = "2010-03-21T21:23:32+01:00" + assert_equal developer.created_at_before_type_cast, "2010-03-21T21:23:32+01:00" + assert_equal developer.created_at, Time.parse("2010-03-21T21:23:32+01:00") + end + end + end + + def test_hash_content + topic = Topic.new + topic.content = { "one" => 1, "two" => 2 } + topic.save + + assert_equal 2, Topic.find(topic.id).content["two"] + + topic.content_will_change! + topic.content["three"] = 3 + topic.save + + assert_equal 3, Topic.find(topic.id).content["three"] + end + + def test_update_array_content + topic = Topic.new + topic.content = %w( one two three ) + + topic.content.push "four" + assert_equal(%w( one two three four ), topic.content) + + topic.save + + topic = Topic.find(topic.id) + topic.content << "five" + assert_equal(%w( one two three four five ), topic.content) + end + + def test_case_sensitive_attributes_hash + # DB2 is not case-sensitive + return true if current_adapter?(:DB2Adapter) + + assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes + end + + def test_hashes_not_mangled + new_topic = { :title => "New Topic" } + new_topic_values = { :title => "AnotherTopic" } + + topic = Topic.new(new_topic) + assert_equal new_topic[:title], topic.title + + topic.attributes= new_topic_values + assert_equal new_topic_values[:title], topic.title + end + + def test_create_through_factory + topic = Topic.create("title" => "New Topic") + topicReloaded = Topic.find(topic.id) + assert_equal(topic, topicReloaded) + end + + def test_write_attribute + topic = Topic.new + topic.send(:write_attribute, :title, "Still another topic") + assert_equal "Still another topic", topic.title + + topic.send(:write_attribute, "title", "Still another topic: part 2") + assert_equal "Still another topic: part 2", topic.title + end + + def test_read_attribute + topic = Topic.new + topic.title = "Don't change the topic" + assert_equal "Don't change the topic", topic.send(:read_attribute, "title") + assert_equal "Don't change the topic", topic["title"] + + assert_equal "Don't change the topic", topic.send(:read_attribute, :title) + assert_equal "Don't change the topic", topic[:title] + end + + def test_read_attribute_when_false + topic = topics(:first) + topic.approved = false + assert !topic.approved?, "approved should be false" + topic.approved = "false" + assert !topic.approved?, "approved should be false" + end + + def test_read_attribute_when_true + topic = topics(:first) + topic.approved = true + assert topic.approved?, "approved should be true" + topic.approved = "true" + assert topic.approved?, "approved should be true" + end + + def test_read_write_boolean_attribute + topic = Topic.new + # puts "" + # puts "New Topic" + # puts topic.inspect + topic.approved = "false" + # puts "Expecting false" + # puts topic.inspect + assert !topic.approved?, "approved should be false" + topic.approved = "false" + # puts "Expecting false" + # puts topic.inspect + assert !topic.approved?, "approved should be false" + topic.approved = "true" + # puts "Expecting true" + # puts topic.inspect + assert topic.approved?, "approved should be true" + topic.approved = "true" + # puts "Expecting true" + # puts topic.inspect + assert topic.approved?, "approved should be true" + # puts "" + end + + def test_query_attribute_string + [nil, "", " "].each do |value| + assert_equal false, Topic.new(:author_name => value).author_name? + end + + assert_equal true, Topic.new(:author_name => "Name").author_name? + end + + def test_query_attribute_number + [nil, 0, "0"].each do |value| + assert_equal false, Developer.new(:salary => value).salary? + end + + assert_equal true, Developer.new(:salary => 1).salary? + assert_equal true, Developer.new(:salary => "1").salary? + end + + def test_query_attribute_boolean + [nil, "", false, "false", "f", 0].each do |value| + assert_equal false, Topic.new(:approved => value).approved? + end + + [true, "true", "1", 1].each do |value| + assert_equal true, Topic.new(:approved => value).approved? + end + end + + def test_query_attribute_with_custom_fields + object = Company.find_by_sql(<<-SQL).first + SELECT c1.*, c2.ruby_type as string_value, c2.rating as int_value + FROM companies c1, companies c2 + WHERE c1.firm_id = c2.id + AND c1.id = 2 + SQL + + assert_equal "Firm", object.string_value + assert object.string_value? + + object.string_value = " " + assert !object.string_value? + + assert_equal 1, object.int_value.to_i + assert object.int_value? + + object.int_value = "0" + assert !object.int_value? + end + + def test_non_attribute_access_and_assignment + topic = Topic.new + assert !topic.respond_to?("mumbo") + assert_raise(NoMethodError) { topic.mumbo } + assert_raise(NoMethodError) { topic.mumbo = 5 } + end + def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing topic = @target.new(:title => 'Budget') assert topic.respond_to?('title') diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 3b89c12a3f..49e7147773 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -13,6 +13,8 @@ require 'models/post' require 'models/reader' require 'models/ship' require 'models/ship_part' +require 'models/tag' +require 'models/tagging' require 'models/treasure' require 'models/company' @@ -171,7 +173,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase - fixtures :companies + fixtures :companies, :posts, :tags, :taggings def test_should_save_parent_but_not_invalid_child client = Client.new(:name => 'Joe (the Plumber)') @@ -312,6 +314,12 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert_equal num_orders +1, Order.count assert_equal num_customers +2, Customer.count end + + def test_store_association_with_a_polymorphic_relationship + num_tagging = Tagging.count + tags(:misc).create_tagging(:taggable => posts(:thinking)) + assert_equal num_tagging +1, Tagging.count + end end class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a4cf5120e1..ca397d3847 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -52,357 +52,6 @@ class BasicsTest < ActiveRecord::TestCase assert Topic.table_exists? end - def test_set_attributes - topic = Topic.find(1) - topic.attributes = { "title" => "Budget", "author_name" => "Jason" } - topic.save - assert_equal("Budget", topic.title) - assert_equal("Jason", topic.author_name) - assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address) - end - - def test_set_attributes_without_hash - topic = Topic.new - assert_nothing_raised do - topic.attributes = '' - end - end - - def test_integers_as_nil - test = AutoId.create('value' => '') - assert_nil AutoId.find(test.id).value - end - - def test_set_attributes_with_block - topic = Topic.new do |t| - t.title = "Budget" - t.author_name = "Jason" - end - - assert_equal("Budget", topic.title) - assert_equal("Jason", topic.author_name) - end - - def test_respond_to? - topic = Topic.find(1) - assert_respond_to topic, "title" - assert_respond_to topic, "title?" - assert_respond_to topic, "title=" - assert_respond_to topic, :title - assert_respond_to topic, :title? - assert_respond_to topic, :title= - assert_respond_to topic, "author_name" - assert_respond_to topic, "attribute_names" - assert !topic.respond_to?("nothingness") - assert !topic.respond_to?(:nothingness) - end - - def test_array_content - topic = Topic.new - topic.content = %w( one two three ) - topic.save - - assert_equal(%w( one two three ), Topic.find(topic.id).content) - end - - def test_read_attributes_before_type_cast - category = Category.new({:name=>"Test categoty", :type => nil}) - category_attrs = {"name"=>"Test categoty", "type" => nil, "categorizations_count" => nil} - assert_equal category_attrs , category.attributes_before_type_cast - end - - if current_adapter?(:MysqlAdapter) - def test_read_attributes_before_type_cast_on_boolean - bool = Booleantest.create({ "value" => false }) - assert_equal "0", bool.reload.attributes_before_type_cast["value"] - end - end - - def test_read_attributes_before_type_cast_on_datetime - developer = Developer.find(:first) - # Oracle adapter returns Time before type cast - unless current_adapter?(:OracleAdapter) - assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"] - else - assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s(:db) - end - end - - def test_hash_content - topic = Topic.new - topic.content = { "one" => 1, "two" => 2 } - topic.save - - assert_equal 2, Topic.find(topic.id).content["two"] - - topic.content_will_change! - topic.content["three"] = 3 - topic.save - - assert_equal 3, Topic.find(topic.id).content["three"] - end - - def test_update_array_content - topic = Topic.new - topic.content = %w( one two three ) - - topic.content.push "four" - assert_equal(%w( one two three four ), topic.content) - - topic.save - - topic = Topic.find(topic.id) - topic.content << "five" - assert_equal(%w( one two three four five ), topic.content) - end - - def test_case_sensitive_attributes_hash - # DB2 is not case-sensitive - return true if current_adapter?(:DB2Adapter) - - assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes - end - - def test_create - topic = Topic.new - topic.title = "New Topic" - topic.save - topic_reloaded = Topic.find(topic.id) - assert_equal("New Topic", topic_reloaded.title) - end - - def test_save! - topic = Topic.new(:title => "New Topic") - assert topic.save! - - reply = WrongReply.new - assert_raise(ActiveRecord::RecordInvalid) { reply.save! } - end - - def test_save_null_string_attributes - topic = Topic.find(1) - topic.attributes = { "title" => "null", "author_name" => "null" } - topic.save! - topic.reload - assert_equal("null", topic.title) - assert_equal("null", topic.author_name) - end - - def test_save_nil_string_attributes - topic = Topic.find(1) - topic.title = nil - topic.save! - topic.reload - assert_nil topic.title - end - - def test_save_for_record_with_only_primary_key - minimalistic = Minimalistic.new - assert_nothing_raised { minimalistic.save } - end - - def test_save_for_record_with_only_primary_key_that_is_provided - assert_nothing_raised { Minimalistic.create!(:id => 2) } - end - - def test_hashes_not_mangled - new_topic = { :title => "New Topic" } - new_topic_values = { :title => "AnotherTopic" } - - topic = Topic.new(new_topic) - assert_equal new_topic[:title], topic.title - - topic.attributes= new_topic_values - assert_equal new_topic_values[:title], topic.title - end - - def test_create_many - topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) - assert_equal 2, topics.size - assert_equal "first", topics.first.title - end - - def test_create_columns_not_equal_attributes - topic = Topic.new - topic.title = 'Another New Topic' - topic.send :write_attribute, 'does_not_exist', 'test' - assert_nothing_raised { topic.save } - end - - def test_create_through_factory - topic = Topic.create("title" => "New Topic") - topicReloaded = Topic.find(topic.id) - assert_equal(topic, topicReloaded) - end - - def test_create_through_factory_with_block - topic = Topic.create("title" => "New Topic") do |t| - t.author_name = "David" - end - topicReloaded = Topic.find(topic.id) - assert_equal("New Topic", topic.title) - assert_equal("David", topic.author_name) - end - - def test_create_many_through_factory_with_block - topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) do |t| - t.author_name = "David" - end - assert_equal 2, topics.size - topic1, topic2 = Topic.find(topics[0].id), Topic.find(topics[1].id) - assert_equal "first", topic1.title - assert_equal "David", topic1.author_name - assert_equal "second", topic2.title - assert_equal "David", topic2.author_name - end - - def test_update - topic = Topic.new - topic.title = "Another New Topic" - topic.written_on = "2003-12-12 23:23:00" - topic.save - topicReloaded = Topic.find(topic.id) - assert_equal("Another New Topic", topicReloaded.title) - - topicReloaded.title = "Updated topic" - topicReloaded.save - - topicReloadedAgain = Topic.find(topic.id) - - assert_equal("Updated topic", topicReloadedAgain.title) - end - - def test_update_columns_not_equal_attributes - topic = Topic.new - topic.title = "Still another topic" - topic.save - - topicReloaded = Topic.find(topic.id) - topicReloaded.title = "A New Topic" - topicReloaded.send :write_attribute, 'does_not_exist', 'test' - assert_nothing_raised { topicReloaded.save } - end - - def test_update_for_record_with_only_primary_key - minimalistic = minimalistics(:first) - assert_nothing_raised { minimalistic.save } - end - - def test_write_attribute - topic = Topic.new - topic.send(:write_attribute, :title, "Still another topic") - assert_equal "Still another topic", topic.title - - topic.send(:write_attribute, "title", "Still another topic: part 2") - assert_equal "Still another topic: part 2", topic.title - end - - def test_read_attribute - topic = Topic.new - topic.title = "Don't change the topic" - assert_equal "Don't change the topic", topic.send(:read_attribute, "title") - assert_equal "Don't change the topic", topic["title"] - - assert_equal "Don't change the topic", topic.send(:read_attribute, :title) - assert_equal "Don't change the topic", topic[:title] - end - - def test_read_attribute_when_false - topic = topics(:first) - topic.approved = false - assert !topic.approved?, "approved should be false" - topic.approved = "false" - assert !topic.approved?, "approved should be false" - end - - def test_read_attribute_when_true - topic = topics(:first) - topic.approved = true - assert topic.approved?, "approved should be true" - topic.approved = "true" - assert topic.approved?, "approved should be true" - end - - def test_read_write_boolean_attribute - topic = Topic.new - # puts "" - # puts "New Topic" - # puts topic.inspect - topic.approved = "false" - # puts "Expecting false" - # puts topic.inspect - assert !topic.approved?, "approved should be false" - topic.approved = "false" - # puts "Expecting false" - # puts topic.inspect - assert !topic.approved?, "approved should be false" - topic.approved = "true" - # puts "Expecting true" - # puts topic.inspect - assert topic.approved?, "approved should be true" - topic.approved = "true" - # puts "Expecting true" - # puts topic.inspect - assert topic.approved?, "approved should be true" - # puts "" - end - - def test_query_attribute_string - [nil, "", " "].each do |value| - assert_equal false, Topic.new(:author_name => value).author_name? - end - - assert_equal true, Topic.new(:author_name => "Name").author_name? - end - - def test_query_attribute_number - [nil, 0, "0"].each do |value| - assert_equal false, Developer.new(:salary => value).salary? - end - - assert_equal true, Developer.new(:salary => 1).salary? - assert_equal true, Developer.new(:salary => "1").salary? - end - - def test_query_attribute_boolean - [nil, "", false, "false", "f", 0].each do |value| - assert_equal false, Topic.new(:approved => value).approved? - end - - [true, "true", "1", 1].each do |value| - assert_equal true, Topic.new(:approved => value).approved? - end - end - - def test_query_attribute_with_custom_fields - object = Company.find_by_sql(<<-SQL).first - SELECT c1.*, c2.ruby_type as string_value, c2.rating as int_value - FROM companies c1, companies c2 - WHERE c1.firm_id = c2.id - AND c1.id = 2 - SQL - - assert_equal "Firm", object.string_value - assert object.string_value? - - object.string_value = " " - assert !object.string_value? - - assert_equal 1, object.int_value.to_i - assert object.int_value? - - object.int_value = "0" - assert !object.int_value? - end - - - def test_non_attribute_access_and_assignment - topic = Topic.new - assert !topic.respond_to?("mumbo") - assert_raise(NoMethodError) { topic.mumbo } - assert_raise(NoMethodError) { topic.mumbo = 5 } - end - def test_preserving_date_objects if current_adapter?(:SybaseAdapter) # Sybase ctlib does not (yet?) support the date type; use datetime instead. @@ -499,29 +148,6 @@ class BasicsTest < ActiveRecord::TestCase assert topic.instance_variable_get("@custom_approved") end - def test_delete - topic = Topic.find(1) - assert_equal topic, topic.delete, 'topic.delete did not return self' - assert topic.frozen?, 'topic not frozen after delete' - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } - end - - def test_delete_doesnt_run_callbacks - Topic.find(1).delete - assert_not_nil Topic.find(2) - end - - def test_destroy - topic = Topic.find(1) - assert_equal topic, topic.destroy, 'topic.destroy did not return self' - assert topic.frozen?, 'topic not frozen after destroy' - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } - end - - def test_record_not_found_exception - assert_raise(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) } - end - def test_initialize_with_attributes topic = Topic.new({ "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23" @@ -657,129 +283,13 @@ class BasicsTest < ActiveRecord::TestCase GUESSED_CLASSES.each(&:reset_table_name) end - def test_destroy_all - conditions = "author_name = 'Mary'" - topics_by_mary = Topic.all(:conditions => conditions, :order => 'id') - assert ! topics_by_mary.empty? - - assert_difference('Topic.count', -topics_by_mary.size) do - destroyed = Topic.destroy_all(conditions).sort_by(&:id) - assert_equal topics_by_mary, destroyed - assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen" - end - end - - def test_destroy_many - clients = Client.find([2, 3], :order => 'id') - assert_difference('Client.count', -2) do - destroyed = Client.destroy([2, 3]).sort_by(&:id) - assert_equal clients, destroyed - assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" - end - end - - def test_delete_many - original_count = Topic.count - Topic.delete(deleting = [1, 2]) - assert_equal original_count - deleting.size, Topic.count - end - - def test_boolean_attributes - assert ! Topic.find(1).approved? - assert Topic.find(2).approved? - end - - def test_update_all - assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'") - assert_equal "bulk updated!", Topic.find(1).content - assert_equal "bulk updated!", Topic.find(2).content - - assert_equal Topic.count, Topic.update_all(['content = ?', 'bulk updated again!']) - assert_equal "bulk updated again!", Topic.find(1).content - assert_equal "bulk updated again!", Topic.find(2).content - - assert_equal Topic.count, Topic.update_all(['content = ?', nil]) - assert_nil Topic.find(1).content - end - - def test_update_all_with_hash - assert_not_nil Topic.find(1).last_read - assert_equal Topic.count, Topic.update_all(:content => 'bulk updated with hash!', :last_read => nil) - assert_equal "bulk updated with hash!", Topic.find(1).content - assert_equal "bulk updated with hash!", Topic.find(2).content - assert_nil Topic.find(1).last_read - assert_nil Topic.find(2).last_read - end - - def test_update_all_with_non_standard_table_name - assert_equal 1, WarehouseThing.update_all(['value = ?', 0], ['id = ?', 1]) - assert_equal 0, WarehouseThing.find(1).value - end - - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def test_update_all_with_order_and_limit assert_equal 1, Topic.update_all("content = 'bulk updated!'", nil, :limit => 1, :order => 'id DESC') end end - # Oracle UPDATE does not support ORDER BY - unless current_adapter?(:OracleAdapter) - def test_update_all_ignores_order_without_limit_from_association - author = authors(:david) - assert_nothing_raised do - assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) - end - end - - def test_update_all_with_order_and_limit_updates_subset_only - author = authors(:david) - assert_nothing_raised do - assert_equal 1, author.posts_sorted_by_id_limited.size - assert_equal 2, author.posts_sorted_by_id_limited.find(:all, :limit => 2).size - assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:welcome).body - assert_not_equal "bulk update!", posts(:thinking).body - end - end - end - - def test_update_many - topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } - updated = Topic.update(topic_data.keys, topic_data.values) - - assert_equal 2, updated.size - assert_equal "1 updated", Topic.find(1).content - assert_equal "2 updated", Topic.find(2).content - end - - def test_delete_all - assert Topic.count > 0 - - assert_equal Topic.count, Topic.delete_all - end - - def test_update_by_condition - Topic.update_all "content = 'bulk updated!'", ["approved = ?", true] - assert_equal "Have a nice day", Topic.find(1).content - assert_equal "bulk updated!", Topic.find(2).content - end - - def test_attribute_present - t = Topic.new - t.title = "hello there!" - t.written_on = Time.now - assert t.attribute_present?("title") - assert t.attribute_present?("written_on") - assert !t.attribute_present?("content") - end - - def test_attribute_keys_on_new_instance - t = Topic.new - assert_equal nil, t.title, "The topics table has a title column, so it should be nil" - assert_raise(NoMethodError) { t.title2 } - end - def test_null_fields assert_nil Topic.find(1).parent_id assert_nil Topic.create("title" => "Hey you").parent_id @@ -863,120 +373,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end - def test_delete_new_record - client = Client.new - client.delete - assert client.frozen? - end - - def test_delete_record_with_associations - client = Client.find(3) - client.delete - assert client.frozen? - assert_kind_of Firm, client.firm - assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } - end - - def test_destroy_new_record - client = Client.new - client.destroy - assert client.frozen? - end - - def test_destroy_record_with_associations - client = Client.find(3) - client.destroy - assert client.frozen? - assert_kind_of Firm, client.firm - assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } - end - - def test_update_attribute - assert !Topic.find(1).approved? - Topic.find(1).update_attribute("approved", true) - assert Topic.find(1).approved? - - Topic.find(1).update_attribute(:approved, false) - assert !Topic.find(1).approved? - end - - def test_update_attribute_with_one_changed_and_one_updated - t = Topic.order('id').limit(1).first - title, author_name = t.title, t.author_name - t.author_name = 'John' - t.update_attribute(:title, 'super_title') - assert_equal 'John', t.author_name - assert_equal 'super_title', t.title - assert t.changed?, "topic should have changed" - assert t.author_name_changed?, "author_name should have changed" - assert !t.title_changed?, "title should not have changed" - assert_nil t.title_change, 'title change should be nil' - assert_equal ['author_name'], t.changed - - t.reload - assert_equal 'David', t.author_name - assert_equal 'super_title', t.title - end - - def test_update_attribute_with_one_updated - t = Topic.first - title = t.title - t.update_attribute(:title, 'super_title') - assert_equal 'super_title', t.title - assert !t.changed?, "topic should not have changed" - assert !t.title_changed?, "title should not have changed" - assert_nil t.title_change, 'title change should be nil' - - t.reload - assert_equal 'super_title', t.title - end - - def test_update_attribute_for_udpated_at_on - developer = Developer.find(1) - updated_at = developer.updated_at - developer.update_attribute(:salary, 80001) - assert_not_equal updated_at, developer.updated_at - developer.reload - assert_not_equal updated_at, developer.updated_at - end - - def test_update_attributes - topic = Topic.find(1) - assert !topic.approved? - assert_equal "The First Topic", topic.title - - topic.update_attributes("approved" => true, "title" => "The First Topic Updated") - topic.reload - assert topic.approved? - assert_equal "The First Topic Updated", topic.title - - topic.update_attributes(:approved => false, :title => "The First Topic") - topic.reload - assert !topic.approved? - assert_equal "The First Topic", topic.title - end - - def test_update_attributes! - Reply.validates_presence_of(:title) - reply = Reply.find(2) - assert_equal "The Second Topic of the day", reply.title - assert_equal "Have a nice day", reply.content - - reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening") - reply.reload - assert_equal "The Second Topic of the day updated", reply.title - assert_equal "Have a nice evening", reply.content - - reply.update_attributes!(:title => "The Second Topic of the day", :content => "Have a nice day") - reply.reload - assert_equal "The Second Topic of the day", reply.title - assert_equal "Have a nice day", reply.content - - assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") } - ensure - Reply.reset_callbacks(:validate) - end - def test_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes @@ -1236,35 +632,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal false, Topic.find(1).new_record? end - def test_destroyed_returns_boolean - developer = Developer.first - assert_equal false, developer.destroyed? - developer.destroy - assert_equal true, developer.destroyed? - - developer = Developer.last - assert_equal false, developer.destroyed? - developer.delete - assert_equal true, developer.destroyed? - end - - def test_persisted_returns_boolean - developer = Developer.new(:name => "Jose") - assert_equal false, developer.persisted? - developer.save! - assert_equal true, developer.persisted? - - developer = Developer.first - assert_equal true, developer.persisted? - developer.destroy - assert_equal false, developer.persisted? - - developer = Developer.last - assert_equal true, developer.persisted? - developer.delete - assert_equal false, developer.persisted? - end - def test_clone topic = Topic.find(1) cloned_topic = nil @@ -1607,67 +974,6 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_class_level_destroy - should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") - Topic.find(1).replies << should_be_destroyed_reply - - Topic.destroy(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) } - end - - def test_class_level_delete - should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") - Topic.find(1).replies << should_be_destroyed_reply - - Topic.delete(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } - assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) } - end - - def test_increment_attribute - assert_equal 50, accounts(:signals37).credit_limit - accounts(:signals37).increment! :credit_limit - assert_equal 51, accounts(:signals37, :reload).credit_limit - - accounts(:signals37).increment(:credit_limit).increment!(:credit_limit) - assert_equal 53, accounts(:signals37, :reload).credit_limit - end - - def test_increment_nil_attribute - assert_nil topics(:first).parent_id - topics(:first).increment! :parent_id - assert_equal 1, topics(:first).parent_id - end - - def test_increment_attribute_by - assert_equal 50, accounts(:signals37).credit_limit - accounts(:signals37).increment! :credit_limit, 5 - assert_equal 55, accounts(:signals37, :reload).credit_limit - - accounts(:signals37).increment(:credit_limit, 1).increment!(:credit_limit, 3) - assert_equal 59, accounts(:signals37, :reload).credit_limit - end - - def test_decrement_attribute - assert_equal 50, accounts(:signals37).credit_limit - - accounts(:signals37).decrement!(:credit_limit) - assert_equal 49, accounts(:signals37, :reload).credit_limit - - accounts(:signals37).decrement(:credit_limit).decrement!(:credit_limit) - assert_equal 47, accounts(:signals37, :reload).credit_limit - end - - def test_decrement_attribute_by - assert_equal 50, accounts(:signals37).credit_limit - accounts(:signals37).decrement! :credit_limit, 5 - assert_equal 45, accounts(:signals37, :reload).credit_limit - - accounts(:signals37).decrement(:credit_limit, 1).decrement!(:credit_limit, 3) - assert_equal 41, accounts(:signals37, :reload).credit_limit - end - def test_toggle_attribute assert !topics(:first).approved? topics(:first).toggle!(:approved) @@ -1794,28 +1100,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal res6, res7 end - def test_clear_association_cache_stored - firm = Firm.find(1) - assert_kind_of Firm, firm - - firm.clear_association_cache - assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort - end - - def test_clear_association_cache_new_record - firm = Firm.new - client_stored = Client.find(3) - client_new = Client.new - client_new.name = "The Joneses" - clients = [ client_stored, client_new ] - - firm.clients << clients - assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set - - firm.clear_association_cache - assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set - end - def test_interpolate_sql assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo@bar') } assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar) baz') } @@ -2015,134 +1299,6 @@ class BasicsTest < ActiveRecord::TestCase assert_no_queries { assert true } end - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - last_read_in_current_timezone = topics(:first).last_read.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - if current_adapter?(:SybaseAdapter) - assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] - else - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - end - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_to_xml_skipping_attributes - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - def test_to_xml_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_to_xml_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_to_xml_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_to_xml_including_multiple_associations_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_to_xml_including_methods - xml = Company.new.to_xml(:methods => :arbitrary_method, :skip_instruct => true) - assert_equal "<company>", xml.first(9) - assert xml.include?(%(<arbitrary-method>I am Jack's profound disappointment</arbitrary-method>)) - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Company.new.to_xml(:skip_instruct => true) do |xml| - xml.tag! "arbitrary-element", value - end - assert_equal "<company>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - def test_to_param_should_return_string assert_kind_of String, Client.find(:first).to_param end @@ -2237,15 +1393,6 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = original_logger end - def test_create_with_custom_timestamps - custom_datetime = 1.hour.ago.beginning_of_day - - %w(created_at created_on updated_at updated_on).each do |attribute| - parrot = LiveParrot.create(:name => "colombian", attribute => custom_datetime) - assert_equal custom_datetime, parrot[attribute] - end - end - def test_dup assert !Minimalistic.new.freeze.dup.frozen? end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 2c9d23c80f..afef31396e 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -325,7 +325,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_from_option_with_specified_index - if Edge.connection.adapter_name == 'MySQL' + if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' assert_equal Edge.count(:all), Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)') assert_equal Edge.count(:all, :conditions => 'sink_id < 5'), Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)', :conditions => 'sink_id < 5') diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index b5767344cd..cc6a6b44f2 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -68,6 +68,40 @@ class ColumnDefinitionTest < ActiveRecord::TestCase end end + if current_adapter?(:Mysql2Adapter) + def test_should_set_default_for_mysql_binary_data_types + binary_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "a", "binary(1)") + assert_equal "a", binary_column.default + + varbinary_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "a", "varbinary(1)") + assert_equal "a", varbinary_column.default + end + + def test_should_not_set_default_for_blob_and_text_data_types + assert_raise ArgumentError do + ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "a", "blob") + end + + assert_raise ArgumentError do + ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "Hello", "text") + end + + text_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "text") + assert_equal nil, text_column.default + + not_null_text_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "text", false) + assert_equal "", not_null_text_column.default + end + + def test_has_default_should_return_false_for_blog_and_test_data_types + blob_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "blob") + assert !blob_column.has_default? + + text_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "text") + assert !text_column.has_default? + end + end + if current_adapter?(:PostgreSQLAdapter) def test_bigint_column_should_map_to_integer bigint_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('number', nil, "bigint") diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb new file mode 100644 index 0000000000..c535119972 --- /dev/null +++ b/activerecord/test/cases/connection_management_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" + +class ConnectionManagementTest < ActiveRecord::TestCase + def setup + @env = {} + @app = stub('App') + @management = ActiveRecord::ConnectionAdapters::ConnectionManagement.new(@app) + + @connections_cleared = false + ActiveRecord::Base.stubs(:clear_active_connections!).with { @connections_cleared = true } + end + + test "clears active connections after each call" do + @app.expects(:call).with(@env) + @management.call(@env) + assert @connections_cleared + end + + test "doesn't clear active connections when running in a test case" do + @env['rack.test'] = true + @app.expects(:call).with(@env) + @management.call(@env) + assert !@connections_cleared + end +end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index cc9b2a45f4..82b3c36ed2 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,25 +1,31 @@ require "cases/helper" -class ConnectionManagementTest < ActiveRecord::TestCase - def setup - @env = {} - @app = stub('App') - @management = ActiveRecord::ConnectionAdapters::ConnectionManagement.new(@app) - - @connections_cleared = false - ActiveRecord::Base.stubs(:clear_active_connections!).with { @connections_cleared = true } - end - - test "clears active connections after each call" do - @app.expects(:call).with(@env) - @management.call(@env) - assert @connections_cleared - end - - test "doesn't clear active connections when running in a test case" do - @env['rack.test'] = true - @app.expects(:call).with(@env) - @management.call(@env) - assert !@connections_cleared +module ActiveRecord + module ConnectionAdapters + class ConnectionPoolTest < ActiveRecord::TestCase + def test_clear_stale_cached_connections! + pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec + + threads = [ + Thread.new { pool.connection }, + Thread.new { pool.connection }] + + threads.map { |t| t.join } + + pool.extend Module.new { + attr_accessor :checkins + def checkin conn + @checkins << conn + conn.object_id + end + } + pool.checkins = [] + + cleared_threads = pool.clear_stale_cached_connections! + assert((cleared_threads - threads.map { |x| x.object_id }).empty?, + "threads should have been removed") + assert_equal pool.checkins.length, threads.length + end + end end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index ef29422824..0e90128907 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -39,7 +39,7 @@ class DefaultTest < ActiveRecord::TestCase end end -if current_adapter?(:MysqlAdapter) +if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will # open a new transaction. When in transactional fixtures mode, this will diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 860d330a7f..4f3e43d77d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -10,7 +10,6 @@ require 'models/entrant' require 'models/project' require 'models/developer' require 'models/customer' -require 'models/job' class DynamicFinderMatchTest < ActiveRecord::TestCase def test_find_no_match @@ -694,6 +693,14 @@ class FinderTest < ActiveRecord::TestCase assert_equal [], Topic.find_all_by_title("The First Topic!!") end + def test_find_all_by_one_attribute_which_is_a_symbol + topics = Topic.find_all_by_content("Have a nice day".to_sym) + assert_equal 2, topics.size + assert topics.include?(topics(:first)) + + assert_equal [], Topic.find_all_by_title("The First Topic!!") + end + def test_find_all_by_one_attribute_that_is_an_aggregate balance = customers(:david).balance assert_kind_of Money, balance diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8008b86f81..93f8749255 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -36,7 +36,7 @@ class FixturesTest < ActiveRecord::TestCase fixtures = nil assert_nothing_raised { fixtures = create_fixtures(name) } assert_kind_of(Fixtures, fixtures) - fixtures.each { |name, fixture| + fixtures.each { |_name, fixture| fixture.each { |key, value| assert_match(MATCH_ATTRIBUTE_NAME, key) } @@ -229,9 +229,9 @@ if Account.connection.respond_to?(:reset_pk_sequence!) def test_create_fixtures_resets_sequences_when_not_cached @instances.each do |instance| - max_id = create_fixtures(instance.class.table_name).inject(0) do |max_id, (name, fixture)| + max_id = create_fixtures(instance.class.table_name).inject(0) do |_max_id, (name, fixture)| fixture_id = fixture['id'].to_i - fixture_id > max_id ? fixture_id : max_id + fixture_id > _max_id ? fixture_id : _max_id end # Clone the last fixture to check that it gets the next greatest id. diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb index ae4dcfb81e..3287626378 100644 --- a/activerecord/test/cases/i18n_test.rb +++ b/activerecord/test/cases/i18n_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'models/topic' require 'models/reply' -class ActiveRecordI18nTests < Test::Unit::TestCase +class ActiveRecordI18nTests < ActiveRecord::TestCase def setup I18n.backend = I18n::Backend::Simple.new diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index 99af7d2986..2de50b224c 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'models/topic' -class InvalidDateTest < Test::Unit::TestCase +class InvalidDateTest < ActiveRecord::TestCase def test_assign_valid_dates valid_dates = [[2007, 11, 30], [1993, 2, 28], [2008, 2, 29]] diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index c275557da8..2bc746c0b8 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -201,4 +201,11 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase } assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, :only => [1, :name]) end + + def test_should_be_able_to_encode_relation + authors_relation = Author.where(:id => [@david.id, @mary.id]) + + json = ActiveSupport::JSON.encode authors_relation, :only => :name + assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json + end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 66874cdad1..e7126964cd 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -53,7 +53,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::StaleObjectError) { p2.destroy } assert p1.destroy - assert_equal true, p1.frozen? + assert p1.frozen? + assert p1.destroyed? assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) } end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 4aeae1fe45..cbaaca764b 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -2,8 +2,9 @@ require "cases/helper" require "models/developer" require "active_support/log_subscriber/test_helper" -class LogSubscriberTest < ActiveSupport::TestCase +class LogSubscriberTest < ActiveRecord::TestCase include ActiveSupport::LogSubscriber::TestHelper + include ActiveSupport::BufferedLogger::Severity def setup @old_logger = ActiveRecord::Base.logger @@ -39,4 +40,25 @@ class LogSubscriberTest < ActiveSupport::TestCase assert_match(/CACHE/, @logger.logged(:debug).last) assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + + def test_basic_query_doesnt_log_when_level_is_not_debug + @logger.level = INFO + Developer.all + wait + assert_equal 0, @logger.logged(:debug).size + end + + def test_cached_queries_doesnt_log_when_level_is_not_debug + @logger.level = INFO + ActiveRecord::Base.cache do + Developer.all + Developer.all + end + wait + assert_equal 0, @logger.logged(:debug).size + end + + def test_initializes_runtime + Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join + end end diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 4e8ce1dac1..5256ab8d11 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -8,7 +8,6 @@ require 'models/author' require 'models/developer' require 'models/project' require 'models/comment' -require 'models/category' class MethodScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects @@ -209,6 +208,13 @@ class MethodScopingTest < ActiveRecord::TestCase end end + def test_scope_for_create_only_uses_equal + table = VerySpecialComment.arel_table + relation = VerySpecialComment.scoped + relation.where_values << table[:id].not_eq(1) + assert_equal({:type => "VerySpecialComment"}, relation.send(:scope_for_create)) + end + def test_scoped_create new_comment = nil @@ -543,4 +549,4 @@ class NestedScopingTest < ActiveRecord::TestCase assert_equal 1, scoped_authors.size assert_equal authors(:david).attributes, scoped_authors.first.attributes end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 2c3fc46831..0cf3979694 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -256,7 +256,7 @@ if ActiveRecord::Base.connection.supports_migrations? def test_create_table_with_defaults # MySQL doesn't allow defaults on TEXT or BLOB columns. - mysql = current_adapter?(:MysqlAdapter) + mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) Person.connection.create_table :testings do |t| t.column :one, :string, :default => "hello" @@ -313,7 +313,7 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type assert_equal 'integer', eleven.sql_type - elsif current_adapter?(:MysqlAdapter) + elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type @@ -581,7 +581,7 @@ if ActiveRecord::Base.connection.supports_migrations? assert_kind_of BigDecimal, bob.wealth end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def test_unabstracted_database_dependent_types Person.delete_all @@ -621,7 +621,7 @@ if ActiveRecord::Base.connection.supports_migrations? assert !Person.column_methods_hash.include?(:last_name) end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def testing_table_for_positioning Person.connection.create_table :testings, :id => false do |t| t.column :first, :integer @@ -1447,7 +1447,7 @@ if ActiveRecord::Base.connection.supports_migrations? columns = Person.connection.columns(:binary_testings) data_column = columns.detect { |c| c.name == "data" } - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_equal '', data_column.default else assert_nil data_column.default @@ -1748,7 +1748,7 @@ if ActiveRecord::Base.connection.supports_migrations? end def integer_column - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) 'int(11)' elsif current_adapter?(:OracleAdapter) 'NUMBER(38)' diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index dc85b395d3..c42dda2ccb 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -270,27 +270,27 @@ class NamedScopeTest < ActiveRecord::TestCase assert Topic.base.many? end - def test_should_build_with_proxy_options + def test_should_build_on_top_of_named_scope topic = Topic.approved.build({}) assert topic.approved end - def test_should_build_new_with_proxy_options + def test_should_build_new_on_top_of_named_scope topic = Topic.approved.new assert topic.approved end - def test_should_create_with_proxy_options + def test_should_create_on_top_of_named_scope topic = Topic.approved.create({}) assert topic.approved end - def test_should_create_with_bang_with_proxy_options + def test_should_create_with_bang_on_top_of_named_scope topic = Topic.approved.create!({}) assert topic.approved end - def test_should_build_with_proxy_options_chained + def test_should_build_on_top_of_chained_named_scopes topic = Topic.approved.by_lifo.build({}) assert topic.approved assert_equal 'lifo', topic.author_name @@ -478,4 +478,10 @@ class DynamicScopeTest < ActiveRecord::TestCase assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1) assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"}) end + + def test_dynamic_scope_should_create_methods_after_hitting_method_missing + assert Developer.methods.grep(/scoped_by_created_at/).blank? + Developer.scoped_by_created_at(nil) + assert Developer.methods.grep(/scoped_by_created_at/).present? + end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index c9ea0d8c40..df09bbd46a 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -59,6 +59,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.save! assert_equal 1, pirate.birds_with_reject_all_blank.count + assert_equal 'Tweetie', pirate.birds_with_reject_all_blank.first.name end def test_should_raise_an_ArgumentError_for_non_existing_associations @@ -74,7 +75,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase ship = pirate.create_ship(:name => 'Nights Dirty Lightning') assert_no_difference('Ship.count') do - pirate.update_attributes(:ship_attributes => { '_destroy' => true }) + pirate.update_attributes(:ship_attributes => { '_destroy' => true, :id => ship.id }) end end @@ -100,7 +101,8 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true } assert_no_difference('Ship.count') { pirate.save! } - # pirate.reject_empty_ships_on_create returns false for saved records + # pirate.reject_empty_ships_on_create returns false for saved pirate records + # in the previous step note that pirate gets saved but ship fails pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true } assert_difference('Ship.count') { pirate.save! } end @@ -266,6 +268,28 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end assert_equal 'Mayflower', @ship.reload.name end + + def test_should_update_existing_when_update_only_is_true_and_id_is_given + @ship.delete + @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning') + + assert_no_difference('Ship.count') do + @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower', :id => @ship.id }) + end + assert_equal 'Mayflower', @ship.reload.name + end + + def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction + Pirate.accepts_nested_attributes_for :update_only_ship, :update_only => true, :allow_destroy => true + @ship.delete + @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning') + + assert_difference('Ship.count', -1) do + @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower', :id => @ship.id, :_destroy => true }) + end + Pirate.accepts_nested_attributes_for :update_only_ship, :update_only => true, :allow_destroy => false + end + end class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase @@ -411,6 +435,27 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end assert_equal 'Arr', @pirate.reload.catchphrase end + + def test_should_update_existing_when_update_only_is_true_and_id_is_given + @pirate.delete + @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye') + + assert_no_difference('Pirate.count') do + @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr', :id => @pirate.id }) + end + assert_equal 'Arr', @pirate.reload.catchphrase + end + + def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction + Ship.accepts_nested_attributes_for :update_only_pirate, :update_only => true, :allow_destroy => true + @pirate.delete + @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye') + + assert_difference('Pirate.count', -1) do + @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr', :id => @pirate.id, :_destroy => true }) + end + Ship.accepts_nested_attributes_for :update_only_pirate, :update_only => true, :allow_destroy => false + end end module NestedAttributesOnACollectionAssociationTests @@ -811,7 +856,25 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR @part = @ship.parts.create!(:name => "Mast") @trinket = @part.trinkets.create!(:name => "Necklace") end - + + test "if association is not loaded and association record is saved and then in memory record attributes should be saved" do + @ship.parts_attributes=[{:id => @part.id,:name =>'Deck'}] + assert_equal 1, @ship.parts.proxy_target.size + assert_equal 'Deck', @ship.parts[0].name + end + + test "if association is not loaded and child doesn't change and I am saving a grandchild then in memory record should be used" do + @ship.parts_attributes=[{:id => @part.id,:trinkets_attributes =>[{:id => @trinket.id, :name => 'Ruby'}]}] + assert_equal 1, @ship.parts.proxy_target.size + assert_equal 'Mast', @ship.parts[0].name + assert_no_difference("@ship.parts[0].trinkets.proxy_target.size") do + @ship.parts[0].trinkets.proxy_target.size + end + assert_equal 'Ruby', @ship.parts[0].trinkets[0].name + @ship.save + assert_equal 'Ruby', @ship.parts[0].trinkets[0].name + end + test "when grandchild changed in memory, saving parent should save grandchild" do @trinket.name = "changed" @ship.save diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb new file mode 100644 index 0000000000..d7666b19f6 --- /dev/null +++ b/activerecord/test/cases/persistence_test.rb @@ -0,0 +1,470 @@ +require "cases/helper" +require 'models/post' +require 'models/author' +require 'models/topic' +require 'models/reply' +require 'models/category' +require 'models/company' +require 'models/developer' +require 'models/project' +require 'models/minimalistic' +require 'models/warehouse_thing' +require 'models/parrot' +require 'models/minivan' +require 'models/loose_person' +require 'rexml/document' +require 'active_support/core_ext/exception' + +class PersistencesTest < ActiveRecord::TestCase + + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans + + # Oracle UPDATE does not support ORDER BY + unless current_adapter?(:OracleAdapter) + def test_update_all_ignores_order_without_limit_from_association + author = authors(:david) + assert_nothing_raised do + assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) + end + end + + def test_update_all_with_order_and_limit_updates_subset_only + author = authors(:david) + assert_nothing_raised do + assert_equal 1, author.posts_sorted_by_id_limited.size + assert_equal 2, author.posts_sorted_by_id_limited.find(:all, :limit => 2).size + assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:welcome).body + assert_not_equal "bulk update!", posts(:thinking).body + end + end + end + + def test_update_many + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } + updated = Topic.update(topic_data.keys, topic_data.values) + + assert_equal 2, updated.size + assert_equal "1 updated", Topic.find(1).content + assert_equal "2 updated", Topic.find(2).content + end + + def test_delete_all + assert Topic.count > 0 + + assert_equal Topic.count, Topic.delete_all + end + + def test_update_by_condition + Topic.update_all "content = 'bulk updated!'", ["approved = ?", true] + assert_equal "Have a nice day", Topic.find(1).content + assert_equal "bulk updated!", Topic.find(2).content + end + + def test_increment_attribute + assert_equal 50, accounts(:signals37).credit_limit + accounts(:signals37).increment! :credit_limit + assert_equal 51, accounts(:signals37, :reload).credit_limit + + accounts(:signals37).increment(:credit_limit).increment!(:credit_limit) + assert_equal 53, accounts(:signals37, :reload).credit_limit + end + + def test_increment_nil_attribute + assert_nil topics(:first).parent_id + topics(:first).increment! :parent_id + assert_equal 1, topics(:first).parent_id + end + + def test_increment_attribute_by + assert_equal 50, accounts(:signals37).credit_limit + accounts(:signals37).increment! :credit_limit, 5 + assert_equal 55, accounts(:signals37, :reload).credit_limit + + accounts(:signals37).increment(:credit_limit, 1).increment!(:credit_limit, 3) + assert_equal 59, accounts(:signals37, :reload).credit_limit + end + + def test_destroy_all + conditions = "author_name = 'Mary'" + topics_by_mary = Topic.all(:conditions => conditions, :order => 'id') + assert ! topics_by_mary.empty? + + assert_difference('Topic.count', -topics_by_mary.size) do + destroyed = Topic.destroy_all(conditions).sort_by(&:id) + assert_equal topics_by_mary, destroyed + assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen" + end + end + + def test_destroy_many + clients = Client.find([2, 3], :order => 'id') + + assert_difference('Client.count', -2) do + destroyed = Client.destroy([2, 3]).sort_by(&:id) + assert_equal clients, destroyed + assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" + end + end + + def test_delete_many + original_count = Topic.count + Topic.delete(deleting = [1, 2]) + assert_equal original_count - deleting.size, Topic.count + end + + def test_decrement_attribute + assert_equal 50, accounts(:signals37).credit_limit + + accounts(:signals37).decrement!(:credit_limit) + assert_equal 49, accounts(:signals37, :reload).credit_limit + + accounts(:signals37).decrement(:credit_limit).decrement!(:credit_limit) + assert_equal 47, accounts(:signals37, :reload).credit_limit + end + + def test_decrement_attribute_by + assert_equal 50, accounts(:signals37).credit_limit + accounts(:signals37).decrement! :credit_limit, 5 + assert_equal 45, accounts(:signals37, :reload).credit_limit + + accounts(:signals37).decrement(:credit_limit, 1).decrement!(:credit_limit, 3) + assert_equal 41, accounts(:signals37, :reload).credit_limit + end + + def test_create + topic = Topic.new + topic.title = "New Topic" + topic.save + topic_reloaded = Topic.find(topic.id) + assert_equal("New Topic", topic_reloaded.title) + end + + def test_save! + topic = Topic.new(:title => "New Topic") + assert topic.save! + + reply = WrongReply.new + assert_raise(ActiveRecord::RecordInvalid) { reply.save! } + end + + def test_save_null_string_attributes + topic = Topic.find(1) + topic.attributes = { "title" => "null", "author_name" => "null" } + topic.save! + topic.reload + assert_equal("null", topic.title) + assert_equal("null", topic.author_name) + end + + def test_save_nil_string_attributes + topic = Topic.find(1) + topic.title = nil + topic.save! + topic.reload + assert_nil topic.title + end + + def test_save_for_record_with_only_primary_key + minimalistic = Minimalistic.new + assert_nothing_raised { minimalistic.save } + end + + def test_save_for_record_with_only_primary_key_that_is_provided + assert_nothing_raised { Minimalistic.create!(:id => 2) } + end + + def test_create_many + topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) + assert_equal 2, topics.size + assert_equal "first", topics.first.title + end + + def test_create_columns_not_equal_attributes + topic = Topic.new + topic.title = 'Another New Topic' + topic.send :write_attribute, 'does_not_exist', 'test' + assert_nothing_raised { topic.save } + end + + def test_create_through_factory_with_block + topic = Topic.create("title" => "New Topic") do |t| + t.author_name = "David" + end + topicReloaded = Topic.find(topic.id) + assert_equal("New Topic", topic.title) + assert_equal("David", topic.author_name) + end + + def test_create_many_through_factory_with_block + topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) do |t| + t.author_name = "David" + end + assert_equal 2, topics.size + topic1, topic2 = Topic.find(topics[0].id), Topic.find(topics[1].id) + assert_equal "first", topic1.title + assert_equal "David", topic1.author_name + assert_equal "second", topic2.title + assert_equal "David", topic2.author_name + end + + def test_update + topic = Topic.new + topic.title = "Another New Topic" + topic.written_on = "2003-12-12 23:23:00" + topic.save + topicReloaded = Topic.find(topic.id) + assert_equal("Another New Topic", topicReloaded.title) + + topicReloaded.title = "Updated topic" + topicReloaded.save + + topicReloadedAgain = Topic.find(topic.id) + + assert_equal("Updated topic", topicReloadedAgain.title) + end + + def test_update_columns_not_equal_attributes + topic = Topic.new + topic.title = "Still another topic" + topic.save + + topicReloaded = Topic.find(topic.id) + topicReloaded.title = "A New Topic" + topicReloaded.send :write_attribute, 'does_not_exist', 'test' + assert_nothing_raised { topicReloaded.save } + end + + def test_update_for_record_with_only_primary_key + minimalistic = minimalistics(:first) + assert_nothing_raised { minimalistic.save } + end + + def test_delete + topic = Topic.find(1) + assert_equal topic, topic.delete, 'topic.delete did not return self' + assert topic.frozen?, 'topic not frozen after delete' + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + + def test_delete_doesnt_run_callbacks + Topic.find(1).delete + assert_not_nil Topic.find(2) + end + + def test_destroy + topic = Topic.find(1) + assert_equal topic, topic.destroy, 'topic.destroy did not return self' + assert topic.frozen?, 'topic not frozen after destroy' + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + + def test_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) } + end + + def test_update_all + assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'") + assert_equal "bulk updated!", Topic.find(1).content + assert_equal "bulk updated!", Topic.find(2).content + + assert_equal Topic.count, Topic.update_all(['content = ?', 'bulk updated again!']) + assert_equal "bulk updated again!", Topic.find(1).content + assert_equal "bulk updated again!", Topic.find(2).content + + assert_equal Topic.count, Topic.update_all(['content = ?', nil]) + assert_nil Topic.find(1).content + end + + def test_update_all_with_hash + assert_not_nil Topic.find(1).last_read + assert_equal Topic.count, Topic.update_all(:content => 'bulk updated with hash!', :last_read => nil) + assert_equal "bulk updated with hash!", Topic.find(1).content + assert_equal "bulk updated with hash!", Topic.find(2).content + assert_nil Topic.find(1).last_read + assert_nil Topic.find(2).last_read + end + + def test_update_all_with_non_standard_table_name + assert_equal 1, WarehouseThing.update_all(['value = ?', 0], ['id = ?', 1]) + assert_equal 0, WarehouseThing.find(1).value + end + + def test_delete_new_record + client = Client.new + client.delete + assert client.frozen? + end + + def test_delete_record_with_associations + client = Client.find(3) + client.delete + assert client.frozen? + assert_kind_of Firm, client.firm + assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } + end + + def test_destroy_new_record + client = Client.new + client.destroy + assert client.frozen? + end + + def test_destroy_record_with_associations + client = Client.find(3) + client.destroy + assert client.frozen? + assert_kind_of Firm, client.firm + assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } + end + + def test_update_attribute + assert !Topic.find(1).approved? + Topic.find(1).update_attribute("approved", true) + assert Topic.find(1).approved? + + Topic.find(1).update_attribute(:approved, false) + assert !Topic.find(1).approved? + end + + def test_update_attribute_for_readonly_attribute + minivan = Minivan.find('m1') + assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } + end + + def test_update_attribute_with_one_changed_and_one_updated + t = Topic.order('id').limit(1).first + title, author_name = t.title, t.author_name + t.author_name = 'John' + t.update_attribute(:title, 'super_title') + assert_equal 'John', t.author_name + assert_equal 'super_title', t.title + assert t.changed?, "topic should have changed" + assert t.author_name_changed?, "author_name should have changed" + assert !t.title_changed?, "title should not have changed" + assert_nil t.title_change, 'title change should be nil' + assert_equal ['author_name'], t.changed + + t.reload + assert_equal 'David', t.author_name + assert_equal 'super_title', t.title + end + + def test_update_attribute_with_one_updated + t = Topic.first + title = t.title + t.update_attribute(:title, 'super_title') + assert_equal 'super_title', t.title + assert !t.changed?, "topic should not have changed" + assert !t.title_changed?, "title should not have changed" + assert_nil t.title_change, 'title change should be nil' + + t.reload + assert_equal 'super_title', t.title + end + + def test_update_attribute_for_udpated_at_on + developer = Developer.find(1) + prev_month = Time.now.prev_month + developer.update_attribute(:updated_at, prev_month) + assert_equal prev_month, developer.updated_at + developer.update_attribute(:salary, 80001) + assert_not_equal prev_month, developer.updated_at + developer.reload + assert_not_equal prev_month, developer.updated_at + end + + def test_update_attributes + topic = Topic.find(1) + assert !topic.approved? + assert_equal "The First Topic", topic.title + + topic.update_attributes("approved" => true, "title" => "The First Topic Updated") + topic.reload + assert topic.approved? + assert_equal "The First Topic Updated", topic.title + + topic.update_attributes(:approved => false, :title => "The First Topic") + topic.reload + assert !topic.approved? + assert_equal "The First Topic", topic.title + end + + def test_update_attributes! + Reply.validates_presence_of(:title) + reply = Reply.find(2) + assert_equal "The Second Topic of the day", reply.title + assert_equal "Have a nice day", reply.content + + reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening") + reply.reload + assert_equal "The Second Topic of the day updated", reply.title + assert_equal "Have a nice evening", reply.content + + reply.update_attributes!(:title => "The Second Topic of the day", :content => "Have a nice day") + reply.reload + assert_equal "The Second Topic of the day", reply.title + assert_equal "Have a nice day", reply.content + + assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") } + ensure + Reply.reset_callbacks(:validate) + end + + def test_destroyed_returns_boolean + developer = Developer.first + assert_equal false, developer.destroyed? + developer.destroy + assert_equal true, developer.destroyed? + + developer = Developer.last + assert_equal false, developer.destroyed? + developer.delete + assert_equal true, developer.destroyed? + end + + def test_persisted_returns_boolean + developer = Developer.new(:name => "Jose") + assert_equal false, developer.persisted? + developer.save! + assert_equal true, developer.persisted? + + developer = Developer.first + assert_equal true, developer.persisted? + developer.destroy + assert_equal false, developer.persisted? + + developer = Developer.last + assert_equal true, developer.persisted? + developer.delete + assert_equal false, developer.persisted? + end + + def test_class_level_destroy + should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_be_destroyed_reply + + Topic.destroy(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) } + end + + def test_class_level_delete + should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") + Topic.find(1).replies << should_be_destroyed_reply + + Topic.delete(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) } + assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) } + end + + def test_create_with_custom_timestamps + custom_datetime = 1.hour.ago.beginning_of_day + + %w(created_at created_on updated_at updated_on).each do |attribute| + parrot = LiveParrot.create(:name => "colombian", attribute => custom_datetime) + assert_equal custom_datetime, parrot[attribute] + end + end + +end diff --git a/activerecord/test/cases/pk_test.rb b/activerecord/test/cases/primary_keys_test.rb index 73f4b3848c..1e44237e0a 100644 --- a/activerecord/test/cases/pk_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -13,7 +13,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase topic = Topic.new assert topic.to_key.nil? topic = Topic.find(1) - assert_equal topic.to_key, [1] + assert_equal [1], topic.to_key end def test_to_key_with_customized_primary_key @@ -26,7 +26,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_to_key_with_primary_key_after_destroy topic = Topic.find(1) topic.destroy - assert_equal topic.to_key, [1] + assert_equal [1], topic.to_key end def test_integer_key diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 68abca70b3..594db1d0ab 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -1,8 +1,6 @@ require "cases/helper" require 'models/topic' -require 'models/reply' require 'models/task' -require 'models/course' require 'models/category' require 'models/post' @@ -59,7 +57,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' + elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' or current_adapter?(:Mysql2Adapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 41dcdbcd37..a50a4d4165 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -5,6 +5,8 @@ require 'models/developer' require 'models/project' require 'models/comment' require 'models/category' +require 'models/person' +require 'models/reference' class RelationScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects @@ -218,7 +220,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase end class HasManyScopingTest< ActiveRecord::TestCase - fixtures :comments, :posts + fixtures :comments, :posts, :people, :references def setup @welcome = Post.find(1) @@ -250,6 +252,23 @@ class HasManyScopingTest< ActiveRecord::TestCase assert_equal 'a comment...', @welcome.comments.what_are_you end end + + def test_should_maintain_default_scope_on_associations + person = people(:michael) + magician = BadReference.find(1) + assert_equal [magician], people(:michael).bad_references + end + + def test_should_default_scope_on_associations_is_overriden_by_association_conditions + person = people(:michael) + assert_equal [], people(:michael).fixed_bad_references + end + + def test_should_maintain_default_scope_on_eager_loaded_associations + michael = Person.where(:id => people(:michael).id).includes(:bad_references).first + magician = BadReference.find(1) + assert_equal [magician], michael.bad_references + end end class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase @@ -364,6 +383,12 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_named_scope_reorders_default + expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.reordered_by_name.find(:all).collect { |dev| dev.name } + assert_equal expected, received + end + def test_nested_exclusive_scope expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do @@ -393,4 +418,4 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index ffde8daa07..ac7b501bb7 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'models/tag' require 'models/tagging' require 'models/post' require 'models/topic' @@ -23,6 +22,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal 5, Post.where(:id => post_authors).size end + def test_multivalue_where + posts = Post.where('author_id = ? AND id = ?', 1, 1) + assert_equal 1, posts.to_a.size + end + def test_scoped topics = Topic.scoped assert_kind_of ActiveRecord::Relation, topics @@ -188,11 +192,23 @@ class RelationTest < ActiveRecord::TestCase end end - def test_respond_to_private_arel_methods + def test_respond_to_delegates_to_relation relation = Topic.scoped + fake_arel = Struct.new(:responds) { + def respond_to? method, access = false + responds << [method, access] + end + }.new [] + + relation.extend(Module.new { attr_accessor :arel }) + relation.arel = fake_arel + + relation.respond_to?(:matching_attributes) + assert_equal [:matching_attributes, false], fake_arel.responds.first - assert ! relation.respond_to?(:matching_attributes) - assert relation.respond_to?(:matching_attributes, true) + fake_arel.responds = [] + relation.respond_to?(:matching_attributes, true) + assert_equal [:matching_attributes, true], fake_arel.responds.first end def test_respond_to_dynamic_finders diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 1c43e3c5b5..66446b6b7e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -93,7 +93,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*:limit}, output - elsif current_adapter?(:MysqlAdapter) + elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_match %r{c_int_1.*:limit => 1}, output assert_match %r{c_int_2.*:limit => 2}, output assert_match %r{c_int_3.*:limit => 3}, output @@ -169,7 +169,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved" end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def test_schema_dump_should_not_add_default_value_for_mysql_text_field output = standard_dump assert_match %r{t.text\s+"body",\s+:null => false$}, output diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 8c385af97c..dab81530af 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -1,7 +1,13 @@ require "cases/helper" require 'models/contact' +require 'models/topic' +require 'models/reply' +require 'models/company' class SerializationTest < ActiveRecord::TestCase + + fixtures :topics, :companies, :accounts + FORMATS = [ :xml, :json ] def setup @@ -17,6 +23,134 @@ class SerializationTest < ActiveRecord::TestCase @contact = Contact.new(@contact_attributes) end + def test_to_xml + xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) + bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema + written_on_in_current_timezone = topics(:first).written_on.xmlschema + last_read_in_current_timezone = topics(:first).last_read.xmlschema + + assert_equal "topic", xml.root.name + assert_equal "The First Topic" , xml.elements["//title"].text + assert_equal "David" , xml.elements["//author-name"].text + assert_match "Have a nice day", xml.elements["//content"].text + + assert_equal "1", xml.elements["//id"].text + assert_equal "integer" , xml.elements["//id"].attributes['type'] + + assert_equal "1", xml.elements["//replies-count"].text + assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] + + assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text + assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] + + assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text + + assert_equal nil, xml.elements["//parent-id"].text + assert_equal "integer", xml.elements["//parent-id"].attributes['type'] + assert_equal "true", xml.elements["//parent-id"].attributes['nil'] + + if current_adapter?(:SybaseAdapter) + assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text + assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] + else + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) + assert_equal "2004-04-15", xml.elements["//last-read"].text + assert_equal "date" , xml.elements["//last-read"].attributes['type'] + end + + # Oracle and DB2 don't have true boolean or time-only fields + unless current_adapter?(:OracleAdapter, :DB2Adapter) + assert_equal "false", xml.elements["//approved"].text + assert_equal "boolean" , xml.elements["//approved"].attributes['type'] + + assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text + assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type'] + end + end + + def test_to_xml_skipping_attributes + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) + assert_equal "<topic>", xml.first(7) + assert !xml.include?(%(<title>The First Topic</title>)) + assert xml.include?(%(<author-name>David</author-name>)) + + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) + assert !xml.include?(%(<title>The First Topic</title>)) + assert !xml.include?(%(<author-name>David</author-name>)) + end + + def test_to_xml_including_has_many_association + xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) + assert_equal "<topic>", xml.first(7) + assert xml.include?(%(<replies type="array"><reply>)) + assert xml.include?(%(<title>The Second Topic of the day</title>)) + end + + def test_array_to_xml_including_has_many_association + xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) + assert xml.include?(%(<replies type="array"><reply>)) + end + + def test_array_to_xml_including_methods + xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) + assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml + assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml + end + + def test_array_to_xml_including_has_one_association + xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) + assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) + assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) + end + + def test_array_to_xml_including_belongs_to_association + xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) + assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) + assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) + assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) + end + + def test_to_xml_including_belongs_to_association + xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) + assert !xml.include?("<firm>") + + xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) + assert xml.include?("<firm>") + end + + def test_to_xml_including_multiple_associations + xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) + assert_equal "<firm>", xml.first(6) + assert xml.include?(%(<account>)) + assert xml.include?(%(<clients type="array"><client>)) + end + + def test_to_xml_including_multiple_associations_with_options + xml = companies(:first_firm).to_xml( + :indent => 0, :skip_instruct => true, + :include => { :clients => { :only => :name } } + ) + + assert_equal "<firm>", xml.first(6) + assert xml.include?(%(<client><name>Summit</name></client>)) + assert xml.include?(%(<clients type="array"><client>)) + end + + def test_to_xml_including_methods + xml = Company.new.to_xml(:methods => :arbitrary_method, :skip_instruct => true) + assert_equal "<company>", xml.first(9) + assert xml.include?(%(<arbitrary-method>I am Jack's profound disappointment</arbitrary-method>)) + end + + def test_to_xml_with_block + value = "Rockin' the block" + xml = Company.new.to_xml(:skip_instruct => true) do |_xml| + _xml.tag! "arbitrary-element", value + end + assert_equal "<company>", xml.first(9) + assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) + end + def test_serialize_should_be_reversible for format in FORMATS @serialized = Contact.new.send("to_#{format}") diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb new file mode 100644 index 0000000000..6f1c170a0c --- /dev/null +++ b/activerecord/test/cases/session_store/session_test.rb @@ -0,0 +1,68 @@ +require 'cases/helper' +require 'action_dispatch' +require 'active_record/session_store' + +module ActiveRecord + class SessionStore + class SessionTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def setup + super + Session.drop_table! if Session.table_exists? + end + + def test_data_column_name + # default column name is 'data' + assert_equal 'data', Session.data_column_name + end + + def test_table_name + assert_equal 'sessions', Session.table_name + end + + def test_create_table! + assert !Session.table_exists? + Session.create_table! + assert Session.table_exists? + Session.drop_table! + assert !Session.table_exists? + end + + def test_find_by_sess_id_compat + klass = Class.new(Session) do + def self.session_id_column + 'sessid' + end + end + klass.create_table! + + assert klass.columns_hash['sessid'], 'sessid column exists' + session = klass.new(:data => 'hello') + session.sessid = "100" + session.save! + + found = klass.find_by_session_id("100") + assert_equal session, found + assert_equal session.sessid, found.session_id + ensure + klass.drop_table! + end + + def test_find_by_session_id + Session.create_table! + session_id = "10" + s = Session.create!(:data => 'world', :session_id => session_id) + t = Session.find_by_session_id(session_id) + assert_equal s, t + assert_equal s.data, t.data + Session.drop_table! + end + + def test_loaded? + s = Session.new + assert !s.loaded?, 'session is not loaded' + end + end + end +end diff --git a/activerecord/test/cases/session_store/sql_bypass.rb b/activerecord/test/cases/session_store/sql_bypass.rb new file mode 100644 index 0000000000..f0ba166465 --- /dev/null +++ b/activerecord/test/cases/session_store/sql_bypass.rb @@ -0,0 +1,56 @@ +require 'cases/helper' +require 'action_dispatch' +require 'active_record/session_store' + +module ActiveRecord + class SessionStore + class SqlBypassTest < ActiveRecord::TestCase + def setup + super + Session.drop_table! if Session.table_exists? + end + + def test_create_table + assert !Session.table_exists? + SqlBypass.create_table! + assert Session.table_exists? + SqlBypass.drop_table! + assert !Session.table_exists? + end + + def test_new_record? + s = SqlBypass.new :data => 'foo', :session_id => 10 + assert s.new_record?, 'this is a new record!' + end + + def test_not_loaded? + s = SqlBypass.new({}) + assert !s.loaded?, 'it is not loaded' + end + + def test_loaded? + s = SqlBypass.new :data => 'hello' + assert s.loaded?, 'it is loaded' + end + + def test_save + SqlBypass.create_table! unless Session.table_exists? + session_id = 20 + s = SqlBypass.new :data => 'hello', :session_id => session_id + s.save + t = SqlBypass.find_by_session_id session_id + assert_equal s.session_id, t.session_id + assert_equal s.data, t.data + end + + def test_destroy + SqlBypass.create_table! unless Session.table_exists? + session_id = 20 + s = SqlBypass.new :data => 'hello', :session_id => session_id + s.save + s.destroy + assert_nil SqlBypass.find_by_session_id session_id + end + end + end +end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 549c4af6b1..401439994d 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -2,9 +2,10 @@ require 'cases/helper' require 'models/developer' require 'models/owner' require 'models/pet' +require 'models/toy' class TimestampTest < ActiveRecord::TestCase - fixtures :developers, :owners, :pets + fixtures :developers, :owners, :pets, :toys def setup @developer = Developer.first @@ -25,16 +26,26 @@ class TimestampTest < ActiveRecord::TestCase end def test_touching_a_record_updates_its_timestamp + previous_salary = @developer.salary + @developer.salary = previous_salary + 10000 @developer.touch assert_not_equal @previously_updated_at, @developer.updated_at + assert_equal previous_salary + 10000, @developer.salary + assert @developer.salary_changed?, 'developer salary should have changed' + assert @developer.changed?, 'developer should be marked as changed' + @developer.reload + assert_equal previous_salary, @developer.salary end def test_touching_a_different_attribute previously_created_at = @developer.created_at @developer.touch(:created_at) + assert !@developer.created_at_changed? , 'created_at should not be changed' + assert !@developer.changed?, 'record should not be changed' assert_not_equal previously_created_at, @developer.created_at + assert_not_equal @previously_updated_at, @developer.updated_at end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at @@ -72,4 +83,21 @@ class TimestampTest < ActiveRecord::TestCase ensure Pet.belongs_to :owner, :touch => true end + + def test_touching_a_record_touches_parent_record_and_grandparent_record + Toy.belongs_to :pet, :touch => true + Pet.belongs_to :owner, :touch => true + + toy = Toy.first + pet = toy.pet + owner = pet.owner + + owner.update_attribute(:updated_at, (time = 3.days.ago)) + toy.touch + owner.reload + + assert_not_equal time, owner.updated_at + ensure + Toy.belongs_to :pet + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index df123c9de8..d72c4bf7c4 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -1,6 +1,5 @@ require "cases/helper" require 'models/topic' -require 'models/reply' class TransactionCallbacksTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -246,3 +245,44 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:after_rollback], @second.history end end + + +class TransactionObserverCallbacksTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + fixtures :topics + + class TopicWithObserverAttached < ActiveRecord::Base + set_table_name :topics + def history + @history ||= [] + end + end + + class TopicWithObserverAttachedObserver < ActiveRecord::Observer + def after_commit(record) + record.history.push :"TopicWithObserverAttachedObserver#after_commit" + end + + def after_rollback(record) + record.history.push :"TopicWithObserverAttachedObserver#after_rollback" + end + end + + def test_after_commit_called + topic = TopicWithObserverAttached.new + topic.save! + + assert topic.history, [:"TopicWithObserverAttachedObserver#after_commit"] + end + + def test_after_rollback_called + topic = TopicWithObserverAttached.new + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert topic.history, [:"TopicWithObserverObserver#after_rollback"] + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 958a4e4f94..9255190613 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -3,10 +3,12 @@ require 'models/topic' require 'models/reply' require 'models/developer' require 'models/book' +require 'models/author' +require 'models/post' class TransactionTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - fixtures :topics, :developers + fixtures :topics, :developers, :authors, :posts def setup @first, @second = Topic.find(1, 2).sort_by { |t| t.id } @@ -103,6 +105,25 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_update_attributes_should_rollback_on_failure + author = Author.find(1) + posts_count = author.posts.size + assert posts_count > 0 + status = author.update_attributes(:name => nil, :post_ids => []) + assert !status + assert_equal posts_count, author.posts(true).size + end + + def test_update_attributes_should_rollback_on_failure! + author = Author.find(1) + posts_count = author.posts.size + assert posts_count > 0 + assert_raise(ActiveRecord::RecordInvalid) do + author.update_attributes!(:name => nil, :post_ids => []) + end + assert_equal posts_count, author.posts(true).size + end + def test_cancellation_from_before_destroy_rollbacks_in_destroy add_cancelling_before_destroy_with_db_side_effect_to_topic begin diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 454e42ed37..628029f8df 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -1,6 +1,5 @@ require "cases/helper" require 'models/topic' -require 'models/reply' class I18nGenerateMessageValidationTest < ActiveRecord::TestCase def setup diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 3f1b0e333f..fd771ef4be 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -4,11 +4,6 @@ require 'models/topic' require 'models/reply' require 'models/person' require 'models/developer' -require 'models/warehouse_thing' -require 'models/guid' -require 'models/owner' -require 'models/pet' -require 'models/event' require 'models/parrot' require 'models/company' diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 751946ffc5..b11b340e94 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -2,7 +2,6 @@ require "cases/helper" require 'models/contact' require 'models/post' require 'models/author' -require 'models/tagging' require 'models/comment' require 'models/company_in_module' diff --git a/activerecord/test/connections/native_mysql2/connection.rb b/activerecord/test/connections/native_mysql2/connection.rb new file mode 100644 index 0000000000..c6f198b1ac --- /dev/null +++ b/activerecord/test/connections/native_mysql2/connection.rb @@ -0,0 +1,25 @@ +print "Using native Mysql2\n" +require_dependency 'models/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost'; +# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; + +ActiveRecord::Base.configurations = { + 'arunit' => { + :adapter => 'mysql2', + :username => 'rails', + :encoding => 'utf8', + :database => 'activerecord_unittest', + }, + 'arunit2' => { + :adapter => 'mysql2', + :username => 'rails', + :database => 'activerecord_unittest2' + } +} + +ActiveRecord::Base.establish_connection 'arunit' +Course.establish_connection 'arunit2' diff --git a/activerecord/test/fixtures/dashboards.yml b/activerecord/test/fixtures/dashboards.yml new file mode 100644 index 0000000000..e75bf46e6c --- /dev/null +++ b/activerecord/test/fixtures/dashboards.yml @@ -0,0 +1,3 @@ +cool_first: + dashboard_id: d1 + name: my_dashboard
\ No newline at end of file diff --git a/activerecord/test/fixtures/minivans.yml b/activerecord/test/fixtures/minivans.yml new file mode 100644 index 0000000000..f1224a4c1a --- /dev/null +++ b/activerecord/test/fixtures/minivans.yml @@ -0,0 +1,5 @@ +cool_first: + minivan_id: m1 + name: my_minivan + speedometer_id: s1 + color: blue diff --git a/activerecord/test/fixtures/speedometers.yml b/activerecord/test/fixtures/speedometers.yml new file mode 100644 index 0000000000..6a471aba0a --- /dev/null +++ b/activerecord/test/fixtures/speedometers.yml @@ -0,0 +1,4 @@ +cool_first: + speedometer_id: s1 + name: my_speedometer + dashboard_id: d1
\ No newline at end of file diff --git a/activerecord/test/fixtures/subscriptions.yml b/activerecord/test/fixtures/subscriptions.yml index 371bfd3422..5a93c12193 100644 --- a/activerecord/test/fixtures/subscriptions.yml +++ b/activerecord/test/fixtures/subscriptions.yml @@ -9,4 +9,4 @@ webster_rfr: alterself_awdr: id: 3 subscriber_id: alterself - book_id: 3
\ No newline at end of file + book_id: 1 diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 655b45bf57..727978431c 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -108,6 +108,8 @@ class Author < ActiveRecord::Base %w(twitter github) end + validates_presence_of :name + private def log_before_adding(object) @post_log << "before_adding#{object.id || '<new>'}" diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index cfd07abddc..1e030b4f59 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -1,4 +1,7 @@ class Book < ActiveRecord::Base has_many :citations, :foreign_key => 'book1_id' has_many :references, :through => :citations, :source => :reference_of, :uniq => true + + has_many :subscriptions + has_many :subscribers, :through => :subscriptions end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 83d71b6909..2c8c30efb4 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/object/misc' +require 'active_support/core_ext/object/with_options' module MyApplication module Business diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb new file mode 100644 index 0000000000..15e3a1de0b --- /dev/null +++ b/activerecord/test/models/country.rb @@ -0,0 +1,7 @@ +class Country < ActiveRecord::Base + + set_primary_key :country_id + + has_and_belongs_to_many :treaties + +end diff --git a/activerecord/test/models/dashboard.rb b/activerecord/test/models/dashboard.rb new file mode 100644 index 0000000000..a8a25834b1 --- /dev/null +++ b/activerecord/test/models/dashboard.rb @@ -0,0 +1,3 @@ +class Dashboard < ActiveRecord::Base + set_primary_key :dashboard_id +end
\ No newline at end of file diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index de68fd7f24..c61c583c1d 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -88,6 +88,7 @@ class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' default_scope :order => 'salary DESC' scope :by_name, :order => 'name DESC' + scope :reordered_by_name, reorder('name DESC') def self.all_ordered_by_name with_scope(:find => { :order => 'name DESC' }) do diff --git a/activerecord/test/models/electron.rb b/activerecord/test/models/electron.rb new file mode 100644 index 0000000000..35af9f679b --- /dev/null +++ b/activerecord/test/models/electron.rb @@ -0,0 +1,3 @@ +class Electron < ActiveRecord::Base + belongs_to :molecule +end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb new file mode 100644 index 0000000000..b96c054f6c --- /dev/null +++ b/activerecord/test/models/liquid.rb @@ -0,0 +1,5 @@ +class Liquid < ActiveRecord::Base + set_table_name :liquid + has_many :molecules, :uniq => true +end + diff --git a/activerecord/test/models/minivan.rb b/activerecord/test/models/minivan.rb new file mode 100644 index 0000000000..602438d16f --- /dev/null +++ b/activerecord/test/models/minivan.rb @@ -0,0 +1,9 @@ +class Minivan < ActiveRecord::Base + set_primary_key :minivan_id + + belongs_to :speedometer + has_one :dashboard, :through => :speedometer + + attr_readonly :color + +end diff --git a/activerecord/test/models/molecule.rb b/activerecord/test/models/molecule.rb new file mode 100644 index 0000000000..69325b8d29 --- /dev/null +++ b/activerecord/test/models/molecule.rb @@ -0,0 +1,4 @@ +class Molecule < ActiveRecord::Base + belongs_to :liquid + has_many :electrons +end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 2a73b1ee01..951ec93c53 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -4,6 +4,8 @@ class Person < ActiveRecord::Base has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, :conditions => 'comments.id is null' has_many :references + has_many :bad_references + has_many :fixed_bad_references, :conditions => { :favourite => true }, :class_name => 'BadReference' has_many :jobs, :through => :references has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true] has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id' diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 479e8b72c6..4a17c936f5 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -2,3 +2,8 @@ class Reference < ActiveRecord::Base belongs_to :person belongs_to :job end + +class BadReference < ActiveRecord::Base + self.table_name ='references' + default_scope :conditions => {:favourite => false } +end diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb new file mode 100644 index 0000000000..94743eff8e --- /dev/null +++ b/activerecord/test/models/speedometer.rb @@ -0,0 +1,4 @@ +class Speedometer < ActiveRecord::Base + set_primary_key :speedometer_id + belongs_to :dashboard +end
\ No newline at end of file diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb new file mode 100644 index 0000000000..b46537f0d2 --- /dev/null +++ b/activerecord/test/models/treaty.rb @@ -0,0 +1,7 @@ +class Treaty < ActiveRecord::Base + + set_primary_key :treaty_id + + has_and_belongs_to_many :countries + +end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb new file mode 100644 index 0000000000..c78d99f4af --- /dev/null +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -0,0 +1,24 @@ +ActiveRecord::Schema.define do + create_table :binary_fields, :force => true, :options => 'CHARACTER SET latin1' do |t| + t.binary :tiny_blob, :limit => 255 + t.binary :normal_blob, :limit => 65535 + t.binary :medium_blob, :limit => 16777215 + t.binary :long_blob, :limit => 2147483647 + t.text :tiny_text, :limit => 255 + t.text :normal_text, :limit => 65535 + t.text :medium_text, :limit => 16777215 + t.text :long_text, :limit => 2147483647 + end + + ActiveRecord::Base.connection.execute <<-SQL +DROP PROCEDURE IF EXISTS ten; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE PROCEDURE ten() SQL SECURITY INVOKER +BEGIN + select 10; +END +SQL + +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index bea351b95a..fc3810f82b 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -164,6 +164,11 @@ ActiveRecord::Schema.define do t.string :address_country t.string :gps_location end + + create_table :dashboards, :force => true, :id => false do |t| + t.string :dashboard_id + t.string :name + end create_table :developers, :force => true do |t| t.string :name @@ -290,6 +295,13 @@ ActiveRecord::Schema.define do t.boolean :favourite t.integer :lock_version, :default => 0 end + + create_table :minivans, :force => true, :id => false do |t| + t.string :minivan_id + t.string :name + t.string :speedometer_id + t.string :color + end create_table :minimalistics, :force => true do |t| end @@ -386,6 +398,7 @@ ActiveRecord::Schema.define do create_table :pets, :primary_key => :pet_id ,:force => true do |t| t.string :name t.integer :owner_id, :integer + t.timestamps end create_table :pirates, :force => true do |t| @@ -452,6 +465,12 @@ ActiveRecord::Schema.define do t.string :name t.integer :ship_id end + + create_table :speedometers, :force => true, :id => false do |t| + t.string :speedometer_id + t.string :name + t.string :dashboard_id + end create_table :sponsors, :force => true do |t| t.integer :club_id @@ -512,6 +531,7 @@ ActiveRecord::Schema.define do create_table :toys, :primary_key => :toy_id ,:force => true do |t| t.string :name t.integer :pet_id, :integer + t.timestamps end create_table :traffic_lights, :force => true do |t| @@ -583,6 +603,34 @@ ActiveRecord::Schema.define do t.string :title end + create_table :countries, :force => true, :id => false, :primary_key => 'country_id' do |t| + t.string :country_id + t.string :name + end + create_table :treaties, :force => true, :id => false, :primary_key => 'treaty_id' do |t| + t.string :treaty_id + t.string :name + end + create_table :countries_treaties, :force => true, :id => false do |t| + t.string :country_id, :null => false + t.string :treaty_id, :null => false + t.datetime :created_at + t.datetime :updated_at + end + + create_table :liquid, :force => true do |t| + t.string :name + end + create_table :molecules, :force => true do |t| + t.integer :liquid_id + t.string :name + end + create_table :electrons, :force => true do |t| + t.integer :molecule_id + t.string :name + end + + except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk create_table :fk_test_has_fk, :force => true do |t| diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 2b2092b9fe..8fa1b0b4b3 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* No material changes + + *Rails 3.0.0 [beta 4] (June 8th, 2010)* * JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. [Santiago Pastorino] diff --git a/activeresource/README b/activeresource/README.rdoc index 127ac5b4a9..127ac5b4a9 100644 --- a/activeresource/README +++ b/activeresource/README.rdoc diff --git a/activeresource/Rakefile b/activeresource/Rakefile index 04b08ed8cb..2145f1017c 100644 --- a/activeresource/Rakefile +++ b/activeresource/Rakefile @@ -1,8 +1,8 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -29,13 +29,13 @@ end # Generate the RDoc documentation -Rake::RDocTask.new { |rdoc| +RDoc::Task.new { |rdoc| rdoc.rdoc_dir = 'doc' rdoc.title = "Active Resource -- Object-oriented REST services" - rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.options << '-f' << 'horo' + rdoc.options << '--main' << 'README.rdoc' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG') rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.exclude('lib/activeresource.rb') } @@ -80,9 +80,3 @@ task :release => :package do Rake::Gemcutter::Tasks.new(spec).define Rake::Task['gem:push'].invoke end - -desc "Publish the API documentation" -task :pdoc => [:rdoc] do - require 'rake/contrib/sshpublisher' - Rake::SshDirPublisher.new("rails@api.rubyonrails.org", "public_html/ar", "doc").upload -end diff --git a/activeresource/activeresource.gemspec b/activeresource/activeresource.gemspec index ca74c0dd7d..a71168722b 100644 --- a/activeresource/activeresource.gemspec +++ b/activeresource/activeresource.gemspec @@ -14,12 +14,12 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'activeresource' - s.files = Dir['CHANGELOG', 'README', 'examples/**/*', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*'] s.require_path = 'lib' s.has_rdoc = true - s.extra_rdoc_files = %w( README ) - s.rdoc_options.concat ['--main', 'README'] + s.extra_rdoc_files = %w( README.rdoc ) + s.rdoc_options.concat ['--main', 'README.rdoc'] s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 6c494a8bcc..62420725ad 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -7,7 +7,6 @@ require 'active_support/core_ext/module/attr_accessor_with_default' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/misc' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/object/duplicable' require 'set' @@ -578,7 +577,7 @@ module ActiveResource # Default value is <tt>site.path</tt>. def prefix=(value = '/') # Replace :placeholders with '#{embedded options[:lookups]}' - prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" } + prefix_call = value.gsub(/:\w+/) { |key| "\#{URI.escape options[#{key}].to_s}" } # Clear prefix parameters in case they have been cached @prefix_parameters = nil @@ -623,7 +622,7 @@ module ActiveResource # def element_path(id, prefix_options = {}, query_options = nil) prefix_options, query_options = split_options(prefix_options) if query_options.nil? - "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}" + "#{prefix(prefix_options)}#{collection_name}/#{URI.escape id.to_s}.#{format.extension}#{query_string(query_options)}" end # Gets the new element path for REST resources. @@ -1223,10 +1222,10 @@ module ActiveResource when Array resource = find_or_create_resource_for_collection(key) value.map do |attrs| - if attrs.is_a?(String) || attrs.is_a?(Numeric) - attrs.duplicable? ? attrs.dup : attrs - else + if attrs.is_a?(Hash) resource.new(attrs) + else + attrs.duplicable? ? attrs.dup : attrs end end when Hash diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index f192c53b4f..75425c01c0 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -123,7 +123,11 @@ module ActiveResource # def post(path, body, headers) # request = ActiveResource::Request.new(:post, path, body, headers) # self.class.requests << request - # self.class.responses.assoc(request).try(:second) || raise(InvalidRequestError.new("No response recorded for #{request}")) + # if response = self.class.responses.assoc(request) + # response[1] + # else + # raise InvalidRequestError.new("No response recorded for #{request}") + # end # end module_eval <<-EOE, __FILE__, __LINE__ + 1 def #{method}(path, #{'body, ' if has_body}headers) diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb index 198e77a3d1..43c00e9cf1 100644 --- a/activeresource/lib/active_resource/version.rb +++ b/activeresource/lib/active_resource/version.rb @@ -3,7 +3,7 @@ module ActiveResource MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activeresource/test/cases/base/load_test.rb b/activeresource/test/cases/base/load_test.rb index 7745a9439b..228dc36d9b 100644 --- a/activeresource/test/cases/base/load_test.rb +++ b/activeresource/test/cases/base/load_test.rb @@ -47,6 +47,8 @@ class BaseLoadTest < Test::Unit::TestCase { :id => 1, :name => 'Willamette' }, { :id => 2, :name => 'Columbia', :rafted_by => @matz }], :postal_codes => [ 97018, 1234567890 ], + :dates => [ Time.now ], + :votes => [ true, false, true ], :places => [ "Columbia City", "Unknown" ]}}} @person = Person.new @@ -149,6 +151,16 @@ class BaseLoadTest < Test::Unit::TestCase assert_kind_of Array, places assert_kind_of String, places.first assert_equal @deep[:street][:state][:places].first, places.first + + dates = state.dates + assert_kind_of Array, dates + assert_kind_of Time, dates.first + assert_equal @deep[:street][:state][:dates].first, dates.first + + votes = state.votes + assert_kind_of Array, votes + assert_kind_of TrueClass, votes.first + assert_equal @deep[:street][:state][:votes].first, votes.first end def test_nested_collections_within_the_same_namespace diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index 4d036d73cc..91b375681b 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -6,6 +6,7 @@ require "fixtures/sound" require "fixtures/beast" require "fixtures/proxy" require 'active_support/json' +require 'active_support/ordered_hash' require 'active_support/core_ext/hash/conversions' require 'mocha' @@ -555,13 +556,14 @@ class BaseTest < Test::Unit::TestCase assert_equal '/people.xml?name[]=bob&name[]=your+uncle%2Bme&name[]=&name[]=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false]) - assert_equal '/people.xml?struct[a][]=2&struct[a][]=1&struct[b]=fred', Person.collection_path(:struct => {:a => [2,1], 'b' => 'fred'}) + assert_equal '/people.xml?struct[a][]=2&struct[a][]=1&struct[b]=fred', Person.collection_path(:struct => ActiveSupport::OrderedHash[:a, [2,1], 'b', 'fred']) end def test_custom_element_path assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, :person_id => 1) assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 1) assert_equal '/people/Greg/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 'Greg') + assert_equal '/people/ann%20mary/addresses/ann%20mary.xml', StreetAddress.element_path(:'ann mary', 'person_id' => 'ann mary') end def test_module_element_path diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 60586cf99a..8485e7d46b 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,4 +1,6 @@ -*Rails 3.0.0 [Release Candidate] (unreleased)* +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* Removed Object#returning, Object#tap should be used instead. [Santiago Pastorino] * Deprecation behavior is no longer hardcoded to the name of the environment. Instead, it is set via config.active_support.deprecation and can be one diff --git a/activesupport/README b/activesupport/README deleted file mode 100644 index 9fb9a80cbe..0000000000 --- a/activesupport/README +++ /dev/null @@ -1,43 +0,0 @@ -= Active Support -- Utility classes and standard library extensions from Rails - -Active Support is a collection of various utility classes and standard library extensions that were found useful -for Rails. All these additions have hence been collected in this bundle as way to gather all that sugar that makes -Ruby sweeter. - - -== Download - -The latest version of Active Support can be found at - -* http://rubyforge.org/project/showfiles.php?group_id=182 - -Documentation can be found at - -* http://as.rubyonrails.com - - -== Installation - -The preferred method of installing Active Support is through its GEM file. You'll need to have -RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have it, -then use: - - % [sudo] gem install activesupport-1.0.0.gem - - -== License - -Active Support is released under the MIT license. - - -== Support - -The Active Support homepage is http://www.rubyonrails.com. You can find the Active Support -RubyForge page at http://rubyforge.org/projects/activesupport. And as Jim from Rake says: - - Feel free to submit commits or feature requests. If you send a patch, - remember to update the corresponding unit tests. If fact, I prefer - new feature to be submitted in the form of new unit tests. - -For other information, feel free to ask on the ruby-talk mailing list -(which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc new file mode 100644 index 0000000000..77b8a64304 --- /dev/null +++ b/activesupport/README.rdoc @@ -0,0 +1,33 @@ += Active Support -- Utility classes and Ruby extensions from Rails + +Active Support is a collection of utility classes and standard library +extensions that were found useful for the Rails framework. These additions +reside in this package so they can be loaded as needed in Ruby projects +outside of Rails. + + +== Download and installation + +The latest version of Active Support can be installed with Rubygems: + + % [sudo] gem install activesupport + +Source code can be downloaded as part of the Rails project on GitHub + +* http://github.com/rails/rails/tree/master/activesupport/ + + +== License + +Active Support is released under the MIT license. + + +== Support + +API documentation is at + +* http://api.rubyonrails.com + +Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: + +* https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 2aebe05de2..8e2683ef89 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -1,7 +1,7 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/gempackagetask' task :default => :test @@ -22,13 +22,13 @@ dist_dirs = [ "lib", "test"] # Genereate the RDoc documentation -Rake::RDocTask.new { |rdoc| +RDoc::Task.new { |rdoc| rdoc.rdoc_dir = 'doc' rdoc.title = "Active Support -- Utility classes and standard library extensions from Rails" - rdoc.options << '--line-numbers' << '--inline-source' + rdoc.options << '-f' << 'horo' + rdoc.options << '--main' << 'README.rdoc' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG') rdoc.rdoc_files.include('lib/active_support.rb') rdoc.rdoc_files.include('lib/active_support/**/*.rb') } @@ -45,9 +45,3 @@ task :release => :package do Rake::Gemcutter::Tasks.new(spec).define Rake::Task['gem:push'].invoke end - -desc "Publish the API documentation" -task :pdoc => [:rdoc] do - require 'rake/contrib/sshpublisher' - Rake::SshDirPublisher.new("rails@api.rubyonrails.org", "public_html/as", "doc").upload -end diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 8611a1e5fa..df7f68fecf 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'activesupport' - s.files = Dir['CHANGELOG', 'README', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' s.has_rdoc = true diff --git a/activesupport/install.rb b/activesupport/install.rb deleted file mode 100644 index 9c54d8c1a9..0000000000 --- a/activesupport/install.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rbconfig' -require 'find' -require 'ftools' - -include Config - -# this was adapted from rdoc's install.rb by ways of Log4r - -$sitedir = CONFIG["sitelibdir"] -unless $sitedir - version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] - $libdir = File.join(CONFIG["libdir"], "ruby", version) - $sitedir = $:.find {|x| x =~ /site_ruby/ } - if !$sitedir - $sitedir = File.join($libdir, "site_ruby") - elsif $sitedir !~ Regexp.quote(version) - $sitedir = File.join($sitedir, version) - end -end - -# the actual gruntwork -Dir.chdir("lib") - -Find.find("active_support", "active_support.rb") { |f| - if f[-3..-1] == ".rb" - File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) - else - File::makedirs(File.join($sitedir, *f.split(/\//))) - end -} diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index 29c3843d16..b861a6f62a 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -101,7 +101,11 @@ module ActiveSupport @guard.synchronize do unless buffer.empty? old_buffer = buffer - @log.write(old_buffer.join) + all_content = StringIO.new + old_buffer.each do |content| + all_content << content + end + @log.write(all_content.string) end # Important to do this even if buffer was empty or else @buffer will diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index bef9c98ecf..30195bdea5 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -19,8 +19,6 @@ module ActiveSupport autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store' autoload :CompressedMemCacheStore, 'active_support/cache/compressed_mem_cache_store' - EMPTY_OPTIONS = {}.freeze - # These options mean something to all cache implementations. Individual cache # implementations may support additional options. UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl] @@ -129,13 +127,6 @@ module ActiveSupport # cache.namespace = lambda { @last_mod_time } # Set the namespace to a variable # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace # - # All caches support auto expiring content after a specified number of seconds. - # To set the cache entry time to live, you can either specify +:expires_in+ as - # an option to the constructor to have it affect all entries or to the +fetch+ - # or +write+ methods for just one entry. - # - # cache = ActiveSupport::Cache::MemoryStore.new(:expire_in => 5.minutes) - # cache.write(key, value, :expire_in => 1.minute) # Set a lower value for one entry # # Caches can also store values in a compressed format to save space and reduce # time spent sending data. Since there is some overhead, values must be large @@ -147,7 +138,7 @@ module ActiveSupport cattr_accessor :logger, :instance_writer => true - attr_reader :silence + attr_reader :silence, :options alias :silence? :silence # Create a new cache. The options will be passed to any write method calls except @@ -156,11 +147,6 @@ module ActiveSupport @options = options ? options.dup : {} end - # Get the default options set when the cache was created. - def options - @options ||= {} - end - # Silence the logger. def silence! @silence = true @@ -175,7 +161,7 @@ module ActiveSupport @silence = previous_silence end - # Set to true if cache stores should be instrumented. By default is false. + # Set to true if cache stores should be instrumented. Default is false. def self.instrument=(boolean) Thread.current[:instrument_cache_store] = boolean end @@ -187,7 +173,7 @@ module ActiveSupport # Fetches data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. # - # If there is no such data in the cache (a cache miss occurred), then + # If there is no such data in the cache (a cache miss occurred), # then nil will be returned. However, if a block has been passed, then # that block will be run in the event of a cache miss. The return value # of the block will be written to the cache under the given cache key, @@ -211,23 +197,30 @@ module ActiveSupport # Setting <tt>:compress</tt> will store a large cache entry set by the call # in a compressed format. # - # Setting <tt>:expires_in</tt> will set an expiration time on the cache - # entry if it is set by call. # - # Setting <tt>:race_condition_ttl</tt> will invoke logic on entries set with - # an <tt>:expires_in</tt> option. If an entry is found in the cache that is - # expired and it has been expired for less than the number of seconds specified - # by this option and a block was passed to the method call, then the expiration - # future time of the entry in the cache will be updated to that many seconds - # in the and the block will be evaluated and written to the cache. + # Setting <tt>:expires_in</tt> will set an expiration time on the cache. All caches + # support auto expiring content after a specified number of seconds. This value can + # be specified as an option to the construction in which call all entries will be + # affected. Or it can be supplied to the +fetch+ or +write+ method for just one entry. # - # This is very useful in situations where a cache entry is used very frequently - # under heavy load. The first process to find an expired cache entry will then - # become responsible for regenerating that entry while other processes continue - # to use the slightly out of date entry. This can prevent race conditions where - # too many processes are trying to regenerate the entry all at once. If the - # process regenerating the entry errors out, the entry will be regenerated - # after the specified number of seconds. + # cache = ActiveSupport::Cache::MemoryStore.new(:expire_in => 5.minutes) + # cache.write(key, value, :expire_in => 1.minute) # Set a lower value for one entry + # + # Setting <tt>:race_condition_ttl</tt> is very useful in situations where a cache entry + # is used very frequently unver heavy load. If a cache expires and due to heavy load + # seven different processes will try to read data natively and then they all will try to + # write to cache. To avoid that case the first process to find an expired cache entry will + # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. Yes + # this process is extending the time for a stale value by another few seconds. Because + # of extended life of the previous cache, other processes will continue to use slightly + # stale data for a just a big longer. In the meantime that first process will go ahead + # and will write into cache the new value. After that all the processes will start + # getting new value. The key is to keep <tt>:race_condition_ttl</tt> small. + # + # If the process regenerating the entry errors out, the entry will be regenerated + # after the specified number of seconds. Also note that the life of stale cache is + # extended only if it expired recently. Otherwise a new value is generated and + # <tt>:race_condition_ttl</tt> does not play any role. # # # Set all values to expire after one minute. # cache = ActiveSupport::Cache::MemoryCache.new(:expires_in => 1.minute) @@ -252,6 +245,7 @@ module ActiveSupport # # # val_1 => "new value 1" # # val_2 => "original value" + # # sleep 10 # First thread extend the life of cache by another 10 seconds # # cache.fetch("foo") => "new value 1" # # Other options will be handled by the specific cache store implementation. @@ -353,11 +347,9 @@ module ActiveSupport results end - # Writes the given value to the cache, with the given key. + # Writes the value to the cache, with the key. # - # You may also specify additional options via the +options+ argument. - # The specific cache store implementation will decide what to do with - # +options+. + # Options are passed to the underlying cache implementation. def write(name, value, options = nil) options = merged_options(options) instrument(:write, name, options) do |payload| @@ -366,7 +358,7 @@ module ActiveSupport end end - # Delete an entry in the cache. Returns +true+ if there was an entry to delete. + # Deletes an entry in the cache. Returns +true+ if an entry is deleted. # # Options are passed to the underlying cache implementation. def delete(name, options = nil) @@ -376,7 +368,7 @@ module ActiveSupport end end - # Return true if the cache contains an entry with this name. + # Return true if the cache contains an entry for the given key. # # Options are passed to the underlying cache implementation. def exist?(name, options = nil) @@ -391,11 +383,11 @@ module ActiveSupport end end - # Delete all entries whose keys match a pattern. + # Delete all entries with keys matching the pattern. # # Options are passed to the underlying cache implementation. # - # Not all implementations may support +delete_matched+. + # All implementations may not support this method. def delete_matched(matcher, options = nil) raise NotImplementedError.new("#{self.class.name} does not support delete_matched") end @@ -404,7 +396,7 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # Not all implementations may support +delete_matched+. + # All implementations may not support this method. def increment(name, amount = 1, options = nil) raise NotImplementedError.new("#{self.class.name} does not support increment") end @@ -413,28 +405,26 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # Not all implementations may support +delete_matched+. + # All implementations may not support this method. def decrement(name, amount = 1, options = nil) raise NotImplementedError.new("#{self.class.name} does not support decrement") end - # Cleanup the cache by removing expired entries. Not all cache implementations may - # support this method. + # Cleanup the cache by removing expired entries. # # Options are passed to the underlying cache implementation. # - # Not all implementations may support +delete_matched+. + # All implementations may not support this method. def cleanup(options = nil) raise NotImplementedError.new("#{self.class.name} does not support cleanup") end - # Clear the entire cache. Not all cache implementations may support this method. - # You should be careful with this method since it could affect other processes - # if you are using a shared cache. + # Clear the entire cache. Be careful with this method since it could + # affect other processes if shared cache is being used. # # Options are passed to the underlying cache implementation. # - # Not all implementations may support +delete_matched+. + # All implementations may not support this method. def clear(options = nil) raise NotImplementedError.new("#{self.class.name} does not support clear") end @@ -483,9 +473,9 @@ module ActiveSupport end end - # Expand a key to be a consistent string value. If the object responds to +cache_key+, - # it will be called. Otherwise, the to_param method will be called. If the key is a - # Hash, the keys will be sorted alphabetically. + # Expand key to be a consistent string value. Invoke +cache_key+ if + # object responds to +cache_key+. Otherwise, to_param method will be + # called. If the key is a Hash, then keys will be sorted alphabetically. def expanded_key(key) # :nodoc: if key.respond_to?(:cache_key) key = key.cache_key.to_s @@ -502,7 +492,7 @@ module ActiveSupport end end - # Prefix a key with the namespace. The two values will be delimited with a colon. + # Prefix a key with the namespace. Namespace and key will be delimited with a colon. def namespaced_key(key, options) key = expanded_key(key) namespace = options[:namespace] if options @@ -599,7 +589,7 @@ module ActiveSupport end end - # Set a new time to live on the entry so it expires at the given time. + # Set a new time when the entry will expire. def expires_at=(time) if time @expires_in = time.to_f - @created_at @@ -608,12 +598,12 @@ module ActiveSupport end end - # Seconds since the epoch when the cache entry will expire. + # Seconds since the epoch when the entry will expire. def expires_at @expires_in ? @created_at + @expires_in : nil end - # Get the size of the cached value. This could be less than value.size + # Returns the size of the cached value. This could be less than value.size # if the data is compressed. def size if @value.nil? diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 852defeae8..f32b562368 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -16,8 +16,7 @@ module ActiveSupport # Special features: # - Clustering and load balancing. One can specify multiple memcached servers, # and MemCacheStore will load balance between all available servers. If a - # server goes down, then MemCacheStore will ignore it until it goes back - # online. + # server goes down, then MemCacheStore will ignore it until it comes back up. # # MemCacheStore implements the Strategy::LocalCache strategy which implements # an in memory cache inside of a block. @@ -69,7 +68,7 @@ module ActiveSupport extend LocalCacheWithRaw end - # Reads multiple keys from the cache using a single call to the + # Reads multiple values from the cache using a single call to the # servers for all keys. Options can be passed in the last argument. def read_multi(*names) options = names.extract_options! @@ -113,7 +112,7 @@ module ActiveSupport end # Clear the entire cache on all memcached servers. This method should - # be used with care when using a shared cache. + # be used with care when shared cache is being used. def clear(options = nil) @data.flush_all end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index f5c2b8af8b..b15bb42c88 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -5,9 +5,9 @@ module ActiveSupport # A cache store implementation which stores everything into memory in the # same process. If you're running multiple Ruby on Rails server processes # (which is the case if you're using mongrel_cluster or Phusion Passenger), - # then this means that your Rails server process instances won't be able + # then this means that Rails server process instances won't be able # to share cache data with each other and this may not be the most - # appropriate cache for you. + # appropriate cache in that scenario. # # This cache has a bounded size specified by the :size options to the # initializer (default is 32Mb). When the cache exceeds the allotted size, @@ -47,8 +47,8 @@ module ActiveSupport end end - # Prune the cache down so the entries fit within the specified memory size by removing - # the least recently accessed entries. + # To ensure entries fit within the specified memory prune the cache by removing the least + # recently accessed entries. def prune(target_size, max_time = nil) return if pruning? @pruning = true @@ -67,7 +67,7 @@ module ActiveSupport end end - # Return true if the cache is currently be pruned to remove older entries. + # Returns true if the cache is currently being pruned. def pruning? @pruning end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index efb5ad26ab..3edba52fc4 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -8,7 +8,7 @@ module ActiveSupport # duration of a block. Repeated calls to the cache for the same key will hit the # in memory cache for faster access. module LocalCache - # Simple memory backed cache. This cache is not thread safe but is intended only + # Simple memory backed cache. This cache is not thread safe and is intended only # for serving as a temporary memory cache for a single thread. class LocalStore < Store def initialize @@ -16,7 +16,7 @@ module ActiveSupport @data = {} end - # Since it isn't thread safe, don't allow synchronizing. + # Don't allow synchronizing since it isn't thread safe, def synchronize # :nodoc: yield end @@ -39,7 +39,7 @@ module ActiveSupport end end - # Use a local cache to front for the cache for the duration of a block. + # Use a local cache for the duration of block. def with_local_cache save_val = Thread.current[thread_local_key] begin @@ -50,8 +50,8 @@ module ActiveSupport end end - # Middleware class can be inserted as a Rack handler to use a local cache for the - # duration of a request. + # Middleware class can be inserted as a Rack handler to be local cache for the + # duration of request. def middleware @middleware ||= begin klass = Class.new diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 1c7802f7de..24e407c253 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -419,7 +419,10 @@ module ActiveSupport @_keyed_callbacks ||= {} @_keyed_callbacks[name] ||= begin str = send("_#{kind}_callbacks").compile(name, object) - class_eval "def #{name}() #{str} end", __FILE__, __LINE__ + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{name}() #{str} end + protected :#{name} + RUBY_EVAL true end end @@ -483,7 +486,11 @@ module ActiveSupport end end - # Skip a previously defined callback for a given type. + # Skip a previously defined callback. + # + # class Writer < Person + # skip_callback :validate, :before, :check_membership, :if => lambda { self.age > 18 } + # end # def skip_callback(name, *filter_list, &block) __update_callbacks(name, filter_list, block) do |chain, type, filters, options| @@ -523,7 +530,8 @@ module ActiveSupport # This macro accepts the following options: # # * <tt>:terminator</tt> - Indicates when a before filter is considered - # to be halted. + # to halted. This is a string to be eval'ed and has the result of the + # very filter available in the <tt>result</tt> variable: # # define_callbacks :validate, :terminator => "result == false" # @@ -568,7 +576,9 @@ module ActiveSupport # # would trigger <tt>Audit#before_save</tt> instead. That's constructed by calling # <tt>"#{kind}_#{name}"</tt> on the given instance. In this case "kind" is "before" and - # "name" is "save". + # "name" is "save". In this context ":kind" and ":name" have special meanings: ":kind" + # refers to the kind of callback (before/after/around) and ":name" refers to the + # method on which callbacks are being defined. # # A declaration like # diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index eb31f7cad4..2d87e8d0e5 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -1,3 +1,38 @@ +# A typical module looks like this +# +# module M +# def self.included(base) +# base.send(:extend, ClassMethods) +# base.send(:include, InstanceMethods) +# scope :foo, :conditions => { :created_at => nil } +# end +# +# module ClassMethods +# def cm; puts 'I am a class method'; end +# end +# +# module InstanceMethods +# def im; puts 'I am an instance method'; end +# end +# end +# +# By using <tt>ActiveSupport::Concern</tt> the above module could instead be written as: +# +# module M +# extend ActiveSupport::Concern +# +# included do +# scope :foo, :conditions => { :created_at => nil } +# end +# +# module ClassMethods +# def cm; puts 'I am a class method'; end +# end +# +# module InstanceMethods +# def im; puts 'I am an instance method'; end +# end +# end module ActiveSupport module Concern def self.extended(base) diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 79e3828817..7585137aca 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -58,12 +58,12 @@ class Array alias_method :to_default_s, :to_s alias_method :to_s, :to_formatted_s - # Returns a string that represents this array in XML by sending +to_xml+ - # to each element. Active Record collections delegate their representation + # Returns a string that represents the array in XML by invoking +to_xml+ + # on each element. Active Record collections delegate their representation # in XML to this method. # # All elements are expected to respond to +to_xml+, if any of them does - # not an exception is raised. + # not then an exception is raised. # # The root node reflects the class name of the first element in plural # if all elements belong to the same type and that's not Hash: @@ -115,8 +115,8 @@ class Array # <?xml version="1.0" encoding="UTF-8"?> # <projects type="array"/> # - # By default root children have as node name the one of the root - # singularized. You can change it with the <tt>:children</tt> option. + # By default name of the node for the children of root is <tt>root.singularize</tt>. + # You can change it with the <tt>:children</tt> option. # # The +options+ hash is passed downwards: # diff --git a/activesupport/lib/active_support/core_ext/array/uniq_by.rb b/activesupport/lib/active_support/core_ext/array/uniq_by.rb index a09b2302fd..bd5c7a187f 100644 --- a/activesupport/lib/active_support/core_ext/array/uniq_by.rb +++ b/activesupport/lib/active_support/core_ext/array/uniq_by.rb @@ -2,7 +2,7 @@ class Array # Return an unique array based on the criteria given as a proc. # # [1, 2, 3, 4].uniq_by { |i| i.odd? } - # #=> [1, 2] + # # => [1, 2] # def uniq_by hash, array = {}, [] diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index e211bdeeca..06b2acd662 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -1,15 +1,41 @@ class Array - # Wraps the object in an Array unless it's an Array. Converts the - # object to an Array using #to_ary if it implements that. + # Wraps its argument in an array unless it is already an array (or array-like). # - # It differs with Array() in that it does not call +to_a+ on - # the argument: + # Specifically: + # + # * If the argument is +nil+ an empty list is returned. + # * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned. + # * Otherwise, returns an array with the argument as its single element. + # + # Array.wrap(nil) # => [] + # Array.wrap([1, 2, 3]) # => [1, 2, 3] + # Array.wrap(0) # => [0] + # + # This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences: + # + # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> + # moves on to try +to_a+ if the returned value is +nil+, but <tt>Arraw.wrap</tt> returns + # such a +nil+ right away. + # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> + # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. + # * It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array. + # + # The last point is particularly worth comparing for some enumerables: # # Array(:foo => :bar) # => [[:foo, :bar]] # Array.wrap(:foo => :bar) # => [{:foo => :bar}] # # Array("foo\nbar") # => ["foo\n", "bar"], in Ruby 1.8 # Array.wrap("foo\nbar") # => ["foo\nbar"] + # + # There's also a related idiom that uses the splat operator: + # + # [*object] + # + # which returns <tt>[nil]</tt> for +nil+, and calls to <tt>Array(object)</tt> otherwise. + # + # Thus, in this case the behavior is different for +nil+, and the differences with + # <tt>Kernel#Array</tt> explained above apply to the rest of +object+s. def self.wrap(object) if object.nil? [] diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 576366e496..bfa57fe1f7 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -2,8 +2,8 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/remove_method' class Class - # Declare a class-level attribute whose value is inheritable and - # overwritable by subclasses: + # Declare a class-level attribute whose value is inheritable by subclasses. + # Subclasses can change their own value and it will not impact parent class. # # class Base # class_attribute :setting @@ -18,12 +18,34 @@ class Class # Subclass.setting # => false # Base.setting # => true # + # In the above case as long as Subclass does not assign a value to setting + # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt> + # would read value assigned to parent class. Once Subclass assigns a value then + # the value assigned by Subclass would be returned. + # # This matches normal Ruby method inheritance: think of writing an attribute - # on a subclass as overriding the reader method. + # on a subclass as overriding the reader method. However, you need to be aware + # when using +class_attribute+ with mutable structures as +Array+ or +Hash+. + # In such cases, you don't want to do changes in places but use setters: + # + # Base.setting = [] + # Base.setting #=> [] + # Subclass.setting #=> [] + # + # # Appending in child changes both parent and child because it is the same object: + # Subclass.setting << :foo + # Base.setting #=> [:foo] + # Subclass.setting #=> [:foo] + # + # # Use setters to not propagate changes: + # Base.setting = [] + # Subclass.setting += [:foo] + # Base.setting #=> [] + # Subclass.setting #=> [:foo] # # For convenience, a query method is defined as well: # - # Subclass.setting? # => false + # Subclass.setting? # => false # # Instances may overwrite the class value in the same way: # diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index feef5d2d57..4e35b1b488 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -3,11 +3,27 @@ require 'active_support/core_ext/array/extract_options' # Extends the class object with class and instance accessors for class attributes, # just like the native attr* accessors for instance attributes. # +# Note that unlike +class_attribute+, if a subclass changes the value then that would +# also change the value for parent class. Similarly if parent class changes the value +# then that would change the value of subclasses too. +# # class Person # cattr_accessor :hair_colors # end # # Person.hair_colors = [:brown, :black, :blonde, :red] +# Person.hair_colors #=> [:brown, :black, :blonde, :red] +# Person.new.hair_colors #=> [:brown, :black, :blonde, :red] +# +# To opt out of the instance writer method, pass :instance_writer => false. +# To opt out of the instance reader method, pass :instance_reader => false. +# +# class Person +# cattr_accessor :hair_colors, :instance_writer => false, :instance_reader => false +# end +# +# Person.new.hair_colors = [:brown] # => NoMethodError +# Person.new.hair_colors # => NoMethodError class Class def cattr_reader(*syms) options = syms.extract_options! diff --git a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb index 7aff05dcdf..e844cf50d1 100644 --- a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb @@ -1,17 +1,39 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/array/extract_options' -# Retain for backward compatibility. Methods are now included in Class. +# Retained for backward compatibility. Methods are now included in Class. module ClassInheritableAttributes # :nodoc: end -# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of +# It is recommend to use <tt>class_attribute</tt> over methods defined in this file. Please +# refer to documentation for <tt>class_attribute</tt> for more information. Officially it is not +# deprected but <tt>class_attribute</tt> is faster. +# +# Allows attributes to be shared within an inheritance hierarchy. Each descendant gets a copy of # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements # to, for example, an array without those additions being shared with either their parent, siblings, or -# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy. +# children. This is unlike the regular class-level attributes that are shared across the entire hierarchy. # # The copies of inheritable parent attributes are added to subclasses when they are created, via the # +inherited+ hook. +# +# class Person +# class_inheritable_accessor :hair_colors +# end +# +# Person.hair_colors = [:brown, :black, :blonde, :red] +# Person.hair_colors #=> [:brown, :black, :blonde, :red] +# Person.new.hair_colors #=> [:brown, :black, :blonde, :red] +# +# To opt out of the instance writer method, pass :instance_writer => false. +# To opt out of the instance reader method, pass :instance_reader => false. +# +# class Person +# class_inheritable_accessor :hair_colors :instance_writer => false, :instance_reader => false +# end +# +# Person.new.hair_colors = [:brown] # => NoMethodError +# Person.new.hair_colors # => NoMethodError class Class # :nodoc: def class_inheritable_reader(*syms) options = syms.extract_options! diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index e6a213625c..c5b54318ce 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -40,23 +40,23 @@ class Date end end - # Tells whether the Date object's date lies in the past + # Returns true if the Date object's date lies in the past. Otherwise returns false. def past? self < ::Date.current end - # Tells whether the Date object's date is today + # Returns true if the Date object's date is today. def today? self.to_date == ::Date.current # we need the to_date because of DateTime end - # Tells whether the Date object's date lies in the future + # Returns true if the Date object's date lies in the future. def future? self > ::Date.current end # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00) - # and then subtracts the specified number of seconds + # and then subtracts the specified number of seconds. def ago(seconds) to_time_in_current_zone.since(-seconds) end @@ -127,22 +127,22 @@ class Date ) end - # Returns a new Date/DateTime representing the time a number of specified months ago + # Returns a new Date/DateTime representing the time a number of specified months ago. def months_ago(months) advance(:months => -months) end - # Returns a new Date/DateTime representing the time a number of specified months in the future + # Returns a new Date/DateTime representing the time a number of specified months in the future. def months_since(months) advance(:months => months) end - # Returns a new Date/DateTime representing the time a number of specified years ago + # Returns a new Date/DateTime representing the time a number of specified years ago. def years_ago(years) advance(:years => -years) end - # Returns a new Date/DateTime representing the time a number of specified years in the future + # Returns a new Date/DateTime representing the time a number of specified years in the future. def years_since(years) advance(:years => years) end @@ -152,22 +152,22 @@ class Date years_ago(1) end unless method_defined?(:prev_year) - # Short-hand for years_since(1) + # Shorthand for years_since(1) def next_year years_since(1) end unless method_defined?(:next_year) - # Short-hand for months_ago(1) + # Shorthand for months_ago(1) def prev_month months_ago(1) end unless method_defined?(:prev_month) - # Short-hand for months_since(1) + # Shorthand for months_since(1) def next_month months_since(1) end unless method_defined?(:next_month) - # Returns a new Date/DateTime representing the "start" of this week (i.e, Monday; DateTime objects will have time set to 0:00) + # Returns a new Date/DateTime representing the "start" of this week (i.e, Monday; DateTime objects will have time set to 0:00). def beginning_of_week days_to_monday = self.wday!=0 ? self.wday-1 : 6 result = self - days_to_monday @@ -176,7 +176,7 @@ class Date alias :monday :beginning_of_week alias :at_beginning_of_week :beginning_of_week - # Returns a new Date/DateTime representing the end of this week (Sunday, DateTime objects will have time set to 23:59:59) + # Returns a new Date/DateTime representing the end of this week (Sunday, DateTime objects will have time set to 23:59:59). def end_of_week days_to_sunday = self.wday!=0 ? 7-self.wday : 0 result = self + days_to_sunday.days diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index d0821a7c68..f76ed401cd 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -66,7 +66,8 @@ module Enumerable # +memo+ to the block. Handy for building up hashes or # reducing collections down to one object. Examples: # - # %w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } #=> {'foo' => 'FOO', 'bar' => 'BAR'} + # %w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } + # # => {'foo' => 'FOO', 'bar' => 'BAR'} # # *Note* that you can't use immutable objects like numbers, true or false as # the memo. You would think the following returns 120, but since the memo is diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 565c9af7fb..2763af6121 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -83,7 +83,7 @@ class Hash case value.class.to_s when 'Hash' if value['type'] == 'array' - child_key, entries = Array.wrap(value.detect { |k,v| k != 'type' }) # child_key is throwaway + _, entries = Array.wrap(value.detect { |k,v| k != 'type' }) if entries.nil? || (c = value['__content__'] && c.blank?) [] else diff --git a/activesupport/lib/active_support/core_ext/kernel/requires.rb b/activesupport/lib/active_support/core_ext/kernel/requires.rb index d2238898d6..3bf46271d7 100644 --- a/activesupport/lib/active_support/core_ext/kernel/requires.rb +++ b/activesupport/lib/active_support/core_ext/kernel/requires.rb @@ -11,13 +11,13 @@ module Kernel # 1. Requiring the module is unsuccessful, maybe it's a gem and nobody required rubygems yet. Try. begin require 'rubygems' - rescue LoadError => rubygems_not_installed + rescue LoadError # => rubygems_not_installed raise cannot_require end # 2. Rubygems is installed and loaded. Try to load the library again begin require library_name - rescue LoadError => gem_not_installed + rescue LoadError # => gem_not_installed raise cannot_require end end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 9c4d5fae26..2d88cb57e5 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -5,9 +5,7 @@ class Module options = syms.extract_options! syms.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end + @@#{sym} = nil unless defined? @@#{sym} def self.#{sym} @@#{sym} @@ -28,10 +26,6 @@ class Module options = syms.extract_options! syms.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - def self.#{sym}=(obj) @@#{sym} = obj end diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb index 2714a46b28..b8c01aca0e 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -3,4 +3,9 @@ class Module remove_method(method) rescue NameError end + + def redefine_method(method, &block) + remove_possible_method(method) + define_method(method, &block) + end end
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 27618b55c6..d671da6711 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -2,12 +2,11 @@ require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/try' +require 'active_support/core_ext/object/returning' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' -require 'active_support/core_ext/object/misc' -require 'active_support/core_ext/object/returning' require 'active_support/core_ext/object/to_json' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' diff --git a/activesupport/lib/active_support/core_ext/object/misc.rb b/activesupport/lib/active_support/core_ext/object/misc.rb deleted file mode 100644 index 3e3af03cc5..0000000000 --- a/activesupport/lib/active_support/core_ext/object/misc.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'active_support/core_ext/object/returning' -require 'active_support/core_ext/object/with_options' diff --git a/activesupport/lib/active_support/core_ext/object/returning.rb b/activesupport/lib/active_support/core_ext/object/returning.rb index 0dc2e1266a..07250b2a27 100644 --- a/activesupport/lib/active_support/core_ext/object/returning.rb +++ b/activesupport/lib/active_support/core_ext/object/returning.rb @@ -25,7 +25,7 @@ class Object # end # # foo # => ['bar', 'baz'] - # + # # # returning with a block argument # def foo # returning [] do |values| @@ -33,10 +33,11 @@ class Object # values << 'baz' # end # end - # + # # foo # => ['bar', 'baz'] def returning(value) + ActiveSupport::Deprecation.warn('Object#returning has been deprecated in favor of Object#tap.', caller) yield(value) value end -end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 06f077e920..f2e7c2351e 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -44,6 +44,6 @@ class Hash def to_param(namespace = nil) collect do |key, value| value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort * '&' + end * '&' end end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 66c4034781..32913a06ad 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -1,3 +1,5 @@ +require 'active_support/inflector/methods' +require 'active_support/inflector/inflections' # String inflections define new methods on the String class to transform names for different purposes. # For instance, you can figure out the name of a database from the name of a class. # diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 16ccd36458..0b974f5e0a 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -12,11 +12,11 @@ class String # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsuled string. # # name = 'Claus Müller' - # name.reverse #=> "rell??M sualC" - # name.length #=> 13 + # name.reverse # => "rell??M sualC" + # name.length # => 13 # - # name.mb_chars.reverse.to_s #=> "rellüM sualC" - # name.mb_chars.length #=> 12 + # name.mb_chars.reverse.to_s # => "rellüM sualC" + # name.mb_chars.length # => 12 # # In Ruby 1.9 and newer +mb_chars+ returns +self+ because String is (mostly) encoding aware. This means that # it becomes easy to run one version of your code on multiple Ruby versions. @@ -26,7 +26,7 @@ class String # All the methods on the Chars proxy which normally return a string will return a Chars object. This allows # method chaining on the result of any of these methods. # - # name.mb_chars.reverse.length #=> 12 + # name.mb_chars.reverse.length # => 12 # # == Interoperability and configuration # diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 7d5143ba37..2b80bd214f 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -72,10 +72,6 @@ module ActiveSupport #:nodoc: methods.each { |m| class_eval "def #{m}(*) lock { super } end", __FILE__, __LINE__ } end - def get(key) - (val = assoc(key)) ? val[1] : [] - end - locked :concat, :each, :delete_if, :<< def new_constants_for(frames) @@ -85,7 +81,18 @@ module ActiveSupport #:nodoc: next unless mod.is_a?(Module) new_constants = mod.local_constant_names - prior_constants - get(mod_name).concat(new_constants) + + # If we are checking for constants under, say, :Object, nested under something + # else that is checking for constants also under :Object, make sure the + # parent knows that we have found, and taken care of, the constant. + # + # In particular, this means that since Kernel.require discards the constants + # it finds, parents will be notified that about those constants, and not + # consider them "new". As a result, they will not be added to the + # autoloaded_constants list. + each do |key, value| + value.concat(new_constants) if key == mod_name + end new_constants.each do |suffix| constants << ([mod_name, suffix] - ["Object"]).join("::") @@ -592,7 +599,7 @@ module ActiveSupport #:nodoc: # Convert the provided const desc to a qualified constant name (as a string). # A module, class, symbol, or string may be provided. def to_constant_name(desc) #:nodoc: - name = case desc + case desc when String then desc.sub(/^::/, '') when Symbol then desc.to_s when Module diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index dec56715be..deb33ab702 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -3,6 +3,13 @@ require 'active_support/inflector' module ActiveSupport module Deprecation class DeprecationProxy #:nodoc: + def self.new(*args, &block) + object = args.first + + return object unless object + super + end + instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$/ } # Don't give a deprecation warning on inspect since test/unit and error diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb index 6cba84d79e..4d1cfacc95 100644 --- a/activesupport/lib/active_support/descendants_tracker.rb +++ b/activesupport/lib/active_support/descendants_tracker.rb @@ -11,9 +11,9 @@ module ActiveSupport end def self.descendants(klass) - @@direct_descendants[klass].inject([]) do |descendants, klass| - descendants << klass - descendants.concat klass.descendants + @@direct_descendants[klass].inject([]) do |descendants, _klass| + descendants << _klass + descendants.concat _klass.descendants end end @@ -40,4 +40,4 @@ module ActiveSupport DescendantsTracker.descendants(self) end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index cd0d66a482..7da357730b 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -4,8 +4,8 @@ require 'active_support/core_ext/object/acts_like' module ActiveSupport # Provides accurate date and time measurements using Date#advance and - # Time#advance, respectively. It mainly supports the methods on Numeric, - # such as in this example: + # Time#advance, respectively. It mainly supports the methods on Numeric. + # Example: # # 1.month.ago # equivalent to Time.now.advance(:months => -1) class Duration < BasicObject diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index f64f0f44cc..eec5d4cf47 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' # This class has dubious semantics and we only have it so that diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index b3dc5b2f3a..de49750083 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -20,6 +20,11 @@ module ActiveSupport # "active_record".camelize(:lower) # => "activeRecord" # "active_record/errors".camelize # => "ActiveRecord::Errors" # "active_record/errors".camelize(:lower) # => "activeRecord::Errors" + # + # As a rule of thumb you can think of +camelize+ as the inverse of +underscore+, + # though there are cases where that does not hold: + # + # "SSLError".underscore.camelize # => "SslError" def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) if first_letter_in_uppercase lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } @@ -28,13 +33,18 @@ module ActiveSupport end end - # The reverse of +camelize+. Makes an underscored, lowercase form from the expression in the string. + # Makes an underscored, lowercase form from the expression in the string. # # Changes '::' to '/' to convert namespaces to paths. # # Examples: # "ActiveRecord".underscore # => "active_record" # "ActiveRecord::Errors".underscore # => active_record/errors + # + # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+, + # though there are cases where that does not hold: + # + # "SSLError".underscore.camelize # => "SslError" def underscore(camel_cased_word) word = camel_cased_word.to_s.dup word.gsub!(/::/, '/') diff --git a/activesupport/lib/active_support/json/backends/yaml.rb b/activesupport/lib/active_support/json/backends/yaml.rb index 215b3d6f90..4cb9d01077 100644 --- a/activesupport/lib/active_support/json/backends/yaml.rb +++ b/activesupport/lib/active_support/json/backends/yaml.rb @@ -13,7 +13,7 @@ module ActiveSupport json = json.read end YAML.load(convert_json_to_yaml(json)) - rescue ArgumentError => e + rescue ArgumentError raise ParseError, "Invalid JSON string" end diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index 3664431a28..ef43fc0431 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -1,3 +1,22 @@ +# lazy_load_hooks allows rails to lazily load a lot of components and thus making the app boot faster. Because of +# this feature now there is no need to require <tt>ActiveRecord::Base</tt> at boot time purely to apply configuration. Instead +# a hook is registered that applies configuration once <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is used +# as example but this feature can be applied elsewhere too. +# +# Here is an example where +on_load+ method is called to register a hook. +# +# initializer "active_record.initialize_timezone" do +# ActiveSupport.on_load(:active_record) do +# self.time_zone_aware_attributes = true +# self.default_timezone = :utc +# end +# end +# +# When the entirety of +activerecord/lib/active_record/base.rb+ has been evaluated then +run_load_hooks+ is invoked. +# The very last line of +activerecord/lib/active_record/base.rb+ is: +# +# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) +# module ActiveSupport @load_hooks = Hash.new {|h,k| h[k] = [] } @loaded = {} @@ -24,4 +43,4 @@ module ActiveSupport execute_hook(base, options, hook) end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index 891d718af3..83930b3f0d 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -21,7 +21,7 @@ module ActiveSupport # ActiveRecord::LogSubscriber.attach_to :active_record # # Since we need to know all instance methods before attaching the log subscriber, - # the line above shuold be called after your ActiveRecord::LogSubscriber definition. + # the line above should be called after your ActiveRecord::LogSubscriber definition. # # After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to @@ -63,15 +63,9 @@ module ActiveSupport @@flushable_loggers = nil log_subscriber.public_methods(false).each do |event| - notifier.subscribe("#{event}.#{namespace}") do |*args| - next if log_subscriber.logger.nil? - - begin - log_subscriber.send(event, ActiveSupport::Notifications::Event.new(*args)) - rescue Exception => e - log_subscriber.logger.error "Could not log #{args[0].inspect} event. #{e.class}: #{e.message}" - end - end + next if 'call' == event.to_s + + notifier.subscribe("#{event}.#{namespace}", log_subscriber) end end @@ -92,6 +86,17 @@ module ActiveSupport flushable_loggers.each(&:flush) end + def call(message, *args) + return unless logger + + method = message.split('.').first + begin + send(method, ActiveSupport::Notifications::Event.new(message, *args)) + rescue Exception => e + logger.error "Could not log #{message.inspect} event. #{e.class}: #{e.message}" + end + end + protected %w(info debug warn error fatal unknown).each do |level| diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 96506a4b2b..9e52cb97a9 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -1,4 +1,5 @@ require 'active_support/log_subscriber' +require 'active_support/buffered_logger' module ActiveSupport class LogSubscriber @@ -33,7 +34,7 @@ module ActiveSupport module TestHelper def setup @logger = MockLogger.new - @notifier = ActiveSupport::Notifications::Notifier.new(queue) + @notifier = ActiveSupport::Notifications::Fanout.new ActiveSupport::LogSubscriber.colorize_logging = false @@ -47,10 +48,14 @@ module ActiveSupport end class MockLogger + include ActiveSupport::BufferedLogger::Severity + attr_reader :flush_count + attr_accessor :level - def initialize + def initialize(level = DEBUG) @flush_count = 0 + @level = level @logged = Hash.new { |h,k| h[k] = [] } end @@ -65,6 +70,14 @@ module ActiveSupport def flush @flush_count += 1 end + + ActiveSupport::BufferedLogger::Severity.constants.each do |severity| + class_eval <<-EOT, __FILE__, __LINE__ + 1 + def #{severity.downcase}? + #{severity} >= @level + end + EOT + end end # Wait notifications to be published. @@ -81,10 +94,6 @@ module ActiveSupport def set_logger(logger) ActiveSupport::LogSubscriber.logger = logger end - - def queue - ActiveSupport::Notifications::Fanout.new - end end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 1031662293..6c46b68eaf 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -47,11 +47,11 @@ module ActiveSupport def secure_compare(a, b) return false unless a.bytesize == b.bytesize - l = a.unpack "C*" + l = a.unpack "C#{a.bytesize}" - res = true - b.each_byte { |byte| res = (byte == l.shift) && res } - res + res = 0 + b.each_byte { |byte| res |= byte ^ l.shift } + res == 0 end def generate_digest(data) diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 51c2a0edac..019fb2df06 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -11,7 +11,7 @@ module ActiveSupport #:nodoc: # String methods are proxied through the Chars object, and can be accessed through the +mb_chars+ method. Methods # which would normally return a String object now return a Chars object so methods can be chained. # - # "The Perfect String ".mb_chars.downcase.strip.normalize #=> "the perfect string" + # "The Perfect String ".mb_chars.downcase.strip.normalize # => "the perfect string" # # Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made. # If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them. @@ -83,12 +83,13 @@ module ActiveSupport #:nodoc: include Comparable - # Returns <tt>-1</tt>, <tt>0</tt> or <tt>+1</tt> depending on whether the Chars object is to be sorted before, - # equal or after the object on the right side of the operation. It accepts any object that implements +to_s+. - # See <tt>String#<=></tt> for more details. + # Returns -1, 0, or 1, depending on whether the Chars object is to be sorted before, + # equal or after the object on the right side of the operation. It accepts any object + # that implements +to_s+: # - # Example: - # 'é'.mb_chars <=> 'ü'.mb_chars #=> -1 + # 'é'.mb_chars <=> 'ü'.mb_chars # => -1 + # + # See <tt>String#<=></tt> for more details. def <=>(other) @wrapped_string <=> other.to_s end @@ -103,7 +104,7 @@ module ActiveSupport #:nodoc: # Returns a new Chars object containing the _other_ object concatenated to the string. # # Example: - # ('Café'.mb_chars + ' périferôl').to_s #=> "Café périferôl" + # ('Café'.mb_chars + ' périferôl').to_s # => "Café périferôl" def +(other) chars(@wrapped_string + other) end @@ -111,7 +112,7 @@ module ActiveSupport #:nodoc: # Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset. # # Example: - # 'Café périferôl'.mb_chars =~ /ô/ #=> 12 + # 'Café périferôl'.mb_chars =~ /ô/ # => 12 def =~(other) translate_offset(@wrapped_string =~ other) end @@ -119,7 +120,7 @@ module ActiveSupport #:nodoc: # Inserts the passed string at specified codepoint offsets. # # Example: - # 'Café'.mb_chars.insert(4, ' périferôl').to_s #=> "Café périferôl" + # 'Café'.mb_chars.insert(4, ' périferôl').to_s # => "Café périferôl" def insert(offset, fragment) unpacked = Unicode.u_unpack(@wrapped_string) unless offset > unpacked.length @@ -135,7 +136,7 @@ module ActiveSupport #:nodoc: # Returns +true+ if contained string contains _other_. Returns +false+ otherwise. # # Example: - # 'Café'.mb_chars.include?('é') #=> true + # 'Café'.mb_chars.include?('é') # => true def include?(other) # We have to redefine this method because Enumerable defines it. @wrapped_string.include?(other) @@ -144,8 +145,8 @@ module ActiveSupport #:nodoc: # Returns the position _needle_ in the string, counting in codepoints. Returns +nil+ if _needle_ isn't found. # # Example: - # 'Café périferôl'.mb_chars.index('ô') #=> 12 - # 'Café périferôl'.mb_chars.index(/\w/u) #=> 0 + # 'Café périferôl'.mb_chars.index('ô') # => 12 + # 'Café périferôl'.mb_chars.index(/\w/u) # => 0 def index(needle, offset=0) wrapped_offset = first(offset).wrapped_string.length index = @wrapped_string.index(needle, wrapped_offset) @@ -157,8 +158,8 @@ module ActiveSupport #:nodoc: # string. Returns +nil+ if _needle_ isn't found. # # Example: - # 'Café périferôl'.mb_chars.rindex('é') #=> 6 - # 'Café périferôl'.mb_chars.rindex(/\w/u) #=> 13 + # 'Café périferôl'.mb_chars.rindex('é') # => 6 + # 'Café périferôl'.mb_chars.rindex(/\w/u) # => 13 def rindex(needle, offset=nil) offset ||= length wrapped_offset = first(offset).wrapped_string.length @@ -190,7 +191,7 @@ module ActiveSupport #:nodoc: # Returns the codepoint of the first character in the string. # # Example: - # 'こんにちは'.mb_chars.ord #=> 12371 + # 'こんにちは'.mb_chars.ord # => 12371 def ord Unicode.u_unpack(@wrapped_string)[0] end @@ -200,10 +201,10 @@ module ActiveSupport #:nodoc: # Example: # # "¾ cup".mb_chars.rjust(8).to_s - # #=> " ¾ cup" + # # => " ¾ cup" # # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace - # #=> " ¾ cup" + # # => " ¾ cup" def rjust(integer, padstr=' ') justify(integer, :right, padstr) end @@ -213,10 +214,10 @@ module ActiveSupport #:nodoc: # Example: # # "¾ cup".mb_chars.rjust(8).to_s - # #=> "¾ cup " + # # => "¾ cup " # # "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace - # #=> "¾ cup " + # # => "¾ cup " def ljust(integer, padstr=' ') justify(integer, :left, padstr) end @@ -226,10 +227,10 @@ module ActiveSupport #:nodoc: # Example: # # "¾ cup".mb_chars.center(8).to_s - # #=> " ¾ cup " + # # => " ¾ cup " # # "¾ cup".mb_chars.center(8, " ").to_s # Use non-breaking whitespace - # #=> " ¾ cup " + # # => " ¾ cup " def center(integer, padstr=' ') justify(integer, :center, padstr) end @@ -244,7 +245,7 @@ module ActiveSupport #:nodoc: # instances instead of String. This makes chaining methods easier. # # Example: - # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } #=> ["CAF", " P", "RIFERÔL"] + # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"] def split(*args) @wrapped_string.split(*args).map { |i| i.mb_chars } end @@ -256,12 +257,12 @@ module ActiveSupport #:nodoc: # s = "Müller" # s.mb_chars[2] = "e" # Replace character with offset 2 # s - # #=> "Müeler" + # # => "Müeler" # # s = "Müller" # s.mb_chars[1, 2] = "ö" # Replace 2 characters at character offset 1 # s - # #=> "Möler" + # # => "Möler" def []=(*args) replace_by = args.pop # Indexed replace with regular expressions already works @@ -292,7 +293,7 @@ module ActiveSupport #:nodoc: # Reverses all characters in the string. # # Example: - # 'Café'.mb_chars.reverse.to_s #=> 'éfaC' + # 'Café'.mb_chars.reverse.to_s # => 'éfaC' def reverse chars(Unicode.g_unpack(@wrapped_string).reverse.flatten.pack('U*')) end @@ -301,7 +302,7 @@ module ActiveSupport #:nodoc: # character. # # Example: - # 'こんにちは'.mb_chars.slice(2..3).to_s #=> "にち" + # 'こんにちは'.mb_chars.slice(2..3).to_s # => "にち" def slice(*args) if args.size > 2 raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" # Do as if we were native @@ -330,7 +331,7 @@ module ActiveSupport #:nodoc: # # Example: # s = 'こんにちは' - # s.mb_chars.limit(7) #=> "こに" + # s.mb_chars.limit(7) # => "こに" def limit(limit) slice(0...translate_offset(limit)) end @@ -338,7 +339,7 @@ module ActiveSupport #:nodoc: # Convert characters in the string to uppercase. # # Example: - # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s #=> "LAURENT, OÙ SONT LES TESTS ?" + # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?" def upcase chars(Unicode.apply_mapping @wrapped_string, :uppercase_mapping) end @@ -346,7 +347,7 @@ module ActiveSupport #:nodoc: # Convert characters in the string to lowercase. # # Example: - # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s #=> "věda a výzkum" + # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum" def downcase chars(Unicode.apply_mapping @wrapped_string, :lowercase_mapping) end @@ -354,7 +355,7 @@ module ActiveSupport #:nodoc: # Converts the first character to uppercase and the remainder to lowercase. # # Example: - # 'über'.mb_chars.capitalize.to_s #=> "Über" + # 'über'.mb_chars.capitalize.to_s # => "Über" def capitalize (slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase end @@ -382,8 +383,8 @@ module ActiveSupport #:nodoc: # Performs canonical decomposition on all the characters. # # Example: - # 'é'.length #=> 2 - # 'é'.mb_chars.decompose.to_s.length #=> 3 + # 'é'.length # => 2 + # 'é'.mb_chars.decompose.to_s.length # => 3 def decompose chars(Unicode.decompose_codepoints(:canonical, Unicode.u_unpack(@wrapped_string)).pack('U*')) end @@ -391,8 +392,8 @@ module ActiveSupport #:nodoc: # Performs composition on all the characters. # # Example: - # 'é'.length #=> 3 - # 'é'.mb_chars.compose.to_s.length #=> 2 + # 'é'.length # => 3 + # 'é'.mb_chars.compose.to_s.length # => 2 def compose chars(Unicode.compose_codepoints(Unicode.u_unpack(@wrapped_string)).pack('U*')) end @@ -400,8 +401,8 @@ module ActiveSupport #:nodoc: # Returns the number of grapheme clusters in the string. # # Example: - # 'क्षि'.mb_chars.length #=> 4 - # 'क्षि'.mb_chars.g_length #=> 3 + # 'क्षि'.mb_chars.length # => 4 + # 'क्षि'.mb_chars.g_length # => 3 def g_length Unicode.g_unpack(@wrapped_string).length end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 11c72d873b..1139783b65 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -64,7 +64,7 @@ module ActiveSupport # valid UTF-8. # # Example: - # Unicode.u_unpack('Café') #=> [67, 97, 102, 233] + # Unicode.u_unpack('Café') # => [67, 97, 102, 233] def u_unpack(string) begin string.unpack 'U*' @@ -85,8 +85,8 @@ module ActiveSupport # Unpack the string at grapheme boundaries. Returns a list of character lists. # # Example: - # Unicode.g_unpack('क्षि') #=> [[2325, 2381], [2359], [2367]] - # Unicode.g_unpack('Café') #=> [[67], [97], [102], [233]] + # Unicode.g_unpack('क्षि') # => [[2325, 2381], [2359], [2367]] + # Unicode.g_unpack('Café') # => [[67], [97], [102], [233]] def g_unpack(string) codepoints = u_unpack(string) unpacked = [] @@ -99,15 +99,15 @@ module ActiveSupport current = codepoints[pos] if ( # CR X LF - one = ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or + ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or # L X (L|V|LV|LVT) - two = ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or + ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or # (LV|V) X (V|T) - three = ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or + ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or # (LVT|T) X (T) - four = ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or + ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or # X Extend - five = (database.boundary[:extend] === current) + (database.boundary[:extend] === current) ) else unpacked << codepoints[marker..pos-1] @@ -120,7 +120,7 @@ module ActiveSupport # Reverse operation of g_unpack. # # Example: - # Unicode.g_pack(Unicode.g_unpack('क्षि')) #=> 'क्षि' + # Unicode.g_pack(Unicode.g_unpack('क्षि')) # => 'क्षि' def g_pack(unpacked) (unpacked.flatten).pack('U*') end @@ -238,7 +238,6 @@ module ActiveSupport bytes.each_index do |i| byte = bytes[i] - is_ascii = byte < 128 is_cont = byte > 127 && byte < 192 is_lead = byte > 191 && byte < 245 is_unused = byte > 240 diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 1444fc1609..fd79188ba4 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -22,9 +22,9 @@ module ActiveSupport # end # # event = @events.first - # event.name #=> :render - # event.duration #=> 10 (in milliseconds) - # event.payload #=> { :extra => :information } + # event.name # => :render + # event.duration # => 10 (in milliseconds) + # event.payload # => { :extra => :information } # # When subscribing to Notifications, you can pass a pattern, to only consume # events that match the pattern: @@ -41,39 +41,37 @@ module ActiveSupport autoload :Event, 'active_support/notifications/instrumenter' autoload :Fanout, 'active_support/notifications/fanout' + @instrumenters = Hash.new { |h,k| h[k] = notifier.listening?(k) } + class << self attr_writer :notifier - delegate :publish, :subscribe, :unsubscribe, :to => :notifier - delegate :instrument, :to => :instrumenter - - def notifier - @notifier ||= Notifier.new - end - - def instrumenter - Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier) - end - end + delegate :publish, :to => :notifier - class Notifier - def initialize(queue = Fanout.new) - @queue = queue + def instrument(name, payload = {}) + if @instrumenters[name] + instrumenter.instrument(name, payload) { yield payload if block_given? } + else + yield payload if block_given? + end end - def publish(*args) - @queue.publish(*args) + def subscribe(*args, &block) + notifier.subscribe(*args, &block).tap do + @instrumenters.clear + end end - def subscribe(pattern = nil, &block) - @queue.bind(pattern).subscribe(&block) + def unsubscribe(*args) + notifier.unsubscribe(*args) + @instrumenters.clear end - def unsubscribe(subscriber) - @queue.unsubscribe(subscriber) + def notifier + @notifier ||= Fanout.new end - def wait - @queue.wait + def instrumenter + Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier) end end end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 300ec842a9..adc34f3286 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -8,85 +8,53 @@ module ActiveSupport @listeners_for = {} end - def bind(pattern) - Binding.new(self, pattern) - end - - def subscribe(pattern = nil, &block) + def subscribe(pattern = nil, block = Proc.new) + subscriber = Subscriber.new(pattern, block).tap do |s| + @subscribers << s + end @listeners_for.clear - @subscribers << Subscriber.new(pattern, &block) - @subscribers.last + subscriber end def unsubscribe(subscriber) - @listeners_for.clear @subscribers.reject! {|s| s.matches?(subscriber)} + @listeners_for.clear end def publish(name, *args) - if listeners = @listeners_for[name] - listeners.each { |s| s.publish(name, *args) } - else - @listeners_for[name] = @subscribers.select { |s| s.publish(name, *args) } - end + listeners_for(name).each { |s| s.publish(name, *args) } end - # This is a sync queue, so there is not waiting. - def wait + def listeners_for(name) + @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } end - # Used for internal implementation only. - class Binding #:nodoc: - def initialize(queue, pattern) - @queue = queue - @pattern = - case pattern - when Regexp, NilClass - pattern - else - /^#{Regexp.escape(pattern.to_s)}$/ - end - end + def listening?(name) + listeners_for(name).any? + end - def subscribe(&block) - @queue.subscribe(@pattern, &block) - end + # This is a sync queue, so there is not waiting. + def wait end class Subscriber #:nodoc: - def initialize(pattern, &block) + def initialize(pattern, delegate) @pattern = pattern - @block = block + @delegate = delegate end - def publish(*args) - return unless subscribed_to?(args.first) - push(*args) - true - end - - def drained? - true + def publish(message, *args) + @delegate.call(message, *args) end def subscribed_to?(name) - !@pattern || @pattern =~ name.to_s + !@pattern || @pattern === name.to_s end def matches?(subscriber_or_name) - case subscriber_or_name - when String - @pattern && @pattern =~ subscriber_or_name - when self - true - end + self === subscriber_or_name || + @pattern && @pattern === subscriber_or_name end - - private - - def push(*args) - @block.call(*args) - end end end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 7e89402822..441fefb491 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -15,14 +15,15 @@ module ActiveSupport # and publish it. Notice that events get sent even if an error occurs # in the passed-in block def instrument(name, payload={}) - time = Time.now + started = Time.now + begin - yield(payload) if block_given? + yield rescue Exception => e payload[:exception] = [e.class.name, e.message] raise e ensure - @notifier.publish(name, time, Time.now, @id, payload) + @notifier.publish(name, started, Time.now, @id, payload) end end @@ -33,7 +34,7 @@ module ActiveSupport end class Event - attr_reader :name, :time, :end, :transaction_id, :payload + attr_reader :name, :time, :end, :transaction_id, :payload, :duration def initialize(name, start, ending, transaction_id, payload) @name = name @@ -41,14 +42,11 @@ module ActiveSupport @time = start @transaction_id = transaction_id @end = ending - end - - def duration - @duration ||= 1000.0 * (@end - @time) + @duration = 1000.0 * (@end - @time) end def parent_of?(event) - start = (self.time - event.time) * 1000 + start = (time - event.time) * 1000 start <= 0 && (start + duration >= event.duration) end end diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 6b563b9063..2e8d538d0b 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -4,8 +4,17 @@ YAML.add_builtin_type("omap") do |type, val| ActiveSupport::OrderedHash[val.map(&:to_a).map(&:first)] end -# OrderedHash is namespaced to prevent conflicts with other implementations module ActiveSupport + # The order of iteration over hashes in Ruby 1.8 is undefined. For example, you do not know the + # order in which +keys+ will return keys, or +each+ yield pairs. <tt>ActiveSupport::OrderedHash</tt> + # implements a hash that preserves insertion order, as in Ruby 1.9: + # + # oh = ActiveSupport::OrderedHash.new + # oh[:a] = 1 + # oh[:b] = 2 + # oh.keys # => [:a, :b], this order is guaranteed + # + # <tt>ActiveSupport::OrderedHash</tt> is namespaced to prevent conflicts with other implementations. class OrderedHash < ::Hash #:nodoc: def to_yaml_type "!tag:yaml.org,2002:omap" diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 61ccb79211..7fc2b45b51 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -1,5 +1,21 @@ require 'active_support/ordered_hash' +# Usually key value pairs are handled something like this: +# +# h = ActiveSupport::OrderedOptions.new +# h[:boy] = 'John' +# h[:girl] = 'Mary' +# h[:boy] # => 'John' +# h[:girl] # => 'Mary' +# +# Using <tt>OrderedOptions</tt> above code could be reduced to: +# +# h = ActiveSupport::OrderedOptions.new +# h.boy = 'John' +# h.girl = 'Mary' +# h.boy # => 'John' +# h.girl # => 'Mary' +# module ActiveSupport #:nodoc: class OrderedOptions < OrderedHash def []=(key, value) diff --git a/activesupport/lib/active_support/secure_random.rb b/activesupport/lib/active_support/secure_random.rb index cfbce4d754..73344498cb 100644 --- a/activesupport/lib/active_support/secure_random.rb +++ b/activesupport/lib/active_support/secure_random.rb @@ -26,25 +26,25 @@ module ActiveSupport # == Example # # # random hexadecimal string. - # p SecureRandom.hex(10) #=> "52750b30ffbc7de3b362" - # p SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559" - # p SecureRandom.hex(11) #=> "6aca1b5c58e4863e6b81b8" - # p SecureRandom.hex(12) #=> "94b2fff3e7fd9b9c391a2306" - # p SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23" + # p SecureRandom.hex(10) # => "52750b30ffbc7de3b362" + # p SecureRandom.hex(10) # => "92b15d6c8dc4beb5f559" + # p SecureRandom.hex(11) # => "6aca1b5c58e4863e6b81b8" + # p SecureRandom.hex(12) # => "94b2fff3e7fd9b9c391a2306" + # p SecureRandom.hex(13) # => "39b290146bea6ce975c37cfc23" # ... # # # random base64 string. - # p SecureRandom.base64(10) #=> "EcmTPZwWRAozdA==" - # p SecureRandom.base64(10) #=> "9b0nsevdwNuM/w==" - # p SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg==" - # p SecureRandom.base64(11) #=> "l7XEiFja+8EKEtY=" - # p SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8" - # p SecureRandom.base64(13) #=> "vKLJ0tXBHqQOuIcSIg==" + # p SecureRandom.base64(10) # => "EcmTPZwWRAozdA==" + # p SecureRandom.base64(10) # => "9b0nsevdwNuM/w==" + # p SecureRandom.base64(10) # => "KO1nIU+p9DKxGg==" + # p SecureRandom.base64(11) # => "l7XEiFja+8EKEtY=" + # p SecureRandom.base64(12) # => "7kJSM/MzBJI+75j8" + # p SecureRandom.base64(13) # => "vKLJ0tXBHqQOuIcSIg==" # ... # # # random binary string. - # p SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301" - # p SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337" + # p SecureRandom.random_bytes(10) # => "\016\t{\370g\310pbr\301" + # p SecureRandom.random_bytes(10) # => "\323U\030TO\234\357\020\a\337" # ... module SecureRandom # SecureRandom.random_bytes generates a random binary string. diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index d8942c3974..b2d9ddfeb7 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -96,7 +96,7 @@ module ActiveSupport protected def retrieve_mocha_counter(result) #:nodoc: - if using_mocha = respond_to?(:mocha_verify) + if respond_to?(:mocha_verify) # using mocha if defined?(Mocha::TestCaseAdapter::AssertionCounter) Mocha::TestCaseAdapter::AssertionCounter.new(result) else diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 62d02bdeb6..ad6c3de1f5 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -5,9 +5,9 @@ module ActiveSupport # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are # limited to UTC and the system's <tt>ENV['TZ']</tt> zone. # - # You shouldn't ever need to create a TimeWithZone instance directly via <tt>new</tt> -- instead, Rails provides the methods - # +local+, +parse+, +at+ and +now+ on TimeZone instances, and +in_time_zone+ on Time and DateTime instances, for a more - # user-friendly syntax. Examples: + # You shouldn't ever need to create a TimeWithZone instance directly via <tt>new</tt> . Instead use methods + # +local+, +parse+, +at+ and +now+ on TimeZone instances, and +in_time_zone+ on Time and DateTime instances. + # Examples: # # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 @@ -18,7 +18,8 @@ module ActiveSupport # # See Time and TimeZone for further documentation of these methods. # - # TimeWithZone instances implement the same API as Ruby Time instances, so that Time and TimeWithZone instances are interchangeable. Examples: + # TimeWithZone instances implement the same API as Ruby Time instances, so that Time and TimeWithZone instances are interchangeable. + # Examples: # # t = Time.zone.now # => Sun, 18 May 2008 13:27:25 EDT -04:00 # t.hour # => 13 @@ -113,8 +114,8 @@ module ActiveSupport end alias_method :iso8601, :xmlschema - # Coerces the date to a string for JSON encoding. The default format is ISO 8601. You can get - # %Y/%m/%d %H:%M:%S +offset style by setting ActiveSupport::JSON::Encoding.use_standard_json_time_format + # Coerces time to a string for JSON encoding. The default format is ISO 8601. You can get + # %Y/%m/%d %H:%M:%S +offset style by setting <tt>ActiveSupport::JSON::Encoding.use_standard_json_time_format</tt> # to false. # # ==== Examples diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 49dd8a1b99..abd585b64f 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -8,10 +8,10 @@ require 'active_support/core_ext/object/try' # * Lazily load TZInfo::Timezone instances only when they're needed. # * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+, +parse+, +at+ and +now+ methods. # -# If you set <tt>config.time_zone</tt> in the Rails Initializer, you can access this TimeZone object via <tt>Time.zone</tt>: +# If you set <tt>config.time_zone</tt> in the Rails Application, you can access this TimeZone object via <tt>Time.zone</tt>: # -# # environment.rb: -# Rails::Initializer.run do |config| +# # application.rb: +# class Application < Rails::Application # config.time_zone = "Eastern Time (US & Canada)" # end # diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 52612c27cb..9d2cf13260 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -3,7 +3,7 @@ module ActiveSupport MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 7594d7b68b..352172027b 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -9,7 +9,7 @@ module ActiveSupport module XmlMini extend self - # This module exists to decorate files deserialized using Hash.from_xml with + # This module decorates files deserialized using Hash.from_xml with # the <tt>original_filename</tt> and <tt>content_type</tt> methods. module FileLike #:nodoc: attr_writer :original_filename, :content_type @@ -129,12 +129,16 @@ module ActiveSupport camelize = options.has_key?(:camelize) && options[:camelize] dasherize = !options.has_key?(:dasherize) || options[:dasherize] key = key.camelize if camelize - key = key.dasherize if dasherize + key = _dasherize(key) if dasherize key end protected + def _dasherize(key) + key.gsub(/(?!^[_]*)_(?![_]*$)/, '-') + end + # TODO: Add support for other encodings def _parse_binary(bin, entity) #:nodoc: case entity['encoding'] diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 9cf187302f..7fdcb11465 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -1,5 +1,4 @@ require 'libxml' -require 'active_support/core_ext/object/returning' require 'active_support/core_ext/object/blank' # = XmlMini LibXML implementation diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index eb61a7fc22..e03a744257 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -1,4 +1,9 @@ -require 'nokogiri' +begin + require 'nokogiri' +rescue LoadError => e + $stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install" + raise e +end require 'active_support/core_ext/object/blank' # = XmlMini Nokogiri implementation diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 8af7b5e565..38c8685390 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -1,4 +1,9 @@ -require 'nokogiri' +begin + require 'nokogiri' +rescue LoadError => e + $stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install" + raise e +end require 'active_support/core_ext/object/blank' # = XmlMini Nokogiri implementation using a SAX-based parser @@ -80,4 +85,4 @@ module ActiveSupport end end end -end
\ No newline at end of file +end diff --git a/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb b/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb new file mode 100644 index 0000000000..e3d1218c96 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/load_path/loaded_constant.rb @@ -0,0 +1,3 @@ +module LoadedConstant +end + diff --git a/activesupport/test/autoloading_fixtures/loads_constant.rb b/activesupport/test/autoloading_fixtures/loads_constant.rb new file mode 100644 index 0000000000..0b30dc8bca --- /dev/null +++ b/activesupport/test/autoloading_fixtures/loads_constant.rb @@ -0,0 +1,5 @@ +module LoadsConstant +end + +# The _ = assignment is to prevent warnings +_ = RequiresConstant diff --git a/activesupport/test/autoloading_fixtures/requires_constant.rb b/activesupport/test/autoloading_fixtures/requires_constant.rb new file mode 100644 index 0000000000..14804a0de0 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/requires_constant.rb @@ -0,0 +1,5 @@ +require "loaded_constant" + +module RequiresConstant +end + diff --git a/activesupport/test/buffered_logger_test.rb b/activesupport/test/buffered_logger_test.rb index 850febb959..97c0ef14db 100644 --- a/activesupport/test/buffered_logger_test.rb +++ b/activesupport/test/buffered_logger_test.rb @@ -1,9 +1,12 @@ require 'abstract_unit' +require 'multibyte_test_helpers' require 'stringio' require 'fileutils' require 'active_support/buffered_logger' class BufferedLoggerTest < Test::Unit::TestCase + include MultibyteTestHelpers + Logger = ActiveSupport::BufferedLogger def setup @@ -146,4 +149,16 @@ class BufferedLoggerTest < Test::Unit::TestCase @logger.expects :clear_buffer @logger.flush end + + def test_buffer_multibyte + @logger.auto_flushing = 2 + @logger.info(UNICODE_STRING) + @logger.info(BYTE_STRING) + assert @output.string.include?(UNICODE_STRING) + byte_string = @output.string.dup + if byte_string.respond_to?(:force_encoding) + byte_string.force_encoding("ASCII-8BIT") + end + assert byte_string.include?(BYTE_STRING) + end end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 3e14c754b7..b79a7bbaec 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -7,6 +7,49 @@ class CacheKeyTest < ActiveSupport::TestCase assert_equal '1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true]) assert_equal 'name/1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true], :name) end + + def test_expand_cache_key_with_rails_cache_id + begin + ENV['RAILS_CACHE_ID'] = 'c99' + assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo) + assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo]) + assert_equal 'c99/c99/foo/c99/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar]) + assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm) + assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm) + assert_equal 'nm/c99/c99/foo/c99/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) + ensure + ENV['RAILS_CACHE_ID'] = nil + end + end + + def test_expand_cache_key_with_rails_app_version + begin + ENV['RAILS_APP_VERSION'] = 'rails3' + assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo) + ensure + ENV['RAILS_APP_VERSION'] = nil + end + end + + def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version + begin + ENV['RAILS_CACHE_ID'] = 'c99' + ENV['RAILS_APP_VERSION'] = 'rails3' + assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo) + ensure + ENV['RAILS_CACHE_ID'] = nil + ENV['RAILS_APP_VERSION'] = nil + end + end + + def test_respond_to_cache_key + key = 'foo' + def key.cache_key + :foo_key + end + assert_equal 'foo_key', ActiveSupport::Cache.expand_cache_key(key) + end + end class CacheStoreSettingTest < ActiveSupport::TestCase @@ -279,7 +322,7 @@ module CacheStoreBehavior assert_equal 'bar', @cache.read('foo') raise ArgumentError.new end - rescue ArgumentError => e + rescue ArgumentError end assert_equal "bar", @cache.read('foo') Time.stubs(:now).returns(time + 71) diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index 54376deee5..009a254c64 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -398,6 +398,18 @@ class ArrayWrapperTests < Test::Unit::TestCase def method_missing(*a) @target.send(*a) end end + class DoubtfulToAry + def to_ary + :not_an_array + end + end + + class NilToAry + def to_ary + nil + end + end + def test_array ary = %w(foo bar) assert_same ary, Array.wrap(ary) @@ -438,4 +450,12 @@ class ArrayWrapperTests < Test::Unit::TestCase o = Struct.new(:foo).new(123) assert_equal [o], Array.wrap(o) end + + def test_wrap_returns_nil_if_to_ary_returns_nil + assert_nil Array.wrap(NilToAry.new) + end + + def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array + assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new) + end end diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb index 08bb13dd35..60ba3b8f88 100644 --- a/activesupport/test/core_ext/class_test.rb +++ b/activesupport/test/core_ext/class_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/core_ext/class' +require 'set' class ClassTest < Test::Unit::TestCase class Parent; end @@ -12,16 +13,16 @@ class ClassTest < Test::Unit::TestCase class C < B; end def test_descendants - assert_equal [Foo, Bar, Baz, A, B, C], Parent.descendants - assert_equal [Bar, Baz], Foo.descendants + assert_equal [Foo, Bar, Baz, A, B, C].to_set, Parent.descendants.to_set + assert_equal [Bar, Baz].to_set, Foo.descendants.to_set assert_equal [Baz], Bar.descendants assert_equal [], Baz.descendants end def test_subclasses - assert_equal [Foo, A], Parent.subclasses + assert_equal [Foo, A].to_set, Parent.subclasses.to_set assert_equal [Bar], Foo.subclasses assert_equal [Baz], Bar.subclasses assert_equal [], Baz.subclasses end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 19d7935211..e8506f5222 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -171,22 +171,29 @@ class DateTimeExtCalculationsTest < Test::Unit::TestCase end def test_advance - assert_equal DateTime.civil(2006,2,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 1) - assert_equal DateTime.civil(2005,6,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:months => 4) - assert_equal DateTime.civil(2005,3,21,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:weeks => 3) - assert_equal DateTime.civil(2005,3,5,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:days => 5) - assert_equal DateTime.civil(2012,9,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 7) - assert_equal DateTime.civil(2013,10,3,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :days => 5) + assert_equal DateTime.civil(2006,2,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 1) + assert_equal DateTime.civil(2005,6,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:months => 4) + assert_equal DateTime.civil(2005,3,21,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:weeks => 3) + assert_equal DateTime.civil(2005,3,5,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:days => 5) + assert_equal DateTime.civil(2012,9,28,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 7) + assert_equal DateTime.civil(2013,10,3,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :days => 5) assert_equal DateTime.civil(2013,10,17,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5) assert_equal DateTime.civil(2001,12,27,15,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:years => -3, :months => -2, :days => -1) - assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10).advance(:years => 1) #leap day plus one year - assert_equal DateTime.civil(2005,2,28,20,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:hours => 5) - assert_equal DateTime.civil(2005,2,28,15,22,10), DateTime.civil(2005,2,28,15,15,10).advance(:minutes => 7) - assert_equal DateTime.civil(2005,2,28,15,15,19), DateTime.civil(2005,2,28,15,15,10).advance(:seconds => 9) - assert_equal DateTime.civil(2005,2,28,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:hours => 5, :minutes => 7, :seconds => 9) - assert_equal DateTime.civil(2005,2,28,10,8,1), DateTime.civil(2005,2,28,15,15,10).advance(:hours => -5, :minutes => -7, :seconds => -9) + assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10).advance(:years => 1) #leap day plus one year + assert_equal DateTime.civil(2005,2,28,20,15,10), DateTime.civil(2005,2,28,15,15,10).advance(:hours => 5) + assert_equal DateTime.civil(2005,2,28,15,22,10), DateTime.civil(2005,2,28,15,15,10).advance(:minutes => 7) + assert_equal DateTime.civil(2005,2,28,15,15,19), DateTime.civil(2005,2,28,15,15,10).advance(:seconds => 9) + assert_equal DateTime.civil(2005,2,28,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:hours => 5, :minutes => 7, :seconds => 9) + assert_equal DateTime.civil(2005,2,28,10,8,1), DateTime.civil(2005,2,28,15,15,10).advance(:hours => -5, :minutes => -7, :seconds => -9) assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9) + end + def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas + # If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead. + assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1) + assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59).advance(:months => 1, :minutes => 1) + assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23).advance(:months => 1, :hours => 1) + assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 22, 58, 59).advance(:months => 1, :hours => 1, :minutes => 1, :seconds => 1) end def test_next_week diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 7b2c10908f..5d9846a216 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -2,6 +2,8 @@ require 'abstract_unit' require 'active_support/core_ext/hash' require 'bigdecimal' require 'active_support/core_ext/string/access' +require 'active_support/ordered_hash' +require 'active_support/core_ext/object/conversions' class HashExtTest < Test::Unit::TestCase def setup @@ -449,6 +451,33 @@ class IWriteMyOwnXML end end +class HashExtToParamTests < Test::Unit::TestCase + class ToParam < String + def to_param + "#{self}-1" + end + end + + def test_string_hash + assert_equal '', {}.to_param + assert_equal 'hello=world', { :hello => "world" }.to_param + assert_equal 'hello=10', { "hello" => 10 }.to_param + assert_equal 'hello=world&say_bye=true', ActiveSupport::OrderedHash[:hello, "world", "say_bye", true].to_param + end + + def test_number_hash + assert_equal '10=20&30=40&50=60', ActiveSupport::OrderedHash[10, 20, 30, 40, 50, 60].to_param + end + + def test_to_param_hash + assert_equal 'custom=param-1&custom2=param2-1', ActiveSupport::OrderedHash[ToParam.new('custom'), ToParam.new('param'), ToParam.new('custom2'), ToParam.new('param2')].to_param + end + + def test_to_param_hash_escapes_its_keys_and_values + assert_equal 'param+1=A+string+with+%2F+characters+%26+that+should+be+%3F+escaped', { 'param 1' => 'A string with / characters & that should be ? escaped' }.to_param + end +end + class HashToXmlTest < Test::Unit::TestCase def setup @xml_options = { :root => :person, :skip_instruct => true, :indent => 0 } diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 39ee0ac748..5d9cdf22c2 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -147,7 +147,7 @@ class ModuleTest < Test::Unit::TestCase end assert_nothing_raised do - child = Class.new(parent) do + Class.new(parent) do class << self delegate :parent_method, :to => :superclass end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 4d655913cc..e28b4cd493 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/ordered_hash' require 'active_support/core_ext/object/to_query' class ToQueryTest < Test::Unit::TestCase @@ -18,12 +19,12 @@ class ToQueryTest < Test::Unit::TestCase def test_nested_conversion assert_query_equal 'person[login]=seckar&person[name]=Nicholas', - :person => {:name => 'Nicholas', :login => 'seckar'} + :person => ActiveSupport::OrderedHash[:login, 'seckar', :name, 'Nicholas'] end def test_multiple_nested assert_query_equal 'account[person][id]=20&person[id]=10', - :person => {:id => 10}, :account => {:person => {:id => 20}} + ActiveSupport::OrderedHash[:account, {:person => {:id => 20}}, :person, {:id => 10}] end def test_array_values diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index c7088638c7..f98d823f00 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -117,6 +117,7 @@ class DependenciesTest < Test::Unit::TestCase assert_equal true, $checked_verbose, 'After first load warnings should be left alone.' assert ActiveSupport::Dependencies.loaded.include?(expanded) + ActiveSupport::Dependencies.warnings_on_first_load = old_warnings end end @@ -212,6 +213,50 @@ class DependenciesTest < Test::Unit::TestCase end end + def test_doesnt_break_normal_require + path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + original_path = $:.dup + original_features = $".dup + $:.push(path) + + with_autoloading_fixtures do + # The _ = assignments are to prevent warnings + _ = RequiresConstant + assert defined?(RequiresConstant) + assert defined?(LoadedConstant) + ActiveSupport::Dependencies.clear + _ = RequiresConstant + assert defined?(RequiresConstant) + assert defined?(LoadedConstant) + end + ensure + remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant) + $".replace(original_features) + $:.replace(original_path) + end + + def test_doesnt_break_normal_require_nested + path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + original_path = $:.dup + original_features = $".dup + $:.push(path) + + with_autoloading_fixtures do + # The _ = assignments are to prevent warnings + _ = LoadsConstant + assert defined?(LoadsConstant) + assert defined?(LoadedConstant) + ActiveSupport::Dependencies.clear + _ = LoadsConstant + assert defined?(LoadsConstant) + assert defined?(LoadedConstant) + end + ensure + remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant) + $".replace(original_features) + $:.replace(original_path) + end + def failing_test_access_thru_and_upwards_fails with_autoloading_fixtures do assert ! defined?(ModuleFolder) @@ -418,7 +463,6 @@ class DependenciesTest < Test::Unit::TestCase def test_removal_from_tree_should_be_detected with_loading 'dependencies' do - root = ActiveSupport::Dependencies.autoload_paths.first c = ServiceOne ActiveSupport::Dependencies.clear assert ! defined?(ServiceOne) @@ -433,7 +477,6 @@ class DependenciesTest < Test::Unit::TestCase def test_references_should_work with_loading 'dependencies' do - root = ActiveSupport::Dependencies.autoload_paths.first c = ActiveSupport::Dependencies.ref("ServiceOne") service_one_first = ServiceOne assert_equal service_one_first, c.get @@ -798,4 +841,11 @@ class DependenciesTest < Test::Unit::TestCase ensure ActiveSupport::Dependencies.hook! end + +private + def remove_constants(*constants) + constants.each do |constant| + Object.send(:remove_const, constant) if Object.const_defined?(constant) + end + end end diff --git a/activesupport/test/deprecation/proxy_wrappers_test.rb b/activesupport/test/deprecation/proxy_wrappers_test.rb new file mode 100644 index 0000000000..aa887f274d --- /dev/null +++ b/activesupport/test/deprecation/proxy_wrappers_test.rb @@ -0,0 +1,22 @@ +require 'abstract_unit' +require 'active_support/deprecation' + +class ProxyWrappersTest < Test::Unit::TestCase + Waffles = false + NewWaffles = :hamburgers + + def test_deprecated_object_proxy_doesnt_wrap_falsy_objects + proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(nil, "message") + assert !proxy + end + + def test_deprecated_instance_variable_proxy_doesnt_wrap_falsy_objects + proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(nil, :waffles) + assert !proxy + end + + def test_deprecated_constant_proxy_doesnt_wrap_falsy_objects + proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(Waffles, NewWaffles) + assert !proxy + end +end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index a679efb41e..1527d02d16 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -148,7 +148,7 @@ class TestJSONEncoding < Test::Unit::TestCase :latitude => 123.234 } } - result = ActiveSupport::JSON.encode(hash) + ActiveSupport::JSON.encode(hash) end end diff --git a/activesupport/test/load_paths_test.rb b/activesupport/test/load_paths_test.rb index 9c83d6f061..36e3726a02 100644 --- a/activesupport/test/load_paths_test.rb +++ b/activesupport/test/load_paths_test.rb @@ -8,8 +8,8 @@ class LoadPathsTest < Test::Unit::TestCase paths[expanded_path] += 1 paths } + load_paths_count[File.expand_path('../../lib', __FILE__)] -= 1 - # CI has a bunch of duplicate load paths - # assert_equal [], load_paths_count.select { |k, v| v > 1 }, $LOAD_PATH.inspect + assert load_paths_count.select { |k, v| v > 1 }.empty?, $LOAD_PATH.inspect end end diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb index 195e3eaa42..b11fa8d346 100644 --- a/activesupport/test/memoizable_test.rb +++ b/activesupport/test/memoizable_test.rb @@ -134,7 +134,6 @@ class MemoizableTest < ActiveSupport::TestCase end def test_reloadable - counter = @calculator.counter assert_equal 1, @calculator.counter assert_equal 2, @calculator.counter(:reload) assert_equal 2, @calculator.counter diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 78232d8eb5..6ebbfdf334 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -234,7 +234,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase def test_include_raises_when_nil_is_passed @chars.include?(nil) flunk "Expected chars.include?(nil) to raise TypeError or NoMethodError" - rescue Exception => e + rescue Exception end def test_index_should_return_character_offset diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 597f949059..8839b75601 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -4,6 +4,9 @@ module MultibyteTestHelpers UNICODE_STRING = 'こにちわ' ASCII_STRING = 'ohayo' BYTE_STRING = "\270\236\010\210\245" + if BYTE_STRING.respond_to?(:force_encoding) + BYTE_STRING.force_encoding("ASCII-8BIT") + end def chars(str) ActiveSupport::Multibyte::Chars.new(str) @@ -16,4 +19,4 @@ module MultibyteTestHelpers def assert_equal_codepoints(expected, actual, message=nil) assert_equal(inspect_codepoints(expected), inspect_codepoints(actual), message) end -end
\ No newline at end of file +end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index e11de5f67a..9faa11efbc 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -11,14 +11,11 @@ module Notifications @named_subscription = @notifier.subscribe("named.subscription") { |*args| @named_events << event(*args) } end - private - def event(*args) - ActiveSupport::Notifications::Event.new(*args) - end + private - def drain - @notifier.wait - end + def event(*args) + ActiveSupport::Notifications::Event.new(*args) + end end class UnsubscribeTest < TestCase @@ -132,13 +129,10 @@ module Notifications def test_instrument_returns_block_result assert_equal 2, instrument(:awesome) { 1 + 1 } - drain end def test_instrument_yields_the_paylod_for_further_modification assert_equal 2, instrument(:awesome) { |p| p[:result] = 1 + 1 } - drain - assert_equal 1, @events.size assert_equal :awesome, @events.first.name assert_equal Hash[:result => 2], @events.first.payload @@ -154,15 +148,11 @@ module Notifications 1 + 1 end - drain - assert_equal 1, @events.size assert_equal :wot, @events.first.name assert_equal Hash[:payload => "child"], @events.first.payload end - drain - assert_equal 2, @events.size assert_equal :awesome, @events.last.name assert_equal Hash[:payload => "notifications"], @events.last.payload @@ -177,7 +167,6 @@ module Notifications assert_equal "FAIL", e.message end - drain assert_equal 1, @events.size assert_equal Hash[:payload => "notifications", :exception => ["RuntimeError", "FAIL"]], @events.last.payload @@ -185,8 +174,6 @@ module Notifications def test_event_is_pushed_even_without_block instrument(:awesome, :payload => "notifications") - drain - assert_equal 1, @events.size assert_equal :awesome, @events.last.name assert_equal Hash[:payload => "notifications"], @events.last.payload @@ -198,9 +185,9 @@ module Notifications time = Time.now event = event(:foo, time, time + 0.01, random_id, {}) - assert_equal :foo, event.name - assert_equal time, event.time - assert_equal 10.0, event.duration + assert_equal :foo, event.name + assert_equal time, event.time + assert_in_delta 10.0, event.duration, 0.00001 end def test_events_consumes_information_given_as_payload diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb index b898292c9c..33e3e69666 100644 --- a/activesupport/test/option_merger_test.rb +++ b/activesupport/test/option_merger_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require 'active_support/core_ext/object/misc' +require 'active_support/core_ext/object/with_options' class OptionMergerTest < Test::Unit::TestCase def setup diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index d340bed444..f47e896487 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -214,7 +214,7 @@ class OrderedHashTest < Test::Unit::TestCase def test_alternate_initialization_raises_exception_on_odd_length_args begin - alternate = ActiveSupport::OrderedHash[1,2,3,4,5] + ActiveSupport::OrderedHash[1,2,3,4,5] flunk "Hash::[] should have raised an exception on initialization " + "with an odd number of parameters" rescue diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index ff77e16edd..1c74ce8b2a 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -9,11 +9,16 @@ end class MadRonon < StandardError end +class CoolError < StandardError +end + class Stargate attr_accessor :result include ActiveSupport::Rescuable + rescue_from WraithAttack, :with => :sos_first + rescue_from WraithAttack, :with => :sos rescue_from NuclearExplosion do @@ -45,11 +50,30 @@ class Stargate def sos @result = 'killed' end + + def sos_first + @result = 'sos_first' + end + +end + +class CoolStargate < Stargate + attr_accessor :result + + include ActiveSupport::Rescuable + + rescue_from CoolError, :with => :sos_cool_error + + def sos_cool_error + @result = 'sos_cool_error' + end end + class RescueableTest < Test::Unit::TestCase def setup @stargate = Stargate.new + @cool_stargate = CoolStargate.new end def test_rescue_from_with_method @@ -66,4 +90,17 @@ class RescueableTest < Test::Unit::TestCase @stargate.dispatch :ronanize assert_equal 'dex', @stargate.result end + + def test_rescues_defined_later_are_added_at_end_of_the_rescue_handlers_array + expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon"] + result = @stargate.send(:rescue_handlers).collect {|e| e.first} + assert_equal expected, result + end + + def test_children_should_inherit_rescue_defintions_from_parents_and_child_rescue_should_be_appended + expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "CoolError"] + result = @cool_stargate.send(:rescue_handlers).collect {|e| e.first} + assert_equal expected, result + end + end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 3092fe01ae..633d3b212b 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -49,8 +49,8 @@ class AssertDifferenceTest < ActiveSupport::TestCase end def test_expression_is_evaluated_in_the_appropriate_scope - local_scope = 'foo' silence_warnings do + local_scope = 'foo' assert_difference('local_scope; @object.num') { @object.increment } end end diff --git a/activesupport/test/test_xml_mini.rb b/activesupport/test/test_xml_mini.rb new file mode 100644 index 0000000000..585eb15c6e --- /dev/null +++ b/activesupport/test/test_xml_mini.rb @@ -0,0 +1,49 @@ +require 'abstract_unit' +require 'active_support/xml_mini' + +class XmlMiniTest < Test::Unit::TestCase + def test_rename_key_dasherizes_by_default + assert_equal "my-key", ActiveSupport::XmlMini.rename_key("my_key") + end + + def test_rename_key_does_nothing_with_dasherize_true + assert_equal "my-key", ActiveSupport::XmlMini.rename_key("my_key", :dasherize => true) + end + + def test_rename_key_does_nothing_with_dasherize_false + assert_equal "my_key", ActiveSupport::XmlMini.rename_key("my_key", :dasherize => false) + end + + def test_rename_key_camelizes_with_camelize_true + assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => true) + end + + def test_rename_key_camelizes_with_camelize_true + assert_equal "MyKey", ActiveSupport::XmlMini.rename_key("my_key", :camelize => true) + end + + def test_rename_key_does_not_dasherize_leading_underscores + assert_equal "_id", ActiveSupport::XmlMini.rename_key("_id") + end + + def test_rename_key_with_leading_underscore_dasherizes_interior_underscores + assert_equal "_my-key", ActiveSupport::XmlMini.rename_key("_my_key") + end + + def test_rename_key_does_not_dasherize_trailing_underscores + assert_equal "id_", ActiveSupport::XmlMini.rename_key("id_") + end + + def test_rename_key_with_trailing_underscore_dasherizes_interior_underscores + assert_equal "my-key_", ActiveSupport::XmlMini.rename_key("my_key_") + end + + def test_rename_key_does_not_dasherize_multiple_leading_underscores + assert_equal "__id", ActiveSupport::XmlMini.rename_key("__id") + end + + def test_rename_key_does_not_dasherize_multiple_leading_underscores + assert_equal "id__", ActiveSupport::XmlMini.rename_key("id__") + end + +end diff --git a/ci/ci_build.rb b/ci/ci_build.rb index ee6c357519..f9933cb922 100755 --- a/ci/ci_build.rb +++ b/ci/ci_build.rb @@ -19,7 +19,7 @@ puts "[CruiseControl] Rails build" build_results = {} # Install required version of bundler. -bundler_install_cmd = "gem install bundler --no-ri --no-rdoc" +bundler_install_cmd = "sudo gem install bundler --pre --no-ri --no-rdoc" puts "Running command: #{bundler_install_cmd}" build_results[:install_bundler] = system bundler_install_cmd @@ -27,7 +27,7 @@ cd root_dir do puts puts "[CruiseControl] Bundling RubyGems" puts - build_results[:bundle] = system 'rm -rf ~/.bundle; env CI=1 bundle install' + build_results[:bundle] = system 'rm -rf ~/.bundle; env CI=1 bundle update' end cd "#{root_dir}/activesupport" do diff --git a/doc/template/horo.rb b/doc/template/horo.rb deleted file mode 100644 index b38fa28cde..0000000000 --- a/doc/template/horo.rb +++ /dev/null @@ -1,618 +0,0 @@ -# Horo RDoc template -# Author: Hongli Lai - http://izumi.plan99.net/blog/ -# -# Based on the Jamis template: -# http://weblog.jamisbuck.org/2005/4/8/rdoc-template - -if defined?(RDoc::Diagram) - RDoc::Diagram.class_eval do - remove_const(:FONT) - const_set(:FONT, "\"Bitstream Vera Sans\"") - end -end - -require 'rdoc/generator/html' - -module RDoc -module Generator -class HTML -class HORO - -FONTS = "\"Bitstream Vera Sans\", Verdana, Arial, Helvetica, sans-serif" - -STYLE = <<CSS -a { - color: #00F; - text-decoration: none; -} - -a:hover { - color: #77F; - text-decoration: underline; -} - -body, td, p { - font-family: <%= values['fonts'] %>; - background: #FFF; - color: #000; - margin: 0px; - font-size: small; -} - -p { - margin-top: 0.5em; - margin-bottom: 0.5em; -} - -#content { - margin: 2em; - margin-left: 3.5em; - margin-right: 3.5em; -} - -#description p { - margin-bottom: 0.5em; -} - -.sectiontitle { - margin-top: 1em; - margin-bottom: 1em; - padding: 0.5em; - padding-left: 2em; - background: #005; - color: #FFF; - font-weight: bold; -} - -.attr-rw { - padding-left: 1em; - padding-right: 1em; - text-align: center; - color: #055; -} - -.attr-name { - font-weight: bold; -} - -.attr-desc { -} - -.attr-value { - font-family: monospace; -} - -.file-title-prefix { - font-size: large; -} - -.file-title { - font-size: large; - font-weight: bold; - background: #005; - color: #FFF; -} - -.banner { - background: #005; - color: #FFF; - border: 1px solid black; - padding: 1em; -} - -.banner td { - background: transparent; - color: #FFF; -} - -h1 a, h2 a, .sectiontitle a, .banner a { - color: #FF0; -} - -h1 a:hover, h2 a:hover, .sectiontitle a:hover, .banner a:hover { - color: #FF7; -} - -.dyn-source { - display: none; - background: #fffde8; - color: #000; - border: #ffe0bb dotted 1px; - margin: 0.5em 2em 0.5em 2em; - padding: 0.5em; -} - -.dyn-source .cmt { - color: #00F; - font-style: italic; -} - -.dyn-source .kw { - color: #070; - font-weight: bold; -} - -.method { - margin-left: 1em; - margin-right: 1em; - margin-bottom: 1em; -} - -.description pre { - padding: 0.5em; - border: #ffe0bb dotted 1px; - background: #fffde8; -} - -.method .title { - font-family: monospace; - font-size: large; - border-bottom: 1px dashed black; - margin-bottom: 0.3em; - padding-bottom: 0.1em; -} - -.method .description, .method .sourcecode { - margin-left: 1em; -} - -.description p, .sourcecode p { - margin-bottom: 0.5em; -} - -.method .sourcecode p.source-link { - text-indent: 0em; - margin-top: 0.5em; -} - -.method .aka { - margin-top: 0.3em; - margin-left: 1em; - font-style: italic; - text-indent: 2em; -} - -h1 { - padding: 1em; - margin-left: -1.5em; - font-size: x-large; - font-weight: bold; - color: #FFF; - background: #007; -} - -h2 { - padding: 0.5em 1em 0.5em 1em; - margin-left: -1.5em; - font-size: large; - font-weight: bold; - color: #FFF; - background: #009; -} - -h3, h4, h5, h6 { - color: #220088; - border-bottom: #5522bb solid 1px; -} - -.sourcecode > pre { - padding: 0.5em; - border: 1px dotted black; - background: #FFE; -} - -dt { - font-weight: bold -} - -dd { - margin-bottom: 0.7em; -} -CSS - -XHTML_PREAMBLE = %{<?xml version="1.0" encoding="<%= values['charset'] %>"?> -<!DOCTYPE html - PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -} - -XHTML_FRAMESET_PREAMBLE = %{ -<!DOCTYPE html - PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"> -} - -HEADER = XHTML_PREAMBLE + <<ENDHEADER -<html> - <head> - <title><%= values['title'] %></title> - <meta http-equiv="Content-Type" content="text/html; charset=<%= values['charset'] %>" /> - <link rel="stylesheet" href="<%= values['style_url'] %>" type="text/css" media="screen" /> - - <script language="JavaScript" type="text/javascript"> - // <![CDATA[ - - function toggleSource( id ) - { - var elem - var link - - if( document.getElementById ) - { - elem = document.getElementById( id ) - link = document.getElementById( "l_" + id ) - } - else if ( document.all ) - { - elem = eval( "document.all." + id ) - link = eval( "document.all.l_" + id ) - } - else - return false; - - if( elem.style.display == "block" ) - { - elem.style.display = "none" - link.innerHTML = "show source" - } - else - { - elem.style.display = "block" - link.innerHTML = "hide source" - } - } - - function openCode( url ) - { - window.open( url, "SOURCE_CODE", "resizable=yes,scrollbars=yes,toolbar=no,status=no,height=480,width=750" ).focus(); - } - // ]]> - </script> - </head> - - <body> -ENDHEADER - -FILE_PAGE = <<HTML -<table border='0' cellpadding='0' cellspacing='0' width="100%" class='banner'> - <tr><td> - <table width="100%" border='0' cellpadding='0' cellspacing='0'><tr> - <td class="file-title" colspan="2"><span class="file-title-prefix">File</span><br /><%= values['short_name'] %></td> - <td align="right"> - <table border='0' cellspacing="0" cellpadding="2"> - <tr> - <td>Path:</td> - <td><%= values['full_path'] %> -<% if values['cvsurl'] %> - (<a href="<%= values['cvsurl'] %>">CVS</a>) -<% end %> - </td> - </tr> - <tr> - <td>Modified:</td> - <td><%= values['dtm_modified'] %></td> - </tr> - </table> - </td></tr> - </table> - </td></tr> -</table><br /> -HTML - -################################################################### - -CLASS_PAGE = <<HTML -<table width="100%" border='0' cellpadding='0' cellspacing='0' class='banner'><tr> - <td class="file-title"><span class="file-title-prefix"><%= values['classmod'] %></span><br /><%= values['full_name'] %></td> - <td align="right"> - <table cellspacing="0" cellpadding="2"> - <tr valign="top"> - <td>In:</td> - <td> -<% values['infiles'].each do |infile| %> -<%= href infile['full_path_url'], infile['full_path'] %>: -<% if infile['cvsurl'] %> - (<a href="<%= infile['cvsurl'] %>">CVS</a>) -<% end %> -<% end %> - </td> - </tr> -<% if values['parent'] %> - <tr> - <td>Parent:</td> - <td> -<% if values['par_url'] %> - <a href="<%= values['par_url'] %>"> -<% end %> -<%= values['parent'] %> -<% if values['par_url'] %> - </a> -<% end %> - </td> - </tr> -<% end %> - </table> - </td> - </tr> - </table> -HTML - -################################################################### - -METHOD_LIST = <<HTML - <div id="content"> -<% if values['diagram'] %> - <table cellpadding='0' cellspacing='0' border='0' width="100%"><tr><td align="center"> - <%= values['diagram'] %> - </td></tr></table> -<% end %> - -<% if values['description'] %> - <div class="description"><%= values['description'] %></div> -<% end %> - -<% if values['requires'] %> - <div class="sectiontitle">Required Files</div> - <ul> -<% values['requires'].each do |require| %> - <li><%= href require['aref'], require['name'] %>:</li> -<% end %> - </ul> -<% end %> - -<% if values['toc'] %> - <div class="sectiontitle">Contents</div> - <ul> -<% values['toc'].each do |toc| %> - <li><a href="#<%= toc['href'] %>"><%= toc['secname'] %></a></li> -<% end %> - </ul> -<% end %> - -<% if values['methods'] %> - <div class="sectiontitle">Methods</div> - <ul> -<% values['methods'].each do |method| %> - <li><%= href method['aref'], method['name'] %></li> -<% end %> - </ul> -<% end %> - -<% if values['includes'] %> -<div class="sectiontitle">Included Modules</div> -<ul> -<% values['includes'].each do |include| %> - <li><%= href include['aref'], include['name'] %>:</li> -<% end %> -</ul> -<% end %> - -<% values['sections'].each do |section| %> -<% if section['sectitle'] %> -<div class="sectiontitle"><a name="<%= section['secsequence'] %>"><%= section['sectitle'] %></a></div> -<% if section['seccomment'] %> -<div class="description"> -<%= section['seccomment'] %> -</div> -<% end %> -<% end %> - -<% if section['classlist'] %> - <div class="sectiontitle">Classes and Modules</div> - <%= section['classlist'] %> -<% end %> - -<% if section['constants'] %> - <div class="sectiontitle">Constants</div> - <table border='0' cellpadding='5'> -<% section['constants'].each do |constant| %> - <tr valign='top'> - <td class="attr-name"><%= constant['name'] %></td> - <td>=</td> - <td class="attr-value"><%= constant['value'] %></td> - </tr> -<% if constant['desc'] %> - <tr valign='top'> - <td> </td> - <td colspan="2" class="attr-desc"><%= constant['desc'] %></td> - </tr> -<% end %> -<% end %> - </table> -<% end %> - -<% if section['attributes'] %> - <div class="sectiontitle">Attributes</div> - <table border='0' cellpadding='5'> -<% section['attributes'].each do |attribute| %> - <tr valign='top'> - <td class='attr-rw'> -<% if attribute['rw'] %> -[<%= attribute['rw'] %>] -<% end %> - </td> - <td class='attr-name'><%= attribute['name'] %></td> - <td class='attr-desc'><%= attribute['a_desc'] %></td> - </tr> -<% end %> - </table> -<% end %> - -<% if section['method_list'] %> -<% section['method_list'].each do |method_list| %> -<% if method_list['methods'] %> -<div class="sectiontitle"><%= method_list['type'] %> <%= method_list['category'] %> methods</div> -<% method_list['methods'].each do |method| %> -<div class="method"> - <div class="title"> -<% if method['callseq'] %> - <a name="<%= method['aref'] %>"></a><b><%= method['callseq'] %></b> -<% end %> -<% unless method['callseq'] %> - <a name="<%= method['aref'] %>"></a><b><%= method['name'] %></b><%= method['params'] %> -<% end %> -<% if method['codeurl'] %> -[ <a href="<%= method['codeurl'] %>" target="SOURCE_CODE" onclick="javascript:openCode('<%= method['codeurl'] %>'); return false;">source</a> ] -<% end %> - </div> -<% if method['m_desc'] %> - <div class="description"> - <%= method['m_desc'] %> - </div> -<% end %> -<% if method['aka'] %> -<div class="aka"> - This method is also aliased as -<% method['aka'].each do |aka| %> - <a href="<%= aka['aref'] %>"><%= aka['name'] %></a> -<% end %> -</div> -<% end %> -<% if method['sourcecode'] %> -<div class="sourcecode"> - <p class="source-link">[ <a href="javascript:toggleSource('<%= method['aref'] %>_source')" id="l_<%= method['aref'] %>_source">show source</a> ]</p> - <div id="<%= method['aref'] %>_source" class="dyn-source"> -<pre> -<%= method['sourcecode'] %> -</pre> - </div> -</div> -<% end %> -</div> -<% end %> -<% end %> -<% end %> -<% end %> -<% end %> -</div> -HTML - -FOOTER = <<ENDFOOTER - </body> -</html> -ENDFOOTER - -BODY = HEADER + <<ENDBODY - <%= template_include %> <!-- banner header --> - - <div id="bodyContent"> - #{METHOD_LIST} - </div> - - #{FOOTER} -ENDBODY - -########################## Source code ########################## - -SRC_PAGE = XHTML_PREAMBLE + <<HTML -<html> -<head><title><%= values['title'] %></title> -<meta http-equiv="Content-Type" content="text/html; charset=<%= values['charset'] %>" /> -<style type="text/css"> -.ruby-comment { color: green; font-style: italic } -.ruby-constant { color: #4433aa; font-weight: bold; } -.ruby-identifier { color: #222222; } -.ruby-ivar { color: #2233dd; } -.ruby-keyword { color: #3333FF; font-weight: bold } -.ruby-node { color: #777777; } -.ruby-operator { color: #111111; } -.ruby-regexp { color: #662222; } -.ruby-value { color: #662222; font-style: italic } - .kw { color: #3333FF; font-weight: bold } - .cmt { color: green; font-style: italic } - .str { color: #662222; font-style: italic } - .re { color: #662222; } -</style> -</head> -<body bgcolor="white"> -<pre><%= values['code'] %></pre> -</body> -</html> -HTML - -########################## Index ################################ - -FR_INDEX_BODY = <<HTML -<%= template_include %> -HTML - -FILE_INDEX = XHTML_PREAMBLE + <<HTML -<html> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=<%= values['charset'] %>" /> -<title>Index</title> -<style type="text/css"> -<!-- - body { - background-color: #EEE; - font-family: #{FONTS}; - color: #000; - margin: 0px; - } - .banner { - background: #005; - color: #FFF; - padding: 0.2em; - font-size: small; - font-weight: bold; - text-align: center; - } - .entries { - margin: 0.25em 1em 0 1em; - font-size: x-small; - } - a { - color: #00F; - text-decoration: none; - white-space: nowrap; - } - a:hover { - color: #77F; - text-decoration: underline; - } ---> -</style> -<base target="docwin" /> -</head> -<body> -<div class="banner"><%= values['list_title'] %></div> -<div class="entries"> -<% values['entries'].each do |entrie| %> -<a href="<%= entrie['href'] %>"><%= entrie['name'] %></a><br /> -<% end %> -</div> -</body></html> -HTML - -CLASS_INDEX = FILE_INDEX -METHOD_INDEX = FILE_INDEX - -INDEX = XHTML_FRAMESET_PREAMBLE + <<HTML -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head> - <title><%= values['title'] %></title> - <meta http-equiv="Content-Type" content="text/html; charset=<%= values['charset'] %>" /> -</head> - -<frameset cols="20%,*"> - <frameset rows="15%,55%,30%"> - <frame src="fr_file_index.html" title="Files" name="Files" /> - <frame src="fr_class_index.html" name="Classes" /> - <frame src="fr_method_index.html" name="Methods" /> - </frameset> - <frame src="<%= values['initial_page'] %>" name="docwin" /> - <noframes> - <body bgcolor="white"> - Click <a href="html/index.html">here</a> for a non-frames - version of this page. - </body> - </noframes> -</frameset> - -</html> -HTML - -end -end -end -end diff --git a/install.rb b/install.rb new file mode 100644 index 0000000000..05bba27a14 --- /dev/null +++ b/install.rb @@ -0,0 +1,11 @@ +version = ARGV.pop + +%w( activesupport activemodel activerecord activeresource actionpack actionmailer railties ).each do |framework| + puts "Installing #{framework}..." + `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem` +end + +puts "Installing Rails..." +`gem build rails.gemspec` +`gem install rails-#{version}.gem --no-ri --no-rdoc ` +`rm rails-#{version}.gem`
\ No newline at end of file diff --git a/rails.gemspec b/rails.gemspec index 2d10ce78a0..8c90584470 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |s| s.add_dependency('activeresource', version) s.add_dependency('actionmailer', version) s.add_dependency('railties', version) - s.add_dependency('bundler', '>= 1.0.0.beta.3') + s.add_dependency('bundler', '>= 1.0.0.rc.2') end diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 8e2bac7f00..fc1ec340c7 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,4 +1,9 @@ -*Rails 3.0.0 [Release Candidate] (unreleased)* +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* Application generation: --skip-testunit and --skip-activerecord become --skip-test-unit + and --skip-active-record respectively. [fxn] + +* Added console to Rails::Railtie as a hook called just after console starts. [José Valim] * Rails no longer autoload code in lib for application. You need to explicitly require it. [José Valim] @@ -13,26 +18,26 @@ *Rails 3.0.0 [beta 4] (June 8th, 2010)* -* Version bump -* Removed Rails Metal [YK & JV]. +* Removed Rails Metal [Yehuda Katz, José Valim]. + *Rails 3.0.0 [beta 3] (April 13th, 2010)* -* Renamed config.cookie_secret to config.secret_token and pass it as env key. [JV] +* Renamed config.cookie_secret to config.secret_token and pass it as env key. [José Valim] *Rails 3.0.0 [beta 2] (April 1st, 2010)* -* Session store configuration has changed [YK & CL] +* Session store configuration has changed [Yehuda Katz, Carl Lerche] config.session_store :cookie_store, {:key => "..."} config.cookie_secret = "fdsfhisdghfidugnfdlg" * railtie_name and engine_name are deprecated. You can now add any object to - the configuration object: config.your_plugin = {} [JV] + the configuration object: config.your_plugin = {} [José Valim] * Added config.generators.templates to provide alternative paths for the generators - to look for templates [JV] + to look for templates [José Valim] *Rails 3.0.0 [beta 1] (February 4, 2010)* diff --git a/railties/README b/railties/README deleted file mode 100644 index d8be15e346..0000000000 --- a/railties/README +++ /dev/null @@ -1,281 +0,0 @@ -== 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, create a new Rails application: - <tt>rails new myapp</tt> (where <tt>myapp</tt> is the application name) - -2. Change directory to <tt>myapp</tt> and start the web server: - <tt>cd myapp; rails server</tt> (run with --help for options) - -3. Go to http://localhost:3000/ and you'll see: - "Welcome aboard: You're riding Ruby on Rails!" - -4. Follow the guidelines to start developing your application. You can find -the following resources handy: - -* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html -* Ruby on Rails Tutorial Book: http://www.railstutorial.org/ - - -== Web Servers - -By default, Rails will try to use Mongrel if it's installed when started with -<tt>rails server</tt>, otherwise Rails will use WEBrick, the web server that -ships with Ruby. - -Mongrel is a Ruby-based web server with a C component (which requires -compilation) that is suitable for development. If you have Ruby Gems installed, -getting up and running with mongrel is as easy as: - <tt>sudo gem install mongrel</tt>. - -You can find more info at: http://mongrel.rubyforge.org - -You can alternatively run Rails applications with other Ruby web servers, e.g., -{Thin}[http://code.macournoyer.com/thin/], {Ebb}[http://ebb.rubyforge.org/], and -Apache with {mod_rails}[http://www.modrails.com/]. However, <tt>rails server</tt> -doesn't search for or start them. - -For production use, often a web/proxy server, e.g., {Apache}[http://apache.org], -{Nginx}[http://nginx.net/], {LiteSpeed}[http://litespeedtech.com/], -{Lighttpd}[http://www.lighttpd.net/], or {IIS}[http://www.iis.net/], is deployed -as the front end server with the chosen Ruby web server running in the back end -and receiving the proxied requests via one of several protocols (HTTP, CGI, FCGI). - - -== 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/. There are -several books available online as well: - -* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) -* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) - -These two 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 <tt>sudo gem install ruby-debug</tt>. 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", "body"=>"Only ten..", "id"=>"2"}>]" - >> @posts.first.title = "hello from a debugger" - => "hello from a debugger" - -...and even better, 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 can enter "cont". - - -== Console - -The console is a Ruby shell, which allows you to interact with your -application's domain model. 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. - -To start the console, run <tt>rails console</tt> from the application -directory. - -Options: - -* Passing the <tt>-s, --sandbox</tt> argument will rollback any modifications - made to the database. -* Passing an environment name as an argument will load the corresponding - environment. Example: <tt>rails console production</tt>. - -To reload your controllers and models after launching the console run -<tt>reload!</tt> - -More information about irb can be found at: -link:http://www.rubycentral.com/pickaxe/irb.html - - -== dbconsole - -You can go to the command line of your database directly through <tt>rails -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>rails dbconsole production</tt>. Currently works for MySQL, -PostgreSQL and SQLite 3. - -== Description of Contents - -The default directory structure of a generated Ruby on Rails application: - - |-- app - | |-- controllers - | |-- helpers - | |-- models - | `-- views - | `-- layouts - |-- config - | |-- environments - | |-- initializers - | `-- locales - |-- db - |-- doc - |-- lib - | `-- tasks - |-- log - |-- public - | |-- images - | |-- javascripts - | `-- stylesheets - |-- script - | `-- performance - |-- test - | |-- fixtures - | |-- functional - | |-- integration - | |-- performance - | `-- unit - |-- tmp - | |-- cache - | |-- pids - | |-- sessions - | `-- sockets - `-- vendor - `-- plugins - -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. Models descend from - ActiveRecord::Base by default. - -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 by default. - -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 generators 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 rails generate - command, 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/README.rdoc b/railties/README.rdoc new file mode 100644 index 0000000000..a1718a7d96 --- /dev/null +++ b/railties/README.rdoc @@ -0,0 +1,25 @@ += Railties -- Gluing the Engine to the Rails + +Railties is responsible to glue all frameworks together. Overall, it: + +* handles all the bootstrapping process for a Rails application; + +* manager rails command line interface; + +* provides Rails generators core; + + +== Download + +The latest version of Railties can be installed with Rubygems: + +* gem install railties + +Documentation can be found at + +* http://api.rubyonrails.org + + +== License + +Railties is released under the MIT license. diff --git a/railties/Rakefile b/railties/Rakefile index ddc872e18b..8e78d2ff4a 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,8 +1,8 @@ -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake' require 'rake/testtask' -require 'rake/rdoctask' +require 'rdoc/task' require 'rake/gempackagetask' require 'date' @@ -39,7 +39,7 @@ desc "Updates application README to the latest version Railties README" task :update_readme do readme = "lib/rails/generators/rails/app/templates/README" rm readme - cp "./README", readme + cp "./README.rdoc", readme end desc 'Generate guides (for authors), use ONLY=foo to process just "foo.textile"' @@ -60,13 +60,13 @@ end # Generate documentation ------------------------------------------------------------------ -Rake::RDocTask.new { |rdoc| +RDoc::Task.new { |rdoc| rdoc.rdoc_dir = 'doc' rdoc.title = "Railties -- Gluing the Engine to the Rails" - rdoc.options << '--line-numbers' << '--inline-source' << '--accessor' << 'cattr_accessor=object' + rdoc.options << '-f' << 'horo' + rdoc.options << '--main' << 'README.rdoc' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG') rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.exclude('lib/rails/generators/**/templates/*') } diff --git a/railties/guides/rails_guides.rb b/railties/guides/rails_guides.rb index e6f0b694a6..dfbb06cc76 100644 --- a/railties/guides/rails_guides.rb +++ b/railties/guides/rails_guides.rb @@ -1,6 +1,13 @@ pwd = File.dirname(__FILE__) $:.unshift pwd +# This is a predicate useful for the doc:guides task of applications. +def bundler? + # Note that rake sets the cwd to the one that contains the Rakefile + # being executed. + File.exists?('Gemfile') +end + # Loading Action Pack requires rack and erubis. require 'rubygems' @@ -20,7 +27,19 @@ begin gem 'RedCloth', '>= 4.1.1' require 'redcloth' rescue Gem::LoadError - $stderr.puts %(Generating Guides requires RedCloth 4.1.1+) + $stderr.puts('Generating guides requires RedCloth 4.1.1+.') + $stderr.puts(<<ERROR) if bundler? +Please add + + gem 'RedCloth', '>= 4.1.1' + +to the Gemfile, run + + bundle install + +and try again. +ERROR + exit 1 end diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile index 7dcaf508c6..14da650e83 100644 --- a/railties/guides/source/3_0_release_notes.textile +++ b/railties/guides/source/3_0_release_notes.textile @@ -1,6 +1,6 @@ h2. Ruby on Rails 3.0 Release Notes -Rails 3.0 is ponies and rainbows! It's going to cook you dinner and fold your laundry. You're going to wonder how life was ever possible before it arrived. It's the Best Version of Rails We've Ever Done! +Rails 3.0 is ponies and rainbows! It's going to cook you dinner and fold your laundry. You're going to wonder how life was ever possible before it arrived. It's the Best Version of Rails We've Ever Done! But seriously now, it's really good stuff. There are all the good ideas brought over from when the Merb team joined the party and brought a focus on framework agnosticism, slimmer and faster internals, and a handful of tasty APIs. If you're coming to Rails 3.0 from Merb 1.x, you should recognize lots. If you're coming from Rails 2.x, you're going to love it too. @@ -12,7 +12,7 @@ Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is g * Unobtrusive JavaScript helpers with drivers for Prototype, jQuery, and more coming (end of inline JS) * Explicit dependency management with Bundler -On top of all that, we've tried our best to deprecate the old APIs with nice warnings. That means that you can move your existing application to Rails 3 without immediately rewriting all your old code to the latest best practices. +On top of all that, we've tried our best to deprecate the old APIs with nice warnings. That means that you can move your existing application to Rails 3 without immediately rewriting all your old code to the latest best practices. These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the "list of commits":http://github.com/rails/rails/commits/master in the main Rails repository on GitHub. @@ -66,7 +66,7 @@ To help with the upgrade process, a plugin named "Rails Upgrade":http://github.c Simply install the plugin, then run +rake rails:upgrade:check+ to check your app for pieces that need to be updated (with links to information on how to update them). It also offers a task to generate a +Gemfile+ based on your current +config.gem+ calls and a task to generate a new routes file from your current one. To get the plugin, simply run the following: <shell> -rails plugin install git://github.com/rails/rails_upgrade.git +ruby script/plugin install git://github.com/rails/rails_upgrade.git </shell> You can see an example of how that works at "Rails Upgrade is now an Official Plugin":http://omgbloglol.com/post/364624593/rails-upgrade-is-now-an-official-plugin @@ -596,3 +596,4 @@ h3. Credits See the "full list of contributors to Rails":http://contributors.rubyonrails.org/ for the many people who spent many hours making Rails 3. Kudos to all of them. Rails 3.0 Release Notes were compiled by "Mikel Lindsaar":http://lindsaar.net. + diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index 038ca903c1..ec2d5b2787 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -162,23 +162,38 @@ If you need a different session storage mechanism, you can change it in the +con # 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 +# YourApp::Application.config.session_store :active_record_store </ruby> -Rails sets up a session key (the name of the cookie) and (for the CookieStore) a secret key used when signing the session data. These can also be changed in +config/initializers/session_store.rb+: +Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in +config/initializers/session_store.rb+: <ruby> -# 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, +# Be sure to restart your server when you modify this file. + +YourApp::Application.config.session_store :cookie_store, :key => '_your_app_session' +</ruby> + +You can also pass a +:domain+ key and specify the domain name for the cookie: + +<ruby> +# Be sure to restart your server when you modify this file. + +YourApp::Application.config.session_store :cookie_store, :key => '_your_app_session', :domain => ".example.com" +</ruby> + +Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in +config/initializers/secret_token.rb+ + +<ruby> +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies 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 => '_yourappname_session', - :secret => '4f50711b8f0f49572...' -} +YourApp::Application.config.secret_token = '49d3f3de9ed86c74b94ad6bd0...' </ruby> -NOTE: Changing the secret when using the CookieStore will invalidate all existing sessions. +NOTE: Changing the secret when using the +CookieStore+ will invalidate all existing sessions. h4. Accessing the Session diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index f5e70aef41..53095a2bd3 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -337,7 +337,7 @@ Just like in Ruby. If you want a shorter syntax be sure to check out the "Hash C h4. Hash Conditions -Active Record also allows you to pass in a hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: +Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: NOTE: Only equality, range and subset checking are possible with Hash conditions. @@ -347,7 +347,7 @@ h5. Equality Conditions Client.where({ :locked => true }) </ruby> -The field name does not have to be a symbol it can also be a string: +The field name can also be a string: <ruby> Client.where({ 'locked' => true }) @@ -447,33 +447,33 @@ h4. Limit and Offset To apply +LIMIT+ to the SQL fired by the +Model.find+, you can specify the +LIMIT+ using +limit+ and +offset+ methods on the relation. -If you want to limit the amount of records to a certain subset of all the records retrieved you usually use +limit+ for this, sometimes coupled with +offset+. Limit is the maximum number of records that will be retrieved from a query, and offset is the number of records it will start reading from from the first record of the set. For example: +You can use +limit+ to specify the number of records to be retrieved, and use +offset+ to specify the number of records to skip before starting to return the records. For example <ruby> Client.limit(5) </ruby> -This code will return a maximum of 5 clients and because it specifies no offset it will return the first 5 clients in the table. The SQL it executes will look like this: +will return a maximum of 5 clients and because it specifies no offset it will return the first 5 in the table. The SQL it executes looks like this: <sql> SELECT * FROM clients LIMIT 5 </sql> -Or chaining both +limit+ and +offset+: +Adding +offset+ to that <ruby> -Client.limit(5).offset(5) +Client.limit(5).offset(30) </ruby> -This code will return a maximum of 5 clients and because it specifies an offset this time, it will return these records starting from the 5th client in the clients table. The SQL looks like: +will return instead a maximum of 5 clients beginning with the 31st. The SQL looks like: <sql> -SELECT * FROM clients LIMIT 5, 5 +SELECT * FROM clients LIMIT 5, 30 </sql> h4. Group -To apply +GROUP BY+ clause to the SQL fired by the finder, you can specify the +group+ method on the find. +To apply a +GROUP BY+ clause to the SQL fired by the finder, you can specify the +group+ method on the find. For example, if you want to find a collection of the dates orders were created on: @@ -491,7 +491,7 @@ SELECT * FROM orders GROUP BY date(created_at) h4. Having -SQL uses +HAVING+ clause to specify conditions on the +GROUP BY+ fields. You can specify the +HAVING+ clause to the SQL fired by the +Model.find+ using +:having+ option on the find. +SQL uses the +HAVING+ clause to specify conditions on the +GROUP BY+ fields. You can add the +HAVING+ clause to the SQL fired by the +Model.find+ by adding the +:having+ option to the find. For example: @@ -517,7 +517,7 @@ Any attempt to alter or destroy the readonly records will not succeed, raising a Client.first.readonly(true) </ruby> -If you assign this record to a variable client, calling the following code will raise an +ActiveRecord::ReadOnlyRecord+ exception: +For example, calling the following code will raise an +ActiveRecord::ReadOnlyRecord+ exception: <ruby> client = Client.first.readonly(true) @@ -527,7 +527,9 @@ client.save h4. Locking Records for Update -Locking is helpful for preventing the race conditions when updating records in the database and ensuring atomic updated. Active Record provides two locking mechanism: +Locking is helpful for preventing race conditions when updating records in the database and ensuring atomic updates. + +Active Record provides two locking mechanisms: * Optimistic Locking * Pessimistic Locking @@ -538,7 +540,7 @@ Optimistic locking allows multiple users to access the same record for edits, an <strong>Optimistic locking column</strong> -In order to use optimistic locking, the table needs to have a column called +lock_version+. Each time the record is updated, Active Record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice will let the last one saved raise an +ActiveRecord::StaleObjectError+ exception if the first was also updated. Example: +In order to use optimistic locking, the table needs to have a column called +lock_version+. Each time the record is updated, Active Record increments the +lock_version+ column. If an update request is made with a lower value in the +lock_version+ field than is currently in the +lock_version+ column in the database, the update request will fail with an +ActiveRecord::StaleObjectError+. Example: <ruby> c1 = Client.find(1) @@ -569,7 +571,7 @@ end h5. Pessimistic Locking -Pessimistic locking uses locking mechanism provided by the underlying database. Passing +:lock => true+ to +Model.find+ obtains an exclusive lock on the selected rows. +Model.find+ using +:lock+ are usually wrapped inside a transaction for preventing deadlock conditions. +Pessimistic locking uses a locking mechanism provided by the underlying database. Passing +:lock => true+ to +Model.find+ obtains an exclusive lock on the selected rows. +Model.find+ using +:lock+ are usually wrapped inside a transaction for preventing deadlock conditions. For example: @@ -601,7 +603,7 @@ end h3. Joining Tables -<tt>Model.find</tt> provides a +:joins+ option for specifying +JOIN+ clauses on the resulting SQL. There multiple different ways to specify the +:joins+ option: +<tt>Model.find</tt> provides a +:joins+ option for specifying +JOIN+ clauses on the resulting SQL. There are multiple ways to specify the +:joins+ option: h4. Using a String SQL Fragment @@ -698,7 +700,7 @@ time_range = (Time.now.midnight - 1.day)..Time.now.midnight Client.joins(:orders).where('orders.created_at' => time_range) </ruby> -An alternative and cleaner syntax to this is to nest the hash conditions: +An alternative and cleaner syntax is to nest the hash conditions: <ruby> time_range = (Time.now.midnight - 1.day)..Time.now.midnight @@ -727,7 +729,7 @@ This code looks fine at the first sight. But the problem lies within the total n <strong>Solution to N <plus> 1 queries problem</strong> -Active Record lets you specify all the associations in advanced that are going to be loaded. This is possible by specifying the +includes+ method of the +Model.find+ call. With +includes+, Active Record ensures that all the specified associations are loaded using minimum possible number of queries. +Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the +includes+ method of the +Model.find+ call. With +includes+, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries. Revisiting the above case, we could rewrite +Client.all+ to use eager load addresses: @@ -749,7 +751,7 @@ SELECT addresses.* FROM addresses h4. Eager Loading Multiple Associations -Active Record lets you eager load any possible number of associations with a single +Model.find+ call by using an array, hash, or a nested hash of array/hash with the +includes+ method. +Active Record lets you eager load any number of associations with a single +Model.find+ call by using an array, hash, or a nested hash of array/hash with the +includes+ method. h5. Array of Multiple Associations @@ -762,10 +764,10 @@ This loads all the posts and the associated category and comments for each post. h5. Nested Associations Hash <ruby> -Category.find(1).includes(:posts => [{:comments => :guest}, :tags]) +Category.includes(:posts => [{:comments => :guest}, :tags]).find(1) </ruby> -The above code finds the category with id 1 and eager loads all the posts associated with the found category. Additionally, it will also eager load every posts' tags and comments. Every comment's guest association will get eager loaded as well. +This will find the category with id 1 and eager load all of the associated posts, the associated posts' tags and comments, and every comment's guest association. h4. Specifying Conditions on Eager Loaded Associations @@ -782,7 +784,7 @@ You can specify an exclamation point (<tt>!</tt>) on the end of the dynamic find If you want to find both by name and locked, you can chain these finders together by simply typing +and+ between the fields for example +Client.find_by_first_name_and_locked("Ryan", true)+. -There's another set of dynamic finders that let you find or create/initialize objects if they aren't found. These work in a similar fashion to the other finders and can be used like +find_or_create_by_first_name(params[:first_name])+. Using this will firstly perform a find and then create if the find returns +nil+. The SQL looks like this for +Client.find_or_create_by_first_name("Ryan")+: +There's another set of dynamic finders that let you find or create/initialize objects if they aren't found. These work in a similar fashion to the other finders and can be used like +find_or_create_by_first_name(params[:first_name])+. Using this will first perform a find and then create if the find returns +nil+. The SQL looks like this for +Client.find_or_create_by_first_name("Ryan")+: <sql> SELECT * FROM clients WHERE (clients.first_name = 'Ryan') LIMIT 1 @@ -792,7 +794,7 @@ INSERT INTO clients (first_name, updated_at, created_at, orders_count, locked) COMMIT </sql> -+find_or_create+'s sibling, +find_or_initialize+, will find an object and if it does not exist will act similar to calling +new+ with the arguments you passed in. For example: ++find_or_create+'s sibling, +find_or_initialize+, will find an object and if it does not exist will act similarly to calling +new+ with the arguments you passed in. For example: <ruby> client = Client.find_or_initialize_by_first_name('Ryan') @@ -836,7 +838,7 @@ Client.exists?(1,2,3) Client.exists?([1,2,3]) </ruby> -Further more, +exists+ takes a +conditions+ option much like find: +The +exists+ method may also take a +conditions+ option much like find: <ruby> Client.exists?(:conditions => "first_name = 'Ryan'") diff --git a/railties/guides/source/active_record_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index 84c33e34f9..37a65d211e 100644 --- a/railties/guides/source/active_record_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -1,22 +1,22 @@ h2. Active Record Validations and Callbacks -This guide teaches you how to hook into the lifecycle of your Active Record objects. You will learn how to validate the state of objects before they go into the database, and how to perform custom operations at certain points in the object lifecycle. +This guide teaches you how to hook into the life cycle of your Active Record objects. You will learn how to validate the state of objects before they go into the database, and how to perform custom operations at certain points in the object life cycle. After reading this guide and trying out the presented concepts, we hope that you'll be able to: -* Understand the lifecycle of Active Record objects +* Understand the life cycle of Active Record objects * Use the built-in Active Record validation helpers * Create your own custom validation methods * Work with the error messages generated by the validation process -* Create callback methods that respond to events in the object lifecycle +* Create callback methods that respond to events in the object life cycle * Create special classes that encapsulate common behavior for your callbacks -* Create Observers that respond to lifecycle events outside of the original class +* Create Observers that respond to life cycle events outside of the original class endprologue. -h3. The Object Lifecycle +h3. The Object Life Cycle -During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object lifecycle</em> so that you can control your application and its data. +During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object life cycle</em> so that you can control your application and its data. Validations allow you to ensure that only valid data is stored in your database. Callbacks and observers allow you to trigger logic before or after an alteration of an object's state. @@ -57,7 +57,7 @@ We can see how it works by looking at some +rails console+ output: => false </shell> -Creating and saving a new record will send an SQL +INSERT+ operation to the database. Updating an existing record will send an SQL +UPDATE+ operation instead. Validations are typically run before these commands are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not perform the +INSERT+ or +UPDATE+ operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated. +Creating and saving a new record will send a SQL +INSERT+ operation to the database. Updating an existing record will send a SQL +UPDATE+ operation instead. Validations are typically run before these commands are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not perform the +INSERT+ or +UPDATE+ operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated. CAUTION: There are many ways to change the state of an object in the database. Some methods will trigger validations, but some will not. This means that it's possible to save an object in the database in an invalid state if you aren't careful. @@ -799,7 +799,7 @@ h4. Customizing the Error Messages CSS The selectors to customize the style of error messages are: -* +.fieldWithErrors+ - Style for the form fields and labels with errors. +* +.field_with_errors+ - Style for the form fields and labels with errors. * +#errorExplanation+ - Style for the +div+ element with the error messages. * +#errorExplanation h2+ - Style for the header of the +div+ element. * +#errorExplanation p+ - Style for the paragraph that holds the message that appears right below the header of the +div+ element. @@ -811,7 +811,7 @@ The name of the class and the id can be changed with the +:class+ and +:id+ opti h4. Customizing the Error Messages HTML -By default, form fields with errors are displayed enclosed by a +div+ element with the +fieldWithErrors+ CSS class. However, it's possible to override that. +By default, form fields with errors are displayed enclosed by a +div+ element with the +field_with_errors+ CSS class. However, it's possible to override that. The way form fields with errors are treated is defined by +ActionView::Base.field_error_proc+. This is a +Proc+ that receives two parameters: @@ -838,7 +838,7 @@ This will result in something like the following: h3. Callbacks Overview -Callbacks are methods that get called at certain moments of an object's lifecycle. With callbacks it's possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database. +Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it's possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database. h4. Callback Registration @@ -984,7 +984,7 @@ h3. Halting Execution As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed. -The whole callback chain is wrapped in a transaction. If any before callback method returns exactly +false+ or raises an exception the execution chain gets halted and a ROLLBACK is issued. After callbacks can only accomplish that by raising an exception. +The whole callback chain is wrapped in a transaction. If any <em>before</em> callback method returns exactly +false+ or raises an exception the execution chain gets halted and a ROLLBACK is issued; <em>after</em> callbacks can only accomplish that by raising an exception. WARNING. Raising an arbitrary exception may break code that expects +save+ and friends not to fail like that. The +ActiveRecord::Rollback+ exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised. @@ -1020,7 +1020,7 @@ Like in validations, we can also make our callbacks conditional, calling them on h4. Using +:if+ and +:unless+ with a Symbol -You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a method that will get called right before the callback. If this method returns +false+ the callback won't be executed. This is the most common option. Using this form of registration it's also possible to register several different methods that should be called to check if the callback should be executed. +You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a method that will get called right before the callback. When using the +:if+ option, the callback won't be executed if the method returns false; when using the +:unless+ option, the callback won't be executed if the method returns true. This is the most common option. Using this form of registration it's also possible to register several different methods that should be called to check if the callback should be executed. <ruby> class Order < ActiveRecord::Base @@ -1135,7 +1135,7 @@ Observers are conventionally placed inside of your +app/models+ directory and re config.active_record.observers = :user_observer </ruby> -As usual, settings in +config/environments+ take precedence over those in +config/environment.rb+. So, if you prefer that an observer not run in all environments, you can simply register it in a specific environment instead. +As usual, settings in +config/environments+ take precedence over those in +config/environment.rb+. So, if you prefer that an observer doesn't run in all environments, you can simply register it in a specific environment instead. h4. Sharing Observers @@ -1162,6 +1162,7 @@ h3. Changelog "Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/26-active-record-validations-and-callbacks +* July 20, 2010: Fixed typos and rephrased some paragraphs for clarity. "Jaime Iniesta":http://jaimeiniesta.com * May 24, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * May 15, 2010: Validation Errors section updated by "Emili Parreño":http://www.eparreno.com * March 7, 2009: Callbacks revision by Trevor Turk diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 58824d7aeb..54f766fffd 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -157,21 +157,6 @@ WARNING. Using +duplicable?+ is discouraged because it depends on a hard-coded l NOTE: Defined in +active_support/core_ext/object/duplicable.rb+. -h4. +returning+ - -The method +returning+ yields its argument to a block and returns it. You typically use it with a mutable object that gets modified in the block: - -<ruby> -def html_options_for_form(url_for_options, options, *parameters_for_url) - returning options.stringify_keys do |html_options| - html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") - html_options["action"] = url_for(url_for_options, *parameters_for_url) - end -end -</ruby> - -NOTE: Defined in +active_support/core_ext/object/returning.rb+. - h4. +try+ Sometimes you want to call a method provided the receiver object is not +nil+, which is something you usually check first. @@ -682,107 +667,6 @@ end NOTE: Defined in +active_support/core_ext/module/attribute_accessors.rb+. -h4. Method Delegation - -The class method +delegate+ offers an easy way to forward methods. - -For example, if +User+ has some details like the age factored out to +Profile+, it could be handy to still be able to access such attributes directly, <tt>user.age</tt>, instead of having to explicit the chain <tt>user.profile.age</tt>. - -That can be accomplished by hand: - -<ruby> -class User - has_one :profile - - def age - profile.age - end -end -</ruby> - -But with +delegate+ you can make that shorter and the intention even more obvious: - -<ruby> -class User - has_one :profile - - delegate :age, to => :profile -end -</ruby> - -The macro accepts more than one method: - -<ruby> -class User - has_one :profile - - delegate :age, :avatar, :twitter_username, to => :profile -end -</ruby> - -Methods can be delegated to objects returned by methods, as in the examples above, but also to instance variables, class variables, and constants. Just pass their names as symbols or strings, including the at signs in the last cases. - -For example, +ActionView::Base+ delegates +erb_trim_mode=+: - -<ruby> -module ActionView - class Base - delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' - end -end -</ruby> - -In fact, you can delegate to any expression passed as a string. It will be evaluated in the context of the receiver. Controllers for example delegate alerts and notices to the current flash: - -<ruby> -delegate :alert, :notice, :to => "request.flash" -</ruby> - -If the target is +nil+ calling any delegated method will raise an exception even if +nil+ responds to such method. You can override this behavior setting the option +:allow_nil+ to true, in which case the forwarded call will simply return +nil+. - -If the target is a method, the name of delegated methods can also be prefixed. If the +:prefix+ option is set to (exactly) the +true+ object, the value of the +:to+ option is prefixed: - -<ruby> -class Invoice - belongs_to :customer - - # defines a method called customer_name - delegate :name, :to => :customer, :prefix => true -end -</ruby> - -And a custom prefix can be set as well, in that case it does not matter wheter the target is a method or not: - -<ruby> -class Account - belongs_to :user - - # defines a method called admin_email - delegate :email, :to => :user, :prefix => 'admin' -end -</ruby> - -NOTE: Defined in +active_support/core_ext/module/delegation.rb+. - -h4. Method Removal - -h5. +remove_possible_method+ - -The method +remove_possible_method+ is like the standard +remove_method+, except it silently returns on failure: - -<ruby> -class A; end - -A.class_eval do - remove_method(:nonexistent) # raises NameError - remove_possible_method(:nonexistent) # no problem, continue -end -</ruby> - -This may come in handy if you need to define a method that may already exist, since redefining a method issues a warning "method redefined; discarding old redefined_method_name". - -NOTE: Defined in +active_support/core_ext/module/remove_method.rb+. - h4. Parents h5. +parent+ @@ -977,11 +861,128 @@ though an anonymous module is unreachable by definition. NOTE: Defined in +active_support/core_ext/module/anonymous.rb+. +h4. Method Delegation + +The macro +delegate+ offers an easy way to forward methods. + +Let's imagine that users in some application have login information in the +User+ model but name and other data in a separate +Profile+ model: + +<ruby> +class User < ActiveRecord::Base + has_one :profile +end +</ruby> + +With that configuration you get a user's name via his profile, +user.profile.name+, but it could be handy to still be able to access such attribute directly: + +<ruby> +class User < ActiveRecord::Base + has_one :profile + + def name + profile.name + end +end +</ruby> + +That is what +delegate+ does for you: + +<ruby> +class User < ActiveRecord::Base + has_one :profile + + delegate :name, :to => :profile +end +</ruby> + +It is shorter, and the intention more obvious. + +The macro accepts several methods: + +<ruby> +delegate :name, :age, :address, :twitter, :to => :profile +</ruby> + +When interpolated into a string, the +:to+ option should become an expression that evaluates to the object the method is delegated to. Typically a string or symbol. Such a expression is evaluated in the context of the receiver: + +<ruby> +# delegates to the Rails constant +delegate :logger, :to => :Rails + +# delegates to the receiver's class +delegate :table_name, :to => 'self.class' +</ruby> + +WARNING: If the +:prefix+ option is +true+ this is less generic, see below. + +By default, if the delegation raises +NoMethodError+ and the target is +nil+ the exception is propagated. You can ask that +nil+ is returned instead with the +:allow_nil+ option: + +<ruby> +delegate :name, :to => :profile, :allow_nil => true +</ruby> + +With +:allow_nil+ the call +user.name+ returns +nil+ if the user has no profile. + +The option +:prefix+ adds a prefix to the name of the generated method. This may be handy for example to get a better name: + +<ruby> +delegate :street, :to => :address, :prefix => true +</ruby> + +The previous example generates +address_street+ rather than +street+. + +WARNING: Since in this case the name of the generated method is composed of the target object and target method names, the +:to+ option must be a method name. + +A custom prefix may also be configured: + +<ruby> +delegate :size, :to => :attachment, :prefix => :avatar +</ruby> + +In the previous example the macro generates +avatar_size+ rather than +size+. + +NOTE: Defined in +active_support/core_ext/module/delegation.rb+ + +h4. Method Names + +The builtin methods +instance_methods+ and +methods+ return method names as strings or symbols depending on the Ruby version. Active Support defines +instance_method_names+ and +method_names+ to be equivalent to them, respectively, but always getting strings back. + +For example, +ActionView::Helpers::FormBuilder+ knows this array difference is going to work no matter the Ruby version: + +<ruby> +self.field_helpers = (FormHelper.instance_method_names - ['form_for']) +</ruby> + +NOTE: Defined in +active_support/core_ext/module/method_names.rb+ + +h4. Redefining Methods + +There are cases where you need to define a method with +define_method+, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either. + +The method +redefine_method+ prevents such a potential warning, removing the existing method before if needed. Rails uses it in a few places, for instance when it generates an association's API: + +<ruby> +redefine_method("#{reflection.name}=") do |new_value| + association = association_instance_get(reflection.name) + + if association.nil? || association.target != new_value + association = association_proxy_class.new(self, reflection) + end + + association.replace(new_value) + association_instance_set(reflection.name, new_value.nil? ? nil : association) +end +</ruby> + +NOTE: Defined in +active_support/core_ext/module/remove_method.rb+ + h3. Extensions to +Class+ h4. Class Attributes -The method +Class#class_attribute+ declares one or more inheritable class attributes that can be overridden at any level down the hierarchy: +h5. +class_attribute+ + +The method +class_attribute+ declares one or more inheritable class attributes that can be overridden at any level down the hierarchy: <ruby> class A @@ -1005,17 +1006,50 @@ A.x # => :a B.x # => :b </ruby> -For example that's the way the +allow_forgery_protection+ flag is implemented for controllers: +For example +ActionMailer::Base+ defines: + +<ruby> +class_attribute :default_params +self.default_params = { + :mime_version => "1.0", + :charset => "UTF-8", + :content_type => "text/plain", + :parts_order => [ "text/plain", "text/enriched", "text/html" ] +}.freeze +</ruby> + +They can be also accessed and overridden at the instance level: <ruby> -class_attribute :allow_forgery_protection -self.allow_forgery_protection = true +A.x = 1 + +a1 = A.new +a2 = A.new +a2.x = 2 + +a1.x # => 1, comes from A +a2.x # => 2, overridden in a2 </ruby> -For convenience +class_attribute+ defines also a predicate, so that declaration also generates +allow_forgery_protection?+. Such predicate returns the double boolean negation of the value. +The generation of the writer instance method can be prevented by setting the option +:instance_writer+ to false, as in + +<ruby> +module AcitveRecord + class Base + class_attribute :table_name_prefix, :instance_writer => false + self.table_name_prefix = "" + end +end +</ruby> + +A model may find that option useful as a way to prevent mass-assignment from setting the attribute. + +For convenience +class_attribute+ defines also an instance predicate which is the double negation of what the instance reader returns. In the examples above it would be called +x?+. NOTE: Defined in +active_support/core_ext/class/attribute.rb+ +h5. +cattr_reader+, +cattr_writer+, and +cattr_accessor+ + The macros +cattr_reader+, +cattr_writer+, and +cattr_accessor+ are analogous to their +attr_*+ counterparts but for classes. They initialize a class variable to +nil+ unless it already exists, and generate the corresponding class methods to access it: <ruby> @@ -1026,17 +1060,18 @@ class MysqlAdapter < AbstractAdapter end </ruby> -Instance methods are created as well for convenience. For example given +Instance methods are created as well for convenience, they are just proxies to the class attribute. So, instances can change the class attribute, but cannot override it as it happens with +class_attribute+ (see above). For example given <ruby> -module ActionController +module ActionView class Base - cattr_accessor :logger + cattr_accessor :field_error_proc + @@field_error_proc = Proc.new{ ... } end end </ruby> -we can access +logger+ in actions. The generation of the writer instance method can be prevented setting +:instance_writer+ to +false+ (not any false value, but exactly +false+): +we can access +field_error_proc+ in views. The generation of the writer instance method can be prevented by setting +:instance_writer+ to +false+ (not any false value, but exactly +false+): <ruby> module ActiveRecord @@ -1047,7 +1082,7 @@ module ActiveRecord end </ruby> -A model may find that option useful as a way to prevent mass-assignment from setting the attribute. +A model may find that option useful as a way to prevent mass-assignment from setting the attribute. NOTE: Defined in +active_support/core_ext/class/attribute_accessors.rb+. @@ -1092,21 +1127,6 @@ Since values are copied when a subclass is defined, if the base class changes th NOTE: Defined in +active_support/core_ext/class/inheritable_attributes.rb+. -There's a related macro called +superclass_delegating_accessor+, however, that does not copy the value when the base class is subclassed. Instead, it delegates reading to the superclass as long as the attribute is not set via its own writer. For example, +ActionMailer::Base+ defines +delivery_method+ this way: - -<ruby> -module ActionMailer - class Base - superclass_delegating_accessor :delivery_method - self.delivery_method = :smtp - end -end -</ruby> - -If for whatever reason an application loads the definition of a mailer class and after that sets +ActionMailer::Base.delivery_method+, the mailer class will still see the new value. In addition, the mailer class is able to change the +delivery_method+ without affecting the value in the parent using its own inherited class attribute writer. - -NOTE: Defined in +active_support/core_ext/class/delegating_attributes.rb+. - h4. Subclasses & Descendants h5. +subclasses+ @@ -1433,13 +1453,15 @@ end That may be handy to compute method names in a language that follows that convention, for example JavaScript. +INFO: As a rule of thumb you can think of +camelize+ as the inverse of +underscore+, though there are cases where that does not hold: <tt>"SSLError".underscore.camelize</tt> gives back <tt>"SslError"</tt>. + +camelize+ is aliased to +camelcase+. NOTE: Defined in +active_support/core_ext/string/inflections.rb+. h5. +underscore+ -The method +underscore+ is the inverse of +camelize+, explained above: +The method +underscore+ goes the other way around, from camel case to paths: <ruby> "Product".underscore # => "product" @@ -1472,6 +1494,8 @@ def load_missing_constant(from_mod, const_name) end </ruby> +INFO: As a rule of thumb you can think of +underscore+ as the inverse of +camelize+, though there are cases where that does not hold. For example, <tt>"SSLError".underscore.camelize</tt> gives back <tt>"SslError"</tt>. + NOTE: Defined in +active_support/core_ext/string/inflections.rb+. h5. +titleize+ @@ -1840,9 +1864,7 @@ h3. Extensions to +Enumerable+ h4. +group_by+ -Ruby 1.8.7 and up define +group_by+, and Active Support does it for previous versions. - -This iterator takes a block and builds an ordered hash with its return values as keys. Each key is mapped to the array of elements for which the block returned that value: +Active Support redefines +group_by+ in Ruby 1.8.7 so that it returns an ordered hash as in 1.9: <ruby> entries_by_surname_initial = address_book.group_by do |entry| @@ -1850,7 +1872,7 @@ entries_by_surname_initial = address_book.group_by do |entry| end </ruby> -WARNING. Active Support redefines +group_by+ in Ruby 1.8.7 so that it still returns an ordered hash. +Distinct block return values are added to the hash as they come, so that's the resulting order. NOTE: Defined in +active_support/core_ext/enumerable.rb+. @@ -2184,7 +2206,27 @@ NOTE: Defined in +active_support/core_ext/array/conversions.rb+. h4. Wrapping -The class method +Array.wrap+ behaves like the function +Array()+ except that it does not try to call +to_a+ on its argument. That changes the behavior for enumerables: +The method +Array.wrap+ wraps its argument in an array unless it is already an array (or array-like). + +Specifically: + +* If the argument is +nil+ an empty list is returned. +* Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned. +* Otherwise, returns an array with the argument as its single element. + +<ruby> +Array.wrap(nil) # => [] +Array.wrap([1, 2, 3]) # => [1, 2, 3] +Array.wrap(0) # => [0] +</ruby> + +This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences: + +* If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> moves on to try +to_a+ if the returned value is +nil+, but <tt>Arraw.wrap</tt> returns such a +nil+ right away. +* If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. +* It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array. + +The last point is particularly worth comparing for some enumerables: <ruby> Array.wrap(:foo => :bar) # => [{:foo => :bar}] @@ -2194,6 +2236,16 @@ Array.wrap("foo\nbar") # => ["foo\nbar"] Array("foo\nbar") # => ["foo\n", "bar"], in Ruby 1.8 </ruby> +There's also a related idiom that uses the splat operator: + +<ruby> +[*object] +</ruby> + +which returns +[nil]+ for +nil+, and calls to <tt>Array(object)</tt> otherwise + +Thus, in this case the behavior is different for +nil+, and the differences with <tt>Kernel#Array</tt> explained above apply to the rest of +object+s. + NOTE: Defined in +active_support/core_ext/array/wrap.rb+. h4. Grouping @@ -2919,11 +2971,11 @@ Note in the previous example that increments may be negative. To perform the computation the method first increments years, then months, then weeks, and finally days. This order is important towards the end of months. Say for example we are at the end of February of 2010, and we want to move one month and one day forward. -The method +advance+ advances first one month, and the one day, the result is: +The method +advance+ advances first one month, and then one day, the result is: <ruby> -Date.new(2010, 2, 28).advance(:months => 1, :day => 1) -# => Sun, 28 Mar 2010 +Date.new(2010, 2, 28).advance(:months => 1, :days => 1) +# => Sun, 29 Mar 2010 </ruby> While if it did it the other way around the result would be different: @@ -2949,6 +3001,26 @@ Date.new(2010, 1, 31).change(:month => 2) # => ArgumentError: invalid date </ruby> +h5. Durations + +Durations can be added and substracted to dates: + +<ruby> +d = Date.current +# => Mon, 09 Aug 2010 +d + 1.year +# => Tue, 09 Aug 2011 +d - 3.hours +# => Sun, 08 Aug 2010 21:00:00 UTC +00:00 +</ruby> + +They translate to calls to +since+ or +advance+. For example here we get the correct jump in the calendar reform: + +<ruby> +Date.new(1582, 10, 4) + 1.day +# => Fri, 15 Oct 1582 +</ruby> + h5. Timestamps INFO: The following methods return a +Time+ object if possible, otherwise a +DateTime+. If set, they honor the user time zone. @@ -2993,30 +3065,30 @@ h4(#date-conversions). Conversions h3. Extensions to +DateTime+ -NOTE: All the following methods are defined in +active_support/core_ext/date_time/calculations.rb+. - WARNING: +DateTime+ is not aware of DST rules and so some of these methods have edge cases when a DST change is going on. For example +seconds_since_midnight+ might not return the real amount in such a day. h4(#calculations-datetime). Calculations +NOTE: All the following methods are defined in +active_support/core_ext/date_time/calculations.rb+. + The class +DateTime+ is a subclass of +Date+ so by loading +active_support/core_ext/date/calculations.rb+ you inherit these methods and their aliases, except that they will always return datetimes: <ruby> yesterday tomorrow -beginning_of_week -end_on_week +beginning_of_week (monday, at_beginning_of_week) +end_on_week (at_end_of_week) next_week months_ago months_since -beginning_of_month -end_of_month +beginning_of_month (at_beginning_of_month) +end_of_month (at_end_of_month) prev_month next_month -beginning_of_quarter -end_of_quarter -beginning_of_year -end_of_year +beginning_of_quarter (at_beginning_of_quarter) +end_of_quarter (at_end_of_quarter) +beginning_of_year (at_beginning_of_year) +end_of_year (at_end_of_year) years_ago years_since prev_year @@ -3026,10 +3098,10 @@ next_year The following methods are reimplemented so you do *not* need to load +active_support/core_ext/date/calculations.rb+ for these ones: <ruby> -beginning_of_day +beginning_of_day (midnight, at_midnight, at_beginning_of_day) end_of_day ago -since +since (in) </ruby> On the other hand, +advance+ and +change+ are also defined and support more options, they are documented below. @@ -3072,6 +3144,37 @@ now.utc? # => false now.utc.utc? # => true </ruby> +h6(#datetime-advance). +advance+ + +The most generic way to jump to another datetime is +advance+. This method receives a hash with keys +:years+, +:months+, +:weeks+, +:days+, +:hours+, +:minutes+, and +:seconds+, and returns a datetime advanced as much as the present keys indicate. + +<ruby> +d = DateTime.current +# => Thu, 05 Aug 2010 11:33:31 +0000 +d.advance(:years => 1, :months => 1, :days => 1, :hours => 1, :minutes => 1, :seconds => 1) +# => Tue, 06 Sep 2011 12:34:32 +0000 +</ruby> + +This method first computes the destination date passing +:years+, +:months+, +:weeks+, and +:days+ to +Date#advance+ documented above. After that, it adjusts the time calling +since+ with the number of seconds to advance. This order is relevant, a different ordering would give different datetimes in some edge-cases. The example in +Date#advance+ applies, and we can extend it to show order relevance related to the time bits. + +If we first move the date bits (that have also a relative order of processing, as documented before), and then the time bits we get for example the following computation: + +<ruby> +d = DateTime.new(2010, 2, 28, 23, 59, 59) +# => Sun, 28 Feb 2010 23:59:59 +0000 +d.advance(:months => 1, :seconds => 1) +# => Mon, 29 Mar 2010 00:00:00 +0000 +</ruby> + +but if we computed them the other way around, the result would be different: + +<ruby> +d.advance(:seconds => 1).advance(:months => 1) +# => Thu, 01 Apr 2010 00:00:00 +0000 +</ruby> + +WARNING: Since +DateTime+ is not DST-aware you can end up in a non-existing point in time with no warning or error telling you so. + h5(#datetime-changing-components). Changing Components The method +change+ allows you to get a new datetime which is the same as the receiver except for the given options, which may include +:year+, +:month+, +:day+, +:hour+, +:min+, +:sec+, +:offset+, +:start+: @@ -3104,15 +3207,144 @@ DateTime.current.change(:month => 2, :day => 30) # => ArgumentError: invalid date </ruby> -h4(#datetime-conversions). Conversions +h5. Durations + +Durations can be added and substracted to datetimes: + +<ruby> +now = DateTime.current +# => Mon, 09 Aug 2010 23:15:17 +0000 +now + 1.year +# => Tue, 09 Aug 2011 23:15:17 +0000 +now - 1.week +# => Mon, 02 Aug 2010 23:15:17 +0000 +</ruby> + +They translate to calls to +since+ or +advance+. For example here we get the correct jump in the calendar reform: + +<ruby> +DateTime.new(1582, 10, 4, 23) + 1.hour +# => Fri, 15 Oct 1582 00:00:00 +0000 +</ruby> h3. Extensions to +Time+ -... +h4(#time-calculations). Calculations + +NOTE: All the following methods are defined in +active_support/core_ext/time/calculations.rb+. + +Active Support adds to +Time+ many of the methods available for +DateTime+: + +<ruby> +past? +today? +future? +yesterday +tomorrow +seconds_since_midnight +change +advance +ago +since (in) +beginning_of_day (midnight, at_midnight, at_beginning_of_day) +end_of_day +beginning_of_week (monday, at_beginning_of_week) +end_on_week (at_end_of_week) +next_week +months_ago +months_since +beginning_of_month (at_beginning_of_month) +end_of_month (at_end_of_month) +prev_month +next_month +beginning_of_quarter (at_beginning_of_quarter) +end_of_quarter (at_end_of_quarter) +beginning_of_year (at_beginning_of_year) +end_of_year (at_end_of_year) +years_ago +years_since +prev_year +next_year +</ruby> + +They are analogous. Please refer to their documentation above and take into account the following differences: + +* +change+ accepts an additional +:usec+ option. +* +Time+ understands DST, so you get correct DST calculations as in + +<ruby> +Time.zone_default +# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> + +# In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST. +t = Time.local_time(2010, 3, 28, 1, 59, 59) +# => Sun Mar 28 01:59:59 +0100 2010 +t.advance(:seconds => 1) +# => Sun Mar 28 03:00:00 +0200 2010 +</ruby> + +* If +since+ or +ago+ jump to a time that can't be expressed with +Time+ a +DateTime+ object is returned instead. + +h4. Time Constructors + +Active Support defines +Time.current+ to be +Time.zone.now+ if there's a user time zone defined, with fallback to +Time.now+: + +<ruby> +Time.zone_default +# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> +Time.current +# => Fri, 06 Aug 2010 17:11:58 CEST +02:00 +</ruby> + +Analogously to +DateTime+, the predicates +past?+, and +future?+ are relative to +Time.current+. + +Use the +local_time+ class method to create time objects honoring the user time zone: + +<ruby> +Time.zone_default +# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> +Time.local_time(2010, 8, 15) +# => Sun Aug 15 00:00:00 +0200 2010 +</ruby> + +The +utc_time+ class method returns a time in UTC: + +<ruby> +Time.zone_default +# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> +Time.utc_time(2010, 8, 15) +# => Sun Aug 15 00:00:00 UTC 2010 +</ruby> + +Both +local_time+ and +utc_time+ accept up to seven positional arguments: year, month, day, hour, min, sec, usec. Year is mandatory, month and day default to 1, and the rest default to 0. + +If the time to be constructed lies beyond the range supported by +Time+ in the runtime platform, usecs are discarded and a +DateTime+ object is returned instead. + +h5. Durations + +Durations can be added and substracted to time objects: + +<ruby> +now = Time.current +# => Mon, 09 Aug 2010 23:20:05 UTC +00:00 +now + 1.year +# => Tue, 09 Aug 2011 23:21:11 UTC +00:00 +now - 1.week +# => Mon, 02 Aug 2010 23:21:11 UTC +00:00 +</ruby> + +They translate to calls to +since+ or +advance+. For example here we get the correct jump in the calendar reform: + +<ruby> +Time.utc_time(1582, 10, 3) + 5.days +# => Mon Oct 18 00:00:00 UTC 1582 +</ruby> h3. Extensions to +Process+ -... +h4. +daemon+ + +Ruby 1.9 provides +Process.daemon+, and Active Support defines it for previous versions. It accepts the same two arguments, whether it should chdir to the root directory (default, true), and whether it should inherit the standard file descriptors from the parent (default, false). h3. Extensions to +File+ @@ -3188,4 +3420,5 @@ h3. Changelog "Lighthouse ticket":https://rails.lighthouseapp.com/projects/16213/tickets/67 +* August 10, 2010: Starts to take shape, added to the index. * April 18, 2009: Initial version by "Xavier Noria":credits.html#fxn diff --git a/railties/guides/source/api_documentation_guidelines.textile b/railties/guides/source/api_documentation_guidelines.textile new file mode 100644 index 0000000000..9f201de49b --- /dev/null +++ b/railties/guides/source/api_documentation_guidelines.textile @@ -0,0 +1,187 @@ +h2. API Documentation Guidelines + +This guide documents the Ruby on Rails API documentation guidelines. + +endprologue. + +h3. RDoc + +The Rails API documentation is generated with RDoc 2.5. Please consult the "RDoc documentation":http://rdoc.rubyforge.org/RDoc.htmlFor for help with its markup. + +h3. Wording + +Write simple, declarative sentences. Brevity is a plus: get to the point. + +Write in present tense: "Returns a hash that...", rather than "Returned a hash that..." or "Will return a hash that...". + +Start comments in upper case, follow regular punctuation rules: + +<ruby> +# Declares an attribute reader backed by an internally-named instance variable. +def attr_internal_reader(*attrs) + ... +end +</ruby> + +Communicate to the reader the current way of doing things, both explicitly and implicitly. Use the recommended idioms in edge, reorder sections to emphasize favored approaches if needed, etc. The documentation should be a model for best practices and canonical, modern Rails usage. + +Documentation has to be concise but comprehensive. Explore and document edge cases. What happens if a module is anonymous? What if a collection is empty? What if an argument is nil? + +The proper names of Rails components have a space in between the words, like "Active Support". +ActiveRecord+ is a Ruby module, whereas Active Record is an ORM. Historically there has been lack of consistency regarding this, but we checked with David when docrails started. All Rails documentation consistently refer to Rails components by their proper name, and if in your next blog post or presentation you remember this tidbit and take it into account that'd be fenomenal :). + +Spell names correctly: HTML, MySQL, JavaScript, ERb. Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". + +h3. Example Code + +Choose meaningful examples that depict and cover the basics as well as interesting points or gotchas. + +Use two spaces to indent chunks of code.—that is two spaces with respect to the left margin; the examples +themselves should use "Rails code conventions":http://rails.lighthouseapp.com/projects/8994/source-style. + +Short docs do not need an explicit "Examples" label to introduce snippets, they just follow paragraphs: + +<ruby> +# Converts a collection of elements into a formatted string by calling +# <tt>to_s</tt> on all elements and joining them. +# +# Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post" +</ruby> + +On the other hand big chunks of structured documentation may have a separate "Examples" section: + +<ruby> +# ==== Examples +# +# Person.exists?(5) +# Person.exists?('5') +# Person.exists?(:name => "David") +# Person.exists?(['name LIKE ?', "%#{query}%"]) +</ruby> + +The result of expressions follow them and are introduced by "# => ", vertically aligned: + +<ruby> +# For checking if a fixnum is even or odd. +# +# 1.even? # => false +# 1.odd? # => true +# 2.even? # => true +# 2.odd? # => false +</ruby> + +If a line is too long, the comment may be placed on the next line: + +<ruby> + # label(:post, :title) + # # => <label for="post_title">Title</label> + # + # label(:post, :title, "A short title") + # # => <label for="post_title">A short title</label> + # + # label(:post, :title, "A short title", :class => "title_label") + # # => <label for="post_title" class="title_label">A short title</label> +</ruby> + +Avoid using any printing methods like +puts+ or +p+ for that purpose. + +On the other hand, regular comments do not use an arrow: + +<ruby> +# polymorphic_url(record) # same as comment_url(record) +</ruby> + +h3. Filenames + +As a rule of thumb use filenames relative to the application root: + +<plain> +config/routes.rb # YES +routes.rb # NO +RAILS_ROOT/config/routes.rb # NO +</plain> + + +h3. Fonts + +h4. Fixed-width Font + +Use fixed-width fonts for: +* constants, in particular class and module names +* method names +* literals like +nil+, +false+, +true+, +self+ +* symbols +* method parameters +* file names + +<ruby> +# Copies the instance variables of +object+ into +self+. +# +# Instance variable names in the +exclude+ array are ignored. If +object+ +# responds to <tt>protected_instance_variables</tt> the ones returned are +# also ignored. For example, Rails controllers implement that method. +# ... +def copy_instance_variables_from(object, exclude = []) + ... +end +</ruby> + +WARNING: Using a pair of ++...++ for fixed-width font only works with *words*; that is: anything matching <tt>\A\w+\z</tt>. For anything else use +<tt>...</tt>+, notably symbols, setters, inline snippets, etc: + +h4. Regular Font + +When "true" and "false" are English words rather than Ruby keywords use a regular font: + +<ruby> +# If <tt>reload_plugins?</tt> is false, add this to your plugin's <tt>init.rb</tt> +# to make it reloadable: +# +# Dependencies.load_once_paths.delete lib_path +</ruby> + +h3. Description Lists + +In lists of options, parameters, etc. use a hyphen between the item and its description (reads better than a colon because normally options are symbols): + +<ruby> +# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. +</ruby> + +The description starts in upper case and ends with a full stop—it's standard English. + +h3. Dynamically Generated Methods + +Methods created with +(module|class)_eval(STRING)+ have a comment by their side with an instance of the generated code. That comment is 2 spaces apart from the template: + +<ruby> +for severity in Severity.constants + class_eval <<-EOT, __FILE__, __LINE__ + def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) + add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) + end # end + # + def #{severity.downcase}? # def debug? + #{severity} >= @level # DEBUG >= @level + end # end + EOT +end +</ruby> + +If the resulting lines are too wide, say 200 columns or more, we put the comment above the call: + +<ruby> +# def self.find_by_login_and_activated(*args) +# options = args.extract_options! +# ... +# end +self.class_eval %{ + def self.#{method_id}(*args) + options = args.extract_options! + ... + end +} +</ruby> + +h3. Changelog + +* July 17, 2010: ported from the docrails wiki and revised by "Xavier Noria":credits.html#fxn + diff --git a/railties/guides/source/association_basics.textile b/railties/guides/source/association_basics.textile index c69f2ae8c9..b1ee4b8be4 100644 --- a/railties/guides/source/association_basics.textile +++ b/railties/guides/source/association_basics.textile @@ -1371,7 +1371,41 @@ The +:through+ option specifies a join model through which to perform the query. h6(#has_many-uniq). +:uniq+ -Specify the +:uniq => true+ option to remove duplicates from the collection. This is most useful in conjunction with the +:through+ option. +Set the +:uniq+ option to true to keep the collection free of duplicates. This is mostly useful together with the +:through+ option. + +<ruby> +class Person < ActiveRecord::Base + has_many :readings + has_many :posts, :through => :readings +end + +person = Person.create(:name => 'john') +post = Post.create(:name => 'a1') +person.posts << post +person.posts << post +person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">] +Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>] +</ruby> + +In the above case there are two readings and +person.posts+ brings out both of them even though these records are pointing to the same post. + +Now let's set +:uniq+ to true: + +<ruby> +class Person + has_many :readings + has_many :posts, :through => :readings, :uniq => true +end + +person = Person.create(:name => 'honda') +post = Post.create(:name => 'a1') +person.posts << post +person.posts << post +person.posts.inspect # => [#<Post id: 7, name: "a1">] +Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>] +</ruby> + +In the above case there are still two readings. However +person.posts+ shows only one post because the collection loads only unique records. h6(#has_many-validate). +:validate+ diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile index 983c04c3d9..fb625f7a44 100644 --- a/railties/guides/source/command_line.textile +++ b/railties/guides/source/command_line.textile @@ -23,18 +23,18 @@ There are a few commands that are absolutely critical to your everyday usage of * <tt>rake</tt> * <tt>rails generate</tt> * <tt>rails dbconsole</tt> -* <tt>rails app_name</tt> +* <tt>rails new app_name</tt> Let's create a simple Rails application to step through each of these commands in context. -h4. +rails+ +h4. +rails new+ -The first thing we'll want to do is create a new Rails application by running the +rails+ command after installing Rails. +The first thing we'll want to do is create a new Rails application by running the +rails new+ command after installing Rails. WARNING: You know you need the rails gem installed by typing +gem install rails+ first, if you don't have this installed, follow the instructions in the "Rails 3 Release Notes":/3_0_release_notes.html <shell> -$ rails commandsapp +$ rails new commandsapp create create README create .gitignore @@ -352,7 +352,7 @@ $ mkdir gitapp $ cd gitapp $ git init Initialized empty Git repository in .git/ -$ rails . --git --database=postgresql +$ rails new . --git --database=postgresql exists create app/controllers create app/helpers @@ -397,7 +397,7 @@ development: ... </shell> -It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the +rails+ command to generate the basis of your app. +It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the +rails new+ command to generate the basis of your app. h4. +server+ with Different Backends diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 86655746e4..9e0c7cd060 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -181,7 +181,7 @@ There are only a few configuration options for Action View, starting with four o * +config.action_view.warn_cache_misses+ tells Rails to display a warning whenever an action results in a cache miss on your view paths. The default is +false+. -* +config.action_view.field_error_proc+ provides an HTML generator for displaying errors that come from Active Record. The default is <tt>Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" }</tt> +* +config.action_view.field_error_proc+ provides an HTML generator for displaying errors that come from Active Record. The default is <tt>Proc.new{ |html_tag, instance| %Q(%<div class="field_with_errors">#{html_tag}</div>).html_safe }</tt> * +config.action_view.default_form_builder+ tells Rails which form builder to use by default. The default is +ActionView::Helpers::FormBuilder+. diff --git a/railties/guides/source/contributing_to_rails.textile b/railties/guides/source/contributing_to_rails.textile index 5590895508..fb81bab98d 100644 --- a/railties/guides/source/contributing_to_rails.textile +++ b/railties/guides/source/contributing_to_rails.textile @@ -48,7 +48,7 @@ h4. Install git Rails uses git for source code control. You won’t be able to do anything without the Rails source code, and this is a prerequisite. The "git homepage":http://git-scm.com/ has installation instructions. If you’re on OS X, use the "Git for OS X":http://code.google.com/p/git-osx-installer/ installer. If you're unfamiliar with git, there are a variety of resources on the net that will help you learn more: -* "Everyday Git":http://www.kernel.org/pub/software/scm/git/docs/everyday.html will teach you just enough about git to get by. +* "Everyday Git":http://www.kernel.org/pub/software/scm/git/docs/everyday.html will teach you just enough about git to get by. * The "PeepCode screencast":https://peepcode.com/products/git on git ($9) is easier to follow. * "GitHub":http://github.com/guides/home offers links to a variety of git resources. * "Pro Git":http://progit.org/book/ is an entire book about git with a Creative Commons license. @@ -58,30 +58,45 @@ h4. Get the Rails Source Code Don’t fork the main Rails repository. Instead, you want to clone it to your own computer. Navigate to the folder where you want the source code (it will create its own /rails subdirectory) and run: <shell> -git clone git://github.com/rails/rails.git +git clone git://github.com/rails/rails.git cd rails </shell> -h4. Pick a Branch +h4. Set up and Run the Tests + +All of the Rails tests must pass with any code you submit, otherwise you have no chance of getting code accepted. This means you need to be able to run the tests. First, you need to install all Rails dependencies with bundler: -Currently, there is active work being done on both the 2-3-stable branch of Rails and on the master branch (which will become Rails 3.0). If you want to work with the master branch, you're all set. To work with 2.3, you'll need to set up and switch to your own local tracking branch: +NOTE: Ensure you install bundler v1.0 <shell> -git branch --track 2-3-stable origin/2-3-stable -git checkout 2-3-stable +gem install -v=1.0.0.rc.2 bundler +bundle install --without db </shell> -TIP: You may want to "put your git branch name in your shell prompt":http://github.com/guides/put-your-git-branch-name-in-your-shell-prompt to make it easier to remember which version of the code you're working with. +The second command will install all dependencies, except MySQL and PostgreSQL. We will come back at these soon. With dependencies installed, you can run the whole Rails test suite with: -h4. Set up and Run the Tests +<shell> +rake test +</shell> -All of the Rails tests must pass with any code you submit, otherwise you have no chance of getting code accepted. This means you need to be able to run the tests. Rails needs the +mocha+ gem for running some tests, so install it with: +You can also run tests for an specific framework, like Action Pack, by going into its directory and executing the same command: <shell> -gem install mocha +cd actionpack +rake test </shell> -For the tests that touch the database, this means creating test databases. If you're using MySQL, create a user named +rails+ with privileges on the test databases. +h4. Testing Active Record + +By default, when you run Active Record tests, it will execute the test suite three times, one for each of the main databases: SQLite3, MySQL and PostgreSQL. If you are adding a feature that is not specific to the database, you can run the test suite (or just one file) for just one of them. Here is an example for SQLite3: + +<shell> +cd activerecord +rake test_sqlite3 +rake test_sqlite3 TEST=test/cases/validations_test.rb +</shell> + +If you want to use another database, as MySQL, you need to create a user named +rails+ with privileges on the test databases. <shell> mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.* @@ -90,7 +105,13 @@ mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; </shell> -Enter this from the +activerecord+ directory to create the test databases: +Then ensure you run bundle install without the +--without db+ option: + +<shell> +bundle install +</shell> + +Finally, enter this from the +activerecord+ directory to create the test databases: <shell> rake mysql:build_databases @@ -100,18 +121,26 @@ NOTE: Using the rake task to create the test databases ensures they have the cor If you’re using another database, check the files under +activerecord/test/connections+ in the Rails source code for default connection information. You can edit these files if you _must_ on your machine to provide different credentials, but obviously you should not push any such changes back to Rails. -Now if you go back to the root of the Rails source on your machine and run +rake+ with no parameters, you should see every test in all of the Rails components pass. If you want to run the all ActiveRecord tests (or just a single one) with another database adapter, enter this from the +activerecord+ directory: +You can now run tests as you did for +sqlite3+: <shell> -rake test_sqlite3 -rake test_sqlite3 TEST=test/cases/validations_test.rb +rake test_mysql </shell> -You can replace +sqlite3+ with +jdbcmysql+, +jdbcsqlite3+, +jdbcpostgresql+, +mysql+ or +postgresql+. Check out the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/ci_build.rb+ to see the test suite that the Rails continuous integration server runs. +You can also +myqsl+ with +postgresql+, +jdbcmysql+, +jdbcsqlite3+ or +jdbcpostgresql+. Check out the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/ci_build.rb+ to see the test suite that the Rails continuous integration server runs. +NOTE: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite 3. Subtle differences between the various Active Record database adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. +h4. Older versions of Rails -NOTE: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite 3. Subtle differences between the various Active Record database adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. +If you want to work add a fix to older versions of Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to Rails 2.3 branch: + +<shell> +git branch --track 2-3-stable origin/2-3-stable +git checkout 2-3-stable +</shell> + +TIP: You may want to "put your git branch name in your shell prompt":http://github.com/guides/put-your-git-branch-name-in-your-shell-prompt to make it easier to remember which version of the code you're working with. h3. Helping to Resolve Existing Issues @@ -231,15 +260,15 @@ h4. Update Rails Update your copy of Rails. It’s pretty likely that other changes to core Rails have happened while you were working. Go get them: <shell> -git checkout master -git pull +git checkout master +git pull </shell> Now reapply your patch on top of the latest changes: <shell> -git checkout my_new_branch -git rebase master +git checkout my_new_branch +git rebase master </shell> No conflicts? Tests still pass? Change still seems reasonable to you? Then move on. @@ -249,8 +278,8 @@ h4. Create a Patch Now you can create a patch file to share with other developers (and with the Rails core team). Still in your branch, run <shell> -git commit -a -git format-patch master --stdout > my_new_patch.diff +git commit -a +git format-patch master --stdout > my_new_patch.diff </shell> Sanity check the results of this operation: open the diff file in your text editor of choice and make sure that no unintended changes crept in. @@ -275,4 +304,5 @@ h3. Changelog * April 6, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com * August 1, 2009: Updates/amplifications by "Mike Gunderloy":credits.html#mgunderloy -* March 2, 2009: Initial draft by "Mike Gunderloy":credits.html#mgunderloy
\ No newline at end of file +* March 2, 2009: Initial draft by "Mike Gunderloy":credits.html#mgunderloy + diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 1f1b7d076e..146b75da3f 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -647,7 +647,7 @@ the +params+ hash will contain {'person' => {'name' => 'Henry'}} </erb> -and +params["name"]+ will retrieve the submitted value in the controller. +and +params[:person][:name]+ will retrieve the submitted value in the controller. Hashes can be nested as many levels as required, for example diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index f547f29087..ffb0310816 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -213,9 +213,9 @@ If you open this file in a new Rails application, you'll see a default database * The +test+ environment is used to run automated tests * The +production+ environment is used when you deploy your application for the world to use. -h5. Configuring a SQLite3 Database +h5. Configuring an SQLite3 Database -Rails comes with built-in support for "SQLite3":http://www.sqlite.org, which is a lightweight serverless database application. While a busy production environment may overload SQLite, it works well for development and testing. Rails defaults to using a SQLite database when creating a new project, but you can always change it later. +Rails comes with built-in support for "SQLite3":http://www.sqlite.org, which is a lightweight serverless database application. While a busy production environment may overload SQLite, it works well for development and testing. Rails defaults to using an SQLite database when creating a new project, but you can always change it later. Here's the section of the default configuration file (<tt>config/database.yml</tt>) with connection information for the development environment: @@ -322,16 +322,15 @@ $ rm public/index.html We need to do this as Rails will deliver any static file in the +public+ directory in preference to any dynamic contact we generate from the controllers. -Now, you have to tell Rails where your actual home page is located. Open the file +config/routes.rb+ in your editor. This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. There are only comments in this file, so we need to add at the top the following: +Now, you have to tell Rails where your actual home page is located. Open the file +config/routes.rb+ in your editor. This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with +:root to+, uncomment it and change it like the following: <ruby> -Blog::Application.routes.draw do |map| +Blog::Application.routes.draw do - root :to => "home#index" - - # The priority is based upon order of creation: - # first created -> highest priority. #... + # You can have the root of your site routed with "root" + # just remember to delete public/index.html. + root :to => "home#index" </ruby> The +root :to => "home#index"+ tells Rails to map the root action to the home controller's index action. @@ -475,7 +474,7 @@ $ rails console After the console loads, you can use it to work with your application's models: <shell> ->> p = Post.create(:content => "A new post") +>> p = Post.new(:content => "A new post") => #<Post id: nil, name: nil, title: nil, content: "A new post", created_at: nil, updated_at: nil> @@ -1194,7 +1193,7 @@ The +destroy+ action will find the post we are looking at, locate the comment wi h4. Deleting Associated Objects -If you delete a post then it's associated comments will also need to be deleted. Otherwise they would simply occupy space in the database. Rails allows you to use the +dependent+ option of an association to achieve this. Modify the Post model, +app/models/post.rb+, as follows: +If you delete a post then its associated comments will also need to be deleted. Otherwise they would simply occupy space in the database. Rails allows you to use the +dependent+ option of an association to achieve this. Modify the Post model, +app/models/post.rb+, as follows: <ruby> class Post < ActiveRecord::Base @@ -1486,6 +1485,7 @@ h3. Changelog "Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/2 +* July 12, 2010: Fixes, editing and updating of code samples by "Jaime Iniesta":http://jaimeiniesta.com * May 16, 2010: Added a section on configuration gotchas to address common encoding problems that people might have by "Yehuda Katz":http://www.yehudakatz.com * April 30, 2010: Fixes, editing and updating of code samples by "Rohit Arondekar":http://rohitarondekar.com * April 25, 2010: Couple of more minor fixups "Mikel Lindsaar":credits.html#raasdnil diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile index b09bb470ee..63d22db485 100644 --- a/railties/guides/source/i18n.textile +++ b/railties/guides/source/i18n.textile @@ -287,7 +287,7 @@ You most probably have something like this in one of your applications: <ruby> # config/routes.rb -Yourapp::Application.routes.draw do |map| +Yourapp::Application.routes.draw do root :to => "home#index" end diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index a930db0f1d..a0db87c188 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -88,6 +88,10 @@ Ruby on Rails Guides <dl> +<%= guide("Active Support Core Extensions", 'active_support_core_extensions.html') do %> + <p>This guide documents the Ruby core extensions defined in Active Support.</p> +<% end %> + <%= guide("Rails Internationalization API", 'i18n.html') do %> <p>This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on.</p> <% end %> @@ -123,10 +127,6 @@ Ruby on Rails Guides <%= guide("Caching with Rails", 'caching_with_rails.html', :ticket => 10) do %> <p>Various caching techniques provided by Rails.</p> <% end %> - -<%= guide("Contributing to Rails", 'contributing_to_rails.html') do %> - <p>Rails is not "somebody else's framework." This guide covers a variety of ways that you can get involved in the ongoing development of Rails.</p> -<% end %> </dl> <h3>Extending Rails</h3> @@ -147,6 +147,18 @@ Ruby on Rails Guides <% end %> </dl> +<h3>Contributing to Rails</h3> + +<dl> + <%= guide("Contributing to Rails", 'contributing_to_rails.html') do %> + <p>Rails is not "somebody else's framework." This guide covers a variety of ways that you can get involved in the ongoing development of Rails.</p> + <% end %> + + <%= guide('API Documentation Guidelines', 'api_documentation_guidelines.html') do %> + <p>This guide documents the Ruby on Rails API documentation guidelines.</p> + <% end %> +</dl> + <h3>Release Notes</h3> <dl> diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 7a44ef7c77..f80c00b280 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -118,7 +118,7 @@ Now with Rails 3 we have a Gemfile which defines the basics our application need # Bundle the extra gems: # gem 'bj' - # gem 'nokogiri', '1.4.1' + # gem 'nokogiri' # gem 'sqlite3-ruby', :require => 'sqlite3' # gem 'aws-s3', :require => 'aws/s3' @@ -141,20 +141,21 @@ Here the only two gems we need are +rails+ and +sqlite3-ruby+, so it seems. This * activesupport-3.0.0.beta4.gem * arel-0.4.0.gem * builder-2.1.2.gem -* bundler-1.0.0.beta.2.gem +* bundler-1.0.0.rc.2.gem * erubis-2.6.6.gem * i18n-0.4.1.gem -* mail-2.2.4.gem -* memcache-client-1.8.3.gem +* mail-2.2.5.gem +* memcache-client-1.8.5.gem * mime-types-1.16.gem +* nokogiri-1.4.3.1.gem * polyglot-0.3.1.gem -* rack-1.1.0.gem -* rack-mount-0.6.5.gem +* rack-1.2.1.gem +* rack-mount-0.6.9.gem * rack-test-0.5.4.gem * rails-3.0.0.beta4.gem * railties-3.0.0.beta4.gem * rake-0.8.7.gem -* sqlite3-ruby-1.3.0.gem +* sqlite3-ruby-1.3.1.gem * text-format-1.0.0.gem * text-hyphen-1.0.0.gem * thor-0.13.7.gem @@ -257,28 +258,23 @@ This file goes on to define some classes that will be automatically loaded using h4. Lazy Hooks -At the top if the +ActiveSupport::Autoload+ module is the +def self.extended+ method: - -<ruby> - def self.extended(base) - base.extend(LazyLoadHooks) - end -</ruby> - -This is called when we extend this module into one of our classes or modules, such is the case later on when we call +extend ActiveSupport::LazyLoadHooks+ not only in the +ActiveSupport+ module, but in all of the Railtie modules (+ActiveRecord+ and so on), as well as in a couple of places. - +ActiveSupport::LazyLoadHooks+ is responsible for defining methods used for running hooks that are defined during the initialization process, such as the one defined inside the +active_record.initialize_timezone+ initializer: <ruby> initializer "active_record.initialize_timezone" do - ActiveRecord.base_hook do + ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true self.default_timezone = :utc end end </ruby> -When the initializer is ran it defines a +base_hook+ for +ActiveRecord+ and will only run it when +run_base_hooks+ is called, which in the case of Active Record, is ran after the entirety of +activerecord/lib/active_record/base.rb+ has been evaluated. +When the initializer runs it invokes method +on_load+ for +ActiveRecord+ and the block passed to it would be called only when +run_load_hooks+ is called. +When the entirety of +activerecord/lib/active_record/base.rb+ has been evaluated then +run_load_hooks+ is invoked. The very last line of +activerecord/lib/active_record/base.rb+ is: + +<ruby> +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) +</ruby> h4. +require 'active_support'+ cont'd. diff --git a/railties/guides/source/layout.html.erb b/railties/guides/source/layout.html.erb index 501d8fef6d..cc7d54c256 100644 --- a/railties/guides/source/layout.html.erb +++ b/railties/guides/source/layout.html.erb @@ -62,6 +62,7 @@ </dl> <dl class="R"> <dt>Digging Deeper</dt> + <dd><a href="active_support_core_extensions.html">Active Support Core Extensions</a></dd> <dd><a href="i18n.html">Rails Internationalization API</a></dd> <dd><a href="action_mailer_basics.html">Action Mailer Basics</a></dd> <dd><a href="testing.html">Testing Rails Applications</a></dd> @@ -71,13 +72,16 @@ <dd><a href="configuring.html">Configuring Rails Applications</a></dd> <dd><a href="command_line.html">Rails Command Line Tools and Rake Tasks</a></dd> <dd><a href="caching_with_rails.html">Caching with Rails</a></dd> - <dd><a href="contributing_to_rails.html">Contributing to Rails</a></dd> <dt>Extending Rails</dt> <dd><a href="plugins.html">The Basics of Creating Rails Plugins</a></dd> <dd><a href="rails_on_rack.html">Rails on Rack</a></dd> <dd><a href="generators.html">Adding a Generator to Your Plugin</a></dd> + <dt>Contributing to Rails</dt> + <dd><a href="contributing_to_rails.html">Contributing to Rails</a></dd> + <dd><a href="api_documentation_guidelines.html">API Documentation Guidelines</a></dd> + <dt>Release Notes</dt> <dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 Release Notes</a></dd> <dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 Release Notes</a></dd> diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index f4ba6dd53b..b9a201e5f0 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -1168,7 +1168,7 @@ Suppose you have the following +ApplicationController+ layout: <body> <div id="top_menu">Top menu items here</div> <div id="menu">Menu items here</div> - <div id="content"><%= yield(:content) or yield %></div> + <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div> </body> </html> </erb> diff --git a/railties/guides/source/migrations.textile b/railties/guides/source/migrations.textile index f05c0589b5..16f616a5bc 100644 --- a/railties/guides/source/migrations.textile +++ b/railties/guides/source/migrations.textile @@ -1,6 +1,6 @@ h2. Migrations -Migrations are a convenient way for you to alter your database in a structured and organized manner. You could edit fragments of SQL by hand but you would then be responsible for telling other developers that they need to go and run it. You'd also have to keep track of which changes need to be run against the production machines next time you deploy. +Migrations are a convenient way for you to alter your database in a structured and organized manner. You could edit fragments of SQL by hand but you would then be responsible for telling other developers that they need to go and run them. You'd also have to keep track of which changes need to be run against the production machines next time you deploy. Active Record tracks which migrations have already been run so all you have to do is update your source and run +rake db:migrate+. Active Record will work out which migrations should be run. It will also update your +db/schema.rb+ file to match the structure of your database. @@ -234,7 +234,7 @@ end which creates a +products+ table with a column called +name+ (and as discussed below, an implicit +id+ column). -The object yielded to the block allows you create columns on the table. There are two ways of doing this: The first (traditional) form looks like +The object yielded to the block allows you to create columns on the table. There are two ways of doing this: The first (traditional) form looks like <ruby> create_table :products do |t| @@ -250,7 +250,7 @@ create_table :products do |t| end </ruby> -By default +create_table+ will create a primary key called +id+. You can change the name of the primary key with the +:primary_key+ option (don't forget to update the corresponding model) or if you don't want a primary key at all (for example for a HABTM join table) you can pass +:id => false+. If you need to pass database specific options you can place an SQL fragment in the +:options+ option. For example +By default +create_table+ will create a primary key called +id+. You can change the name of the primary key with the +:primary_key+ option (don't forget to update the corresponding model) or if you don't want a primary key at all (for example for a HABTM join table) you can pass +:id => false+. If you need to pass database specific options you can place a SQL fragment in the +:options+ option. For example <ruby> create_table :products, :options => "ENGINE=BLACKHOLE" do |t| @@ -592,4 +592,5 @@ h3. Changelog "Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/6 +* July 15, 2010: minor typos corrected by "Jaime Iniesta":http://jaimeiniesta.com * September 14, 2008: initial version by "Frederick Cheung":credits.html#fcheung diff --git a/railties/guides/source/plugins.textile b/railties/guides/source/plugins.textile index e853ba79e9..82f2276153 100644 --- a/railties/guides/source/plugins.textile +++ b/railties/guides/source/plugins.textile @@ -1274,7 +1274,7 @@ class YaffleMigrationGenerator < Rails::Generator::NamedBase end def yaffle_local_assigns - returning(assigns = {}) do + {}.tap do |assigns| assigns[:migration_action] = "add" assigns[:class_name] = "add_yaffle_fields_to_#{custom_file_name}" assigns[:table_name] = custom_file_name diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 72a76e25bb..625941ba31 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -3,16 +3,16 @@ h2. Rails Routing from the Outside In This guide covers the user-facing features of Rails routing. By referring to this guide, you will be able to: * Understand the code in +routes.rb+ -* Construct your own routes, using either the preferred resourceful style or with the @match@ method +* Construct your own routes, using either the preferred resourceful style or with the <tt>match</tt> method * Identify what parameters to expect an action to receive -* Automatically create URLs using route helpers +* Automatically create paths and URLs using route helpers * Use advanced techniques such as constraints and Rack endpoints endprologue. h3. The Purpose of the Rails Router -The Rails router recognizes URLs and dispatches them to a controller's action. It can also generate URLs, avoiding the need to hardcode URL strings in your views. +The Rails router recognizes URLs and dispatches them to a controller's action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. h4. Connecting URLs to Code @@ -30,9 +30,9 @@ match "/patients/:id" => "patients#show" the request is dispatched to the +patients+ controller's +show+ action with <tt>{ :id => "17" }</tt> in +params+. -h4. Generating URLs from Code +h4. Generating Paths and URLs from Code -You can also generate URLs. If your application contains this code: +You can also generate paths and URLs. If your application contains this code: <ruby> @patient = Patient.find(17) @@ -76,7 +76,7 @@ resources :photos creates seven different routes in your application, all mapping to the +Photos+ controller: -|_. Verb |_.URL |_.action |_.used for| +|_. Verb |_.Path |_.action |_.used for| |GET |/photos |index |display a list of all photos| |GET |/photos/new |new |return an HTML form for creating a new photo| |POST |/photos |create |create a new photo| @@ -85,7 +85,7 @@ creates seven different routes in your application, all mapping to the +Photos+ |PUT |/photos/:id |update |update a specific photo| |DELETE |/photos/:id |destroy |delete a specific photo| -h4. URLs and Paths +h4. Paths and URLs Creating a resourceful route will also expose a number of helpers to the controllers in your application. In the case of +resources :photos+: @@ -130,7 +130,7 @@ resource :geocoder creates six different routes in your application, all mapping to the +Geocoders+ controller: -|_. Verb |_.URL |_.action |_.used for| +|_. Verb |_.Path |_.action |_.used for| |GET |/geocoder/new |new |return an HTML form for creating the geocoder| |POST |/geocoder |create |create the new geocoder| |GET |/geocoder |show |display the one and only geocoder resource| @@ -160,7 +160,7 @@ end This will create a number of routes for each of the +posts+ and +comments+ controller. For +Admin::PostsController+, Rails will create: -|_. Verb |_.URL |_.action |_. helper | +|_. Verb |_.Path |_.action |_. helper | |GET |/admin/photos |index | admin_photos_path | |GET |/admin/photos/new |new | new_admin_photos_path | |POST |/admin/photos |create | admin_photos_path | @@ -197,16 +197,16 @@ or, for a single case resources :posts, :path => "/admin" </ruby> -In each of these cases, the named routes remain the same as if you did not use +scope+. In the last case, the following URLs map to +PostsController+: +In each of these cases, the named routes remain the same as if you did not use +scope+. In the last case, the following paths map to +PostsController+: -|_. Verb |_.URL |_.action |_. helper | -|GET |photos |index | photos_path | -|GET |photos/new |new | photos_path | -|POST |photos |create | photos_path | -|GET |photos/1 |show | photo_path(id) | -|GET |photos/1/edit |edit | edit_photo_path(id) | -|PUT |photos/1 |update | photo_path(id) | -|DELETE |photos/1 |destroy | photo_path(id) | +|_. Verb |_.Path |_.action |_. helper | +|GET |/admin/photos |index | photos_path | +|GET |/admin/photos/new |new | photos_path | +|POST |/admin/photos |create | photos_path | +|GET |/admin/photos/1 |show | photo_path(id) | +|GET |/admin/photos/1/edit |edit | edit_photo_path(id) | +|PUT |/admin/photos/1 |update | photo_path(id) | +|DELETE |/admin/photos/1 |destroy | photo_path(id) | h4. Nested Resources @@ -232,7 +232,7 @@ end In addition to the routes for magazines, this declaration will also route ads to an +AdsController+. The ad URLs require a magazine: -|_.Verb |_.URL |_.action |_.used for| +|_.Verb |_.Path |_.action |_.used for| |GET |/magazines/1/ads |index |display a list of all ads for a specific magazine| |GET |/magazines/1/ads/new |new |return an HTML form for creating a new ad belonging to a specific magazine| |POST |/magazines/1/ads |create |create a new ad belonging to a specific magazine| @@ -256,7 +256,7 @@ resources :publishers do end </ruby> -Deeply-nested resources quickly become cumbersome. In this case, for example, the application would recognize URLs such as +Deeply-nested resources quickly become cumbersome. In this case, for example, the application would recognize paths such as <pre> /publishers/1/magazines/2/photos/3 @@ -266,9 +266,9 @@ The corresponding route helper would be +publisher_magazine_photo_url+, requirin TIP: _Resources should never be nested more than 1 level deep._ -h4. Creating URLs From Objects +h4. Creating Paths and URLs From Objects -In addition to using the routing helpers, Rails can also create URLs from an array of parameters. For example, suppose you have this set of routes: +In addition to using the routing helpers, Rails can also create paths and URLs from an array of parameters. For example, suppose you have this set of routes: <ruby> resources :magazines do @@ -340,7 +340,7 @@ resources :photos do end </ruby> -This will enable Rails to recognize URLs such as +/photos/search+ with GET, and route to the +search+ action of +PhotosController+. It will also create the +search_photos_url+ and +search_photos_path+ route helpers. +This will enable Rails to recognize paths such as +/photos/search+ with GET, and route to the +search+ action of +PhotosController+. It will also create the +search_photos_url+ and +search_photos_path+ route helpers. Just as with member routes, you can pass +:on+ to a route: @@ -380,7 +380,7 @@ You can set up as many dynamic segments within a regular route as you like. Anyt match ':controller/:action/:id/:user_id' </ruby> -An incoming URL of +/photos/show/1/2+ will be dispatched to the +show+ action of the +PhotosController+. +params[:id]+ will be +"1"+, and +params[:user_id]+ will be +"2"+. +An incoming path of +/photos/show/1/2+ will be dispatched to the +show+ action of the +PhotosController+. +params[:id]+ will be +"1"+, and +params[:user_id]+ will be +"2"+. NOTE: You can't use +namespace+ or +:module+ with a +:controller+ path segment. If you need to do this then use a constraint on :controller that matches the namespace you require. e.g: @@ -396,7 +396,7 @@ You can specify static segments when creating a route: match ':controller/:action/:id/with_user/:user_id' </ruby> -This route would respond to URLs such as +/photos/show/1/with_user/2+. In this case, +params+ would be <tt>{ :controller => "photos", :action => "show", :id => "1", :user_id => "2" }</tt>. +This route would respond to paths such as +/photos/show/1/with_user/2+. In this case, +params+ would be <tt>{ :controller => "photos", :action => "show", :id => "1", :user_id => "2" }</tt>. h4. The Query String @@ -406,7 +406,7 @@ The +params+ will also include any parameters from the query string. For example match ':controller/:action/:id' </ruby> -An incoming URL of +/photos/show/1?user_id=2+ will be dispatched to the +show+ action of the +Photos+ controller. +params+ will be <tt>{ :controller => "photos", :action => "show", :id => "1", :user_id => "2" }</tt>. +An incoming path of +/photos/show/1?user_id=2+ will be dispatched to the +show+ action of the +Photos+ controller. +params+ will be <tt>{ :controller => "photos", :action => "show", :id => "1", :user_id => "2" }</tt>. h4. Defining Defaults @@ -416,7 +416,7 @@ You do not need to explicitly use the +:controller+ and +:action+ symbols within match 'photos/:id' => 'photos#show' </ruby> -With this route, Rails will match an incoming URL of +/photos/12+ to the +show+ action of +PhotosController+. +With this route, Rails will match an incoming path of +/photos/12+ to the +show+ action of +PhotosController+. You can also define other defaults in a route by supplying a hash for the +:defaults+ option. This even applies to parameters that you do not specify as dynamic segments. For example: @@ -441,13 +441,13 @@ h4. Segment Constraints You can use the +:constraints+ option to enforce a format for a dynamic segment: <ruby> -match 'photo/:id' => 'photos#show', :constraints => { :id => /[A-Z]\d{5}/ } +match 'photos/:id' => 'photos#show', :constraints => { :id => /[A-Z]\d{5}/ } </ruby> -This route would match URLs such as +/photo/A12345+. You can more succinctly express the same route this way: +This route would match paths such as +/photos/A12345+. You can more succinctly express the same route this way: <ruby> -match 'photo/:id' => 'photos#show', :id => /[A-Z]\d{5}/ +match 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ </ruby> +:constraints+ takes regular expression. However note that regexp anchors can't be used within constraints. For example following route will not work: @@ -472,7 +472,7 @@ You can also constrain a route based on any method on the <a href="action_contro You specify a request-based constraint the same way that you specify a segment constraint: <ruby> -match "photo", :constraints => {:subdomain => "admin"} +match "photos", :constraints => {:subdomain => "admin"} </ruby> You can also specify constrains in a block form: @@ -511,10 +511,10 @@ h4. Route Globbing Route globbing is a way to specify that a particular parameter should be matched to all the remaining parts of a route. For example <ruby> -match 'photo/*other' => 'photos#unknown' +match 'photos/*other' => 'photos#unknown' </ruby> -This route would match +photo/12+ or +/photo/long/path/to/12+, setting +params[:other]+ to +"12"+ or +"long/path/to/12"+. +This route would match +photos/12+ or +/photos/long/path/to/12+, setting +params[:other]+ to +"12"+ or +"long/path/to/12"+. h4. Redirection @@ -573,9 +573,9 @@ The +:controller+ option lets you explicitly specify a controller to use for the resources :photos, :controller => "images" </ruby> -will recognize incoming URLs beginning with +/photo+ but route to the +Images+ controller: +will recognize incoming paths beginning with +/photo+ but route to the +Images+ controller: -|_. Verb |_.URL |_.action | +|_. Verb |_.Path |_.action | |GET |/photos |index | |GET |/photos/new |new | |POST |/photos |create | @@ -584,7 +584,7 @@ will recognize incoming URLs beginning with +/photo+ but route to the +Images+ c |PUT |/photos/1 |update | |DELETE |/photos/1 |destroy | -NOTE: Use +photos_path+, +new_photos_path+, etc. to generate URLs for this resource. +NOTE: Use +photos_path+, +new_photos_path+, etc. to generate paths for this resource. h4. Specifying Constraints @@ -615,9 +615,9 @@ The +:as+ option lets you override the normal naming for the named route helpers resources :photos, :as => "images" </ruby> -will recognize incoming URLs beginning with +/photos+ and route the requests to +PhotosController+: +will recognize incoming paths beginning with +/photos+ and route the requests to +PhotosController+: -|_.HTTP verb|_.URL |_.action |_.named helper | +|_.HTTP verb|_.Path |_.action |_.named helper | |GET |/photos |index | images_path | |GET |/photos/new |new | new_image_path | |POST |/photos |create | images_path | @@ -628,20 +628,20 @@ will recognize incoming URLs beginning with +/photos+ and route the requests to h4. Overriding the +new+ and +edit+ Segments -The +:path_names+ option lets you override the automatically-generated "new" and "edit" segments in URLs: +The +:path_names+ option lets you override the automatically-generated "new" and "edit" segments in paths: <ruby> resources :photos, :path_names => { :new => 'make', :edit => 'change' } </ruby> -This would cause the routing to recognize URLs such as +This would cause the routing to recognize paths such as <plain> /photos/make /photos/1/change </plain> -NOTE: The actual action names aren't changed by this option. The two URLs shown would still route to the new and edit actions. +NOTE: The actual action names aren't changed by this option. The two paths shown would still route to the +new+ and +edit+ actions. TIP: If you find yourself wanting to change this option uniformly for all of your routes, you can use a scope: @@ -709,7 +709,7 @@ end Rails now creates routes to the +CategoriesControlleR+. -|_.HTTP verb|_.URL |_.action | +|_.HTTP verb|_.Path |_.action | |GET |/kategorien |index | |GET |/kategorien/neu |new | |POST |/kategorien |create | @@ -762,6 +762,12 @@ formatted_users GET /users.:format {:controller=>"users", :action=>"index"} POST /users.:format {:controller=>"users", :action=>"create"} </pre> +You may restrict the listing to the routes that map to a particular controller setting the +CONTROLLER+ environment variable: + +<shell> +$ CONTROLLER=users rake routes +</shell> + TIP: You'll find that the output from +rake routes+ is much more readable if you widen your terminal window until the output lines don't wrap. h4. Testing Routes diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile index 60108d5ab3..8ce0001080 100644 --- a/railties/guides/source/security.textile +++ b/railties/guides/source/security.textile @@ -286,7 +286,7 @@ When filtering user input file names, _(highlight)don't try to remove malicious <ruby> def sanitize_filename(filename) - returning filename.strip do |name| + filename.strip.tap do |name| # NOTE: File.basename doesn't work right with Windows paths on Unix # get only the filename, not the whole path name.sub! /\A.*(\\|\/)/, '' diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 458177b954..5b26333486 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -149,6 +149,13 @@ module Rails self end + def load_console(sandbox=false) + initialize_console(sandbox) + railties.all { |r| r.load_console } + super() + self + end + def app @app ||= begin config.middleware = config.middleware.merge_into(default_middleware_stack) @@ -198,6 +205,7 @@ module Rails middleware.use ::ActionDispatch::ParamsParser middleware.use ::Rack::MethodOverride middleware.use ::ActionDispatch::Head + middleware.use ::ActionDispatch::BestStandardsSupport, config.action_dispatch.best_standards_support if config.action_dispatch.best_standards_support end end @@ -212,5 +220,11 @@ module Rails def initialize_generators require "rails/generators" end + + def initialize_console(sandbox=false) + require "rails/console/app" + require "rails/console/sandbox" if sandbox + require "rails/console/helpers" + end end end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index b9353ba336..60a93c9848 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -70,4 +70,4 @@ In addition to those, there are: All commands can be run with -h for more information. EOT -end
\ No newline at end of file +end diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index 50df6ba405..834a120c01 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -23,10 +23,7 @@ module Rails opt.parse!(ARGV) end - @app.initialize! - require "rails/console/app" - require "rails/console/sandbox" if options[:sandbox] - require "rails/console/helpers" + @app.load_console(options[:sandbox]) if options[:debugger] begin diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index 0becb780de..e5af12b901 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -56,12 +56,11 @@ module Rails return @options[method] if args.empty? - if method == :rails - namespace, configuration = :rails, args.shift - elsif args.first.is_a?(Hash) + if method == :rails || args.first.is_a?(Hash) namespace, configuration = method, args.shift else namespace, configuration = args.shift, args.shift + namespace = namespace.to_sym if namespace.respond_to?(:to_sym) @options[:rails][method] = namespace end diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 2f465670cf..521ed95447 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -50,4 +50,4 @@ module Rails end end end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index a27d38e23a..668ef48892 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -40,7 +40,7 @@ module Rails end end - # Adds an entry into config/environment.rb for the supplied gem. If env + # Adds an entry into Gemfile for the supplied gem. If env # is specified, add the gem to the given environment. # # ==== Example @@ -100,7 +100,7 @@ module Rails end end - # Adds a line inside the Initializer block for config/environment.rb. + # Adds a line inside the Application class for config/application.rb. # # If options :env is specified, the line is appended to the corresponding # file in config/environments. @@ -275,7 +275,7 @@ module Rails # def route(routing_code) log :route, routing_code - sentinel = /\.routes\.draw do(\s*\|map\|)?\s*$/ + sentinel = /\.routes\.draw do(?:\s*\|map\|)?\s*$/ in_root do inject_into_file 'config/routes.rb', "\n #{routing_code}\n", { :after => sentinel, :verbose => false } diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb index fe6321af30..4b828340d2 100644 --- a/railties/lib/rails/generators/active_model.rb +++ b/railties/lib/rails/generators/active_model.rb @@ -9,16 +9,16 @@ module Rails # For example: # # ActiveRecord::Generators::ActiveModel.find(Foo, "params[:id]") - # #=> "Foo.find(params[:id])" + # # => "Foo.find(params[:id])" # # Datamapper::Generators::ActiveModel.find(Foo, "params[:id]") - # #=> "Foo.get(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" + # builder.save # => "@foo.save" # # The only exception in ActiveModel for ActiveRecord is the use of self.build # instead of self.new. diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 67a9a6030d..fdfceb14ce 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -3,7 +3,7 @@ begin rescue LoadError puts "Thor is not available.\nIf you ran this command from a git checkout " \ "of Rails, please make sure thor is installed,\nand run this command " \ - "as `ruby /path/to/rails new myapp --dev`" + "as `ruby #{$0} #{ARGV.join(" ")} --dev`" exit end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 7d50e7da67..a90f109844 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -115,6 +115,7 @@ module Rails directory "public/javascripts" else empty_directory_with_gitkeep "public/javascripts" + create_file "public/javascripts/application.js" end end @@ -167,7 +168,7 @@ module Rails :desc => "Path to an application builder (can be a filesystem path or URL)" class_option :template, :type => :string, :aliases => "-m", - :desc => "Path to an application template (can be a filesystem path or URL)." + :desc => "Path to an application template (can be a filesystem path or URL)" class_option :dev, :type => :boolean, :default => false, :desc => "Setup the application with Gemfile pointing to your Rails checkout" @@ -178,11 +179,11 @@ module Rails class_option :skip_gemfile, :type => :boolean, :default => false, :desc => "Don't create a Gemfile" - class_option :skip_activerecord, :type => :boolean, :aliases => "-O", :default => false, - :desc => "Skip ActiveRecord files" + class_option :skip_active_record, :type => :boolean, :aliases => "-O", :default => false, + :desc => "Skip Active Record files" - class_option :skip_testunit, :type => :boolean, :aliases => "-T", :default => false, - :desc => "Skip TestUnit files" + class_option :skip_test_unit, :type => :boolean, :aliases => "-T", :default => false, + :desc => "Skip Test::Unit files" class_option :skip_prototype, :type => :boolean, :aliases => "-J", :default => false, :desc => "Skip Prototype files" @@ -204,7 +205,7 @@ module Rails super - if !options[:skip_activerecord] && !DATABASES.include?(options[:database]) + if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end end @@ -215,7 +216,7 @@ module Rails empty_directory '.' set_default_accessors! - FileUtils.cd(destination_root) + FileUtils.cd(destination_root) unless options[:pretend] end def create_root_files @@ -238,8 +239,8 @@ module Rails template "config/boot.rb" end - def create_activerecord_files - return if options[:skip_activerecord] + def create_active_record_files + return if options[:skip_active_record] build(:database_yml) end @@ -280,7 +281,7 @@ module Rails end def create_test_files - build(:test) unless options[:skip_testunit] + build(:test) unless options[:skip_test_unit] end def create_tmp_files @@ -355,8 +356,13 @@ module Rails @app_name ||= File.basename(destination_root) end + def defined_app_const_base + Rails.respond_to?(:application) && defined?(Rails::Application) && + Rails.application.is_a?(Rails::Application) && Rails.application.class.name.sub(/::Application$/, "") + end + def app_const_base - @app_const_base ||= app_name.gsub(/\W/, '_').squeeze('_').camelize + @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, '_').squeeze('_').camelize end def app_const diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index a108968b97..1980684a94 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -28,7 +28,7 @@ gem '<%= gem_for_database %>'<% if require_for_database %>, :require => '<%= req # Bundle the extra gems: # gem 'bj' -# gem 'nokogiri', '1.4.1' +# gem 'nokogiri' # gem 'sqlite3-ruby', :require => 'sqlite3' # gem 'aws-s3', :require => 'aws/s3' diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index 67a38ea1d5..7d63e99e05 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -1,6 +1,6 @@ require File.expand_path('../boot', __FILE__) -<% unless options[:skip_activerecord] -%> +<% unless options[:skip_active_record] -%> require 'rails/all' <% else -%> # Pick the frameworks you want: @@ -22,7 +22,7 @@ module <%= app_const_base %> # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - # config.autoload_paths += %W( #{config.root}/extras ) + # config.autoload_paths += %W(#{config.root}/extras) # 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. @@ -39,12 +39,12 @@ module <%= app_const_base %> # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # 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 + # JavaScript files you want as :defaults (application.js is always included). +<% if options[:skip_prototype] -%> + config.action_view.javascript_expansions[:defaults] = %w() +<% else -%> + # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) +<% end -%> # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" 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 index f99ee937f3..fddf8b8144 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -4,7 +4,7 @@ # 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: +# tnsnames.ora service name, or an SQL connect string of the form: # # //host:[port][/service name] # diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index 99758dfcf7..7616614aff 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -19,4 +19,8 @@ # Print deprecation notices to the Rails logger config.active_support.deprecation = :log + + # Only use best-standards-support built into browsers + config.action_dispatch.best_standards_support = :builtin end + diff --git a/railties/lib/rails/generators/rails/app/templates/public/index.html b/railties/lib/rails/generators/rails/app/templates/public/index.html index c65593e8bc..75d5edd06d 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/index.html +++ b/railties/lib/rails/generators/rails/app/templates/public/index.html @@ -151,19 +151,6 @@ } - #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; @@ -194,16 +181,6 @@ info.innerHTML = xhr.responseText; info.style.display = 'block' } - - function prepend() { - search = document.getElementById('search-text'); - text = search.value; - search.value = 'site:rubyonrails.org ' + text; - } - - window.onload = function() { - document.getElementById('search-text').value = ''; - } </script> </head> <body> 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 index 9fe6e1243b..06249a6ae3 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js +++ b/railties/lib/rails/generators/rails/app/templates/public/javascripts/prototype.js @@ -1,5 +1,5 @@ -/* Prototype JavaScript framework, version 1.6.1 - * (c) 2005-2009 Sam Stephenson +/* Prototype JavaScript framework, version 1.7_rc2 + * (c) 2005-2010 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/ @@ -7,7 +7,8 @@ *--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.6.1', + + Version: '1.7_rc2', Browser: (function(){ var ua = navigator.userAgent; @@ -17,13 +18,15 @@ var Prototype = { Opera: isOpera, WebKit: ua.indexOf('AppleWebKit/') > -1, Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, - MobileSafari: /Apple.*Mobile.*Safari/.test(ua) + MobileSafari: /Apple.*Mobile/.test(ua) } })(), BrowserFeatures: { XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: (function() { var constructor = window.Element || window.HTMLElement; return !!(constructor && constructor.prototype); @@ -32,9 +35,9 @@ var Prototype = { if (typeof window.HTMLDivElement !== 'undefined') return true; - var div = document.createElement('div'); - var form = document.createElement('form'); - var isSupported = false; + var div = document.createElement('div'), + form = document.createElement('form'), + isSupported = false; if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { isSupported = true; @@ -50,6 +53,7 @@ var Prototype = { JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, emptyFunction: function() { }, + K: function(x) { return x } }; @@ -79,6 +83,14 @@ var Try = { /* Based on Alex Arnell's inheritance implementation. */ var Class = (function() { + + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; + } + return true; + })(); + function subclass() {}; function create() { var parent = null, properties = $A(arguments); @@ -99,7 +111,7 @@ var Class = (function() { parent.subclasses.push(klass); } - for (var i = 0; i < properties.length; i++) + for (var i = 0, length = properties.length; i < length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) @@ -110,10 +122,10 @@ var Class = (function() { } function addMethods(source) { - var ancestor = this.superclass && this.superclass.prototype; - var properties = Object.keys(source); + var ancestor = this.superclass && this.superclass.prototype, + properties = Object.keys(source); - if (!Object.keys({ toString: true }).length) { + if (IS_DONTENUM_BUGGY) { if (source.toString != Object.prototype.toString) properties.push("toString"); if (source.valueOf != Object.prototype.valueOf) @@ -123,7 +135,7 @@ var Class = (function() { 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") { + value.argumentNames()[0] == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments); }; @@ -147,7 +159,35 @@ var Class = (function() { })(); (function() { - var _toString = Object.prototype.toString; + var _toString = Object.prototype.toString, + NULL_TYPE = 'Null', + UNDEFINED_TYPE = 'Undefined', + BOOLEAN_TYPE = 'Boolean', + NUMBER_TYPE = 'Number', + STRING_TYPE = 'String', + OBJECT_TYPE = 'Object', + BOOLEAN_CLASS = '[object Boolean]', + NUMBER_CLASS = '[object Number]', + STRING_CLASS = '[object String]', + ARRAY_CLASS = '[object Array]', + NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON && + typeof JSON.stringify === 'function' && + JSON.stringify(0) === '0' && + typeof JSON.stringify(Prototype.K) === 'undefined'; + + function Type(o) { + switch(o) { + case null: return NULL_TYPE; + case (void 0): return UNDEFINED_TYPE; + } + var type = typeof o; + switch(type) { + case 'boolean': return BOOLEAN_TYPE; + case 'number': return NUMBER_TYPE; + case 'string': return STRING_TYPE; + } + return OBJECT_TYPE; + } function extend(destination, source) { for (var property in source) @@ -166,27 +206,70 @@ var Class = (function() { } } - function toJSON(object) { - var type = typeof object; - switch (type) { - case 'undefined': - case 'function': - case 'unknown': return; - case 'boolean': return object.toString(); + function toJSON(value) { + return Str('', { '': value }, []); + } + + function Str(key, holder, stack) { + var value = holder[key], + type = typeof value; + + if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') { + value = value.toJSON(key); } - if (object === null) return 'null'; - if (object.toJSON) return object.toJSON(); - if (isElement(object)) return; + var _class = _toString.call(value); - var results = []; - for (var property in object) { - var value = toJSON(object[property]); - if (!isUndefined(value)) - results.push(property.toJSON() + ': ' + value); + switch (_class) { + case NUMBER_CLASS: + case BOOLEAN_CLASS: + case STRING_CLASS: + value = value.valueOf(); + } + + switch (value) { + case null: return 'null'; + case true: return 'true'; + case false: return 'false'; } - return '{' + results.join(', ') + '}'; + type = typeof value; + switch (type) { + case 'string': + return value.inspect(true); + case 'number': + return isFinite(value) ? String(value) : 'null'; + case 'object': + + for (var i = 0, length = stack.length; i < length; i++) { + if (stack[i] === value) { throw new TypeError(); } + } + stack.push(value); + + var partial = []; + if (_class === ARRAY_CLASS) { + for (var i = 0, length = value.length; i < length; i++) { + var str = Str(i, value, stack); + partial.push(typeof str === 'undefined' ? 'null' : str); + } + partial = '[' + partial.join(',') + ']'; + } else { + var keys = Object.keys(value); + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i], str = Str(key, value, stack); + if (typeof str !== "undefined") { + partial.push(key.inspect(true)+ ':' + str); + } + } + partial = '{' + partial.join(',') + '}'; + } + stack.pop(); + return partial; + } + } + + function stringify(object) { + return JSON.stringify(object); } function toQueryString(object) { @@ -198,9 +281,13 @@ var Class = (function() { } function keys(object) { + if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } var results = []; - for (var property in object) - results.push(property); + for (var property in object) { + if (object.hasOwnProperty(property)) { + results.push(property); + } + } return results; } @@ -220,9 +307,15 @@ var Class = (function() { } function isArray(object) { - return _toString.call(object) == "[object Array]"; + return _toString.call(object) === ARRAY_CLASS; } + var hasNativeIsArray = (typeof Array.isArray == 'function') + && Array.isArray([]) && !Array.isArray({}); + + if (hasNativeIsArray) { + isArray = Array.isArray; + } function isHash(object) { return object instanceof Hash; @@ -233,11 +326,11 @@ var Class = (function() { } function isString(object) { - return _toString.call(object) == "[object String]"; + return _toString.call(object) === STRING_CLASS; } function isNumber(object) { - return _toString.call(object) == "[object Number]"; + return _toString.call(object) === NUMBER_CLASS; } function isUndefined(object) { @@ -247,10 +340,10 @@ var Class = (function() { extend(Object, { extend: extend, inspect: inspect, - toJSON: toJSON, + toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON, toQueryString: toQueryString, toHTML: toHTML, - keys: keys, + keys: Object.keys || keys, values: values, clone: clone, isElement: isElement, @@ -311,7 +404,7 @@ Object.extend(Function.prototype, (function() { function delay(timeout) { var __method = this, args = slice.call(arguments, 1); - timeout = timeout * 1000 + timeout = timeout * 1000; return window.setTimeout(function() { return __method.apply(__method, args); }, timeout); @@ -352,14 +445,28 @@ Object.extend(Function.prototype, (function() { })()); -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"'; -}; + +(function(proto) { + + + function toISOString() { + 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'; + } + + + function toJSON() { + return this.toISOString(); + } + + if (!proto.toISOString) proto.toISOString = toISOString; + if (!proto.toJSON) proto.toJSON = toJSON; + +})(Date.prototype); RegExp.prototype.match = RegExp.prototype.test; @@ -418,6 +525,9 @@ Object.extend(String, { }); Object.extend(String.prototype, (function() { + var NATIVE_JSON_PARSE_SUPPORT = window.JSON && + typeof JSON.parse === 'function' && + JSON.parse('{"test": true}').test; function prepareReplacement(replacement) { if (Object.isFunction(replacement)) return replacement; @@ -484,8 +594,8 @@ Object.extend(String.prototype, (function() { } function extractScripts() { - var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); - var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), + matchOne = new RegExp(Prototype.ScriptFragment, 'im'); return (this.match(matchAll) || []).map(function(scriptTag) { return (scriptTag.match(matchOne) || ['', ''])[1]; }); @@ -510,8 +620,9 @@ Object.extend(String.prototype, (function() { 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]; + var key = decodeURIComponent(pair.shift()), + value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); if (key in hash) { @@ -538,17 +649,9 @@ Object.extend(String.prototype, (function() { } function camelize() { - 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; + return this.replace(/-+(.)?/g, function(match, chr) { + return chr ? chr.toUpperCase() : ''; + }); } function capitalize() { @@ -578,10 +681,6 @@ Object.extend(String.prototype, (function() { return "'" + escapedString.replace(/'/g, '\\\'') + "'"; } - function toJSON() { - return this.inspect(true); - } - function unfilterJSON(filter) { return this.replace(filter || Prototype.JSONFilter, '$1'); } @@ -589,29 +688,42 @@ Object.extend(String.prototype, (function() { function isJSON() { 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); + str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); + str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); + str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); + return (/^[\],:{}\s]*$/).test(str); } function evalJSON(sanitize) { - var json = this.unfilterJSON(); + var json = this.unfilterJSON(), + cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + if (cx.test(json)) { + json = json.replace(cx, function (a) { + return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } try { if (!sanitize || json.isJSON()) return eval('(' + json + ')'); } catch (e) { } throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); } + function parseJSON() { + var json = this.unfilterJSON(); + return JSON.parse(json); + } + function include(pattern) { return this.indexOf(pattern) > -1; } function startsWith(pattern) { - return this.indexOf(pattern) === 0; + return this.lastIndexOf(pattern, 0) === 0; } function endsWith(pattern) { var d = this.length - pattern.length; - return d >= 0 && this.lastIndexOf(pattern) === d; + return d >= 0 && this.indexOf(pattern, d) === d; } function empty() { @@ -631,7 +743,7 @@ Object.extend(String.prototype, (function() { sub: sub, scan: scan, truncate: truncate, - strip: String.prototype.trim ? String.prototype.trim : strip, + strip: String.prototype.trim || strip, stripTags: stripTags, stripScripts: stripScripts, extractScripts: extractScripts, @@ -648,10 +760,9 @@ Object.extend(String.prototype, (function() { underscore: underscore, dasherize: dasherize, inspect: inspect, - toJSON: toJSON, unfilterJSON: unfilterJSON, isJSON: isJSON, - evalJSON: evalJSON, + evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON, include: include, startsWith: startsWith, endsWith: endsWith, @@ -677,8 +788,9 @@ var Template = Class.create({ var before = match[1] || ''; if (before == '\\') return match[2]; - var ctx = object, expr = match[3]; - var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + var ctx = object, expr = match[3], + pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); if (match == null) return before; @@ -943,6 +1055,7 @@ var Enumerable = (function() { find: detect }; })(); + function $A(iterable) { if (!iterable) return []; if ('toArray' in Object(iterable)) return iterable.toArray(); @@ -951,6 +1064,7 @@ function $A(iterable) { return results; } + function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); @@ -1007,7 +1121,7 @@ Array.from = $A; } function reverse(inline) { - return (inline !== false ? this : this.toArray())._reverse(); + return (inline === false ? this.toArray() : this)._reverse(); } function uniq(sorted) { @@ -1037,15 +1151,6 @@ Array.from = $A; return '[' + this.map(Object.inspect).join(', ') + ']'; } - function toJSON() { - var results = []; - this.each(function(object) { - var value = Object.toJSON(object); - if (!Object.isUndefined(value)) results.push(value); - }); - return '[' + results.join(', ') + ']'; - } - function indexOf(item, i) { i || (i = 0); var length = this.length; @@ -1094,8 +1199,7 @@ Array.from = $A; clone: clone, toArray: clone, size: size, - inspect: inspect, - toJSON: toJSON + inspect: inspect }); var CONCAT_ARGUMENTS_BUGGY = (function() { @@ -1116,6 +1220,7 @@ var Hash = Class.create(Enumerable, (function() { this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); } + function _each(iterator) { for (var key in this._object) { var value = this._object[key], pair = [key, value]; @@ -1144,6 +1249,8 @@ var Hash = Class.create(Enumerable, (function() { return Object.clone(this._object); } + + function keys() { return this.pluck('key'); } @@ -1193,10 +1300,6 @@ var Hash = Class.create(Enumerable, (function() { }).join(', ') + '}>'; } - function toJSON() { - return Object.toJSON(this.toObject()); - } - function clone() { return new Hash(this); } @@ -1216,7 +1319,7 @@ var Hash = Class.create(Enumerable, (function() { update: update, toQueryString: toQueryString, inspect: inspect, - toJSON: toJSON, + toJSON: toObject, clone: clone }; })()); @@ -1241,10 +1344,6 @@ Object.extend(Number.prototype, (function() { return '0'.times(length - string.length) + string; } - function toJSON() { - return isFinite(this) ? this.toString() : 'null'; - } - function abs() { return Math.abs(this); } @@ -1266,7 +1365,6 @@ Object.extend(Number.prototype, (function() { succ: succ, times: times, toPaddedString: toPaddedString, - toJSON: toJSON, abs: abs, round: round, ceil: ceil, @@ -1558,14 +1656,14 @@ Ajax.Response = Class.create({ var transport = this.transport = request.transport, readyState = this.readyState = transport.readyState; - if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + 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) { + if (readyState == 4) { var xml = transport.responseXML; this.responseXML = Object.isUndefined(xml) ? null : xml; this.responseJSON = this._getResponseJSON(); @@ -1705,7 +1803,6 @@ Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { }); - function $(element) { if (arguments.length > 1) { for (var i = 0, elements = [], length = arguments.length; i < length; i++) @@ -1730,7 +1827,7 @@ if (Prototype.BrowserFeatures.XPath) { /*--------------------------------------------------------------------------*/ -if (!window.Node) var Node = { }; +if (!Node) var Node = { }; if (!Node.ELEMENT_NODE) { Object.extend(Node, { @@ -1750,29 +1847,26 @@ if (!Node.ELEMENT_NODE) { } + (function(global) { - var SETATTRIBUTE_IGNORES_NAME = (function(){ - var elForm = document.createElement("form"); - var elInput = document.createElement("input"); - var root = document.documentElement; - elInput.setAttribute("name", "test"); - elForm.appendChild(elInput); - root.appendChild(elForm); - var isBuggy = elForm.elements - ? (typeof elForm.elements.test == "undefined") - : null; - root.removeChild(elForm); - elForm = elInput = null; - return isBuggy; + var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){ + try { + var el = document.createElement('<input name="x">'); + return el.tagName.toLowerCase() === 'input' && el.name === 'x'; + } + catch(err) { + return false; + } })(); var element = global.Element; + global.Element = function(tagName, attributes) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; - if (SETATTRIBUTE_IGNORES_NAME && attributes.name) { + if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); @@ -1780,12 +1874,23 @@ if (!Node.ELEMENT_NODE) { if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); }; + Object.extend(global.Element, element || { }); if (element) global.Element.prototype = element.prototype; + })(this); -Element.cache = { }; Element.idCounter = 1; +Element.cache = { }; + +function purgeElement(element) { + var uid = element._prototypeUID; + if (uid) { + Element.stopObserving(element); + element._prototypeUID = void 0; + delete Element.Storage[uid]; + } +} Element.Methods = { visible: function(element) { @@ -1798,7 +1903,6 @@ Element.Methods = { return element; }, - hide: function(element) { element = $(element); element.style.display = 'none'; @@ -1861,6 +1965,10 @@ Element.Methods = { function update(element, content) { element = $(element); + var descendants = element.getElementsByTagName('*'), + i = descendants.length; + while (i--) purgeElement(descendants[i]); + if (content && content.toElement) content = content.toElement(); @@ -1967,19 +2075,26 @@ Element.Methods = { 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(); + var property = pair.first(), + attribute = pair.last(), + value = (element[property] || '').toString(); if (value) result += ' ' + attribute + '=' + value.inspect(true); }); return result + '>'; }, - recursivelyCollect: function(element, property) { + recursivelyCollect: function(element, property, maximumLength) { element = $(element); + maximumLength = maximumLength || -1; var elements = []; - while (element = element[property]) + + while (element = element[property]) { if (element.nodeType == 1) elements.push(Element.extend(element)); + if (elements.length == maximumLength) + break; + } + return elements; }, @@ -1998,13 +2113,17 @@ Element.Methods = { }, immediateDescendants: function(element) { - if (!(element = $(element).firstChild)) return []; - while (element && element.nodeType != 1) element = element.nextSibling; - if (element) return [element].concat($(element).nextSiblings()); - return []; + var results = [], child = $(element).firstChild; + while (child) { + if (child.nodeType === 1) { + results.push(Element.extend(child)); + } + child = child.nextSibling; + } + return results; }, - previousSiblings: function(element) { + previousSiblings: function(element, maximumLength) { return Element.recursivelyCollect(element, 'previousSibling'); }, @@ -2019,9 +2138,10 @@ Element.Methods = { }, match: function(element, selector) { + element = $(element); if (Object.isString(selector)) - selector = new Selector(selector); - return selector.match($(element)); + return Prototype.Selector.match(element, selector); + return selector.match(element); }, up: function(element, expression, index) { @@ -2029,7 +2149,7 @@ Element.Methods = { if (arguments.length == 1) return $(element.parentNode); var ancestors = Element.ancestors(element); return Object.isNumber(expression) ? ancestors[expression] : - Selector.findElement(ancestors, expression, index); + Prototype.Selector.find(ancestors, expression, index); }, down: function(element, expression, index) { @@ -2041,29 +2161,40 @@ Element.Methods = { previous: function(element, expression, index) { element = $(element); - if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); - var previousSiblings = Element.previousSiblings(element); - return Object.isNumber(expression) ? previousSiblings[expression] : - Selector.findElement(previousSiblings, expression, index); + if (Object.isNumber(expression)) index = expression, expression = false; + if (!Object.isNumber(index)) index = 0; + + if (expression) { + return Prototype.Selector.find(element.previousSiblings(), expression, index); + } else { + return element.recursivelyCollect("previousSibling", index + 1)[index]; + } }, next: function(element, expression, index) { element = $(element); - if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); - var nextSiblings = Element.nextSiblings(element); - return Object.isNumber(expression) ? nextSiblings[expression] : - Selector.findElement(nextSiblings, expression, index); + if (Object.isNumber(expression)) index = expression, expression = false; + if (!Object.isNumber(index)) index = 0; + + if (expression) { + return Prototype.Selector.find(element.nextSiblings(), expression, index); + } else { + var maximumLength = Object.isNumber(index) ? index + 1 : 1; + return element.recursivelyCollect("nextSibling", index + 1)[index]; + } }, select: function(element) { - var args = Array.prototype.slice.call(arguments, 1); - return Selector.findChildElements(element, args); + element = $(element); + var expressions = Array.prototype.slice.call(arguments, 1).join(', '); + return Prototype.Selector.select(expressions, element); }, adjacent: function(element) { - var args = Array.prototype.slice.call(arguments, 1); - return Selector.findChildElements(element.parentNode, args).without(element); + element = $(element); + var expressions = Array.prototype.slice.call(arguments, 1).join(', '); + return Prototype.Selector.select(expressions, element.parentNode).without(element); }, identify: function(element) { @@ -2227,28 +2358,6 @@ Element.Methods = { return element; }, - getDimensions: function(element) { - element = $(element); - var display = Element.getStyle(element, 'display'); - if (display != 'none' && display != null) // Safari bug - return {width: element.offsetWidth, height: element.offsetHeight}; - - var els = element.style; - var originalVisibility = els.visibility; - var originalPosition = els.position; - var originalDisplay = els.display; - els.visibility = 'hidden'; - if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari - 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'); @@ -2295,11 +2404,13 @@ Element.Methods = { cumulativeOffset: function(element) { var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); + if (element.parentNode) { + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + } return Element._returnOffset(valueL, valueT); }, @@ -2322,11 +2433,11 @@ Element.Methods = { element = $(element); if (Element.getStyle(element, 'position') == 'absolute') return element; - var offsets = Element.positionedOffset(element); - var top = offsets[1]; - var left = offsets[0]; - var width = element.clientWidth; - var height = element.clientHeight; + var offsets = Element.positionedOffset(element), + top = offsets[1], + left = offsets[0], + width = element.clientWidth, + height = element.clientHeight; element._originalLeft = left - parseFloat(element.style.left || 0); element._originalTop = top - parseFloat(element.style.top || 0); @@ -2346,8 +2457,8 @@ Element.Methods = { if (Element.getStyle(element, 'position') == 'relative') return element; element.style.position = 'relative'; - var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); - var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0), + left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); element.style.top = top + 'px'; element.style.left = left + 'px'; @@ -2378,9 +2489,10 @@ Element.Methods = { }, viewportOffset: function(forElement) { - var valueT = 0, valueL = 0; + var valueT = 0, + valueL = 0, + element = forElement; - var element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; @@ -2412,11 +2524,10 @@ Element.Methods = { }, arguments[2] || { }); source = $(source); - var p = Element.viewportOffset(source); + var p = Element.viewportOffset(source), delta = [0, 0], parent = null; element = $(element); - var delta = [0, 0]; - var parent = null; + if (Element.getStyle(element, 'position') == 'absolute') { parent = Element.getOffsetParent(element); delta = Element.viewportOffset(parent); @@ -2495,8 +2606,7 @@ else if (Prototype.Browser.IE) { Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( function(proceed, element) { element = $(element); - try { element.offsetParent } - catch(e) { return $(document.body) } + if (!element.parentNode) return $(document.body); var position = element.getStyle('position'); if (position !== 'static') return proceed(element); element.setStyle({ position: 'relative' }); @@ -2510,8 +2620,7 @@ else if (Prototype.Browser.IE) { Element.Methods[method] = Element.Methods[method].wrap( function(proceed, element) { element = $(element); - try { element.offsetParent } - catch(e) { return Element._returnOffset(0,0) } + if (!element.parentNode) return Element._returnOffset(0, 0); var position = element.getStyle('position'); if (position !== 'static') return proceed(element); var offsetParent = element.getOffsetParent(); @@ -2525,14 +2634,6 @@ else if (Prototype.Browser.IE) { ); }); - 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(); @@ -2576,10 +2677,9 @@ else if (Prototype.Browser.IE) { Element._attributeTranslations = (function(){ - var classProp = 'className'; - var forProp = 'for'; - - var el = document.createElement('div'); + var classProp = 'className', + forProp = 'for', + el = document.createElement('div'); el.setAttribute(classProp, 'x'); @@ -2622,10 +2722,9 @@ else if (Prototype.Browser.IE) { }, _getEv: (function(){ - var el = document.createElement('div'); + var el = document.createElement('div'), f; el.onclick = Prototype.emptyFunction; var value = el.getAttribute('onclick'); - var f; if (String(value).indexOf('{') > -1) { f = function(element, attribute) { @@ -2753,7 +2852,7 @@ else if (Prototype.Browser.WebKit) { (value < 0.00001) ? 0 : value; if (value == 1) - if(element.tagName.toUpperCase() == 'IMG' && element.width) { + if (element.tagName.toUpperCase() == 'IMG' && element.width) { element.width++; element.width--; } else try { var n = document.createTextNode(' '); @@ -2793,8 +2892,8 @@ if ('outerHTML' in document.documentElement) { var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); if (Element._insertionTranslations.tags[tagName]) { - var nextSibling = element.next(); - var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + var nextSibling = element.next(), + fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); parent.removeChild(element); if (nextSibling) fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); @@ -2816,11 +2915,17 @@ Element._returnOffset = function(l, t) { }; Element._getContentFromAnonymousElement = function(tagName, html) { - var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + 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; + for (var i = t[2]; i--; ) { + div = div.firstChild; + } + } + else { + div.innerHTML = html; + } return $A(div.childNodes); }; @@ -2877,7 +2982,7 @@ Object.extend(Element, Element.Methods); div = null; -})(document.createElement('div')) +})(document.createElement('div')); Element.extend = (function() { @@ -2885,8 +2990,8 @@ Element.extend = (function() { if (typeof window.Element != 'undefined') { var proto = window.Element.prototype; if (proto) { - var id = '_' + (Math.random()+'').slice(2); - var el = document.createElement(tagName); + var id = '_' + (Math.random()+'').slice(2), + el = document.createElement(tagName); proto[id] = 'x'; var isBuggy = (el[id] !== 'x'); delete proto[id]; @@ -2953,10 +3058,14 @@ Element.extend = (function() { return extend; })(); -Element.hasAttribute = function(element, attribute) { - if (element.hasAttribute) return element.hasAttribute(attribute); - return Element.Methods.Simulated.hasAttribute(element, attribute); -}; +if (document.documentElement.hasAttribute) { + Element.hasAttribute = function(element, attribute) { + return element.hasAttribute(attribute); + }; +} +else { + Element.hasAttribute = Element.Methods.Simulated.hasAttribute; +} Element.addMethods = function(methods) { var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; @@ -3020,8 +3129,9 @@ Element.addMethods = function(methods) { klass = 'HTML' + tagName.capitalize() + 'Element'; if (window[klass]) return window[klass]; - var element = document.createElement(tagName); - var proto = element['__proto__'] || element.constructor.prototype; + var element = document.createElement(tagName), + proto = element['__proto__'] || element.constructor.prototype; + element = null; return proto; } @@ -3104,8 +3214,8 @@ Element.addMethods({ uid = 0; } else { if (typeof element._prototypeUID === "undefined") - element._prototypeUID = [Element.Storage.UID++]; - uid = element._prototypeUID[0]; + element._prototypeUID = Element.Storage.UID++; + uid = element._prototypeUID; } if (!Element.Storage[uid]) @@ -3150,770 +3260,1698 @@ Element.addMethods({ } } return Element.extend(clone); + }, + + purge: function(element) { + if (!(element = $(element))) return; + purgeElement(element); + + var descendants = element.getElementsByTagName('*'), + i = descendants.length; + + while (i--) purgeElement(descendants[i]); + + return null; } }); -/* 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(); - } - }, +(function() { - shouldUseXPath: (function() { + function toDecimal(pctString) { + var match = pctString.match(/^(\d+)%?$/i); + if (!match) return null; + return (Number(match[1]) / 100); + } - var IS_DESCENDANT_SELECTOR_BUGGY = (function(){ - var isBuggy = false; - if (document.evaluate && window.XPathResult) { - var el = document.createElement('div'); - el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>'; + function getPixelValue(value, property) { + if (Object.isElement(value)) { + element = value; + value = element.getStyle(property); + } + if (value === null) { + return null; + } - var xpath = ".//*[local-name()='ul' or local-name()='UL']" + - "//*[local-name()='li' or local-name()='LI']"; + if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) { + return window.parseFloat(value); + } - var result = document.evaluate(xpath, el, null, - XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + if (/\d/.test(value) && element.runtimeStyle) { + var style = element.style.left, rStyle = element.runtimeStyle.left; + element.runtimeStyle.left = element.currentStyle.left; + element.style.left = value || 0; + value = element.style.pixelLeft; + element.style.left = style; + element.runtimeStyle.left = rStyle; - isBuggy = (result.snapshotLength !== 2); - el = null; + return value; + } + + if (value.include('%')) { + var decimal = toDecimal(value); + var whole; + if (property.include('left') || property.include('right') || + property.include('width')) { + whole = $(element.parentNode).measure('width'); + } else if (property.include('top') || property.include('bottom') || + property.include('height')) { + whole = $(element.parentNode).measure('height'); } - return isBuggy; - })(); - return function() { - if (!Prototype.BrowserFeatures.XPath) return false; + return whole * decimal; + } - var e = this.expression; + return 0; + } - if (Prototype.Browser.WebKit && - (e.include("-of-type") || e.include(":empty"))) - return false; + function toCSSPixels(number) { + if (Object.isString(number) && number.endsWith('px')) { + return number; + } + return number + 'px'; + } - if ((/(\[[\w-]*?:|:checked)/).test(e)) + function isDisplayed(element) { + var originalElement = element; + while (element && element.parentNode) { + var display = element.getStyle('display'); + if (display === 'none') { return false; + } + element = $(element.parentNode); + } + return true; + } - if (IS_DESCENDANT_SELECTOR_BUGGY) return false; + var hasLayout = Prototype.K; + if ('currentStyle' in document.documentElement) { + hasLayout = function(element) { + if (!element.currentStyle.hasLayout) { + element.style.zoom = 1; + } + return element; + }; + } - return true; - } + function cssNameFor(key) { + if (key.include('border')) key = key + '-width'; + return key.camelize(); + } - })(), + Element.Layout = Class.create(Hash, { + initialize: function($super, element, preCompute) { + $super(); + this.element = $(element); - shouldUseSelectorsAPI: function() { - if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + Element.Layout.PROPERTIES.each( function(property) { + this._set(property, null); + }, this); - if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false; + if (preCompute) { + this._preComputing = true; + this._begin(); + Element.Layout.PROPERTIES.each( this._compute, this ); + this._end(); + this._preComputing = false; + } + }, - if (!Selector._div) Selector._div = new Element('div'); + _set: function(property, value) { + return Hash.prototype.set.call(this, property, value); + }, - try { - Selector._div.querySelector(this.expression); - } catch(e) { - return false; - } + set: function(property, value) { + throw "Properties of Element.Layout are read-only."; + }, - return true; - }, + get: function($super, property) { + var value = $super(property); + return value === null ? this._compute(property) : value; + }, - compileMatcher: function() { - var e = this.expression, ps = Selector.patterns, h = Selector.handlers, - c = Selector.criteria, le, p, m, len = ps.length, name; + _begin: function() { + if (this._prepared) return; - if (Selector._cache[e]) { - this.matcher = Selector._cache[e]; - return; - } + var element = this.element; + if (isDisplayed(element)) { + this._prepared = true; + return; + } - this.matcher = ["this.matcher = function(root) {", - "var r = root, h = Selector.handlers, c = false, n;"]; + var originalStyles = { + position: element.style.position || '', + width: element.style.width || '', + visibility: element.style.visibility || '', + display: element.style.display || '' + }; - while (e && le != e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - p = ps[i].re; - name = ps[i].name; - if (m = e.match(p)) { - this.matcher.push(Object.isFunction(c[name]) ? c[name](m) : - new Template(c[name]).evaluate(m)); - e = e.replace(m[0], ''); - break; - } + element.store('prototype_original_styles', originalStyles); + + var position = element.getStyle('position'), + width = element.getStyle('width'); + + element.setStyle({ + position: 'absolute', + visibility: 'hidden', + display: 'block' + }); + + var positionedWidth = element.getStyle('width'); + + var newWidth; + if (width && (positionedWidth === width)) { + newWidth = getPixelValue(width); + } else if (width && (position === 'absolute' || position === 'fixed')) { + newWidth = getPixelValue(width); + } else { + var parent = element.parentNode, pLayout = $(parent).getLayout(); + + newWidth = pLayout.get('width') - + this.get('margin-left') - + this.get('border-left') - + this.get('padding-left') - + this.get('padding-right') - + this.get('border-right') - + this.get('margin-right'); } - } - this.matcher.push("return h.unique(n);\n}"); - eval(this.matcher.join('\n')); - Selector._cache[this.expression] = this.matcher; - }, + element.setStyle({ width: newWidth + 'px' }); - compileXPathMatcher: function() { - var e = this.expression, ps = Selector.patterns, - x = Selector.xpath, le, m, len = ps.length, name; + this._prepared = true; + }, - if (Selector._cache[e]) { - this.xpath = Selector._cache[e]; return; - } + _end: function() { + var element = this.element; + var originalStyles = element.retrieve('prototype_original_styles'); + element.store('prototype_original_styles', null); + element.setStyle(originalStyles); + this._prepared = false; + }, - this.matcher = ['.//*']; - while (e && le != e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - name = ps[i].name; - if (m = e.match(ps[i].re)) { - this.matcher.push(Object.isFunction(x[name]) ? x[name](m) : - new Template(x[name]).evaluate(m)); - e = e.replace(m[0], ''); - break; - } + _compute: function(property) { + var COMPUTATIONS = Element.Layout.COMPUTATIONS; + if (!(property in COMPUTATIONS)) { + throw "Property not found."; } - } + return this._set(property, COMPUTATIONS[property].call(this, this.element)); + }, - this.xpath = this.matcher.join(''); - Selector._cache[this.expression] = this.xpath; - }, + toObject: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var obj = {}; + keys.each( function(key) { + if (!Element.Layout.PROPERTIES.include(key)) return; + var value = this.get(key); + if (value != null) obj[key] = value; + }, this); + return obj; + }, - findElements: function(root) { - root = root || document; - var e = this.expression, results; + toHash: function() { + var obj = this.toObject.apply(this, arguments); + return new Hash(obj); + }, - switch (this.mode) { - case 'selectorsAPI': - if (root !== document) { - var oldId = root.id, id = $(root).identify(); - id = id.replace(/([\.:])/g, "\\$1"); - e = "#" + id + " " + e; - } + toCSS: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var css = {}; - results = $A(root.querySelectorAll(e)).map(Element.extend); - root.id = oldId; + keys.each( function(key) { + if (!Element.Layout.PROPERTIES.include(key)) return; + if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; - return results; - case 'xpath': - return document._getElementsByXPath(this.xpath, root); - default: - return this.matcher(root); + var value = this.get(key); + if (value != null) css[cssNameFor(key)] = value + 'px'; + }, this); + return css; + }, + + inspect: function() { + return "#<Element.Layout>"; } - }, + }); - match: function(element) { - this.tokens = []; + Object.extend(Element.Layout, { + PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), - var e = this.expression, ps = Selector.patterns, as = Selector.assertions; - var le, p, m, len = ps.length, name; + COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), - while (e && le !== e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - p = ps[i].re; - name = ps[i].name; - if (m = e.match(p)) { - if (as[name]) { - this.tokens.push([name, Object.clone(m)]); - e = e.replace(m[0], ''); - } else { - return this.findElements(document).include(element); - } - } - } - } + COMPUTATIONS: { + 'height': function(element) { + if (!this._preComputing) this._begin(); - 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; - } - } + var bHeight = this.get('border-box-height'); + if (bHeight <= 0) return 0; - return match; - }, + var bTop = this.get('border-top'), + bBottom = this.get('border-bottom'); - toString: function() { - return this.expression; - }, + var pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); - inspect: function() { - return "#<Selector:" + this.expression.inspect() + ">"; - } -}); + if (!this._preComputing) this._end(); -if (Prototype.BrowserFeatures.SelectorsAPI && - document.compatMode === 'BackCompat') { - Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){ - var div = document.createElement('div'), - span = document.createElement('span'); - - div.id = "prototype_test_id"; - span.className = 'Test'; - div.appendChild(span); - var isIgnored = (div.querySelector('#prototype_test_id .test') !== null); - div = span = null; - return isIgnored; - })(); -} + return bHeight - bTop - bBottom - pTop - pBottom; + }, -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, len = p.length, name; - - var exclusion = []; - while (e && le != e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - name = p[i].name - if (m = e.match(p[i].re)) { - v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m); - exclusion.push("(" + v.substring(1, v.length - 1) + ")"); - e = e.replace(m[0], ''); - break; - } - } - } - return "[not(" + exclusion.join(" and ") + ")]"; + 'width': function(element) { + if (!this._preComputing) this._begin(); + + var bWidth = this.get('border-box-width'); + if (bWidth <= 0) return 0; + + var bLeft = this.get('border-left'), + bRight = this.get('border-right'); + + var pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + if (!this._preComputing) this._end(); + + return bWidth - bLeft - bRight - pLeft - pRight; + }, + + 'padding-box-height': function(element) { + var height = this.get('height'), + pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + return height + pTop + pBottom; + }, + + 'padding-box-width': function(element) { + var width = this.get('width'), + pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + return width + pLeft + pRight; }, - 'nth-child': function(m) { - return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + + 'border-box-height': function(element) { + return element.offsetHeight; }, - 'nth-last-child': function(m) { - return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + + 'border-box-width': function(element) { + return element.offsetWidth; }, - 'nth-of-type': function(m) { - return Selector.xpath.pseudos.nth("position() ", m); + + 'margin-box-height': function(element) { + var bHeight = this.get('border-box-height'), + mTop = this.get('margin-top'), + mBottom = this.get('margin-bottom'); + + if (bHeight <= 0) return 0; + + return bHeight + mTop + mBottom; }, - 'nth-last-of-type': function(m) { - return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + + 'margin-box-width': function(element) { + var bWidth = this.get('border-box-width'), + mLeft = this.get('margin-left'), + mRight = this.get('margin-right'); + + if (bWidth <= 0) return 0; + + return bWidth + mLeft + mRight; }, - 'first-of-type': function(m) { - m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + + 'top': function(element) { + var offset = element.positionedOffset(); + return offset.top; }, - 'last-of-type': function(m) { - m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + + 'bottom': function(element) { + var offset = element.positionedOffset(), + parent = element.getOffsetParent(), + pHeight = parent.measure('height'); + + var mHeight = this.get('border-box-height'); + + return pHeight - mHeight - offset.top; }, - 'only-of-type': function(m) { - var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + + 'left': function(element) { + var offset = element.positionedOffset(); + return offset.left; }, - 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 }); - } + + 'right': function(element) { + var offset = element.positionedOffset(), + parent = element.getOffsetParent(), + pWidth = parent.measure('width'); + + var mWidth = this.get('border-box-width'); + + return pWidth - mWidth - offset.left; + }, + + 'padding-top': function(element) { + return getPixelValue(element, 'paddingTop'); + }, + + 'padding-bottom': function(element) { + return getPixelValue(element, 'paddingBottom'); + }, + + 'padding-left': function(element) { + return getPixelValue(element, 'paddingLeft'); + }, + + 'padding-right': function(element) { + return getPixelValue(element, 'paddingRight'); + }, + + 'border-top': function(element) { + return Object.isNumber(element.clientTop) ? element.clientTop : + getPixelValue(element, 'borderTopWidth'); + }, + + 'border-bottom': function(element) { + return Object.isNumber(element.clientBottom) ? element.clientBottom : + getPixelValue(element, 'borderBottomWidth'); + }, + + 'border-left': function(element) { + return Object.isNumber(element.clientLeft) ? element.clientLeft : + getPixelValue(element, 'borderLeftWidth'); + }, + + 'border-right': function(element) { + return Object.isNumber(element.clientRight) ? element.clientRight : + getPixelValue(element, 'borderRightWidth'); + }, + + 'margin-top': function(element) { + return getPixelValue(element, 'marginTop'); + }, + + 'margin-bottom': function(element) { + return getPixelValue(element, 'marginBottom'); + }, + + 'margin-left': function(element) { + return getPixelValue(element, 'marginLeft'); + }, + + 'margin-right': function(element) { + return getPixelValue(element, 'marginRight'); } } - }, + }); - 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: [ - { name: 'laterSibling', re: /^\s*~\s*/ }, - { name: 'child', re: /^\s*>\s*/ }, - { name: 'adjacent', re: /^\s*\+\s*/ }, - { name: 'descendant', re: /^\s/ }, - - { name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ }, - { name: 'id', re: /^#([\w\-\*]+)(\b|$)/ }, - { name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ }, - { name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ }, - { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ }, - { name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ } - ], - - assertions: { - tagName: function(element, matches) { - return matches[1].toUpperCase() == element.tagName.toUpperCase(); + if ('getBoundingClientRect' in document.documentElement) { + Object.extend(Element.Layout.COMPUTATIONS, { + 'right': function(element) { + var parent = hasLayout(element.getOffsetParent()); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.right - rect.right).round(); + }, + + 'bottom': function(element) { + var parent = hasLayout(element.getOffsetParent()); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.bottom - rect.bottom).round(); + } + }); + } + + Element.Offset = Class.create({ + initialize: function(left, top) { + this.left = left.round(); + this.top = top.round(); + + this[0] = this.left; + this[1] = this.top; }, - className: function(element, matches) { - return Element.hasClassName(element, matches[1]); + relativeTo: function(offset) { + return new Element.Offset( + this.left - offset.left, + this.top - offset.top + ); }, - id: function(element, matches) { - return element.id === matches[1]; + inspect: function() { + return "#<Element.Offset left: #{left} top: #{top}>".interpolate(this); }, - attrPresence: function(element, matches) { - return Element.hasAttribute(element, matches[1]); + toString: function() { + return "[#{left}, #{top}]".interpolate(this); }, - attr: function(element, matches) { - var nodeValue = Element.readAttribute(element, matches[1]); - return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + toArray: function() { + return [this.left, this.top]; } - }, + }); - handlers: { - concat: function(a, b) { - for (var i = 0, node; node = b[i]; i++) - a.push(node); - return a; - }, + function getLayout(element, preCompute) { + return new Element.Layout(element, preCompute); + } - mark: function(nodes) { - var _true = Prototype.emptyFunction; - for (var i = 0, node; node = nodes[i]; i++) - node._countedByPrototype = _true; - return nodes; - }, + function measure(element, property) { + return $(element).getLayout().get(property); + } - unmark: (function(){ + function getDimensions(element) { + var layout = $(element).getLayout(); + return { + width: layout.get('width'), + height: layout.get('height') + }; + } - var PROPERTIES_ATTRIBUTES_MAP = (function(){ - var el = document.createElement('div'), - isBuggy = false, - propName = '_countedByPrototype', - value = 'x' - el[propName] = value; - isBuggy = (el.getAttribute(propName) === value); - el = null; - return isBuggy; - })(); - - return PROPERTIES_ATTRIBUTES_MAP ? - function(nodes) { - for (var i = 0, node; node = nodes[i]; i++) - node.removeAttribute('_countedByPrototype'); - return nodes; - } : - function(nodes) { - for (var i = 0, node; node = nodes[i]; i++) - node._countedByPrototype = void 0; - return nodes; - } - })(), + function getOffsetParent(element) { + if (isDetached(element)) return $(document.body); - 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++; + var isInline = (Element.getStyle(element, 'display') === 'inline'); + if (!isInline && 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.nodeName === 'HTML') ? $(document.body) : $(element); } - }, + } - unique: function(nodes) { - if (nodes.length == 0) return nodes; - var results = [], n; - for (var i = 0, l = nodes.length; i < l; i++) - if (typeof (n = nodes[i])._countedByPrototype == 'undefined') { - n._countedByPrototype = Prototype.emptyFunction; - results.push(Element.extend(n)); - } - return Selector.handlers.unmark(results); - }, + return $(document.body); + } - 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; - }, + function cumulativeOffset(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return new Element.Offset(valueL, valueT); + } + + function positionedOffset(element) { + var layout = element.getLayout(); - adjacent: function(nodes) { - for (var i = 0, results = [], node; node = nodes[i]; i++) { - var next = this.nextElementSibling(node); - if (next) results.push(next); + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (isBody(element)) break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; } - return results; - }, + } while (element); - 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; - }, + valueL -= layout.get('margin-top'); + valueT -= layout.get('margin-left'); - nextElementSibling: function(node) { - while (node = node.nextSibling) - if (node.nodeType == 1) return node; - return null; - }, + return new Element.Offset(valueL, valueT); + } - previousElementSibling: function(node) { - while (node = node.previousSibling) - if (node.nodeType == 1) return node; - return null; - }, + function cumulativeScrollOffset(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return new Element.Offset(valueL, valueT); + } - tagName: function(nodes, root, tagName, combinator) { - var uTagName = tagName.toUpperCase(); - var results = [], h = Selector.handlers; - if (nodes) { - if (combinator) { - 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); - }, + function viewportOffset(forElement) { + var valueT = 0, valueL = 0, docBody = document.body; - id: function(nodes, root, id, combinator) { - var targetNode = $(id), h = Selector.handlers; + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == docBody && + Element.getStyle(element, 'position') == 'absolute') break; + } while (element = element.offsetParent); - if (root == document) { - if (!targetNode) return []; - if (!nodes) return [targetNode]; - } else { - if (!root.sourceIndex || root.sourceIndex < 1) { - var nodes = root.getElementsByTagName('*'); - for (var j = 0, node; node = nodes[j]; j++) { - if (node.id === id) return [node]; - } - } + element = forElement; + do { + if (element != docBody) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; } + } while (element = element.parentNode); + return new Element.Offset(valueL, valueT); + } - 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] : []; - }, + function absolutize(element) { + element = $(element); - className: function(nodes, root, className, combinator) { - if (nodes && combinator) nodes = this[combinator](nodes); - return Selector.handlers.byClassName(nodes, root, className); - }, + if (Element.getStyle(element, 'position') === 'absolute') { + return element; + } - 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; - }, + var offsetParent = getOffsetParent(element); + var eOffset = element.viewportOffset(), + pOffset = offsetParent.viewportOffset(); - 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; - }, + var offset = eOffset.relativeTo(pOffset); + var layout = element.getLayout(); - 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; - }, + element.store('prototype_absolutize_original_styles', { + left: element.getStyle('left'), + top: element.getStyle('top'), + width: element.getStyle('width'), + height: element.getStyle('height') + }); + + element.setStyle({ + position: 'absolute', + top: offset.top + 'px', + left: offset.left + 'px', + width: layout.get('width') + 'px', + height: layout.get('height') + 'px' + }); + + return element; + } - 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); + function relativize(element) { + element = $(element); + if (Element.getStyle(element, 'position') === 'relative') { + return element; } - }, - 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); - }, + var originalStyles = + element.retrieve('prototype_absolutize_original_styles'); - 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; - }); - }, + if (originalStyles) element.setStyle(originalStyles); + return element; + } - 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; - }, + Element.addMethods({ + getLayout: getLayout, + measure: measure, + getDimensions: getDimensions, + getOffsetParent: getOffsetParent, + cumulativeOffset: cumulativeOffset, + positionedOffset: positionedOffset, + cumulativeScrollOffset: cumulativeScrollOffset, + viewportOffset: viewportOffset, + absolutize: absolutize, + relativize: relativize + }); - 'empty': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) { - if (node.tagName == '!' || node.firstChild) continue; - results.push(node); - } - return results; - }, + function isBody(element) { + return element.nodeName.toUpperCase() === 'BODY'; + } - '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; - }, + function isDetached(element) { + return element !== document.body && + !Element.descendantOf(element, document.body); + } - '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; - }, + if ('getBoundingClientRect' in document.documentElement) { + Element.addMethods({ + viewportOffset: function(element) { + element = $(element); + if (isDetached(element)) return new Element.Offset(0, 0); - 'disabled': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (node.disabled) results.push(node); - return results; - }, + var rect = element.getBoundingClientRect(), + docEl = document.documentElement; + return new Element.Offset(rect.left - docEl.clientLeft, + rect.top - docEl.clientTop); + }, - 'checked': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (node.checked) results.push(node); - return results; - } - }, + positionedOffset: function(element) { + element = $(element); + var parent = element.getOffsetParent(); + if (isDetached(element)) return new Element.Offset(0, 0); - 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 + ' ').include(' ' + v + ' '); }, - '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + - '-').include('-' + (v || "").toUpperCase() + '-'); } - }, + if (element.offsetParent && + element.offsetParent.nodeName.toUpperCase() === 'HTML') { + return positionedOffset(element); + } + + var eOffset = element.viewportOffset(), + pOffset = isBody(parent) ? viewportOffset(parent) : + parent.viewportOffset(); + var retOffset = eOffset.relativeTo(pOffset); - split: function(expression) { - var expressions = []; - expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { - expressions.push(m[1].strip()); + var layout = element.getLayout(); + var top = retOffset.top - layout.get('margin-top'); + var left = retOffset.left - layout.get('margin-left'); + + return new Element.Offset(left, top); + } }); - return expressions; - }, + } +})(); +window.$$ = function() { + var expression = $A(arguments).join(', '); + return Prototype.Selector.select(expression, document); +}; - 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; - }, +Prototype.Selector = (function() { + + function select() { + throw new Error('Method "Prototype.Selector.select" must be defined.'); + } + + function match() { + throw new Error('Method "Prototype.Selector.match" must be defined.'); + } - findElement: function(elements, expression, index) { - if (Object.isNumber(expression)) { - index = expression; expression = false; + function find(elements, expression, index) { + index = index || 0; + var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; + + for (i = 0; i < length; i++) { + if (match(elements[i], expression) && index == matchIndex++) { + return Element.extend(elements[i]); + } } - 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)); + function extendElements(elements) { + for (var i = 0, length = elements.length; i < length; i++) { + Element.extend(elements[i]); } - return (l > 1) ? h.unique(results) : results; + return elements; } + + + var K = Prototype.K; + + return { + select: select, + match: match, + find: find, + extendElements: (Element.extend === K) ? K : extendElements, + extendElement: Element.extend + }; +})(); +Prototype._original_property = window.Sizzle; +/*! + * Sizzle CSS Selector Engine - v1.0 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +[0, 0].sort(function(){ + baseHasDuplicate = false; + return 0; }); -if (Prototype.Browser.IE) { - Object.extend(Selector.handlers, { - concat: function(a, b) { - for (var i = 0, node; node = b[i]; i++) - if (node.tagName !== "!") a.push(node); - return a; - } - }); +var Sizzle = function(selector, context, results, seed) { + results = results || []; + var origContext = context = context || document; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var parts = [], m, set, checkSet, check, mode, extra, prune = true, contextXML = isXML(context), + soFar = selector; + + while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) + selector += parts.shift(); + + set = posProcess( selector, set ); + } + } + } else { + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + var ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + } + + if ( context ) { + var ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray(set); + } else { + prune = false; + } + + while ( parts.length ) { + var cur = parts.pop(), pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + throw "Syntax error, unrecognized expression: " + (cur || selector); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { + for ( var i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + } else { + for ( var i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); + } + } + } + } + + return results; +}; + +Sizzle.matches = function(expr, set){ + return Sizzle(expr, null, null, set); +}; + +Sizzle.find = function(expr, context, isXML){ + var set, match; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var type = Expr.order[i], match; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice(1,1); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = context.getElementsByTagName("*"); + } + + return {set: set, expr: expr}; +}; + +Sizzle.filter = function(expr, set, inplace, not){ + var old = expr, result = [], curLoop = set, match, anyFound, + isXMLFilter = set && set[0] && isXML(set[0]); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.match[ type ].exec( expr )) != null ) { + var filter = Expr.filter[ type ], found, item; + anyFound = false; + + if ( curLoop == result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + } else { + curLoop[i] = false; + } + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + if ( expr == old ) { + if ( anyFound == null ) { + throw "Syntax error, unrecognized expression: " + expr; + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + match: { + ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/ + }, + leftMatch: {}, + attrMap: { + "class": "className", + "for": "htmlFor" + }, + attrHandle: { + href: function(elem){ + return elem.getAttribute("href"); + } + }, + relative: { + "+": function(checkSet, part, isXML){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test(part), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag && !isXML ) { + part = part.toUpperCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + ">": function(checkSet, part, isXML){ + var isPartStr = typeof part === "string"; + + if ( isPartStr && !/\W/.test(part) ) { + part = isXML ? part : part.toUpperCase(); + + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName === part ? parent : false; + } + } + } else { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + "": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck; + + if ( !/\W/.test(part) ) { + var nodeCheck = part = isXML ? part : part.toUpperCase(); + checkFn = dirNodeCheck; + } + + checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + }, + "~": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + var nodeCheck = part = isXML ? part : part.toUpperCase(); + checkFn = dirNodeCheck; + } + + checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + } + }, + find: { + ID: function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? [m] : []; + } + }, + NAME: function(match, context, isXML){ + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], results = context.getElementsByName(match[1]); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + TAG: function(match, context){ + return context.getElementsByTagName(match[1]); + } + }, + preFilter: { + CLASS: function(match, curLoop, inplace, result, not, isXML){ + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) { + if ( !inplace ) + result.push( elem ); + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + ID: function(match){ + return match[1].replace(/\\/g, ""); + }, + TAG: function(match, curLoop){ + for ( var i = 0; curLoop[i] === false; i++ ){} + return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase(); + }, + CHILD: function(match){ + if ( match[1] == "nth" ) { + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + + match[0] = done++; + + return match; + }, + ATTR: function(match, curLoop, inplace, result, not, isXML){ + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + PSEUDO: function(match, curLoop, inplace, result, not){ + if ( match[1] === "not" ) { + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { + result.push.apply( result, ret ); + } + return false; + } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + POS: function(match){ + match.unshift( true ); + return match; + } + }, + filters: { + enabled: function(elem){ + return elem.disabled === false && elem.type !== "hidden"; + }, + disabled: function(elem){ + return elem.disabled === true; + }, + checked: function(elem){ + return elem.checked === true; + }, + selected: function(elem){ + elem.parentNode.selectedIndex; + return elem.selected === true; + }, + parent: function(elem){ + return !!elem.firstChild; + }, + empty: function(elem){ + return !elem.firstChild; + }, + has: function(elem, i, match){ + return !!Sizzle( match[3], elem ).length; + }, + header: function(elem){ + return /h\d/i.test( elem.nodeName ); + }, + text: function(elem){ + return "text" === elem.type; + }, + radio: function(elem){ + return "radio" === elem.type; + }, + checkbox: function(elem){ + return "checkbox" === elem.type; + }, + file: function(elem){ + return "file" === elem.type; + }, + password: function(elem){ + return "password" === elem.type; + }, + submit: function(elem){ + return "submit" === elem.type; + }, + image: function(elem){ + return "image" === elem.type; + }, + reset: function(elem){ + return "reset" === elem.type; + }, + button: function(elem){ + return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON"; + }, + input: function(elem){ + return /input|select|textarea|button/i.test(elem.nodeName); + } + }, + setFilters: { + first: function(elem, i){ + return i === 0; + }, + last: function(elem, i, match, array){ + return i === array.length - 1; + }, + even: function(elem, i){ + return i % 2 === 0; + }, + odd: function(elem, i){ + return i % 2 === 1; + }, + lt: function(elem, i, match){ + return i < match[3] - 0; + }, + gt: function(elem, i, match){ + return i > match[3] - 0; + }, + nth: function(elem, i, match){ + return match[3] - 0 == i; + }, + eq: function(elem, i, match){ + return match[3] - 0 == i; + } + }, + filter: { + PSEUDO: function(elem, match, i, array){ + var name = match[1], filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { + var not = match[3]; + + for ( var i = 0, l = not.length; i < l; i++ ) { + if ( not[i] === elem ) { + return false; + } + } + + return true; + } + }, + CHILD: function(elem, match){ + var type = match[1], node = elem; + switch (type) { + case 'only': + case 'first': + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) return false; + } + if ( type == 'first') return true; + node = elem; + case 'last': + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) return false; + } + return true; + case 'nth': + var first = match[2], last = match[3]; + + if ( first == 1 && last == 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + if ( first == 0 ) { + return diff == 0; + } else { + return ( diff % first == 0 && diff / first >= 0 ); + } + } + }, + ID: function(elem, match){ + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + TAG: function(elem, match){ + return (match === "*" && elem.nodeType === 1) || elem.nodeName === match; + }, + CLASS: function(elem, match){ + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + ATTR: function(elem, match){ + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value != check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + POS: function(elem, match, i, array){ + var name = match[2], filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source ); +} + +var makeArray = function(array, results) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 ); + +} catch(e){ + makeArray = function(array, results) { + var ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + } else { + if ( typeof array.length === "number" ) { + for ( var i = 0, l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + } else { + for ( var i = 0; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + if ( a == b ) { + hasDuplicate = true; + } + return 0; + } + + var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( "sourceIndex" in document.documentElement ) { + sortOrder = function( a, b ) { + if ( !a.sourceIndex || !b.sourceIndex ) { + if ( a == b ) { + hasDuplicate = true; + } + return 0; + } + + var ret = a.sourceIndex - b.sourceIndex; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( document.createRange ) { + sortOrder = function( a, b ) { + if ( !a.ownerDocument || !b.ownerDocument ) { + if ( a == b ) { + hasDuplicate = true; + } + return 0; + } + + var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); + aRange.setStart(a, 0); + aRange.setEnd(a, 0); + bRange.setStart(b, 0); + bRange.setEnd(b, 0); + var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} + +(function(){ + var form = document.createElement("div"), + id = "script" + (new Date).getTime(); + form.innerHTML = "<a name='" + id + "'/>"; + + var root = document.documentElement; + root.insertBefore( form, root.firstChild ); + + if ( !!document.getElementById( id ) ) { + Expr.find.ID = function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + } + }; + + Expr.filter.ID = function(elem, match){ + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + root = form = null; // release memory in IE +})(); + +(function(){ + + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function(match, context){ + var results = context.getElementsByTagName(match[1]); + + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + div.innerHTML = "<a href='#'></a>"; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + Expr.attrHandle.href = function(elem){ + return elem.getAttribute("href", 2); + }; + } + + div = null; // release memory in IE +})(); + +if ( document.querySelectorAll ) (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "<p class='TEST'></p>"; + + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function(query, context, extra, seed){ + context = context || document; + + if ( !seed && context.nodeType === 9 && !isXML(context) ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(e){} + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + div = null; // release memory in IE +})(); + +if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){ + var div = document.createElement("div"); + div.innerHTML = "<div class='test e'></div><div class='test'></div>"; + + if ( div.getElementsByClassName("e").length === 0 ) + return; + + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) + return; + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function(match, context, isXML) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + div = null; // release memory in IE +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + var sibDir = dir == "previousSibling" && !isXML; + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + if ( sibDir && elem.nodeType === 1 ){ + elem.sizcache = doneName; + elem.sizset = i; + } + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } } -function $$() { - return Selector.findChildElements(document, $A(arguments)); +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + var sibDir = dir == "previousSibling" && !isXML; + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + if ( sibDir && elem.nodeType === 1 ) { + elem.sizcache = doneName; + elem.sizset = i; + } + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } } +var contains = document.compareDocumentPosition ? function(a, b){ + return a.compareDocumentPosition(b) & 16; +} : function(a, b){ + return a !== b && (a.contains ? a.contains(b) : true); +}; + +var isXML = function(elem){ + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML"; +}; + +var posProcess = function(selector, context){ + var tmpSet = [], later = "", match, + root = context.nodeType ? [context] : context; + + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + + +window.Sizzle = Sizzle; + +})(); + +;(function(engine) { + var extendElements = Prototype.Selector.extendElements; + + function select(selector, scope) { + return extendElements(engine(selector, scope || document)); + } + + function match(element, selector) { + return engine.matches(selector, [element]).length == 1; + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = match; +})(Sizzle); + +window.Sizzle = Prototype._original_property; +delete Prototype._original_property; + var Form = { reset: function(form) { form = $(form); @@ -4334,8 +5372,12 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { function findElement(event, expression) { var element = Event.element(event); if (!expression) return element; - var elements = [element].concat(element.ancestors()); - return Selector.findElement(elements, expression, 0); + while (element) { + if (Object.isElement(element) && Prototype.Selector.match(element, expression)) { + return Element.extend(element); + } + element = element.parentNode; + } } function pointer(event) { @@ -4504,12 +5546,12 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { window.addEventListener('unload', Prototype.emptyFunction, false); - var _getDOMEventName = Prototype.K; + var _getDOMEventName = Prototype.K, + translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) { _getDOMEventName = function(eventName) { - var translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; - return eventName in translations ? translations[eventName] : eventName; + return (translations[eventName] || eventName); }; } @@ -4543,38 +5585,29 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { element = $(element); var registry = Element.retrieve(element, 'prototype_event_registry'); + if (!registry) return element; - if (Object.isUndefined(registry)) return element; - - if (eventName && !handler) { - var responders = registry.get(eventName); - - if (Object.isUndefined(responders)) return element; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); - return element; - } else if (!eventName) { + if (!eventName) { registry.each( function(pair) { - var eventName = pair.key, responders = pair.value; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); + var eventName = pair.key; + stopObserving(element, eventName); }); return element; } var responders = registry.get(eventName); + if (!responders) return element; - if (!responders) return; + if (!handler) { + responders.each(function(r) { + stopObserving(element, eventName, r.handler); + }); + return element; + } var responder = responders.find( function(r) { return r.handler === handler; }); if (!responder) return element; - var actualEventName = _getDOMEventName(eventName); - if (eventName.include(':')) { if (element.removeEventListener) element.removeEventListener("dataavailable", responder, false); @@ -4583,6 +5616,7 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { element.detachEvent("onfilterchange", responder); } } else { + var actualEventName = _getDOMEventName(eventName); if (element.removeEventListener) element.removeEventListener(actualEventName, responder, false); else @@ -4623,13 +5657,47 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { return Event.extend(event); } + Event.Handler = Class.create({ + initialize: function(element, eventName, selector, callback) { + this.element = $(element); + this.eventName = eventName; + this.selector = selector; + this.callback = callback; + this.handler = this.handleEvent.bind(this); + }, + + start: function() { + Event.observe(this.element, this.eventName, this.handler); + return this; + }, + + stop: function() { + Event.stopObserving(this.element, this.eventName, this.handler); + return this; + }, + + handleEvent: function(event) { + var element = event.findElement(this.selector); + if (element) this.callback.call(this.element, event, element); + } + }); + + function on(element, eventName, selector, callback) { + element = $(element); + if (Object.isFunction(selector) && Object.isUndefined(callback)) { + callback = selector, selector = null; + } + + return new Event.Handler(element, eventName, selector, callback).start(); + } Object.extend(Event, Event.Methods); Object.extend(Event, { fire: fire, observe: observe, - stopObserving: stopObserving + stopObserving: stopObserving, + on: on }); Element.addMethods({ @@ -4637,7 +5705,9 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { observe: observe, - stopObserving: stopObserving + stopObserving: stopObserving, + + on: on }); Object.extend(document, { @@ -4647,6 +5717,8 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { stopObserving: stopObserving.methodize(), + on: on.methodize(), + loaded: false }); @@ -4872,3 +5944,58 @@ Element.ClassNames.prototype = { Object.extend(Element.ClassNames.prototype, Enumerable); /*--------------------------------------------------------------------------*/ + +(function() { + window.Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + }, + + findElements: function(rootElement) { + return Prototype.Selector.select(this.expression, rootElement); + }, + + match: function(element) { + return Prototype.Selector.match(element, this.expression); + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector: " + this.expression + ">"; + } + }); + + Object.extend(Selector, { + matchElements: function(elements, expression) { + var match = Prototype.Selector.match, + results = []; + + for (var i = 0, length = elements.length; i < length; i++) { + var element = elements[i]; + if (match(element, expression)) { + results.push(Element.extend(element)); + } + } + return results; + }, + + findElement: function(elements, expression, index) { + index = index || 0; + var matchIndex = 0, element; + for (var i = 0, length = elements.length; i < length; i++) { + element = elements[i]; + if (Prototype.Selector.match(element, expression) && index === matchIndex++) { + return Element.extend(element); + } + } + }, + + findChildElements: function(element, expressions) { + var selector = expressions.toArray().join(', '); + return Prototype.Selector.select(selector, element || document); + } + }); +})(); diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt index 86564031f5..a8f7aeac7d 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt @@ -3,7 +3,7 @@ require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase -<% unless options[:skip_activerecord] -%> +<% unless options[:skip_active_record] -%> # 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 diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb index 0dfb5cd1c9..3376b422cb 100644 --- a/railties/lib/rails/generators/test_case.rb +++ b/railties/lib/rails/generators/test_case.rb @@ -51,7 +51,7 @@ module Rails # Sets default arguments on generator invocation. This can be overwritten when # invoking it. # - # arguments %w(app_name --skip-activerecord) + # arguments %w(app_name --skip-active-record) # def self.arguments(array) self.default_arguments = array @@ -68,7 +68,7 @@ module Rails # Captures the given stream and returns it: # # stream = capture(:stdout){ puts "Cool" } - # stream #=> "Cool\n" + # stream # => "Cool\n" # def capture(stream) begin @@ -214,8 +214,8 @@ module Rails # destination File.expand_path("../tmp", File.dirname(__FILE__)) # teardown :cleanup_destination_root # - # test "database.yml is not created when skipping activerecord" do - # run_generator %w(myapp --skip-activerecord) + # test "database.yml is not created when skipping Active Record" do + # run_generator %w(myapp --skip-active-record) # assert_no_file "config/database.yml" # end # end diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index e9c3ebe685..6cbd1f21c0 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -1,4 +1,3 @@ -require "active_support/core_ext/object/misc" require "cgi" require "active_support/core_ext/cgi" @@ -89,7 +88,7 @@ module Rails end property 'Middleware' do - Rails.configuration.middleware.active.map(&:inspect) + Rails.configuration.middleware.map(&:inspect) end # The application's location on the filesystem. diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 7a65188a9a..d303212f52 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -116,36 +116,20 @@ module Rails @paths.concat paths end - def autoload_once! - @autoload_once = true - end - - def autoload_once? - @autoload_once - end - - def eager_load! - @eager_load = true - end - - def eager_load? - @eager_load - end - - def autoload! - @autoload = true - end - - def autoload? - @autoload - end + %w(autoload_once eager_load autoload load_path).each do |m| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{m}! + @#{m} = true + end - def load_path! - @load_path = true - end + def skip_#{m}! + @#{m} = false + end - def load_path? - @load_path + def #{m}? + @#{m} + end + RUBY end def paths diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb index 3fa45156c3..011ac6cecc 100644 --- a/railties/lib/rails/rack/log_tailer.rb +++ b/railties/lib/rails/rack/log_tailer.rb @@ -6,7 +6,6 @@ module Rails path = Pathname.new(log || "#{File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath @cursor = ::File.size(path) - @last_checked = Time.now.to_f @file = ::File.open(path, 'r') end @@ -20,11 +19,9 @@ module Rails def tail! @file.seek @cursor - mod = @file.mtime.to_f - if mod > @last_checked + if !@file.eof? contents = @file.read - @last_checked = mod - @cursor += contents.size + @cursor = @file.tell $stdout.print contents end end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index dbdbfea509..8a6e2716dc 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -6,53 +6,53 @@ require 'active_support/deprecation' module Rails # Railtie is the core of the Rails Framework and provides several hooks to extend # Rails and/or modify the initialization process. - # + # # Every major component of Rails (Action Mailer, Action Controller, # Action View, Active Record and Active Resource) are all Railties, so each of # them is responsible to set their own initialization. This makes, for example, # Rails absent of any Active Record hook, allowing any other ORM framework to hook in. - # + # # Developing a Rails extension does _not_ require any implementation of # Railtie, but if you need to interact with the Rails framework during # or after boot, then Railtie is what you need to do that interaction. - # + # # For example, the following would need you to implement Railtie in your # plugin: - # + # # * creating initializers # * configuring a Rails framework or the Application, like setting a generator # * adding Rails config.* keys to the environment # * setting up a subscriber to the Rails +ActiveSupport::Notifications+ # * adding rake tasks into rails - # + # # == Creating your Railtie # # Implementing Railtie in your Rails extension is done by creating a class # Railtie that has your extension name and making sure that this gets loaded # during boot time of the Rails stack. - # + # # You can do this however you wish, but here is an example if you want to provide # it for a gem that can be used with or without Rails: - # + # # * Create a file (say, lib/my_gem/railtie.rb) which contains class Railtie inheriting from # Rails::Railtie and is namespaced to your gem: # - # # lib/my_gem/railtie.rb - # module MyGem - # class Railtie < Rails::Railtie + # # lib/my_gem/railtie.rb + # module MyGem + # class Railtie < Rails::Railtie + # end # end - # end - # + # # * Require your own gem as well as rails in this file: - # - # # lib/my_gem/railtie.rb - # require 'my_gem' - # require 'rails' - # - # module MyGem - # class Railtie < Rails::Railtie + # + # # lib/my_gem/railtie.rb + # require 'my_gem' + # require 'rails' + # + # module MyGem + # class Railtie < Rails::Railtie + # end # end - # end # # == Initializers # @@ -65,12 +65,12 @@ module Rails # end # end # - # If specified, the block can also receive the application object, in case you + # If specified, the block can also receive the application object, in case you # need to access some application specific configuration, like middleware: # # class MyRailtie < Rails::Railtie # initializer "my_railtie.configure_rails_initialization" do |app| - # app.middlewares.use MyRailtie::Middleware + # app.middleware.use MyRailtie::Middleware # end # end # @@ -156,6 +156,12 @@ module Rails @rake_tasks end + def console(&blk) + @load_console ||= [] + @load_console << blk if blk + @load_console + end + def generators(&blk) @generators ||= [] @generators << blk if blk @@ -170,20 +176,16 @@ module Rails def eager_load! end - def rake_tasks - self.class.rake_tasks - end - - def generators - self.class.generators + def load_console + self.class.console.each(&:call) end def load_tasks - rake_tasks.each { |blk| blk.call } + self.class.rake_tasks.each(&:call) end def load_generators - generators.each { |blk| blk.call } + self.class.generators.each(&:call) end end end diff --git a/railties/lib/rails/script_rails_loader.rb b/railties/lib/rails/script_rails_loader.rb index 8fbd3bf492..91672e5d81 100644 --- a/railties/lib/rails/script_rails_loader.rb +++ b/railties/lib/rails/script_rails_loader.rb @@ -7,6 +7,7 @@ module Rails def self.exec_script_rails! cwd = Dir.pwd + return unless in_rails_application? || in_rails_application_subdirectory? exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application? Dir.chdir("..") do # Recurse in a chdir block: if the search fails we want to be sure @@ -18,7 +19,7 @@ module Rails end def self.in_rails_application? - File.exists?(SCRIPT_RAILS) || in_rails_application_subdirectory? + File.exists?(SCRIPT_RAILS) end def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd)) diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake index 492f05e3cc..c1b1a41d48 100644 --- a/railties/lib/rails/tasks/documentation.rake +++ b/railties/lib/rails/tasks/documentation.rake @@ -58,43 +58,43 @@ namespace :doc do rdoc.rdoc_files.include('README') gem_path('actionmailer') do |actionmailer| - %w(README CHANGELOG MIT-LICENSE lib/action_mailer/base.rb).each do |file| + %w(README.rdoc CHANGELOG MIT-LICENSE lib/action_mailer/base.rb).each do |file| rdoc.rdoc_files.include("#{actionmailer}/#{file}") end end gem_path('actionpack') do |actionpack| - %w(README CHANGELOG MIT-LICENSE lib/action_controller/**/*.rb lib/action_view/**/*.rb).each do |file| + %w(README.rdoc CHANGELOG MIT-LICENSE lib/action_controller/**/*.rb lib/action_view/**/*.rb).each do |file| rdoc.rdoc_files.include("#{actionpack}/#{file}") end end gem_path('activemodel') do |activemodel| - %w(README CHANGELOG MIT-LICENSE lib/active_model/**/*.rb).each do |file| + %w(README.rdoc CHANGELOG MIT-LICENSE lib/active_model/**/*.rb).each do |file| rdoc.rdoc_files.include("#{activemodel}/#{file}") end end gem_path('activerecord') do |activerecord| - %w(README CHANGELOG lib/active_record/**/*.rb).each do |file| + %w(README.rdoc CHANGELOG lib/active_record/**/*.rb).each do |file| rdoc.rdoc_files.include("#{activerecord}/#{file}") end end gem_path('activeresource') do |activeresource| - %w(README CHANGELOG lib/active_resource.rb lib/active_resource/*).each do |file| + %w(README.rdoc CHANGELOG lib/active_resource.rb lib/active_resource/*).each do |file| rdoc.rdoc_files.include("#{activeresource}/#{file}") end end gem_path('activesupport') do |activesupport| - %w(README CHANGELOG lib/active_support/**/*.rb).each do |file| + %w(README.rdoc CHANGELOG lib/active_support/**/*.rb).each do |file| rdoc.rdoc_files.include("#{activesupport}/#{file}") end end gem_path('railties') do |railties| - %w(README CHANGELOG lib/{*.rb,commands/*.rb,generators/*.rb}).each do |file| + %w(README.rdoc CHANGELOG lib/{*.rb,commands/*.rb,generators/*.rb}).each do |file| rdoc.rdoc_files.include("#{railties}/#{file}") end end diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index e7bd0c38dc..443dacd739 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -54,7 +54,7 @@ namespace :rails do namespace :update do def invoke_from_app_generator(method) - app_generator.invoke(method) + app_generator.send(method) end def app_generator diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index ecd513c2c8..38c14fcd6b 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -70,7 +70,7 @@ module Kernel end end -desc 'Runs test:unit, test:functional, test:integration together (also available: test:benchmark, test:profile, test:plugins)' +desc 'Runs test:units, test:functionals, test:integration together (also available: test:benchmark, test:profile, test:plugins)' task :test do errors = %w(test:units test:functionals test:integration).collect do |task| begin diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 34d96fd0f3..c5d1d02bc1 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 247b926af9..d76b74190b 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -13,14 +13,14 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'rails' - s.files = Dir['CHANGELOG', 'README', 'bin/**/*', 'guides/**/*', 'lib/**/{*,.[a-z]*}'] + s.files = Dir['CHANGELOG', 'README.rdoc', 'bin/**/*', 'guides/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' s.rdoc_options << '--exclude' << '.' s.has_rdoc = false s.add_dependency('rake', '>= 0.8.3') - s.add_dependency('thor', '~> 0.13.7') + s.add_dependency('thor', '~> 0.14.0') s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 8ff69f0208..a72e6916dd 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -9,10 +9,8 @@ class ConsoleTest < Test::Unit::TestCase end def load_environment - # Load steps taken from rails/commands/console.rb require "#{rails_root}/config/environment" - require 'rails/console/app' - require 'rails/console/helpers' + Rails.application.load_console end def test_app_method_should_return_integration_session @@ -75,4 +73,21 @@ class ConsoleTest < Test::Unit::TestCase assert_equal 'Once upon a time in a world...', helper.truncate('Once upon a time in a world far far away') end + + def test_active_record_does_not_panic_when_referencing_an_observed_constant + add_to_config "config.active_record.observers = :user_observer" + + app_file "app/models/user.rb", <<-MODEL + class User < ActiveRecord::Base + end + MODEL + + app_file "app/models/user_observer.rb", <<-MODEL + class UserObserver < ActiveRecord::Observer + end + MODEL + + load_environment + assert_nothing_raised { User } + end end diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index cbf0decd07..d258625f42 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -103,5 +103,20 @@ module ApplicationTests assert_equal({ :plugin => { :generator => "-g" } }, c.generators.aliases) end end + + test "generators with string and hash for options should generate symbol keys" do + with_bare_config do |c| + c.generators do |g| + g.orm 'datamapper', :migration => false + end + + expected = { + :rails => { :orm => :datamapper }, + :datamapper => { :migration => false } + } + + assert_equal expected, c.generators.options + end + end end end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 4ff10091b1..d8e916f45f 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -98,7 +98,17 @@ module ApplicationTests require "#{app_path}/config/environment" - expects = [ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActiveRecord::SessionStore] + expects = [ActiveRecord::QueryCache, ActiveRecord::SessionStore] + middleware = Rails.application.config.middleware.map { |m| m.klass } + assert_equal expects, middleware & expects + end + + test "database middleware initializes when allow concurrency is true" do + add_to_config "config.threadsafe!" + + require "#{app_path}/config/environment" + + expects = [ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache] middleware = Rails.application.config.middleware.map { |m| m.klass } assert_equal expects, middleware & expects end diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb index fc8548af1f..7e035be764 100644 --- a/railties/test/application/initializers/notifications_test.rb +++ b/railties/test/application/initializers/notifications_test.rb @@ -1,17 +1,6 @@ require "isolation/abstract_unit" module ApplicationTests - class MockLogger - def method_missing(*args) - @logged ||= [] - @logged << args.last - end - - def logged - @logged.compact.map { |l| l.to_s.strip } - end - end - class NotificationsTest < Test::Unit::TestCase include ActiveSupport::Testing::Isolation @@ -34,15 +23,17 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" + require "active_support/log_subscriber/test_helper" - ActiveRecord::Base.logger = logger = MockLogger.new + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActiveRecord::Base.logger = logger # Mimic Active Record notifications instrument "sql.active_record", :name => "SQL", :sql => "SHOW tables" wait - assert_equal 1, logger.logged.size - assert_match /SHOW tables/, logger.logged.last + assert_equal 1, logger.logged(:debug).size + assert_match /SHOW tables/, logger.logged(:debug).last end end end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index e66e81ea2c..1f127cee78 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -28,18 +28,18 @@ module ApplicationTests "ActionDispatch::RemoteIp", "Rack::Sendfile", "ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement", "ActiveRecord::QueryCache", "ActionDispatch::Cookies", "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", "ActionDispatch::ParamsParser", "Rack::MethodOverride", - "ActionDispatch::Head" + "ActionDispatch::Head", + "ActionDispatch::BestStandardsSupport" ], middleware end - test "removing activerecord omits its middleware" do + test "removing Active Record omits its middleware" do use_frameworks [] boot! assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement") diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index f0268164d0..febc53bac9 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -11,19 +11,19 @@ module ApplicationTests extend Rack::Test::Methods end - def app + def app(env = "production") + old_env = ENV["RAILS_ENV"] + @app ||= begin + ENV["RAILS_ENV"] = env require "#{app_path}/config/environment" Rails.application end + ensure + ENV["RAILS_ENV"] = old_env end - test "rails/info/properties" do - get "/rails/info/properties" - assert_equal 200, last_response.status - end - - test "simple controller" do + def simple_controller controller :foo, <<-RUBY class FooController < ApplicationController def index @@ -37,11 +37,42 @@ module ApplicationTests match ':controller(/:action)' end RUBY + end + + test "rails/info/properties in development" do + app("development") + get "/rails/info/properties" + assert_equal 200, last_response.status + end + + test "rails/info/properties in production" do + app("production") + get "/rails/info/properties" + assert_equal 404, last_response.status + end + + test "simple controller" do + simple_controller get '/foo' assert_equal 'foo', last_response.body end + test "simple controller in production mode returns best standards" do + simple_controller + + get '/foo' + assert_equal "IE=Edge,chrome=1", last_response.headers["X-UA-Compatible"] + end + + test "simple controller in development mode leaves out Chrome" do + simple_controller + app("development") + + get "/foo" + assert_equal "IE=Edge", last_response.headers["X-UA-Compatible"] + end + test "simple controller with helper" do controller :foo, <<-RUBY class FooController < ApplicationController @@ -146,38 +177,42 @@ module ApplicationTests assert_equal 'admin::foo', last_response.body end - test "reloads routes when configuration is changed" do - controller :foo, <<-RUBY - class FooController < ApplicationController - def bar - render :text => "bar" + {"development" => "baz", "production" => "bar"}.each do |mode, expected| + test "reloads routes when configuration is changed in #{mode}" do + controller :foo, <<-RUBY + class FooController < ApplicationController + def bar + render :text => "bar" + end + + def baz + render :text => "baz" + end end + RUBY - def baz - render :text => "baz" + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do |map| + match 'foo', :to => 'foo#bar' end - end - RUBY + RUBY - app_file 'config/routes.rb', <<-RUBY - AppTemplate::Application.routes.draw do |map| - match 'foo', :to => 'foo#bar' - end - RUBY + app(mode) - get '/foo' - assert_equal 'bar', last_response.body + get '/foo' + assert_equal 'bar', last_response.body - app_file 'config/routes.rb', <<-RUBY - AppTemplate::Application.routes.draw do |map| - match 'foo', :to => 'foo#baz' - end - RUBY + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do |map| + match 'foo', :to => 'foo#baz' + end + RUBY - sleep 0.1 + sleep 0.1 - get '/foo' - assert_equal 'baz', last_response.body + get '/foo' + assert_equal expected, last_response.body + end end test 'resource routing with irrigular inflection' do diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 6a3b5de9de..08cfb585f9 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -58,6 +58,12 @@ class AppGeneratorTest < Rails::Generators::TestCase DEFAULT_APP_FILES.each{ |path| assert_file path } end + def test_application_generate_pretend + run_generator ["testapp", "--pretend"] + + DEFAULT_APP_FILES.each{ |path| assert_no_file path } + end + def test_application_controller_and_layout_files run_generator assert_file "app/views/layouts/application.html.erb", /stylesheet_link_tag :all/ @@ -65,7 +71,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_options_before_application_name_raises_an_error - content = capture(:stderr){ run_generator(["--skip-activerecord", destination_root]) } + content = capture(:stderr){ run_generator(["--skip-active-record", destination_root]) } assert_equal "Options should be given after the application name. For details run: rails --help\n", content end @@ -100,6 +106,30 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "things-43/config/application.rb", /^module Things43$/ end + def test_application_name_is_detected_if_it_exists_and_app_folder_renamed + app_root = File.join(destination_root, "myapp") + app_moved_root = File.join(destination_root, "myapp_moved") + + run_generator [app_root] + + Rails.application.config.root = app_moved_root + Rails.application.class.stubs(:name).returns("Myapp") + Rails.application.stubs(:is_a?).returns(Rails::Application) + + FileUtils.mv(app_root, app_moved_root) + + # forces the shell to automatically overwrite all files + Thor::Base.shell.send(:attr_accessor, :always_force) + shell = Thor::Base.shell.new + shell.send(:always_force=, true) + + generator = Rails::Generators::AppGenerator.new ["rails"], { :with_dispatchers => true }, + :destination_root => app_moved_root, :shell => shell + generator.send(:app_const) + silence(:stdout){ generator.send(:create_config_files) } + assert_file "myapp_moved/config/environment.rb", /Myapp::Application\.initialize!/ + end + def test_application_names_are_not_singularized run_generator [File.join(destination_root, "hats")] assert_file "hats/config/environment.rb", /Hats::Application\.initialize!/ @@ -117,26 +147,31 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile", /^gem\s+["']mysql["']$/ end - def test_config_database_is_not_added_if_skip_activerecord_is_given - run_generator [destination_root, "--skip-activerecord"] + def test_config_database_is_not_added_if_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] assert_no_file "config/database.yml" end - def test_activerecord_is_removed_from_frameworks_if_skip_activerecord_is_given - run_generator [destination_root, "--skip-activerecord"] + def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ end def test_prototype_and_test_unit_are_added_by_default run_generator + assert_file "config/application.rb", /#\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(jquery rails\)/ + assert_file "public/javascripts/application.js" assert_file "public/javascripts/prototype.js" + assert_file "public/javascripts/rails.js" assert_file "test" end def test_prototype_and_test_unit_are_skipped_if_required - run_generator [destination_root, "--skip-prototype", "--skip-testunit"] + run_generator [destination_root, "--skip-prototype", "--skip-test-unit"] + assert_file "config/application.rb", /^\s+config\.action_view\.javascript_expansions\[:defaults\]\s+=\s+%w\(\)/ + assert_file "public/javascripts/application.js" assert_no_file "public/javascripts/prototype.js" - assert_file "public/javascripts" + assert_no_file "public/javascripts/rails.js" assert_no_file "test" end @@ -167,7 +202,7 @@ class AppGeneratorTest < Rails::Generators::TestCase template.instance_eval "def read; self; end" # Make the string respond to read generator([destination_root], :template => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - assert_match /It works!/, silence(:stdout){ generator.invoke } + assert_match /It works!/, silence(:stdout){ generator.invoke_all } end def test_usage_read_from_file @@ -191,14 +226,14 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_dev_option generator([destination_root], :dev => true).expects(:run).with("#{@bundle_command} install") - silence(:stdout){ generator.invoke } + silence(:stdout){ generator.invoke_all } rails_path = File.expand_path('../../..', Rails.root) assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:path\s+=>\s+["']#{Regexp.escape(rails_path)}["']$/ end def test_edge_option generator([destination_root], :edge => true).expects(:run).with("#{@bundle_command} install") - silence(:stdout){ generator.invoke } + silence(:stdout){ generator.invoke_all } assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:git\s+=>\s+["']#{Regexp.escape("git://github.com/rails/rails.git")}["']$/ end @@ -262,7 +297,7 @@ class CustomAppGeneratorTest < Rails::Generators::TestCase template.instance_eval "def read; self; end" # Make the string respond to read generator([destination_root], :builder => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - capture(:stdout) { generator.invoke } + capture(:stdout) { generator.invoke_all } DEFAULT_APP_FILES.each{ |path| assert_no_file path } end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index ea469cb3c8..f12445ae35 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -216,4 +216,19 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Stylesheets (should not be removed) assert_file "public/stylesheets/scaffold.css" end + + def test_scaffold_generator_on_revoke_does_not_mutilate_legacy_map_parameter + run_generator + + # Add a |map| parameter to the routes block manually + route_path = File.expand_path("config/routes.rb", destination_root) + content = File.read(route_path).gsub(/\.routes\.draw do/) do |match| + "#{match} |map|" + end + File.open(route_path, "wb") { |file| file.write(content) } + + run_generator ["product_line"], :behavior => :revoke + + assert_file "config/routes.rb", /\.routes\.draw do\s*\|map\|\s*$/ + end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 74a09d4bde..f93800a5ae 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -108,7 +108,7 @@ class GeneratorsTest < Rails::Generators::TestCase assert_match /^ fixjour$/, output end - def test_rails_generators_does_not_show_activerecord_hooks + def test_rails_generators_does_not_show_active_record_hooks output = capture(:stdout){ Rails::Generators.help } assert_match /ActiveRecord:/, output assert_match /^ active_record:fixjour$/, output diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 008247cb1a..80fae8c543 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -118,13 +118,23 @@ class PathsTest < ActiveSupport::TestCase assert_raise(RuntimeError) { @root << "/biz" } end - test "it is possible to add a path that should be loaded only once" do + test "it is possible to add a path that should be autoloaded only once" do @root.app = "/app" @root.app.autoload_once! assert @root.app.autoload_once? assert @root.autoload_once.include?(@root.app.paths.first) end + test "it is possible to remove a path that should be autoloaded only once" do + @root.app = "/app" + @root.app.autoload_once! + assert @root.app.autoload_once? + + @root.app.skip_autoload_once! + assert !@root.app.autoload_once? + assert !@root.autoload_once.include?(@root.app.paths.first) + end + test "it is possible to add a path without assignment and specify it should be loaded only once" do @root.app "/app", :autoload_once => true assert @root.app.autoload_once? @@ -152,13 +162,23 @@ class PathsTest < ActiveSupport::TestCase assert_equal 2, @root.autoload_once.size end - test "it is possible to mark a path as eager" do + test "it is possible to mark a path as eager loaded" do @root.app = "/app" @root.app.eager_load! assert @root.app.eager_load? assert @root.eager_load.include?(@root.app.paths.first) end + test "it is possible to skip a path from eager loading" do + @root.app = "/app" + @root.app.eager_load! + assert @root.app.eager_load? + + @root.app.skip_eager_load! + assert !@root.app.eager_load? + assert !@root.eager_load.include?(@root.app.paths.first) + end + test "it is possible to add a path without assignment and mark it as eager" do @root.app "/app", :eager_load => true assert @root.app.eager_load? diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index c74cc01dc1..db0fd87491 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -103,6 +103,22 @@ module RailtiesTest assert $ran_block end + test "console block is executed when MyApp.load_console is called" do + $ran_block = false + + class MyTie < Rails::Railtie + console do + $ran_block = true + end + end + + require "#{app_path}/config/environment" + + assert !$ran_block + AppTemplate::Application.load_console + assert $ran_block + end + test "railtie can add initializers" do $ran_block = false diff --git a/version.rb b/version.rb index 34d96fd0f3..c5d1d02bc1 100644 --- a/version.rb +++ b/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end |