diff options
416 files changed, 7063 insertions, 4554 deletions
@@ -3,30 +3,30 @@ source 'https://rubygems.org' gemspec if ENV['AREL'] - gem 'arel', :path => ENV['AREL'] + gem 'arel', path: ENV['AREL'] else gem 'arel' end -gem 'rack-test', :git => "git://github.com/brynary/rack-test.git" +gem 'rack-test', github: "brynary/rack-test" gem 'bcrypt-ruby', '~> 3.0.0' gem 'jquery-rails' if ENV['JOURNEY'] - gem 'journey', :path => ENV['JOURNEY'] + gem 'journey', path: ENV['JOURNEY'] else - gem 'journey', :git => "git://github.com/rails/journey" + gem 'journey', github: "rails/journey" end if ENV['AR_DEPRECATED_FINDERS'] gem 'active_record_deprecated_finders', path: ENV['AR_DEPRECATED_FINDERS'] else - gem 'active_record_deprecated_finders', git: 'git://github.com/rails/active_record_deprecated_finders' + gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders' end # This needs to be with require false to avoid # it being automatically loaded by sprockets -gem 'uglifier', '>= 1.0.3', :require => false +gem 'uglifier', '>= 1.0.3', require: false gem 'rake', '>= 0.8.7' gem 'mocha', '>= 0.11.2' @@ -36,7 +36,7 @@ group :doc do # to a bug, but the PR that fixes it has been there # for some weeks unapplied. As a temporary solution # this is our own fork with the fix. - gem 'sdoc', :git => 'git://github.com/fxn/sdoc.git' + gem 'sdoc', github: 'fxn/sdoc' gem 'RedCloth', '~> 4.2' gem 'w3c_validators' end @@ -50,7 +50,7 @@ instance_eval File.read local_gemfile if File.exists? local_gemfile platforms :mri do group :test do - gem 'ruby-prof' + gem 'ruby-prof', '0.10.8' end end @@ -90,9 +90,9 @@ if ENV['ORACLE_ENHANCED_PATH'] || ENV['ORACLE_ENHANCED'] gem 'ruby-oci8', '>= 2.0.4' end if ENV['ORACLE_ENHANCED_PATH'] - gem 'activerecord-oracle_enhanced-adapter', :path => ENV['ORACLE_ENHANCED_PATH'] + gem 'activerecord-oracle_enhanced-adapter', path: ENV['ORACLE_ENHANCED_PATH'] else - gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' + gem 'activerecord-oracle_enhanced-adapter', github: 'rsim/oracle-enhanced' end end diff --git a/Rakefile b/Rakefile index 0d218844b2..21eb60bbe1 100755..100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,3 @@ -#!/usr/bin/env rake - require 'rdoc/task' require 'sdoc' require 'net/http' diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 755717cfba..cf10bfffdb 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -22,12 +22,12 @@ the email. This can be as simple as: class Notifier < ActionMailer::Base - delivers_from 'system@loudthinking.com' + default from: 'system@loudthinking.com' def welcome(recipient) @recipient = recipient - mail(:to => recipient, - :subject => "[Signed up] Welcome #{recipient}") + mail(to: recipient, + subject: "[Signed up] Welcome #{recipient}") end end diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index 8f5aeb9603..c1c4171cdf 100755..100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 1045dd58ef..e45a1cd5ff 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -21,9 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -actionpack_path = File.expand_path('../../../actionpack/lib', __FILE__) -$:.unshift(actionpack_path) if File.directory?(actionpack_path) && !$:.include?(actionpack_path) - require 'abstract_controller' require 'action_view' require 'action_mailer/version' diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index c508512ff6..e24a7f52d5 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -486,7 +486,7 @@ module ActionMailer #:nodoc: self.class.mailer_name end - # Allows you to pass random and unusual headers to the new +Mail::Message+ object + # Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt> object # which will add them to itself. # # headers['X-Special-Domain-Specific-Header'] = "SecretValue" @@ -497,7 +497,7 @@ module ActionMailer #:nodoc: # headers 'X-Special-Domain-Specific-Header' => "SecretValue", # 'In-Reply-To' => incoming.message_id # - # The resulting Mail::Message will have the following in it's header: + # The resulting Mail::Message will have the following in its header: # # X-Special-Domain-Specific-Header: SecretValue def headers(args=nil) diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index d1467fb526..3b38dbccc7 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -65,7 +65,7 @@ module ActionMailer when NilClass raise "Delivery method cannot be nil" when Symbol - if klass = delivery_methods[method.to_sym] + if klass = delivery_methods[method] mail.delivery_method(klass, send(:"#{method}_settings")) else raise "Invalid delivery method #{method.inspect}" diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 3a519253f9..99c44179fd 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -1,16 +1,4 @@ -# Pathname has a warning, so require it first while silencing -# warnings to shut it up. -# -# Also, in 1.9, Bundler creates warnings due to overriding -# Rubygems methods -begin - old, $VERBOSE = $VERBOSE, nil - require 'pathname' - require File.expand_path('../../../load_paths', __FILE__) -ensure - $VERBOSE = old -end - +require File.expand_path('../../../load_paths', __FILE__) require 'active_support/core_ext/kernel/reporting' # These are the normal settings that will be set up by Railties @@ -20,9 +8,6 @@ silence_warnings do Encoding.default_external = "UTF-8" end -lib = File.expand_path("#{File.dirname(__FILE__)}/../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - require 'minitest/autorun' require 'action_mailer' require 'action_mailer/test_case' diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index b3d6ae0ac7..24cbe35acc 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,37 @@ ## Rails 4.0.0 (unreleased) ## +* Copy literal route constraints to defaults so that url generation know about them. + The copied constraints are `:protocol`, `:subdomain`, `:domain`, `:host` and `:port`. + + *Andrew White* + +* `respond_to` and `respond_with` now raise ActionController::UnknownFormat instead + of directly returning head 406. The exception is rescued and converted to 406 + in the exception handling middleware. *Steven Soroka* + +* Allows `assert_redirected_to` to match against a regular expression. *Andy Lindeman* + +* Add backtrace to development routing error page. *Richard Schneeman* + +* Replace `include_seconds` boolean argument with `:include_seconds => true` option + in `distance_of_time_in_words` and `time_ago_in_words` signature. *Dmitriy Kiriyenko* + +* Remove `button_to_function` and `link_to_function` helpers. *Rafael Mendonça França* + +* Make current object and counter (when it applies) variables accessible when + rendering templates with :object / :collection. *Carlos Antonio da Silva* + +* JSONP now uses mimetype application/javascript instead of application/json. *omjokine* + +* Allow to lazy load `default_form_builder` by passing a `String` instead of a constant. *Piotr Sarnacki* + +* Session arguments passed to `process` calls in functional tests are now merged into + the existing session, whereas previously they would replace the existing session. + This change may break some existing tests if they are asserting the exact contents of + the session but should not break existing tests that only assert individual keys. + + *Andrew White* + * Add `index` method to FormBuilder class. *Jorge Bejar* * Remove the leading \n added by textarea on assert_select. *Santiago Pastorino* @@ -5748,7 +5780,7 @@ == Rendering a collection of partials The example of partial use describes a familar pattern where a template needs - to iterate over a array and render a sub template for each of the elements. + to iterate over an array and render a sub template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial by the same name of as the elements contained within. So the three-lined example in "Using partials" can be rewritten with a single line: diff --git a/actionpack/Rakefile b/actionpack/Rakefile index bb1e704767..50e3bb0d48 100755..100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' @@ -24,6 +23,7 @@ end namespace :test do Rake::TestTask.new(:isolated) do |t| + t.libs << 'test' t.pattern = 'test/ts_isolated.rb' end diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index cc5878c88e..b95ea5f0b2 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -1,9 +1,5 @@ -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/concern' -require 'active_support/ruby/shim' require 'active_support/dependencies/autoload' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/module/attr_internal' diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index c0fa28cae9..5705ab590c 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -14,7 +14,7 @@ module AbstractController # Override AbstractController::Base's process_action to run the # process_action callbacks around the normal behavior. def process_action(*args) - run_callbacks(:process_action, action_name) do + run_callbacks(:process_action) do super end end @@ -163,11 +163,11 @@ module AbstractController class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 # Append a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before, name, options) - end # end - end # end + def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before, name, options) + end # end + end # end # Prepend a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. @@ -179,11 +179,11 @@ module AbstractController # Skip a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def skip_#{filter}_filter(*names) # def skip_before_filter(*names) - _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options| - skip_callback(:process_action, :#{filter}, name, options) # skip_callback(:process_action, :before, name, options) - end # end - end # end + def skip_#{filter}_filter(*names) # def skip_before_filter(*names) + _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options| + skip_callback(:process_action, :#{filter}, name, options) # skip_callback(:process_action, :before, name, options) + end # end + end # end # *_filter is the same as append_*_filter alias_method :append_#{filter}_filter, :#{filter}_filter # alias_method :append_before_filter, :before_filter diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 77cc4c07d9..4e0672d590 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -49,9 +49,9 @@ module AbstractController meths.each do |meth| _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 - def #{meth}(*args, &blk) - controller.send(%(#{meth}), *args, &blk) - end + def #{meth}(*args, &blk) # def current_user(*args, &blk) + controller.send(%(#{meth}), *args, &blk) # controller.send(:current_user, *args, &blk) + end # end ruby_eval end end diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 59ec197347..80901b8bf3 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -133,6 +133,8 @@ module ActionController #:nodoc: end def filter(controller) + cache_layout = @cache_layout.respond_to?(:call) ? @cache_layout.call(controller) : @cache_layout + path_options = if @cache_path.respond_to?(:call) controller.instance_exec(controller, &@cache_path) else @@ -144,13 +146,13 @@ module ActionController #:nodoc: body = controller.read_fragment(cache_path.path, @store_options) unless body - controller.action_has_layout = false unless @cache_layout + controller.action_has_layout = false unless cache_layout yield controller.action_has_layout = true body = controller._save_fragment(cache_path.path, @store_options) end - body = controller.render_to_string(:text => body, :layout => true) unless @cache_layout + body = controller.render_to_string(:text => body, :layout => true) unless cache_layout controller.response_body = body controller.content_type = Mime[cache_path.extension || :html] diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 4c76f4c43b..11aa393bf9 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -20,7 +20,7 @@ module ActionController status = payload[:status] if status.nil? && payload[:exception].present? - status = Rack::Utils.status_code(ActionDispatch::ExceptionWrapper.new({}, payload[:exception]).status_code) + status = ActionDispatch::ExceptionWrapper.new({}, payload[:exception]).status_code end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 369741fb12..379ff97048 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -8,15 +8,13 @@ module ActionController #:nodoc: include ActionController::Rendering - DEFAULT_SEND_FILE_OPTIONS = { - :type => 'application/octet-stream'.freeze, - :disposition => 'attachment'.freeze, - }.freeze + DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc: + DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc: protected # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header. + # +config.action_dispatch.x_sendfile_header+. # Your server can also configure this for you by setting the X-Sendfile-Type header. # # Be careful to sanitize the path parameter if it is coming from a web @@ -127,7 +125,7 @@ module ActionController #:nodoc: # # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: - send_file_headers! options.dup + send_file_headers! options render options.slice(:status, :content_type).merge(:text => data) end @@ -135,15 +133,8 @@ module ActionController #:nodoc: def send_file_headers!(options) type_provided = options.has_key?(:type) - options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) - [:type, :disposition].each do |arg| - raise ArgumentError, ":#{arg} option required" if options[arg].nil? - end - - disposition = options[:disposition] - disposition += %(; filename="#{options[:filename]}") if options[:filename] - - content_type = options[:type] + content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE) + raise ArgumentError, ":type option required" if content_type.nil? if content_type.is_a?(Symbol) extension = Mime[content_type] @@ -157,10 +148,13 @@ module ActionController #:nodoc: self.content_type = content_type end - headers.merge!( - 'Content-Disposition' => disposition, - 'Content-Transfer-Encoding' => 'binary' - ) + disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) + unless disposition.nil? + disposition += %(; filename="#{options[:filename]}") if options[:filename] + headers['Content-Disposition'] = disposition + end + + headers['Content-Transfer-Encoding'] = 'binary' response.sending_file = true diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 9a9db0fe5f..90648c37ad 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -38,4 +38,7 @@ module ActionController class UnknownHttpMethod < ActionControllerError #:nodoc: end + + class UnknownFormat < ActionControllerError #:nodoc: + end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index a618533d09..2fcd933d32 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -20,6 +20,7 @@ module ActionController options, status = status, nil if status.is_a?(Hash) status ||= options.delete(:status) || :ok location = options.delete(:location) + content_type = options.delete(:content_type) options.each do |key, value| headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s @@ -27,8 +28,28 @@ module ActionController self.status = status self.location = url_for(location) if location - self.content_type = Mime[formats.first] if formats + + if include_content_headers?(self.status) + self.content_type = content_type || (Mime[formats.first] if formats) + else + headers.delete('Content-Type') + headers.delete('Content-Length') + end + self.response_body = " " end + + private + # :nodoc: + def include_content_headers?(status) + case status + when 100..199 + false + when 204, 205, 304 + false + else + true + end + end end end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 87225d74c1..57bb0e2a32 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -2,8 +2,9 @@ require 'base64' require 'active_support/core_ext/object/blank' module ActionController + # Makes it dead easy to do HTTP Basic, Digest and Token authentication. module HttpAuthentication - # Makes it dead easy to do HTTP \Basic and \Digest authentication. + # Makes it dead easy to do HTTP \Basic authentication. # # === Simple \Basic example # @@ -60,47 +61,6 @@ module ActionController # # assert_equal 200, status # end - # - # === Simple \Digest example - # - # require 'digest/md5' - # class PostsController < ApplicationController - # REALM = "SuperSecret" - # USERS = {"dhh" => "secret", #plain text password - # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password - # - # before_filter :authenticate, :except => [:index] - # - # def index - # render :text => "Everyone can see me!" - # end - # - # def edit - # render :text => "I'm only accessible if you know the password" - # end - # - # private - # def authenticate - # authenticate_or_request_with_http_digest(REALM) do |username| - # USERS[username] - # end - # end - # end - # - # === Notes - # - # The +authenticate_or_request_with_http_digest+ block must return the user's password - # or the ha1 digest hash so the framework can appropriately hash to check the user's - # credentials. Returning +nil+ will cause authentication to fail. - # - # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If - # the password file or database is compromised, the attacker would be able to use the ha1 hash to - # authenticate as the user at this +realm+, but would not have the user's password to try using at - # other sites. - # - # In rare instances, web servers or front proxies strip authorization headers before - # they reach your application. You can debug this situation by logging all environment - # variables, and check for HTTP_AUTHORIZATION, amongst others. module Basic extend self @@ -155,6 +115,48 @@ module ActionController end end + # Makes it dead easy to do HTTP \Digest authentication. + # + # === Simple \Digest example + # + # require 'digest/md5' + # class PostsController < ApplicationController + # REALM = "SuperSecret" + # USERS = {"dhh" => "secret", #plain text password + # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password + # + # before_filter :authenticate, :except => [:index] + # + # def index + # render :text => "Everyone can see me!" + # end + # + # def edit + # render :text => "I'm only accessible if you know the password" + # end + # + # private + # def authenticate + # authenticate_or_request_with_http_digest(REALM) do |username| + # USERS[username] + # end + # end + # end + # + # === Notes + # + # The +authenticate_or_request_with_http_digest+ block must return the user's password + # or the ha1 digest hash so the framework can appropriately hash to check the user's + # credentials. Returning +nil+ will cause authentication to fail. + # + # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If + # the password file or database is compromised, the attacker would be able to use the ha1 hash to + # authenticate as the user at this +realm+, but would not have the user's password to try using at + # other sites. + # + # In rare instances, web servers or front proxies strip authorization headers before + # they reach your application. You can debug this situation by logging all environment + # variables, and check for HTTP_AUTHORIZATION, amongst others. module Digest extend self diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index f467b74256..7917926978 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -375,8 +375,7 @@ module ActionController #:nodoc: lookup_context.rendered_format = lookup_context.formats.first collector else - head :not_acceptable - nil + raise ActionController::UnknownFormat end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index fa760f2658..1f52c164de 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -48,7 +48,7 @@ module ActionController # method attribute_names. # # If you're going to pass the parameters to an +ActiveModel+ object (such as - # +User.new(params[:user])+), you might consider passing the model class to + # <tt>User.new(params[:user])</tt>), you might consider passing the model class to # the method instead. The +ParamsWrapper+ will actually try to determine the # list of attribute names from the model and only wrap those attributes: # @@ -66,7 +66,7 @@ module ActionController # class Admin::UsersController < ApplicationController # end # - # will try to check if +Admin::User+ or +User+ model exists, and use it to + # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to # determine the wrapper key respectively. If both models don't exist, # it will then fallback to use +user+ as the key. module ParamsWrapper @@ -166,8 +166,9 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:accessible_attributes) && model.accessible_attributes.present? - options[:include] = model.accessible_attributes.to_a + role = options.fetch(:as, :default) + if model.respond_to?(:accessible_attributes) && model.accessible_attributes(role).present? + options[:include] = model.accessible_attributes(role).to_a elsif model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 35ded8a56c..5e7bd44562 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -45,6 +45,16 @@ module ActionController # integer, or a symbol representing the downcased, underscored and symbolized description. # Note that the status code must be a 3xx HTTP code, or redirection will not occur. # + # If you are using XHR requests other than GET or POST and redirecting after the + # request then some browsers will follow the redirect using the original request + # method. This may lead to undesirable behavior such as a double DELETE. To work + # around this you can return a <tt>303 See Other</tt> status code which will be + # followed using a GET request. + # + # Examples: + # redirect_to posts_url, :status => :see_other + # redirect_to :action => 'index', :status => 303 + # # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 6e9ce450ac..4a0c1c7dd7 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -91,9 +91,14 @@ module ActionController add :json do |json, options| json = json.to_json(options) unless json.kind_of?(String) - json = "#{options[:callback]}(#{json})" unless options[:callback].blank? - self.content_type ||= Mime::JSON - json + + if options[:callback].present? + self.content_type ||= Mime::JS + "#{options[:callback]}(#{json})" + else + self.content_type ||= Mime::JSON + json + end end add :js do |js, options| diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 52aaed0ed4..eeb37db2e7 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -139,17 +139,17 @@ module ActionController #:nodoc: # session or flash after the template starts rendering will not propagate # to the client. # - # If you try to modify cookies, session or flash, an +ActionDispatch::ClosedError+ + # If you try to modify cookies, session or flash, an <tt>ActionDispatch::ClosedError</tt> # will be raised, showing those objects are closed for modification. # # == Middlewares # # Middlewares that need to manipulate the body won't work with streaming. # You should disable those middlewares whenever streaming in development - # or production. For instance, +Rack::Bug+ won't work when streaming as it + # or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it # needs to inject contents in the HTML body. # - # Also +Rack::Cache+ won't work with streaming as it does not support + # Also <tt>Rack::Cache</tt> won't work with streaming as it does not support # streaming bodies yet. Whenever streaming Cache-Control is automatically # set to "no-cache". # diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 5c40889f6b..e7af3f5b8d 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -75,12 +75,7 @@ module ActionController def record_key_for_dom_id(record) record = record.to_model if record.respond_to?(:to_model) key = record.to_key - key ? sanitize_dom_id(key.join('_')) : key - end - - # Replaces characters that are invalid in HTML DOM ids with valid ones. - def sanitize_dom_id(candidate_id) - candidate_id # TODO implement conversion to valid DOM id values + key ? key.join('_') : key end end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 55a9819316..ad02375f12 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -20,20 +20,25 @@ module ActionController ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| path = payload[:layout] - @layouts[path] += 1 + if path + @layouts[path] += 1 + if path =~ /^layouts\/(.*)/ + @layouts[$1] += 1 + end + end end ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| path = payload[:virtual_path] next unless path partial = path =~ /^.*\/_[^\/]*$/ + if partial @partials[path] += 1 @partials[path.split("/").last] += 1 - @templates[path] += 1 - else - @templates[path] += 1 end + + @templates[path] += 1 end end @@ -59,6 +64,15 @@ module ActionController # # assert that the exact template "admin/posts/new" was rendered # assert_template %r{\Aadmin/posts/new\Z} # + # # assert that the layout 'admin' was rendered + # assert_template :layout => 'admin' + # assert_template :layout => 'layouts/admin' + # assert_template :layout => :admin + # + # # assert that no layout was rendered + # assert_template :layout => nil + # assert_template :layout => false + # # # assert that the "_customer" partial was rendered twice # assert_template :partial => '_customer', :count => 2 # @@ -90,16 +104,17 @@ module ActionController end end when Hash - if expected_layout = options[:layout] + if options.key?(:layout) + expected_layout = options[:layout] msg = message || sprintf("expecting layout <%s> but action rendered <%s>", expected_layout, @layouts.keys) case expected_layout - when String - assert_includes @layouts.keys, expected_layout, msg + when String, Symbol + assert_includes @layouts.keys, expected_layout.to_s, msg when Regexp assert(@layouts.keys.any? {|l| l =~ expected_layout }, msg) - when nil + when nil, false assert(@layouts.empty?, msg) end end @@ -120,7 +135,7 @@ module ActionController options[:partial], @partials.keys) assert_includes @partials, expected_partial, msg end - else + elsif options.key?(:partial) assert @partials.empty?, "Expected no partials to be rendered" end @@ -147,17 +162,23 @@ module ActionController extra_keys = routes.extra_keys(parameters) non_path_parameters = get? ? query_parameters : request_parameters parameters.each do |key, value| - if value.is_a? Fixnum - value = value.to_s - elsif value.is_a? Array - value = Result.new(value.map { |v| v.is_a?(String) ? v.dup : v }) - elsif value.is_a? String + if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) + value = value.map{ |v| v.duplicable? ? v.dup : v } + elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) + value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] + elsif value.frozen? && value.duplicable? value = value.dup end if extra_keys.include?(key.to_sym) non_path_parameters[key] = value else + if value.is_a?(Array) + value = Result.new(value.map(&:to_param)) + else + value = value.to_param + end + path_parameters[key.to_s] = value end end @@ -453,7 +474,7 @@ module ActionController # Ensure that numbers and symbols passed as params are converted to # proper params, as is the case when engaging rack. - parameters = paramify_values(parameters) + parameters = paramify_values(parameters) if html_format?(parameters) @request.recycle! @response.recycle! @@ -466,14 +487,13 @@ module ActionController parameters ||= {} controller_class_name = @controller.class.anonymous? ? - "anonymous_controller" : + "anonymous" : @controller.class.name.underscore.sub(/_controller$/, '') @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) - @request.session = ActionController::TestSession.new(session) if session + @request.session.update(session) if session @request.session["flash"] = @request.flash.update(flash || {}) - @request.session["flash"].sweep @controller.request = @request build_request_uri(action, parameters) @@ -546,6 +566,12 @@ module ActionController @request.env["QUERY_STRING"] = query_string || "" end end + + def html_format?(parameters) + return true unless parameters.is_a?(Hash) + format = Mime[parameters[:format]] + format.nil? || format.html? + end end # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index 114b0e73c9..6b269e7a31 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,6 +1,7 @@ require 'set' require 'cgi' require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/class/attribute_accessors' module HTML class Sanitizer diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index e3b04ac097..1e4ac70f3d 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -21,12 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -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 'active_support/core_ext/module/attribute_accessors' diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 040b51e040..a3bb25f75a 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -14,17 +14,18 @@ module ActionDispatch end def [](header_name) - if include?(header_name) - super - else - super(env_name(header_name)) - end + super env_name(header_name) + end + + def fetch(header_name, default=nil, &block) + super env_name(header_name), default, &block end private - # Converts a HTTP header name to an environment variable name. + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. def env_name(header_name) - @@env_cache[header_name] + include?(header_name) ? header_name : @@env_cache[header_name] end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 26f4c16218..0eaae80461 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -179,11 +179,11 @@ module Mime end end - # input: 'text' - # returned value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, + # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. # - # input: 'application' - # returned value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM] + # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS, + # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>. def parse_data_with_trailing_star(input) Mime::SET.select { |m| m =~ input } end @@ -192,7 +192,7 @@ module Mime # # Usage: # - # Mime::Type.unregister(:mobile) + # Mime::Type.unregister(:mobile) def unregister(symbol) symbol = symbol.to_s.upcase mime = Mime.const_get(symbol) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index d9b63faf5e..bcfd0b0d00 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -35,6 +35,10 @@ module ActionDispatch @env["action_dispatch.request.path_parameters"] ||= {} end + def reset_parameters #:nodoc: + @env.delete("action_dispatch.request.parameters") + end + private # TODO: Validate that the characters are UTF-8. If they aren't, diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 796e0dbc45..9748956052 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' require 'active_support/inflector' require 'action_dispatch/http/headers' +require 'action_dispatch/request/session' require 'action_controller/metal/exceptions' module ActionDispatch @@ -220,11 +221,11 @@ module ActionDispatch end def session=(session) #:nodoc: - @env['rack.session'] = session + Session.set @env, session end def session_options=(options) - @env['rack.session.options'] = options + Session::Options.set @env, options end # Override Rack's GET method to support indifferent access diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 0c717c8503..771f075275 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request + class Request < Rack::Request def cookie_jar env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) end @@ -26,9 +26,9 @@ module ActionDispatch # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # - # # Sets a signed cookie, which prevents a user from tampering with its value. + # # Sets a signed cookie, which prevents users from tampering with its value. # # The cookie is signed by your app's <tt>config.secret_token</tt> value. - # # Rails generates this value by default when you create a new Rails app. + # # It can be read using the signed method <tt>cookies.signed[:key]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -39,9 +39,10 @@ module ActionDispatch # # Examples for reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -82,7 +83,7 @@ module ActionDispatch TOKEN_KEY = "action_dispatch.secret_token".freeze # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end + CookieOverflow = Class.new StandardError class CookieJar #:nodoc: include Enumerable @@ -117,7 +118,6 @@ module ActionDispatch @delete_cookies = {} @host = host @secure = secure - @closed = false @cookies = {} end @@ -154,7 +154,7 @@ module ActionDispatch end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] } + options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } end end @@ -169,12 +169,14 @@ module ActionDispatch options = { :value => value } end - @cookies[key.to_s] = value - handle_options(options) - @set_cookies[key.to_s] = options - @delete_cookies.delete(key.to_s) + if @cookies[key.to_s] != value or options[:expires] + @cookies[key.to_s] = value + @set_cookies[key.to_s] = options + @delete_cookies.delete(key.to_s) + end + value end @@ -182,8 +184,9 @@ module ActionDispatch # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in # an options hash to delete cookies with extra data such as a <tt>:path</tt>. def delete(key, options = {}) - options.symbolize_keys! + return unless @cookies.has_key? key.to_s + options.symbolize_keys! handle_options(options) value = @cookies.delete(key.to_s) @@ -225,7 +228,7 @@ module ActionDispatch # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will # be raised. # - # This jar requires that you set a suitable secret for the verification on your app's config.secret_token. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+. # # Example: # @@ -339,7 +342,6 @@ module ActionDispatch end def call(env) - cookie_jar = nil status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index c0532c80c4..a8f49bd3bd 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,6 @@ require 'action_controller/metal/exceptions' require 'active_support/core_ext/exception' +require 'active_support/core_ext/class/attribute_accessors' module ActionDispatch class ExceptionWrapper @@ -10,6 +11,7 @@ module ActionDispatch 'AbstractController::ActionNotFound' => :not_found, 'ActionController::MethodNotAllowed' => :method_not_allowed, 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity ) @@ -75,4 +77,4 @@ module ActionDispatch @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index c92c91df65..17776c2356 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,10 +1,10 @@ module ActionDispatch - class Request + class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new) + @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new).tap(&:sweep) end end @@ -79,7 +79,6 @@ module ActionDispatch def initialize #:nodoc: @discard = Set.new - @closed = false @flashes = {} @now = nil end @@ -217,13 +216,9 @@ module ActionDispatch end def call(env) - if (session = env['rack.session']) && (flash = session['flash']) - flash.sweep - end - @app.call(env) ensure - session = env['rack.session'] || {} + session = Request::Session.find(env) || {} flash_hash = env[KEY] if flash_hash @@ -237,7 +232,8 @@ module ActionDispatch env[KEY] = new_hash end - if session.key?('flash') && session['flash'].empty? + if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + session.key?('flash') && session['flash'].empty? session.delete('flash') end end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index a0388e0e13..2f6968eb2e 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -18,10 +18,10 @@ module ActionDispatch # classes before they are unloaded. # # By default, ActionDispatch::Reloader is included in the middleware stack - # only in the development environment; specifically, when config.cache_classes + # only in the development environment; specifically, when +config.cache_classes+ # is false. Callbacks may be registered even when it is not included in the - # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+ - # or +ActionDispatch::Reloader.cleanup!+ are called manually. + # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt> + # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually. # class Reloader include ActiveSupport::Callbacks diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 6a8e690d18..64159fa8e7 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -2,26 +2,23 @@ require 'rack/utils' require 'rack/request' require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' +require 'action_dispatch/request/session' require 'active_support/core_ext/object/blank' module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - end + attr_reader :original_exception + + def initialize(const_error) + @original_exception = const_error - module DestroyableSession - def destroy - clear - options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env - options ||= {} - @by.send(:destroy_session, @env, options[:id], options) if @by - options[:id] = nil - @loaded = false + super("Session contains objects whose class definition isn't available.\n" + + "Remember to require the classes for all objects kept in the session.\n" + + "(Original exception: #{const_error.message} [#{const_error.class}])\n") end end - ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession - module Compatibility def initialize(app, options = {}) options[:key] ||= '_session_id' @@ -58,11 +55,8 @@ module ActionDispatch begin # Note that the regexp does not allow $1 to end with a ':' $1.constantize - rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, - "Session contains objects whose class definition isn't available.\n" + - "Remember to require the classes for all objects kept in the session.\n" + - "(Original exception: #{const_error.message} [#{const_error.class}])\n" + rescue LoadError, NameError => e + raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace end retry else @@ -71,9 +65,27 @@ module ActionDispatch end end + module SessionObject # :nodoc: + def prepare_session(env) + Request::Session.create(self, env, @default_options) + end + + def loaded_session?(session) + !session.is_a?(Request::Session) || session.loaded? + end + end + class AbstractStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck + include SessionObject + + private + + def set_cookie(env, session_id, cookie) + request = ActionDispatch::Request.new(env) + request.cookie_jar[key] = cookie + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index a4866f5a8f..dbcf703ec3 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -43,6 +43,7 @@ module ActionDispatch class CookieStore < Rack::Session::Cookie include Compatibility include StaleSessionCheck + include SessionObject private diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 4dd9a946c2..38a737cd2b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -6,6 +6,7 @@ module ActionDispatch class MemCacheStore < Rack::Session::Memcache include Compatibility include StaleSessionCheck + include SessionObject def initialize(app, options = {}) require 'memcache' diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index 0c5bafa666..823f5d25b6 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -12,8 +12,8 @@ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") - def debug_hash(hash) - hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + def debug_hash(object) + object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") end unless self.class.method_defined?(:debug_hash) %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb index f06c07daa5..177d383e94 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb @@ -12,4 +12,6 @@ <% end %> <p> Try running <code>rake routes</code> for more information on available routes. -</p>
\ No newline at end of file +</p> + +<%= render :template => "rescues/_trace" %> diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb new file mode 100644 index 0000000000..4ad7071820 --- /dev/null +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -0,0 +1,166 @@ +require 'rack/session/abstract/id' + +module ActionDispatch + class Request < Rack::Request + # SessionHash is responsible to lazily load the session from store. + class Session # :nodoc: + ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: + ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + + def self.create(store, env, default_options) + session_was = find env + session = Request::Session.new(store, env) + session.merge! session_was if session_was + + set(env, session) + Options.set(env, Request::Session::Options.new(store, env, default_options)) + session + end + + def self.find(env) + env[ENV_SESSION_KEY] + end + + def self.set(env, session) + env[ENV_SESSION_KEY] = session + end + + class Options #:nodoc: + def self.set(env, options) + env[ENV_SESSION_OPTIONS_KEY] = options + end + + def self.find(env) + env[ENV_SESSION_OPTIONS_KEY] + end + + def initialize(by, env, default_options) + @by = by + @env = env + @delegate = default_options.dup + end + + def [](key) + if key == :id + @delegate.fetch(key) { + @delegate[:id] = @by.send(:extract_session_id, @env) + } + else + @delegate[key] + end + end + + def []=(k,v); @delegate[k] = v; end + def to_hash; @delegate.dup; end + def values_at(*args); @delegate.values_at(*args); end + end + + def initialize(by, env) + @by = by + @env = env + @delegate = {} + @loaded = false + @exists = nil # we haven't checked yet + end + + def options + Options.find @env + end + + def destroy + clear + options = self.options || {} + @by.send(:destroy_session, @env, options[:id], options) + options[:id] = nil + @loaded = false + end + + def [](key) + load_for_read! + @delegate[key.to_s] + end + + def has_key?(key) + load_for_read! + @delegate.key?(key.to_s) + end + alias :key? :has_key? + alias :include? :has_key? + + def []=(key, value) + load_for_write! + @delegate[key.to_s] = value + end + + def clear + load_for_write! + @delegate.clear + end + + def to_hash + load_for_read! + @delegate.dup.delete_if { |_,v| v.nil? } + end + + def update(hash) + load_for_write! + @delegate.update stringify_keys(hash) + end + + def delete(key) + load_for_write! + @delegate.delete key.to_s + end + + def inspect + if loaded? + super + else + "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>" + end + end + + def exists? + return @exists unless @exists.nil? + @exists = @by.send(:session_exists?, @env) + end + + def loaded? + @loaded + end + + def empty? + load_for_read! + @delegate.empty? + end + + def merge!(other) + load_for_write! + @delegate.merge!(other) + end + + private + + def load_for_read! + load! if !loaded? && exists? + end + + def load_for_write! + load! unless loaded? + end + + def load! + id, session = @by.load_session @env + options[:id] = id + @delegate.replace(stringify_keys(session)) + @loaded = true + end + + def stringify_keys(other) + other.each_with_object({}) { |(key, value), hash| + hash[key.to_s] = value + } + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 716e2d2271..7a22b65c44 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/reverse_merge' +require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/enumerable' require 'active_support/inflector' @@ -35,6 +36,8 @@ module ActionDispatch } return true + ensure + req.reset_parameters end def call(env) @@ -98,6 +101,10 @@ module ActionDispatch raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" end end + + if @options[:constraints].is_a?(Hash) + (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints])) + end end # match "account/overview" @@ -243,6 +250,11 @@ module ActionDispatch def default_action @options[:action] || @scope[:action] end + + def defaults_from_constraints(constraints) + url_keys = [:protocol, :subdomain, :domain, :host, :port] + constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) } + end end # Invokes Rack::Mount::Utils.normalize path and ensure that @@ -533,7 +545,8 @@ module ActionDispatch private def map_method(method, args, &block) options = args.extract_options! - options[:via] = method + options[:via] = method + options[:path] ||= args.first if args.first.is_a?(String) match(*args, options, &block) self end @@ -638,6 +651,10 @@ module ActionDispatch block, options[:constraints] = options[:constraints], {} end + if options[:constraints].is_a?(Hash) + (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints])) + end + scope_options.each do |option| if value = options.delete(option) recover[option] = @scope[option] @@ -846,6 +863,11 @@ module ActionDispatch def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end + + def defaults_from_constraints(constraints) + url_keys = [:protocol, :subdomain, :domain, :host, :port] + constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) } + end end # Resource routing allows you to quickly declare all of the common routes @@ -1306,14 +1328,14 @@ module ActionDispatch end def draw(name) - path = @draw_paths.find do |path| - path.join("#{name}.rb").file? + path = @draw_paths.find do |_path| + _path.join("#{name}.rb").file? end unless path msg = "Your router tried to #draw the external file #{name}.rb,\n" \ "but the file was not found in:\n\n" - msg += @draw_paths.map { |path| " * #{path}" }.join("\n") + msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n") raise msg end @@ -1507,7 +1529,7 @@ module ActionDispatch prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] - path = if canonical_action?(action, path.blank?) + if canonical_action?(action, path.blank?) prefix.to_s else "#{prefix}/#{action_path(action, path)}" diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 013cf93dbc..8fde667108 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -43,16 +43,14 @@ module ActionDispatch # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" # - # == Using with mounted engines + # == Usage with mounted engines # - # If you use mounted engine, there is a possibility that you will need to use - # polymorphic_url pointing at engine's routes. To do that, just pass proxy used - # to reach engine's routes as a first argument: + # If you are using a mounted engine and you need to use a polymorphic_url + # pointing at the engine's routes, pass in the engine's route proxy as the first + # argument to the method. For example: # - # For example: - # - # polymorphic_url([blog, @post]) # it will call blog.post_path(@post) - # form_for([blog, @post]) # => "/blog/posts/1 + # polymorphic_url([blog, @post]) # calls blog.post_path(@post) + # form_for([blog, @post]) # => "/blog/posts/1" # module PolymorphicRoutes # Constructs a call to a named RESTful route for the given record and returns the diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index ae01781013..95c588c00a 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -1,4 +1,7 @@ require 'action_dispatch/http/request' +require 'active_support/core_ext/uri' +require 'active_support/core_ext/array/extract_options' +require 'rack/utils' module ActionDispatch module Routing @@ -46,8 +49,17 @@ module ActionDispatch :params => request.query_parameters }.merge options + if !params.empty? && url_options[:path].match(/%\{\w*\}/) + url_options[:path] = (url_options[:path] % escape_path(params)) + end + ActionDispatch::Http::URL.url_for url_options end + + private + def escape_path(params) + Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }] + end end module Redirection @@ -88,7 +100,7 @@ module ActionDispatch # match 'accounts/:name' => redirect(SubdomainRedirector.new('api')) # def redirect(*args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} + options = args.extract_options! status = options.delete(:status) || 301 return OptionRedirect.new(status, options) if options.any? @@ -96,13 +108,18 @@ module ActionDispatch path = args.shift block = lambda { |params, request| - (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) + (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % escape(params)) } if String === path block = path if path.respond_to? :call raise ArgumentError, "redirection argument not supported" unless block Redirect.new status, block end + + private + def escape(params) + Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + 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 7a7810a95c..0ae668d42a 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -99,12 +99,12 @@ module ActionDispatch @module = Module.new do protected - def handle_positional_args(args, options, route) + def handle_positional_args(args, options, segment_keys) inner_options = args.extract_options! result = options.dup - if args.any? - keys = route.segment_keys + if args.size > 0 + keys = segment_keys if args.size < keys.size - 1 # take format into account keys -= self.url_options.keys if self.respond_to?(:url_options) keys -= options.keys @@ -161,34 +161,13 @@ module ActionDispatch end end - def hash_access_name(name, only_path) - if only_path - :"hash_for_#{name}_path" - else - :"hash_for_#{name}_url" - end - end - def define_named_route_methods(name, route) [true, false].each do |only_path| hash = route.defaults.merge(:use_route => name, :only_path => only_path) - define_hash_access route, name, hash define_url_helper route, name, hash end end - def define_hash_access(route, name, options) - selector = hash_access_name(name, options[:only_path]) - - @module.module_eval do - redefine_method(selector) do |*args| - self.handle_positional_args(args, options, route) - end - protected selector - end - helpers << selector - end - # Create a url helper allowing ordered parameters to be associated # with corresponding dynamic segments, so you can do: # @@ -204,36 +183,27 @@ module ActionDispatch # def define_url_helper(route, name, options) selector = url_helper_name(name, options[:only_path]) - hash_access_method = hash_access_name(name, options[:only_path]) - - if optimize_helper?(route) - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - if args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? - options = #{options.inspect}.merge!(url_options) - options[:path] = "#{optimized_helper(route)}" - ActionDispatch::Http::URL.url_for(options) - else - url_for(#{hash_access_method}(*args)) - end - end - END_EVAL - else - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - url_for(#{hash_access_method}(*args)) + + @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 + remove_possible_method :#{selector} + def #{selector}(*args) + if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? + options = #{options.inspect} + options.merge!(url_options) if respond_to?(:url_options) + options[:path] = "#{optimized_helper(route)}" + ActionDispatch::Http::URL.url_for(options) + else + url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect})) end - END_EVAL - end + end + END_EVAL helpers << selector end # Clause check about when we need to generate an optimized helper. def optimize_helper?(route) #:nodoc: - route.ast.grep(Journey::Nodes::Star).empty? && route.requirements.except(:controller, :action).empty? + route.requirements.except(:controller, :action).empty? end # Generates the interpolation to be used in the optimized helper. @@ -245,7 +215,10 @@ module ActionDispatch end route.required_parts.each_with_index do |part, i| - string_route.gsub!(part.inspect, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}") + # Replace each route parameter + # e.g. :id for regular parameter or *path for globbing + # with ruby string interpolation code + string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}") end string_route @@ -487,12 +460,12 @@ module ActionDispatch normalize_options! normalize_controller_action_id! use_relative_controller! - controller.sub!(%r{^/}, '') if controller + normalize_controller! handle_nil_action! end def controller - @controller ||= @options[:controller] + @options[:controller] end def current_controller @@ -549,10 +522,15 @@ module ActionDispatch old_parts = current_controller.split('/') size = controller.count("/") + 1 parts = old_parts[0...-size] << controller - @controller = @options[:controller] = parts.join("/") + @options[:controller] = parts.join("/") end end + # Remove leading slashes from controllers + def normalize_controller! + @options[:controller] = controller.sub(%r{^/}, '') if controller + end + # This handles the case of :action => nil being explicitly passed. # It is identical to :action => "index" def handle_nil_action! diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index d75bb1c2de..ee02f4b531 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -68,7 +68,7 @@ module ActionDispatch # This generates, among other things, the method <tt>users_path</tt>. By default, # this method is accessible from your controllers, views and mailers. If you need # to access this auto-generated method from other places (such as a model), then - # you can do that by including ActionController::UrlFor in your class: + # you can do that by including Rails.application.routes.url_helpers in your class: # # class User < ActiveRecord::Base # include Rails.application.routes.url_helpers diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 40d38c59d6..8f6fff5d32 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -53,15 +53,18 @@ module ActionDispatch # # assert that the redirection was to the url for @customer # assert_redirected_to @customer # + # # asserts that the redirection matches the regular expression + # assert_redirected_to %r(\Ahttp://example.org) + # def assert_redirected_to(options = {}, message=nil) assert_response(:redirect, message) - return true if options == @response.location + return true if options === @response.location redirect_is = normalize_argument_to_redirection(@response.location) redirect_expected = normalize_argument_to_redirection(options) message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>" - assert_equal redirect_expected, redirect_is, message + assert_operator redirect_expected, :===, redirect_is, message end private @@ -71,17 +74,21 @@ module ActionDispatch end def normalize_argument_to_redirection(fragment) - case fragment - when %r{^\w[A-Za-z\d+.-]*:.*} - fragment - when String - @request.protocol + @request.host_with_port + fragment - when :back - raise RedirectBackError unless refer = @request.headers["Referer"] - refer - else - @controller.url_for(fragment) - end.delete("\0\r\n") + normalized = case fragment + when Regexp + fragment + when %r{^\w[A-Za-z\d+.-]*:.*} + fragment + when String + @request.protocol + @request.host_with_port + fragment + when :back + raise RedirectBackError unless refer = @request.headers["Referer"] + refer + else + @controller.url_for(fragment) + end + + normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 69d54f6981..08fd28d72d 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -201,9 +201,16 @@ module ActionDispatch reset! end - remove_method :default_url_options - def default_url_options - { :host => host, :protocol => https? ? "https" : "http" } + def url_options + @url_options ||= default_url_options.dup.tap do |url_options| + url_options.reverse_merge!(controller.url_options) if controller + + if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options) + url_options.reverse_merge!(@app.routes.default_url_options) + end + + url_options.reverse_merge!(:host => host, :protocol => https? ? "https" : "http") + end end # Resets the instance. This can be used to reset the state information @@ -216,6 +223,7 @@ module ActionDispatch @controller = @request = @response = nil @_mock_session = nil @request_count = 0 + @url_options = nil self.host = DEFAULT_HOST self.remote_addr = "127.0.0.1" @@ -310,6 +318,7 @@ module ActionDispatch response = _mock_session.last_response @response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body) @html_document = nil + @url_options = nil @controller = session.last_request.env['action_controller.instance'] @@ -367,12 +376,14 @@ module ActionDispatch end end - extend ActiveSupport::Concern - include ActionDispatch::Routing::UrlFor + def default_url_options + reset! unless integration_session + integration_session.default_url_options + end - def url_options + def default_url_options=(options) reset! unless integration_session - integration_session.url_options + integration_session.default_url_options = options end def respond_to?(method, include_private = false) @@ -476,6 +487,7 @@ module ActionDispatch class IntegrationTest < ActiveSupport::TestCase include Integration::Runner include ActionController::TemplateAssertions + include ActionDispatch::Routing::UrlFor @@app = nil @@ -495,5 +507,10 @@ module ActionDispatch def app super || self.class.app end + + def url_options + reset! unless integration_session + integration_session.url_options + end end end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 349a3fcc6e..3823f87027 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -21,9 +21,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -require 'active_support/ruby/shim' -require 'active_support/core_ext/class/attribute_accessors' - +require 'active_support' require 'action_pack' module ActionView @@ -78,7 +76,8 @@ module ActionView ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' end -require 'active_support/i18n' require 'active_support/core_ext/string/output_safety' -I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" +ActiveSupport.on_load(:i18n) do + I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" +end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 5f81f24a2e..f98648d930 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/class/attribute_accessors' require 'active_support/ordered_options' require 'action_view/log_subscriber' diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 7985683a72..adc62ec6a9 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -13,15 +13,16 @@ module ActionView # the assets exist before linking to them: # # image_tag("rails.png") - # # => <img alt="Rails" src="/images/rails.png?1230601161" /> + # # => <img alt="Rails" src="/assets/rails.png" /> # stylesheet_link_tag("application") - # # => <link href="/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" /> + # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" /> + # # # === Using asset hosts # # By default, Rails links to these assets on the current host in the public # folder, but you can direct Rails to link to assets from a dedicated asset - # server by setting ActionController::Base.asset_host in the application + # server by setting <tt>ActionController::Base.asset_host</tt> in the application # configuration, typically in <tt>config/environments/production.rb</tt>. # For example, you'd define <tt>assets.example.com</tt> to be your asset # host this way, inside the <tt>configure</tt> block of your environment-specific @@ -32,9 +33,9 @@ module ActionView # Helpers take that into account: # # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets.example.com/images/rails.png?1230601161" /> + # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" /> # stylesheet_link_tag("application") - # # => <link href="http://assets.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" /> + # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" /> # # Browsers typically open at most two simultaneous connections to a single # host, which means your assets often have to wait for other assets to finish @@ -45,9 +46,9 @@ module ActionView # will open eight simultaneous connections rather than two. # # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets0.example.com/images/rails.png?1230601161" /> + # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" /> # stylesheet_link_tag("application") - # # => <link href="http://assets2.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" /> + # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> # # To do this, you can either setup four actual hosts, or you can use wildcard # DNS to CNAME the wildcard to a single asset host. You can read more about @@ -64,29 +65,28 @@ module ActionView # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com" # } # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets1.example.com/images/rails.png?1230601161" /> + # # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" /> # stylesheet_link_tag("application") - # # => <link href="http://assets2.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" /> + # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> # # The example above generates "http://assets1.example.com" and # "http://assets2.example.com". This option is useful for example if # you need fewer/more than four hosts, custom host names, etc. # # As you see the proc takes a +source+ parameter. That's a string with the - # absolute path of the asset with any extensions and timestamps in place, - # for example "/images/rails.png?1230601161". + # absolute path of the asset, for example "/assets/rails.png". # # ActionController::Base.asset_host = Proc.new { |source| - # if source.starts_with?('/images') - # "http://images.example.com" + # if source.ends_with?('.css') + # "http://stylesheets.example.com" # else # "http://assets.example.com" # end # } # image_tag("rails.png") - # # => <img alt="Rails" src="http://images.example.com/images/rails.png?1230601161" /> + # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" /> # stylesheet_link_tag("application") - # # => <link href="http://assets.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" /> + # # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" /> # # Alternatively you may ask for a second parameter +request+. That one is # particularly useful for serving assets from an SSL-protected page. The diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb index dd4e9ae4cc..35f91cec18 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -1,5 +1,6 @@ require 'thread' require 'active_support/core_ext/file' +require 'active_support/core_ext/module/attribute_accessors' module ActionView module Helpers diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb index e3329c0345..7bff0c1149 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -136,8 +136,6 @@ module ActionView # # <script src="/javascripts/rails.js?1284139606"></script> # # <script src="/javascripts/application.js?1284139606"></script> # - # * = The application.js file is only referenced if it exists - # # You can also include all JavaScripts in the +javascripts+ directory using <tt>:all</tt> as the source: # # javascript_include_tag :all diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index 60d4af178a..4bcb8b9718 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -113,7 +113,7 @@ module ActionView # == Caching multiple stylesheets into one # # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # compressed by gzip (leading to faster transfers). Caching will only happen if +config.perform_caching+ # is set to true (which is the case by default for the Rails production environment, but not for the development # environment). Examples: # diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 81f856feda..f0a593d2c1 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -19,7 +19,7 @@ module ActionView # of \date[month]. module DateHelper # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds. - # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs. + # Pass <tt>:include_seconds => true</tt> if you want more detailed approximations when distance < 1 min, 29 secs. # Distances are reported based on the following table: # # 0 <-> 29 secs # => less than a minute @@ -29,14 +29,15 @@ module ActionView # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days - # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month + # 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month + # 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months # 1 yr <-> 1 yr, 3 months # => about 1 year # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years # 2 yrs <-> max time or date # => (same rules as 1 yr) # - # With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds: + # With <tt>:include_seconds => true</tt> and the difference < 1 minute 29 seconds: # 0-4 secs # => less than 5 seconds # 5-9 secs # => less than 10 seconds # 10-19 secs # => less than 20 seconds @@ -46,28 +47,33 @@ module ActionView # # ==== Examples # from_time = Time.now - # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour - # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour - # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute - # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds - # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years - # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days - # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute - # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute - # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute - # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year - # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years + # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour + # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour + # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute + # distance_of_time_in_words(from_time, from_time + 15.seconds, :include_seconds => true) # => less than 20 seconds + # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years + # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days + # distance_of_time_in_words(from_time, from_time + 45.seconds, :include_seconds => true) # => less than a minute + # distance_of_time_in_words(from_time, from_time - 45.seconds, :include_seconds => true) # => less than a minute + # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute + # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year + # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years # # to_time = Time.now + 6.years + 19.days - # distance_of_time_in_words(from_time, to_time, true) # => about 6 years - # distance_of_time_in_words(to_time, from_time, true) # => about 6 years - # distance_of_time_in_words(Time.now, Time.now) # => less than a minute + # distance_of_time_in_words(from_time, to_time, :include_seconds => true) # => about 6 years + # distance_of_time_in_words(to_time, from_time, :include_seconds => true) # => about 6 years + # distance_of_time_in_words(Time.now, Time.now) # => less than a minute # - # distance_of_time_in_words(70) # => 1 minute - # distance_of_time_in_words(60*60) # => about 1 hour - # - def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) + def distance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {}, options = {}) + if include_seconds_or_options.is_a?(Hash) + options = include_seconds_or_options + else + ActiveSupport::Deprecation.warn "distance_of_time_in_words and time_ago_in_words now accept :include_seconds " + + "as a part of options hash, not a boolean argument", caller + options[:include_seconds] ||= !!include_seconds_or_options + end + from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) from_time, to_time = to_time, from_time if from_time > to_time @@ -79,7 +85,7 @@ module ActionView when 0..1 return distance_in_minutes == 0 ? locale.t(:less_than_x_minutes, :count => 1) : - locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds + locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds] case distance_in_seconds when 0..4 then locale.t :less_than_x_seconds, :count => 5 @@ -90,13 +96,18 @@ module ActionView else locale.t :x_minutes, :count => 1 end - when 2..44 then locale.t :x_minutes, :count => distance_in_minutes - when 45..89 then locale.t :about_x_hours, :count => 1 - when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round - when 1440..2519 then locale.t :x_days, :count => 1 - when 2520..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round - when 43200..86399 then locale.t :about_x_months, :count => 1 - when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round + when 2...45 then locale.t :x_minutes, :count => distance_in_minutes + when 45...90 then locale.t :about_x_hours, :count => 1 + # 90 mins up to 24 hours + when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round + # 24 hours up to 42 hours + when 1440...2520 then locale.t :x_days, :count => 1 + # 42 hours up to 30 days + when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round + # 30 days up to 60 days + when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round + # 60 days up to 365 days + when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round else if from_time.acts_like?(:time) && to_time.acts_like?(:time) fyear = from_time.year @@ -130,15 +141,16 @@ module ActionView # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. # # ==== Examples - # time_ago_in_words(3.minutes.from_now) # => 3 minutes - # time_ago_in_words(Time.now - 15.hours) # => about 15 hours - # time_ago_in_words(Time.now) # => less than a minute + # time_ago_in_words(3.minutes.from_now) # => 3 minutes + # time_ago_in_words(Time.now - 15.hours) # => about 15 hours + # time_ago_in_words(Time.now) # => less than a minute + # time_ago_in_words(Time.now, :include_seconds => true) # => less than 5 seconds # # from_time = Time.now - 3.days - 14.minutes - 25.seconds # time_ago_in_words(from_time) # => 3 days # - def time_ago_in_words(from_time, include_seconds = false) - distance_of_time_in_words(from_time, Time.now, include_seconds) + def time_ago_in_words(from_time, include_seconds_or_options = {}) + distance_of_time_in_words(from_time, Time.now, include_seconds_or_options) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words @@ -1005,6 +1017,8 @@ module ActionView # Returns the separator for a given datetime component. def separator(type) + return "" if @options[:use_hidden] + case type when :year, :month, :day @options[:"discard_#{type}"] ? "" : @options[:date_separator] diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 8e7224937d..0fbd6ac41b 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -5,11 +5,13 @@ require 'action_view/helpers/form_tag_helper' require 'action_view/helpers/active_model_helper' require 'action_view/helpers/tags' require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/array/extract_options' require 'active_support/deprecation' +require 'active_support/core_ext/string/inflections' module ActionView # = Action View Form Helpers @@ -1039,9 +1041,14 @@ module ActionView object_name = ActiveModel::Naming.param_key(object) end - builder = options[:builder] || ActionView::Base.default_form_builder + builder = options[:builder] || default_form_builder builder.new(object_name, object, self, options) end + + def default_form_builder + builder = ActionView::Base.default_form_builder + builder.respond_to?(:constantize) ? builder.constantize : builder + end end class FormBuilder diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index d61c2bbee2..cafcd93f58 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -288,38 +288,55 @@ module ActionView # # Examples (call, result): # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) - # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option> + # # <option value="$">Dollar</option> + # # <option value="DKK">Kroner</option> # # options_for_select([ "VISA", "MasterCard" ], "MasterCard") - # <option>VISA</option>\n<option selected="selected">MasterCard</option> + # # <option>VISA</option> + # # <option selected="selected">MasterCard</option> # # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") - # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option> + # # <option value="$20">Basic</option> + # # <option value="$40" selected="selected">Plus</option> # # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) - # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option> + # # <option selected="selected">VISA</option> + # # <option>MasterCard</option> + # # <option selected="selected">Discover</option> # # You can optionally provide html attributes as the last element of the array. # # Examples: # options_for_select([ "Denmark", ["USA", {:class => 'bold'}], "Sweden" ], ["USA", "Sweden"]) - # <option value="Denmark">Denmark</option>\n<option value="USA" class="bold" selected="selected">USA</option>\n<option value="Sweden" selected="selected">Sweden</option> + # # <option value="Denmark">Denmark</option> + # # <option value="USA" class="bold" selected="selected">USA</option> + # # <option value="Sweden" selected="selected">Sweden</option> # # options_for_select([["Dollar", "$", {:class => "bold"}], ["Kroner", "DKK", {:onclick => "alert('HI');"}]]) - # <option value="$" class="bold">Dollar</option>\n<option value="DKK" onclick="alert('HI');">Kroner</option> + # # <option value="$" class="bold">Dollar</option> + # # <option value="DKK" onclick="alert('HI');">Kroner</option> # # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags. # # Examples: # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum") - # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # <option value="Free">Free</option> + # # <option value="Basic">Basic</option> + # # <option value="Advanced">Advanced</option> + # # <option value="Super Platinum" disabled="disabled">Super Platinum</option> # # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"]) - # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced" disabled="disabled">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # <option value="Free">Free</option> + # # <option value="Basic">Basic</option> + # # <option value="Advanced" disabled="disabled">Advanced</option> + # # <option value="Super Platinum" disabled="disabled">Super Platinum</option> # # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum") - # <option value="Free" selected="selected">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # <option value="Free" selected="selected">Free</option> + # # <option value="Basic">Basic</option> + # # <option value="Advanced">Advanced</option> + # # <option value="Super Platinum" disabled="disabled">Super Platinum</option> # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def options_for_select(container, selected = nil) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index b5e0970612..248cc2f6a3 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -45,7 +45,7 @@ module ActionView # # => <form action="/posts" method="post"> # # form_tag('/posts/1', :method => :put) - # # => <form action="/posts/1" method="put"> + # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ... # # form_tag('/upload', :multipart => true) # # => <form action="/upload" method="post" enctype="multipart/form-data"> @@ -408,7 +408,7 @@ module ActionView # # => <input class="form_submit" name="commit" type="submit" /> # # submit_tag "Edit", :disable_with => "Editing...", :class => "edit_button" - # # => <input class="edit_button" data-disable_with="Editing..." name="commit" type="submit" value="Edit" /> + # # => <input class="edit_button" data-disable-with="Editing..." name="commit" type="submit" value="Edit" /> # # submit_tag "Save", :confirm => "Are you sure?" # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> @@ -502,6 +502,9 @@ module ActionView # # image_submit_tag("agree.png", :disabled => true, :class => "agree_disagree_button") # # => <input class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" /> + # + # image_submit_tag("save.png", :confirm => "Are you sure?") + # # => <input src="/images/save.png" data-confirm="Are you sure?" type="image" /> def image_submit_tag(source, options = {}) options = options.stringify_keys diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 5a869d6303..cc20518b93 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -15,7 +15,6 @@ module ActionView JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' JS_ESCAPE_MAP["\342\200\251".force_encoding('UTF-8').encode!] = '
' - # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -68,40 +67,6 @@ module ActionView def javascript_cdata_section(content) #:nodoc: "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe end - - # Returns a button whose +onclick+ handler triggers the passed JavaScript. - # - # The helper receives a name, JavaScript code, and an optional hash of HTML options. The - # name is used as button label and the JavaScript code goes into its +onclick+ attribute. - # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. - # - # button_to_function "Greeting", "alert('Hello world!')", :class => "ok" - # # => <input class="ok" onclick="alert('Hello world!');" type="button" value="Greeting" /> - # - def button_to_function(name, function=nil, html_options={}) - onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};" - - tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick)) - end - - # Returns a link whose +onclick+ handler triggers the passed JavaScript. - # - # The helper receives a name, JavaScript code, and an optional hash of HTML options. The - # name is used as the link text and the JavaScript code goes into the +onclick+ attribute. - # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. Once all - # the JavaScript is set, the helper appends "; return false;". - # - # The +href+ attribute of the tag is set to "#" unless +html_options+ has one. - # - # link_to_function "Greeting", "alert('Hello world!')", :class => "nav_link" - # # => <a class="nav_link" href="#" onclick="alert('Hello world!'); return false;">Greeting</a> - # - def link_to_function(name, function, html_options={}) - onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;" - href = html_options[:href] || '#' - - content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick)) - end end end end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index d3a7a1bb96..dfc26acfad 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -28,17 +28,20 @@ module ActionView end end - # Formats a +number+ into a US phone number (e.g., (555) 123-9876). You can customize the format - # in the +options+ hash. + # Formats a +number+ into a US phone number (e.g., (555) + # 123-9876). You can customize the format in the +options+ hash. # # ==== Options # - # * <tt>:area_code</tt> - Adds parentheses around the area code. - # * <tt>:delimiter</tt> - Specifies the delimiter to use (defaults to "-"). - # * <tt>:extension</tt> - Specifies an extension to add to the end of the - # generated number. - # * <tt>:country_code</tt> - Sets the country code for the phone number. - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. + # * <tt>:area_code</tt> - Adds parentheses around the area code. + # * <tt>:delimiter</tt> - Specifies the delimiter to use + # (defaults to "-"). + # * <tt>:extension</tt> - Specifies an extension to add to the + # end of the generated number. + # * <tt>:country_code</tt> - Sets the country code for the phone + # number. + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. # # ==== Examples # @@ -81,24 +84,31 @@ module ActionView ERB::Util.html_escape(str) end - # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format - # in the +options+ hash. + # Formats a +number+ into a currency string (e.g., $13.65). You + # can customize the format in the +options+ hash. # # ==== Options # - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). - # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). - # * <tt>:format</tt> - Sets the format for non-negative numbers (defaults to "%u%n"). - # Fields are <tt>%u</tt> for the currency, and <tt>%n</tt> - # for the number. - # * <tt>:negative_format</tt> - Sets the format for negative numbers (defaults to prepending - # an hyphen to the formatted number given by <tt>:format</tt>). - # Accepts the same fields than <tt>:format</tt>, except - # <tt>%n</tt> is here the absolute value of the number. - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the level of precision (defaults + # to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency + # (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units + # (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:format</tt> - Sets the format for non-negative numbers + # (defaults to "%u%n"). Fields are <tt>%u</tt> for the + # currency, and <tt>%n</tt> for the number. + # * <tt>:negative_format</tt> - Sets the format for negative + # numbers (defaults to prepending an hyphen to the formatted + # number given by <tt>:format</tt>). Accepts the same fields + # than <tt>:format</tt>, except <tt>%n</tt> is here the + # absolute value of the number. + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. # # ==== Examples # @@ -148,23 +158,29 @@ module ActionView end end - # Formats a +number+ as a percentage string (e.g., 65%). You can customize the format in the +options+ hash. + # Formats a +number+ as a percentage string (e.g., 65%). You can + # customize the format in the +options+ hash. # # ==== Options # - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current - # locale). - # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, - # the # of fractional digits (defaults to +false+). - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults - # to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). - # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator - # (defaults to +false+). - # * <tt>:format</tt> - Specifies the format of the percentage string - # The number field is <tt>%n</tt> (defaults to "%n%"). - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # * <tt>:format</tt> - Specifies the format of the percentage + # string The number field is <tt>%n</tt> (defaults to "%n%"). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. # # ==== Examples # @@ -200,15 +216,20 @@ module ActionView end end - # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can - # customize the format in the +options+ hash. + # Formats a +number+ with grouped thousands using +delimiter+ + # (e.g., 12,324). You can customize the format in the +options+ + # hash. # # ==== Options # - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ","). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. # # ==== Examples # @@ -233,26 +254,35 @@ module ActionView parts = number.to_s.to_str.split('.') parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - parts.join(options[:separator]).html_safe + safe_join(parts, options[:separator]) end - # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision - # of 2 if +:significant+ is +false+, and 5 if +:significant+ is +true+). + # Formats a +number+ with the specified level of + # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if + # +:significant+ is +false+, and 5 if +:significant+ is +true+). # You can customize the format in the +options+ hash. # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, - # the # of fractional digits (defaults to +false+). - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults - # to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). - # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator - # (defaults to +false+). - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. # # ==== Examples + # # number_with_precision(111.2345) # => 111.235 # number_with_precision(111.2345, :precision => 2) # => 111.23 # number_with_precision(13, :precision => 5) # => 13.00000 @@ -305,23 +335,37 @@ module ActionView STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze - # Formats the bytes in +number+ into a more understandable representation - # (e.g., giving it 1500 yields 1.5 KB). This method is useful for - # reporting file sizes to users. You can customize the - # format in the +options+ hash. + # Formats the bytes in +number+ into a more understandable + # representation (e.g., giving it 1500 yields 1.5 KB). This + # method is useful for reporting file sizes to users. You can + # customize the format in the +options+ hash. # - # See <tt>number_to_human</tt> if you want to pretty-print a generic number. + # See <tt>number_to_human</tt> if you want to pretty-print a + # generic number. # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +true+) - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). - # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator (defaults to +true+) - # * <tt>:prefix</tt> - If +:si+ formats the number using the SI prefix (defaults to :binary) - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:prefix</tt> - If +:si+ formats the number using the SI + # prefix (defaults to :binary) + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. + # # ==== Examples + # # number_to_human_size(123) # => 123 Bytes # number_to_human_size(1234) # => 1.21 KB # number_to_human_size(12345) # => 12.1 KB @@ -332,8 +376,10 @@ module ActionView # number_to_human_size(483989, :precision => 2) # => 470 KB # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,2 MB # - # Non-significant zeros after the fractional separator are stripped out by default (set - # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): + # Non-significant zeros after the fractional separator are + # stripped out by default (set + # <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" def number_to_human_size(number, options = {}) @@ -371,33 +417,55 @@ module ActionView DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze - # Pretty prints (formats and approximates) a number in a way it is more readable by humans - # (eg.: 1200000000 becomes "1.2 Billion"). This is useful for numbers that - # can get very large (and too hard to read). + # Pretty prints (formats and approximates) a number in a way it + # is more readable by humans (eg.: 1200000000 becomes "1.2 + # Billion"). This is useful for numbers that can get very large + # (and too hard to read). # - # See <tt>number_to_human_size</tt> if you want to print a file size. + # See <tt>number_to_human_size</tt> if you want to print a file + # size. # - # You can also define you own unit-quantifier names if you want to use other decimal units - # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 milliliters", etc). You may define - # a wide range of unit quantifiers, even fractional ones (centi, deci, mili, etc). + # You can also define you own unit-quantifier names if you want + # to use other decimal units (eg.: 1500 becomes "1.5 + # kilometers", 0.150 becomes "150 milliliters", etc). You may + # define a wide range of unit quantifiers, even fractional ones + # (centi, deci, mili, etc). # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +true+) - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). - # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator (defaults to +true+) - # * <tt>:units</tt> - A Hash of unit quantifier names. Or a string containing an i18n scope where to find this hash. It might have the following keys: - # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, <tt>:billion</tt>, <tt>:trillion</tt>, <tt>:quadrillion</tt> - # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, <tt>:pico</tt>, <tt>:femto</tt> - # * <tt>:format</tt> - Sets the format of the output string (defaults to "%n %u"). The field types are: - # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. - # - # %u The quantifier (ex.: 'thousand') - # %n The number + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting + # (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number + # (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # + # of significant_digits. If +false+, the # of fractional + # digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the + # fractional and integer digits (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults + # to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +true+) + # * <tt>:units</tt> - A Hash of unit quantifier names. Or a + # string containing an i18n scope where to find this hash. It + # might have the following keys: + # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, + # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # *<tt>:billion</tt>, <tt>:trillion</tt>, + # *<tt>:quadrillion</tt> + # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, + # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # *<tt>:pico</tt>, <tt>:femto</tt> + # * <tt>:format</tt> - Sets the format of the output string + # (defaults to "%n %u"). The field types are: + # * %u - The quantifier (ex.: 'thousand') + # * %n - The number + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when + # the argument is invalid. # # ==== Examples + # # number_to_human(123) # => "123" # number_to_human(1234) # => "1.23 Thousand" # number_to_human(12345) # => "12.3 Thousand" @@ -414,8 +482,9 @@ module ActionView # :separator => ',', # :significant => false) # => "1,2 Million" # - # Unsignificant zeros after the decimal separator are stripped out by default (set - # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): + # Non-significant zeros after the decimal separator are stripped + # out by default (set <tt>:strip_insignificant_zeros</tt> to + # +false+ to change that): # number_to_human(12345012345, :significant_digits => 6) # => "12.345 Billion" # number_to_human(500000000, :precision => 5) # => "500 Million" # diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 1a15459406..9b35f076e5 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -94,10 +94,10 @@ module ActionView # for each record. def content_tag_for_single_record(tag_name, record, prefix, options, &block) options = options ? options.dup : {} - options.merge!(:class => "#{dom_class(record, prefix)} #{options[:class]}".rstrip, :id => dom_id(record, prefix)) + options[:class] = "#{dom_class(record, prefix)} #{options[:class]}".rstrip + options[:id] = dom_id(record, prefix) - content = block.arity == 0 ? capture(&block) : capture(record, &block) - content_tag(tag_name, content, options) + content_tag(tag_name, capture(record, &block), options) end end end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 23777721f6..f7afa48256 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -109,8 +109,12 @@ module ActionView # # cdata_section(File.read("hello_world.txt")) # # => <![CDATA[<hello from a text file]]> + # + # cdata_section("hello]]>world") + # # => <![CDATA[hello]]]]><![CDATA[>world]]> def cdata_section(content) - "<![CDATA[#{content}]]>".html_safe + splitted = content.gsub(']]>', ']]]]><![CDATA[>') + "<![CDATA[#{splitted}]]>".html_safe end # Returns an escaped version of +html+ without affecting existing escaped entities. diff --git a/actionpack/lib/action_view/helpers/tags/check_box.rb b/actionpack/lib/action_view/helpers/tags/check_box.rb index 1a4aebb936..9d17a1dde3 100644 --- a/actionpack/lib/action_view/helpers/tags/check_box.rb +++ b/actionpack/lib/action_view/helpers/tags/check_box.rb @@ -41,17 +41,15 @@ module ActionView def checked?(value) case value when TrueClass, FalseClass - value + value == !!@checked_value when NilClass false - when Integer - value != 0 when String value == @checked_value when Array value.include?(@checked_value) else - value.to_i != 0 + value.to_i == @checked_value.to_i end end diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb index 6a1479069f..4e33e79a36 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb @@ -49,7 +49,7 @@ module ActionView accept = if current_value.respond_to?(:call) current_value.call(item) else - Array(current_value).include?(value) + Array(current_value).map(&:to_s).include?(value.to_s) end if accept diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 12bb162da2..698f4434ba 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -45,7 +45,7 @@ module ActionView # if logged_in # concat "Logged in!" # else - # concat link_to('login', :action => login) + # concat link_to('login', :action => :login) # end # # will either display "Logged in!" or a login link # %> @@ -90,7 +90,7 @@ module ActionView # Highlights one or more +phrases+ everywhere in +text+ by inserting it into # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt> - # as a single-quoted string with \1 where the phrase is to be inserted (defaults to + # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to # '<mark>\1</mark>') # # ==== Examples @@ -108,7 +108,9 @@ module ActionView # # You can still use <tt>highlight</tt> with the old API that accepts the # +highlighter+ as its optional third parameter: - # highlight('You searched for: rails', 'rails', '<a href="search?q=\1">\1</a>') # => You searched for: <a href="search?q=rails">rails</a> + # + # highlight('You searched for: rails', 'rails', '<a href="search?q=\1">\1</a>') + # # => You searched for: <a href="search?q=rails">rails</a> def highlight(text, phrases, *args) options = args.extract_options! unless args.empty? diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index cc74eff53a..8171bea8ed 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -45,6 +45,7 @@ module ActionView # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) options.merge!(:rescue_format => :html) unless options.key?(:rescue_format) + options[:default] = wrap_translate_defaults(options[:default]) if options[:default] if html_safe_translation_key?(key) html_safe_options = options.dup options.except(*I18n::RESERVED_KEYS).each do |name, value| @@ -62,6 +63,9 @@ module ActionView alias :t :translate # Delegates to <tt>I18n.localize</tt> with no additional functionality. + # + # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize + # for more information. def localize(*args) I18n.localize(*args) end @@ -83,6 +87,21 @@ module ActionView def html_safe_translation_key?(key) key.to_s =~ /(\b|_|\.)html$/ end + + def wrap_translate_defaults(defaults) + new_defaults = [] + defaults = Array(defaults) + while key = defaults.shift + if key.is_a?(Symbol) + new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) } + break + else + new_defautls << key + end + end + + new_defaults + end end end end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index d0f716cc80..1145f348c2 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -303,7 +303,10 @@ module ActionView # # <%= button_to "Create", :action => "create", :remote => true, :form => { "data-type" => "json" } %> # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> - # # <div><input value="Create" type="submit" /></div> + # # <div> + # # <input value="Create" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> + # # </div> # # </form>" # # @@ -312,7 +315,8 @@ module ActionView # # => "<form method="post" action="/images/delete/1" class="button_to"> # # <div> # # <input type="hidden" name="_method" value="delete" /> - # # <input data-confirm='Are you sure?' value="Delete" type="submit" /> + # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" # @@ -323,6 +327,7 @@ module ActionView # # <div> # # <input name='_method' value='delete' type='hidden' /> # # <input value='Destroy' type='submit' disable_with='loading...' data-confirm='Are you sure?' /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" # # diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 34ea06c9cf..87609fd5ff 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -158,8 +158,8 @@ module ActionView # Name: <%= user.name %> # </div> # - # If a collection is given, the layout will be rendered once for each item in the collection. Just think - # these two snippets have the same output: + # If a collection is given, the layout will be rendered once for each item in + # the collection. Just think these two snippets have the same output: # # <%# app/views/users/_user.html.erb %> # Name: <%= user.name %> @@ -184,7 +184,7 @@ module ActionView # <%= render :partial => "user", :layout => "li_layout", :collection => users %> # </ul> # - # Given two users whose names are Alice and Bob, these snippets return: + # Given two users whose names are Alice and Bob, these snippets return: # # <ul> # <li> @@ -195,6 +195,10 @@ module ActionView # </li> # </ul> # + # The current object being rendered, as well as the object_counter, will be + # available as local variables inside the layout template under the same names + # as available in the partial. + # # You can also apply a layout to a block within any template: # # <%# app/views/users/_chief.html.erb &> @@ -282,14 +286,7 @@ module ActionView spacer = find_template(@options[:spacer_template]).render(@view, @locals) end - if layout = @options[:layout] - layout = find_template(layout) - end - result = @template ? collection_with_template : collection_without_template - - result.map!{|content| layout.render(@view, @locals) { content } } if layout - result.join(spacer).html_safe end @@ -298,7 +295,7 @@ module ActionView object, as = @object, @variable if !block && (layout = @options[:layout]) - layout = find_template(layout) + layout = find_template(layout, @locals.keys + [@variable]) end object ||= locals[as] @@ -346,8 +343,8 @@ module ActionView if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/ raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.") + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.") end self @@ -384,17 +381,23 @@ module ActionView segments, locals, template = [], @locals, @template as, counter = @variable, @variable_counter + if layout = @options[:layout] + layout = find_template(layout, @locals.keys + [@variable, @variable_counter]) + end + locals[counter] = -1 @collection.each do |object| locals[counter] += 1 locals[as] = object - segments << template.render(@view, locals) + + content = template.render(@view, locals) + content = layout.render(@view, locals) { content } if layout + segments << content end - + segments end - def collection_without_template segments, locals, collection_data = [], @locals, @collection_data diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 8ea2e5bfe4..08155af013 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -1,5 +1,6 @@ require "pathname" require "active_support/core_ext/class" +require "active_support/core_ext/class/attribute_accessors" require "action_view/template" module ActionView diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 22ba047328..ba06bcae51 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -1,11 +1,5 @@ require File.expand_path('../../../load_paths', __FILE__) -lib = File.expand_path("#{File.dirname(__FILE__)}/../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - -activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__) -$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path) - $:.unshift(File.dirname(__FILE__) + '/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb index fceebe1858..275f25f597 100644 --- a/actionpack/test/activerecord/active_record_store_test.rb +++ b/actionpack/test/activerecord/active_record_store_test.rb @@ -254,6 +254,13 @@ class ActiveRecordStoreTest < ActionDispatch::IntegrationTest end end + def test_session_store_with_all_domains + with_test_route_set(:domain => :all) do + get '/set_session_value' + assert_response :success + end + end + private def with_test_route_set(options = {}) diff --git a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb b/actionpack/test/activerecord/render_partial_with_record_identification_test.rb index 8187eb72d5..409370104d 100644 --- a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionpack/test/activerecord/render_partial_with_record_identification_test.rb @@ -16,7 +16,7 @@ class RenderPartialWithRecordIdentificationController < ActionController::Base end def render_with_has_many_through_association - @developer = Developer.find(:first) + @developer = Developer.first render :partial => @developer.topics end @@ -31,7 +31,7 @@ class RenderPartialWithRecordIdentificationController < ActionController::Base end def render_with_record - @developer = Developer.find(:first) + @developer = Developer.first render :partial => @developer end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index b121ca9481..9b0d4d0f4c 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -76,6 +76,11 @@ class ActionPackAssertionsController < ActionController::Base render "test/hello_world", :layout => "layouts/standard" end + def render_with_layout_and_partial + @variable_for_layout = nil + render "test/hello_world_with_partial", :layout => "layouts/standard" + end + def session_stuffing session['xmas'] = 'turkey' render :text => "ho ho ho" @@ -179,6 +184,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_redirected_to 'http://test.host/route_two' end assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to %r(^http://test.host/route_two) + end + assert_raise(ActiveSupport::TestCase::Assertion) do assert_redirected_to :controller => 'action_pack_assertions', :action => 'nothing', :id => 'two' end assert_raise(ActiveSupport::TestCase::Assertion) do @@ -212,6 +220,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase process :redirect_to_top_level_named_route # assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return assert_redirected_to "/action_pack_assertions/foo" + assert_redirected_to %r(/action_pack_assertions/foo) end end @@ -469,11 +478,43 @@ class AssertTemplateTest < ActionController::TestCase end end + def test_fails_expecting_no_layout + get :render_with_layout + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template :layout => nil + end + end + def test_passes_with_correct_layout get :render_with_layout assert_template :layout => "layouts/standard" end + def test_passes_with_layout_and_partial + get :render_with_layout_and_partial + assert_template :layout => "layouts/standard" + end + + def test_passed_with_no_layout + get :hello_world + assert_template :layout => nil + end + + def test_passed_with_no_layout_false + get :hello_world + assert_template :layout => false + end + + def test_passes_with_correct_layout_without_layouts_prefix + get :render_with_layout + assert_template :layout => "standard" + end + + def test_passes_with_correct_layout_symbol + get :render_with_layout + assert_template :layout => :standard + end + def test_assert_template_reset_between_requests get :hello_world assert_template 'test/hello_world' diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index e8546cb154..9efe328d62 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -236,6 +236,7 @@ class ActionCachingTestController < CachingController caches_action :with_layout caches_action :with_format_and_http_param, :cache_path => Proc.new { |c| { :key => 'value' } } caches_action :layout_false, :layout => false + caches_action :with_layout_proc_param, :layout => Proc.new { |c| c.params[:layout] } caches_action :record_not_found, :four_oh_four, :simple_runtime_error caches_action :streaming @@ -282,6 +283,7 @@ class ActionCachingTestController < CachingController alias_method :edit, :index alias_method :destroy, :index alias_method :layout_false, :with_layout + alias_method :with_layout_proc_param, :with_layout def expire expire_action :controller => 'action_caching_test', :action => 'index' @@ -403,11 +405,40 @@ class ActionCacheTest < ActionController::TestCase get :layout_false assert_response :success assert_not_equal cached_time, @response.body - body = body_to_string(read_fragment('hostname.com/action_caching_test/layout_false')) assert_equal cached_time, body end + def test_action_cache_with_layout_and_layout_cache_false_via_proc + get :with_layout_proc_param, :layout => false + assert_response :success + cached_time = content_to_cache + assert_not_equal cached_time, @response.body + assert fragment_exist?('hostname.com/action_caching_test/with_layout_proc_param') + reset! + + get :with_layout_proc_param, :layout => false + assert_response :success + assert_not_equal cached_time, @response.body + body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout_proc_param')) + assert_equal cached_time, body + end + + def test_action_cache_with_layout_and_layout_cache_true_via_proc + get :with_layout_proc_param, :layout => true + assert_response :success + cached_time = content_to_cache + assert_not_equal cached_time, @response.body + assert fragment_exist?('hostname.com/action_caching_test/with_layout_proc_param') + reset! + + get :with_layout_proc_param, :layout => true + assert_response :success + assert_not_equal cached_time, @response.body + body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout_proc_param')) + assert_equal @response.body, body + end + def test_action_cache_conditional_options @request.env['HTTP_ACCEPT'] = 'application/json' get :index diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 877b91b563..fb41dcb33a 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -405,6 +405,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_request_with_bad_format + with_test_route_set do + xhr :get, '/get.php' + assert_equal 406, status + assert_response 406 + assert_response :not_acceptable + end + end + def test_get_with_query_string with_test_route_set do get '/get_with_params?foo=bar' @@ -606,3 +615,83 @@ class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest assert_equal '[FILTERED]', request.filtered_env['rack.request.form_vars'] end end + +class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def index + render :text => "foo#index" + end + + def show + render :text => "foo#show" + end + + def edit + render :text => "foo#show" + end + end + + class BarController < ActionController::Base + def default_url_options + { :host => "bar.com" } + end + + def index + render :text => "foo#index" + end + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + def self.call(env) + routes.call(env) + end + + def app + self.class + end + + routes.draw do + default_url_options :host => "foo.com" + + scope :module => "url_options_integration_test" do + get "/foo" => "foo#index", :as => :foos + get "/foo/:id" => "foo#show", :as => :foo + get "/foo/:id/edit" => "foo#edit", :as => :edit_foo + get "/bar" => "bar#index", :as => :bars + end + end + + test "session uses default url options from routes" do + assert_equal "http://foo.com/foo", foos_url + end + + test "current host overrides default url options from routes" do + get "/foo" + assert_response :success + assert_equal "http://www.example.com/foo", foos_url + end + + test "controller can override default url options from request" do + get "/bar" + assert_response :success + assert_equal "http://bar.com/foo", foos_url + end + + test "test can override default url options" do + default_url_options[:host] = "foobar.com" + assert_equal "http://foobar.com/foo", foos_url + + get "/bar" + assert_response :success + assert_equal "http://foobar.com/foo", foos_url + end + + test "current request path parameters are recalled" do + get "/foo/1" + assert_response :success + assert_equal "/foo/1/edit", url_for(:action => 'edit', :only_path => true) + end +end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index ac056319fc..bdcd5561a8 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -207,8 +207,9 @@ class RespondToControllerTest < ActionController::TestCase get :html_or_xml assert_equal 'HTML', @response.body - get :just_xml - assert_response 406 + assert_raises(ActionController::UnknownFormat) do + get :just_xml + end end def test_all @@ -239,8 +240,10 @@ class RespondToControllerTest < ActionController::TestCase assert_equal 'HTML', @response.body @request.accept = "text/javascript, text/html" - xhr :get, :just_xml - assert_response 406 + + assert_raises(ActionController::UnknownFormat) do + xhr :get, :just_xml + end end def test_json_or_yaml_with_leading_star_star @@ -495,9 +498,9 @@ class RespondToControllerTest < ActionController::TestCase end def test_invalid_format - get :using_defaults, :format => "invalidformat" - assert_equal " ", @response.body - assert_equal "text/html", @response.content_type + assert_raises(ActionController::UnknownFormat) do + get :using_defaults, :format => "invalidformat" + end end end @@ -701,12 +704,14 @@ class RespondWithControllerTest < ActionController::TestCase def test_not_acceptable @request.accept = "application/xml" - get :using_resource_with_block - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :using_resource_with_block + end @request.accept = "text/javascript" - get :using_resource_with_overwrite_block - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :using_resource_with_overwrite_block + end end def test_using_resource_for_post_with_html_redirects_on_success @@ -984,8 +989,9 @@ class RespondWithControllerTest < ActionController::TestCase def test_clear_respond_to @controller = InheritedRespondWithController.new @request.accept = "text/html" - get :index - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :index + end end def test_first_in_respond_to_has_higher_priority diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index c424dcbd8d..5bcd79ebec 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -37,6 +37,36 @@ module BareMetalTest def index head :not_found end + + def continue + self.content_type = "text/html" + head 100 + end + + def switching_protocols + self.content_type = "text/html" + head 101 + end + + def processing + self.content_type = "text/html" + head 102 + end + + def no_content + self.content_type = "text/html" + head 204 + end + + def reset_content + self.content_type = "text/html" + head 205 + end + + def not_modified + self.content_type = "text/html" + head 304 + end end class HeadTest < ActiveSupport::TestCase @@ -44,6 +74,42 @@ module BareMetalTest status = HeadController.action(:index).call(Rack::MockRequest.env_for("/")).first assert_equal 404, status end + + test "head :continue (100) does not return a content-type header" do + headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :continue (101) does not return a content-type header" do + headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :processing (102) does not return a content-type header" do + headers = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :no_content (204) does not return a content-type header" do + headers = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :reset_content (205) does not return a content-type header" do + headers = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :not_modified (304) does not return a content-type header" do + headers = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end end class BareControllerTest < ActionController::TestCase diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index e0b38b29fa..f8d02e8b6c 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -65,7 +65,7 @@ module RenderText class RenderTextTest < Rack::TestCase describe "Rendering text using render :text" - test "rendering text from a action with default options renders the text with the layout" do + test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| set.draw { get ':controller', :action => 'index' } @@ -75,7 +75,7 @@ module RenderText end end - test "rendering text from a action with default options renders the text without the layout" do + test "rendering text from an action with default options renders the text without the layout" do with_routing do |set| set.draw { get ':controller', :action => 'index' } diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index c4d2614200..fa1608b9df 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -174,7 +174,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_accessible_wrapped_keys_from_matching_model User.expects(:respond_to?).with(:accessible_attributes).returns(true) - User.expects(:accessible_attributes).twice.returns(["username"]) + User.expects(:accessible_attributes).with(:default).twice.returns(["username"]) with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' @@ -186,7 +186,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_accessible_wrapped_keys_from_specified_model with_default_wrapper_options do Person.expects(:respond_to?).with(:accessible_attributes).returns(true) - Person.expects(:accessible_attributes).twice.returns(["username"]) + Person.expects(:accessible_attributes).with(:default).twice.returns(["username"]) UsersController.wrap_parameters Person @@ -195,6 +195,19 @@ class ParamsWrapperTest < ActionController::TestCase assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end + + def test_accessible_wrapped_keys_with_role_from_specified_model + with_default_wrapper_options do + Person.expects(:respond_to?).with(:accessible_attributes).returns(true) + Person.expects(:accessible_attributes).with(:admin).twice.returns(["username"]) + + UsersController.wrap_parameters Person, :as => :admin + + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) + end + end def test_not_wrapping_abstract_model User.expects(:respond_to?).with(:accessible_attributes).returns(false) diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 75fed8e933..7c0a6bd67e 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -102,7 +102,7 @@ class RenderJsonTest < ActionController::TestCase def test_render_json_with_callback get :render_json_hello_world_with_callback assert_equal 'alert({"hello":"world"})', @response.body - assert_equal 'application/json', @response.content_type + assert_equal 'text/javascript', @response.content_type end def test_render_json_with_custom_content_type diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 10f62dad65..3d58c02338 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -102,12 +102,12 @@ class TestController < ActionController::Base end def conditional_hello_with_expires_in_with_public_with_more_keys - expires_in 1.minute, :public => true, 'max-stale' => 5.hours + expires_in 1.minute, :public => true, 's-maxage' => 5.hours render :action => 'hello_world' end def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax - expires_in 1.minute, :public => true, :private => nil, 'max-stale' => 5.hours + expires_in 1.minute, :public => true, :private => nil, 's-maxage' => 5.hours render :action => 'hello_world' end @@ -505,6 +505,14 @@ class TestController < ActionController::Base render :text => "hello world!" end + def head_created + head :created + end + + def head_created_with_application_json_content_type + head :created, :content_type => "application/json" + end + def head_with_location_header head :location => "/foo" end @@ -1177,6 +1185,19 @@ class RenderTest < ActionController::TestCase assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body end + def test_head_created + post :head_created + assert_blank @response.body + assert_response :created + end + + def test_head_created_with_application_json_content_type + post :head_created_with_application_json_content_type + assert_blank @response.body + assert_equal "application/json", @response.content_type + assert_response :created + end + def test_head_with_location_header get :head_with_location_header assert_blank @response.body @@ -1478,12 +1499,12 @@ class ExpiresInRenderTest < ActionController::TestCase def test_expires_in_header_with_additional_headers get :conditional_hello_with_expires_in_with_public_with_more_keys - assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] end def test_expires_in_old_syntax get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax - assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] end def test_expires_now diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 9441b46d47..cd91064ab8 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -891,23 +891,6 @@ class RouteSetTest < ActiveSupport::TestCase MockController.build(set.url_helpers).new end - def test_named_route_hash_access_method - controller = setup_named_route_test - - assert_equal( - { :controller => 'people', :action => 'show', :id => 5, :use_route => "show", :only_path => false }, - controller.send(:hash_for_show_url, :id => 5)) - - assert_equal( - { :controller => 'people', :action => 'index', :use_route => "index", :only_path => false }, - controller.send(:hash_for_index_url)) - - assert_equal( - { :controller => 'people', :action => 'show', :id => 5, :use_route => "show", :only_path => true }, - controller.send(:hash_for_show_path, :id => 5) - ) - end - def test_named_route_url_method controller = setup_named_route_test @@ -919,7 +902,6 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal "http://test.host/admin/users", controller.send(:users_url) assert_equal '/admin/users', controller.send(:users_path) - assert_equal '/admin/users', url_for(set, controller.send(:hash_for_users_url), { :controller => 'users', :action => 'index' }) end def test_named_route_url_method_with_anchor @@ -1423,7 +1405,7 @@ class RouteSetTest < ActiveSupport::TestCase end end end - + def test_route_with_subdomain_and_constraints_must_receive_params name_param = nil set.draw do @@ -1436,7 +1418,7 @@ class RouteSetTest < ActiveSupport::TestCase set.recognize_path('http://subdomain.example.org/page/mypage')) assert_equal(name_param, 'mypage') end - + def test_route_requirement_recognize_with_ignore_case set.draw do get 'page/:name' => 'pages#show', diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 3af17f495c..6fc3556e31 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -154,6 +154,17 @@ class SendFileTest < ActionController::TestCase end end + def test_send_file_with_default_content_disposition_header + process('data') + assert_equal 'attachment', @controller.headers['Content-Disposition'] + end + + def test_send_file_without_content_disposition_header + @controller.options = {:disposition => nil} + process('data') + assert_nil @controller.headers['Content-Disposition'] + end + %w(file data).each do |method| define_method "test_send_#{method}_status" do @controller.options = { :stream => false, :status => 500 } diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index e8411bae0c..0d6d303b51 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -45,6 +45,10 @@ class TestCaseTest < ActionController::TestCase render :text => request.fullpath end + def test_format + render :text => request.format + end + def test_query_string render :text => request.query_string end @@ -224,6 +228,26 @@ XML assert_equal 'value2', session[:symbol] end + def test_process_merges_session_arg + session[:foo] = 'bar' + get :no_op, nil, { :bar => 'baz' } + assert_equal 'bar', session[:foo] + assert_equal 'baz', session[:bar] + end + + def test_merged_session_arg_is_retained_across_requests + get :no_op, nil, { :foo => 'bar' } + assert_equal 'bar', session[:foo] + get :no_op + assert_equal 'bar', session[:foo] + end + + def test_process_overwrites_existing_session_arg + session[:foo] = 'bar' + get :no_op, nil, { :foo => 'baz' } + assert_equal 'baz', session[:foo] + end + def test_session_is_cleared_from_controller_after_reset_session process :set_session process :reset_the_session @@ -559,14 +583,34 @@ XML ) end + def test_params_passing_with_fixnums_when_not_html_request + get :test_params, :format => 'json', :count => 999 + parsed_params = eval(@response.body) + assert_equal( + {'controller' => 'test_case_test/test', 'action' => 'test_params', + 'format' => 'json', 'count' => 999 }, + parsed_params + ) + end + + def test_params_passing_path_parameter_is_string_when_not_html_request + get :test_params, :format => 'json', :id => 1 + parsed_params = eval(@response.body) + assert_equal( + {'controller' => 'test_case_test/test', 'action' => 'test_params', + 'format' => 'json', 'id' => '1' }, + parsed_params + ) + end + def test_params_passing_with_frozen_values assert_nothing_raised do - get :test_params, :frozen => 'icy'.freeze, :frozens => ['icy'.freeze].freeze + get :test_params, :frozen => 'icy'.freeze, :frozens => ['icy'.freeze].freeze, :deepfreeze => { :frozen => 'icy'.freeze }.freeze end parsed_params = eval(@response.body) assert_equal( {'controller' => 'test_case_test/test', 'action' => 'test_params', - 'frozen' => 'icy', 'frozens' => ['icy']}, + 'frozen' => 'icy', 'frozens' => ['icy'], 'deepfreeze' => { 'frozen' => 'icy' }}, parsed_params ) end @@ -667,6 +711,20 @@ XML assert_equal "http://", @response.body end + def test_request_format + get :test_format, :format => 'html' + assert_equal 'text/html', @response.body + + get :test_format, :format => 'json' + assert_equal 'application/json', @response.body + + get :test_format, :format => 'xml' + assert_equal 'application/xml', @response.body + + get :test_format + assert_equal 'text/html', @response.body + end + def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set cookies['foo'] = 'bar' get :no_op @@ -828,3 +886,24 @@ class NamedRoutesControllerTest < ActionController::TestCase end end end + +class AnonymousControllerTest < ActionController::TestCase + def setup + @controller = Class.new(ActionController::Base) do + def index + render :text => params[:controller] + end + end.new + + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + get ':controller(/:action(/:id))' + end + end + end + + def test_controller_name + get :index + assert_equal 'anonymous', @response.body + end +end
\ No newline at end of file diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 3e48d97e67..2467654a70 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -38,6 +38,8 @@ class CookiesTest < ActionController::TestCase head :ok end + alias delete_cookie logout + def delete_cookie_with_path cookies.delete("user_name", :path => '/beaten') head :ok @@ -179,6 +181,18 @@ class CookiesTest < ActionController::TestCase assert_equal({"user_name" => "david"}, @response.cookies) end + def test_setting_the_same_value_to_cookie + request.cookies[:user_name] = 'david' + get :authenticate + assert response.cookies.empty? + end + + def test_setting_the_same_value_to_permanent_cookie + request.cookies[:user_name] = 'Jamie' + get :set_permanent_cookie + assert response.cookies, 'user_name' => 'Jamie' + end + def test_setting_with_escapable_characters get :set_with_with_escapable_characters assert_cookie_header "that+%26+guy=foo+%26+bar+%3D%3E+baz; path=/" @@ -235,23 +249,33 @@ class CookiesTest < ActionController::TestCase end def test_expiring_cookie + request.cookies[:user_name] = 'Joe' get :logout assert_cookie_header "user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" assert_equal({"user_name" => nil}, @response.cookies) end def test_delete_cookie_with_path + request.cookies[:user_name] = 'Joe' get :delete_cookie_with_path assert_cookie_header "user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT" end + def test_delete_unexisting_cookie + request.cookies.clear + get :delete_cookie + assert @response.cookies.empty? + end + def test_deleted_cookie_predicate + cookies[:user_name] = 'Joe' cookies.delete("user_name") assert cookies.deleted?("user_name") assert_equal false, cookies.deleted?("another") end def test_deleted_cookie_predicate_with_mismatching_options + cookies[:user_name] = 'Joe' cookies.delete("user_name", :path => "/path") assert_equal false, cookies.deleted?("user_name", :path => "/different") end @@ -284,6 +308,7 @@ class CookiesTest < ActionController::TestCase end def test_delete_and_set_cookie + request.cookies[:user_name] = 'Joe' get :delete_and_set_cookie assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT" assert_equal({"user_name" => "david"}, @response.cookies) @@ -387,6 +412,7 @@ class CookiesTest < ActionController::TestCase end def test_deleting_cookie_with_all_domain_option + request.cookies[:user_name] = 'Joe' get :delete_cookie_with_domain assert_response :success assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" @@ -413,6 +439,7 @@ class CookiesTest < ActionController::TestCase end def test_deleting_cookie_with_all_domain_option_and_tld_length + request.cookies[:user_name] = 'Joe' get :delete_cookie_with_domain_and_tld assert_response :success assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" @@ -441,6 +468,7 @@ class CookiesTest < ActionController::TestCase def test_deletings_cookie_with_several_preset_domains_using_one_of_these_domains @request.host = "example2.com" + request.cookies[:user_name] = 'Joe' get :delete_cookie_with_domains assert_response :success assert_cookie_header "user_name=; domain=example2.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" @@ -448,19 +476,19 @@ class CookiesTest < ActionController::TestCase def test_deletings_cookie_with_several_preset_domains_using_other_domain @request.host = "other-domain.com" + request.cookies[:user_name] = 'Joe' get :delete_cookie_with_domains assert_response :success assert_cookie_header "user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" end - def test_cookies_hash_is_indifferent_access - get :symbol_key - assert_equal "david", cookies[:user_name] - assert_equal "david", cookies['user_name'] - get :string_key - assert_equal "dhh", cookies[:user_name] - assert_equal "dhh", cookies['user_name'] + get :symbol_key + assert_equal "david", cookies[:user_name] + assert_equal "david", cookies['user_name'] + get :string_key + assert_equal "dhh", cookies[:user_name] + assert_equal "dhh", cookies['user_name'] end @@ -575,4 +603,4 @@ class CookiesTest < ActionController::TestCase assert_not_equal expected.split("\n"), header end end -end
\ No newline at end of file +end diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index ec6ba494dc..bc7cad8db5 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -13,4 +13,9 @@ class HeaderTest < ActiveSupport::TestCase assert_equal "text/plain", @headers["CONTENT_TYPE"] assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"] end + + test "fetch" do + assert_equal "text/plain", @headers.fetch("content-type", nil) + assert_equal "not found", @headers.fetch('not-found', 'not found') + end end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index f7a746120e..536e35ab2e 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -37,6 +37,11 @@ class TestRoutingMount < ActionDispatch::IntegrationTest assert_equal "/sprockets -- /omg", response.body end + def test_mounting_works_with_nested_script_name + get "/foo/sprockets/omg", {}, 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/sprockets/omg' + assert_equal "/foo/sprockets -- /omg", response.body + end + def test_mounting_works_with_scope get "/its_a/sprocket/omg" assert_equal "/its_a/sprocket -- /omg", response.body diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb new file mode 100644 index 0000000000..4d24456ba6 --- /dev/null +++ b/actionpack/test/dispatch/request/session_test.rb @@ -0,0 +1,48 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/session/abstract_store' + +module ActionDispatch + class Request + class SessionTest < ActiveSupport::TestCase + def test_create_adds_itself_to_env + env = {} + s = Session.create(store, env, {}) + assert_equal s, env[Rack::Session::Abstract::ENV_SESSION_KEY] + end + + def test_to_hash + env = {} + s = Session.create(store, env, {}) + s['foo'] = 'bar' + assert_equal 'bar', s['foo'] + assert_equal({'foo' => 'bar'}, s.to_hash) + end + + def test_create_merges_old + env = {} + s = Session.create(store, env, {}) + s['foo'] = 'bar' + + s1 = Session.create(store, env, {}) + refute_equal s, s1 + assert_equal 'bar', s1['foo'] + end + + def test_find + env = {} + assert_nil Session.find(env) + + s = Session.create(store, env, {}) + assert_equal s, Session.find(env) + end + + private + def store + Class.new { + def load_session(env); [1, {}]; end + def session_exists?(env); true; end + }.new + end + end + end +end diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb index e953029456..517354ae58 100644 --- a/actionpack/test/dispatch/routing_assertions_test.rb +++ b/actionpack/test/dispatch/routing_assertions_test.rb @@ -47,7 +47,7 @@ class RoutingAssertionsTest < ActionController::TestCase def test_assert_recognizes_with_extras assert_recognizes({ :controller => 'articles', :action => 'index', :page => '1' }, '/articles', { :page => '1' }) end - + def test_assert_recognizes_with_method assert_recognizes({ :controller => 'articles', :action => 'create' }, { :path => '/articles', :method => :post }) assert_recognizes({ :controller => 'articles', :action => 'update', :id => '1' }, { :path => '/articles/1', :method => :put }) @@ -57,7 +57,7 @@ class RoutingAssertionsTest < ActionController::TestCase assert_raise(ActionController::RoutingError) do assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'http://test.host/secure/articles') end - assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'https://test.host/secure/articles') + assert_recognizes({ :controller => 'secure_articles', :action => 'index', :protocol => 'https://' }, 'https://test.host/secure/articles') end def test_assert_recognizes_with_block_constraint @@ -90,7 +90,7 @@ class RoutingAssertionsTest < ActionController::TestCase assert_raise(ActionController::RoutingError) do assert_routing('http://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index' }) end - assert_routing('https://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index' }) + assert_routing('https://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index', :protocol => 'https://' }) end def test_assert_routing_with_block_constraint diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 463dd6cb85..3cec816f1c 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -63,8 +63,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get 'secure', :to => redirect("/secure/login") get 'mobile', :to => redirect(:subdomain => 'mobile') + get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') + get 'new_documentation', :to => redirect(:path => '/documentation/new') get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') + get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') + get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) constraints(lambda { |req| true }) do @@ -166,6 +171,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest post :preview, :on => :collection end end + + post 'new', :action => 'new', :on => :collection, :as => :new end resources :replies do @@ -693,11 +700,31 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest verify_redirect 'http://mobile.example.com/mobile' end + def test_redirect_hash_with_domain_and_path + get '/documentation' + verify_redirect 'http://www.example-documentation.com' + end + + def test_redirect_hash_with_path + get '/new_documentation' + verify_redirect 'http://www.example.com/documentation/new' + end + def test_redirect_hash_with_host get '/super_new_documentation?section=top' verify_redirect 'http://super-docs.com/super_new_documentation?section=top' end + def test_redirect_hash_path_substitution + get '/stores/iernest' + verify_redirect 'http://stores.example.com/iernest' + end + + def test_redirect_hash_path_substitution_with_catch_all + get '/stores/iernest/products' + verify_redirect 'http://stores.example.com/iernest/products' + end + def test_redirect_class get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' @@ -802,6 +829,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal original_options, options end + def test_url_for_does_not_modify_controller + controller = '/projects' + options = {:controller => controller, :action => 'status', :only_path => true} + url = url_for(options) + + assert_equal '/projects/status', url + assert_equal '/projects', controller + end + # tests the arguments modification free version of define_hash_access def test_named_route_with_no_side_effects original_options = { :host => 'test.host' } @@ -851,6 +887,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/edit', edit_project_path(:id => '1') end + def test_projects_with_post_action_and_new_path_on_collection + post '/projects/new' + assert_equal "project#new", @response.body + assert_equal "/projects/new", new_projects_path + end + def test_projects_involvements get '/projects/1/involvements' assert_equal 'involvements#index', @response.body @@ -2425,7 +2467,6 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest get "/foo/bar/baz" assert_equal "/pooh", @response.body end - end class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest @@ -2452,3 +2493,158 @@ class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest end end + +class TestRedirectInterpolation < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/foo/:id" => redirect("/foo/bar/%{id}") + get "/bar/:id" => redirect(:path => "/foo/bar/%{id}") + get "/foo/bar/:id" => ok + end + end + + def app; Routes end + + test "redirect escapes interpolated parameters with redirect proc" do + get "/foo/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + + test "redirect escapes interpolated parameters with option proc" do + get "/bar/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + +private + 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 + +class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == 'foo' } + get "/:bar" => ok + end + end + + def app; Routes end + + test "parameters are reset between constraint checks" do + get "/bar" + assert_equal nil, @request.params[:foo] + assert_equal "bar", @request.params[:bar] + end +end + +class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/foo' => ok, as: :foo + end + end + + include Routes.url_helpers + def app; Routes end + + test 'enabled when not mounted and default_url_options is empty' do + assert Routes.url_helpers.optimize_routes_generation? + end + + test 'named route called as singleton method' do + assert_equal '/foo', Routes.url_helpers.foo_path + end + + test 'named route called on included module' do + assert_equal '/foo', foo_path + end +end + +class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest + class CategoriesController < ActionController::Base + def show + render :text => "categories#show" + end + end + + class ProductsController < ActionController::Base + def show + render :text => "products#show" + end + end + + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + scope :module => "test_named_route_url_helpers" do + get "/categories/:id" => 'categories#show', :as => :category + get "/products/:id" => 'products#show', :as => :product + end + end + end + + def app; Routes end + + include Routes.url_helpers + + test "url helpers do not ignore nil parameters when using non-optimized routes" do + Routes.stubs(:optimize_routes_generation?).returns(false) + + get "/categories/1" + assert_response :success + assert_raises(ActionController::RoutingError) { product_path(nil) } + end +end + +class TestUrlConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + constraints :subdomain => 'admin' do + get '/' => ok, :as => :admin_root + end + + scope :constraints => { :protocol => 'https://' } do + get '/' => ok, :as => :secure_root + end + + get '/' => ok, :as => :alternate_root, :constraints => { :port => 8080 } + end + end + + include Routes.url_helpers + def app; Routes end + + test "constraints are copied to defaults when using constraints method" do + assert_equal 'http://admin.example.com/', admin_root_url + + get 'http://admin.example.com/' + assert_response :success + end + + test "constraints are copied to defaults when using scope constraints hash" do + assert_equal 'https://www.example.com/', secure_root_url + + get 'https://www.example.com/' + assert_response :success + end + + test "constraints are copied to defaults when using route constraints hash" do + assert_equal 'http://www.example.com:8080/', alternate_root_url + + get 'http://www.example.com:8080/' + assert_response :success + end +end diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb new file mode 100644 index 0000000000..8daf3d3f5e --- /dev/null +++ b/actionpack/test/dispatch/session/abstract_store_test.rb @@ -0,0 +1,56 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/session/abstract_store' + +module ActionDispatch + module Session + class AbstractStoreTest < ActiveSupport::TestCase + class MemoryStore < AbstractStore + def initialize(app) + @sessions = {} + super + end + + def get_session(env, sid) + sid ||= 1 + session = @sessions[sid] ||= {} + [sid, session] + end + + def set_session(env, sid, session, options) + @sessions[sid] = session + end + end + + def test_session_is_set + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + assert Request::Session.find @env + end + + def test_new_session_object_is_merged_with_old + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + session = Request::Session.find @env + session['foo'] = 'bar' + + as.call(@env) + session1 = Request::Session.find @env + + refute_equal session, session1 + assert_equal session.to_hash, session1.to_hash + end + + private + def app(&block) + @env = nil + lambda { |env| @env = env } + end + end + end +end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index 985ff2e81a..e56e8ddc57 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' module TestUrlGeneration class WithMountPoint < ActionDispatch::IntegrationTest Routes = ActionDispatch::Routing::RouteSet.new - Routes.draw { get "/foo", :to => "my_route_generating#index", :as => :foo } + include Routes.url_helpers class ::MyRouteGeneratingController < ActionController::Base include Routes.url_helpers @@ -12,7 +12,11 @@ module TestUrlGeneration end end - include Routes.url_helpers + Routes.draw do + get "/foo", :to => "my_route_generating#index", :as => :foo + + mount MyRouteGeneratingController.action(:index), at: '/bar' + end def _routes Routes @@ -30,11 +34,16 @@ module TestUrlGeneration assert_equal "/bar/foo", foo_path(:script_name => "/bar") end - test "the request's SCRIPT_NAME takes precedence over the routes'" do + test "the request's SCRIPT_NAME takes precedence over the route" do get "/foo", {}, 'SCRIPT_NAME' => "/new", 'action_dispatch.routes' => Routes assert_equal "/new/foo", response.body end + test "the request's SCRIPT_NAME wraps the mounted app's" do + get '/new/bar/foo', {}, 'SCRIPT_NAME' => '/new', 'PATH_INFO' => '/bar/foo', 'action_dispatch.routes' => Routes + assert_equal "/new/bar/foo", response.body + end + test "handling http protocol with https set" do https! assert_equal "http://www.example.com/foo", foo_url(:protocol => "http") diff --git a/actionpack/test/fixtures/test/_b_layout_for_partial_with_object.html.erb b/actionpack/test/fixtures/test/_b_layout_for_partial_with_object.html.erb new file mode 100644 index 0000000000..bdd53014cd --- /dev/null +++ b/actionpack/test/fixtures/test/_b_layout_for_partial_with_object.html.erb @@ -0,0 +1 @@ +<b class="<%= customer.name.downcase %>"><%= yield %></b>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb b/actionpack/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb new file mode 100644 index 0000000000..44d6121297 --- /dev/null +++ b/actionpack/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb @@ -0,0 +1 @@ +<b data-counter="<%= customer_counter %>"><%= yield %></b>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world_with_partial.html.erb b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb new file mode 100644 index 0000000000..ec31545356 --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb @@ -0,0 +1,2 @@ +Hello world! +<%= render '/test/partial' %> diff --git a/actionpack/test/fixtures/translations/templates/default.erb b/actionpack/test/fixtures/translations/templates/default.erb new file mode 100644 index 0000000000..8b70031071 --- /dev/null +++ b/actionpack/test/fixtures/translations/templates/default.erb @@ -0,0 +1 @@ +<%= t('.missing', :default => :'.foo') %> diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index 82b62e64f3..63066d40cd 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -12,24 +12,24 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase def test_distance_of_time_in_words_calls_i18n { # with include_seconds - [2.seconds, true] => [:'less_than_x_seconds', 5], - [9.seconds, true] => [:'less_than_x_seconds', 10], - [19.seconds, true] => [:'less_than_x_seconds', 20], - [30.seconds, true] => [:'half_a_minute', nil], - [59.seconds, true] => [:'less_than_x_minutes', 1], - [60.seconds, true] => [:'x_minutes', 1], + [2.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 5], + [9.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 10], + [19.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 20], + [30.seconds, { :include_seconds => true }] => [:'half_a_minute', nil], + [59.seconds, { :include_seconds => true }] => [:'less_than_x_minutes', 1], + [60.seconds, { :include_seconds => true }] => [:'x_minutes', 1], # without include_seconds - [29.seconds, false] => [:'less_than_x_minutes', 1], - [60.seconds, false] => [:'x_minutes', 1], - [44.minutes, false] => [:'x_minutes', 44], - [61.minutes, false] => [:'about_x_hours', 1], - [24.hours, false] => [:'x_days', 1], - [30.days, false] => [:'about_x_months', 1], - [60.days, false] => [:'x_months', 2], - [1.year, false] => [:'about_x_years', 1], - [3.years + 6.months, false] => [:'over_x_years', 3], - [3.years + 10.months, false] => [:'almost_x_years', 4] + [29.seconds, { :include_seconds => false }] => [:'less_than_x_minutes', 1], + [60.seconds, { :include_seconds => false }] => [:'x_minutes', 1], + [44.minutes, { :include_seconds => false }] => [:'x_minutes', 44], + [61.minutes, { :include_seconds => false }] => [:'about_x_hours', 1], + [24.hours, { :include_seconds => false }] => [:'x_days', 1], + [30.days, { :include_seconds => false }] => [:'about_x_months', 1], + [60.days, { :include_seconds => false }] => [:'x_months', 2], + [1.year, { :include_seconds => false }] => [:'about_x_years', 1], + [3.years + 6.months, { :include_seconds => false }] => [:'over_x_years', 3], + [3.years + 10.months, { :include_seconds => false }] => [:'almost_x_years', 4] }.each do |passed, expected| assert_distance_of_time_in_words_translates_key passed, expected @@ -37,7 +37,7 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase end def assert_distance_of_time_in_words_translates_key(passed, expected) - diff, include_seconds = *passed + diff, passed_options = *passed key, count = *expected to = @from + diff @@ -45,7 +45,12 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase options[:count] = count if count I18n.expects(:t).with(key, options) - distance_of_time_in_words(@from, to, include_seconds, :locale => 'en') + distance_of_time_in_words(@from, to, passed_options.merge(:locale => 'en')) + end + + def test_time_ago_in_words_passes_locale + I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru') + time_ago_in_words(15.seconds.ago, :locale => 'ru') end def test_distance_of_time_pluralizations diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 4835bc578f..ff85a675a2 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -21,56 +21,80 @@ class DateHelperTest < ActionView::TestCase def assert_distance_of_time_in_words(from, to=nil) to ||= from - # 0..1 with include_seconds - assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, true) - assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, true) - assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, true) - assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, true) - assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, true) - assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, true) - assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, true) - assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, true) - assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, true) - assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, true) - assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, true) - assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, true) - - # First case 0..1 + # 0..1 minute with :include_seconds => true + assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => true) + assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => true) + assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => true) + assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => true) + + # 0..1 minute with :include_seconds => false + assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => false) + + # Note that we are including a 30-second boundary around the interval we + # want to test. For instance, "1 minute" is actually 30s to 1m29s. The + # reason for doing this is simple -- in `distance_of_time_to_words`, when we + # take the distance between our two Time objects in seconds and convert it + # to minutes, we round the number. So 29s gets rounded down to 0m, 30s gets + # rounded up to 1m, and 1m29s gets rounded down to 1m. A similar thing + # happens with the other cases. + + # First case 0..1 minute assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds) assert_equal "less than a minute", distance_of_time_in_words(from, to + 29.seconds) assert_equal "1 minute", distance_of_time_in_words(from, to + 30.seconds) assert_equal "1 minute", distance_of_time_in_words(from, to + 1.minutes + 29.seconds) - # 2..44 + # 2 minutes up to 45 minutes assert_equal "2 minutes", distance_of_time_in_words(from, to + 1.minutes + 30.seconds) assert_equal "44 minutes", distance_of_time_in_words(from, to + 44.minutes + 29.seconds) - # 45..89 + # 45 minutes up to 90 minutes assert_equal "about 1 hour", distance_of_time_in_words(from, to + 44.minutes + 30.seconds) assert_equal "about 1 hour", distance_of_time_in_words(from, to + 89.minutes + 29.seconds) - # 90..1439 + # 90 minutes up to 24 hours assert_equal "about 2 hours", distance_of_time_in_words(from, to + 89.minutes + 30.seconds) assert_equal "about 24 hours", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 29.seconds) - # 1440..2519 + # 24 hours up to 42 hours assert_equal "1 day", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 30.seconds) assert_equal "1 day", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 29.seconds) - # 2520..43199 + # 42 hours up to 30 days assert_equal "2 days", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 30.seconds) assert_equal "3 days", distance_of_time_in_words(from, to + 2.days + 12.hours) assert_equal "30 days", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 29.seconds) - # 43200..86399 + # 30 days up to 60 days assert_equal "about 1 month", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 30.seconds) - assert_equal "about 1 month", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds) + assert_equal "about 1 month", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 29.seconds) + assert_equal "about 2 months", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 30.seconds) + assert_equal "about 2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds) - # 86400..525599 + # 60 days up to 365 days assert_equal "2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 30.seconds) assert_equal "12 months", distance_of_time_in_words(from, to + 1.years - 31.seconds) - # > 525599 + # >= 365 days assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years - 30.seconds) assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years + 3.months - 1.day) assert_equal "over 1 year", distance_of_time_in_words(from, to + 1.years + 6.months) @@ -95,7 +119,8 @@ class DateHelperTest < ActionView::TestCase # test to < from assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to) - assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false) end def test_distance_in_words @@ -103,6 +128,11 @@ class DateHelperTest < ActionView::TestCase assert_distance_of_time_in_words(from) end + def test_time_ago_in_words_passes_include_seconds + assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true) + assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false) + end + def test_distance_in_words_with_time_zones from = Time.mktime(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from.in_time_zone('Alaska')) @@ -148,10 +178,10 @@ class DateHelperTest < ActionView::TestCase assert_equal "about 1 hour", distance_of_time_in_words(60.minutes) # include seconds - assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, true) - assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, true) - assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, true) - assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, true) + assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, :include_seconds => true) end def test_time_ago_in_words @@ -587,7 +617,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_html_options - expected = expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n) + expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -968,6 +998,15 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) end + def test_select_date_with_hidden + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :prefix => "date[first]", :use_hidden => true }) + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :prefix => "date[first]", :use_hidden => true }) + end + def test_select_datetime expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -1204,6 +1243,18 @@ class DateHelperTest < ActionView::TestCase :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'}) end + def test_select_datetime_with_hidden + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n) + + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true) + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :datetime_separator => "—", :date_separator => "/", + :time_separator => ":", :prefix => "date[first]", :use_hidden => true) + end + def test_select_time expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) @@ -1379,6 +1430,17 @@ class DateHelperTest < ActionView::TestCase :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) end + def test_select_time_with_hidden + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n) + + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true) + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ":", :prefix => "date[first]", :use_hidden => true) + end + def test_date_select @post = Post.new @post.written_on = Date.new(2004, 6, 15) diff --git a/actionpack/test/template/form_collections_helper_test.rb b/actionpack/test/template/form_collections_helper_test.rb index 4d878635ef..c73e80ed88 100644 --- a/actionpack/test/template/form_collections_helper_test.rb +++ b/actionpack/test/template/form_collections_helper_test.rb @@ -195,6 +195,15 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=checkbox][value=2][checked=checked]' end + test 'collection check boxes accepts selected string values as :checked option' do + collection = (1..3).map{|i| [i, "Category #{i}"] } + with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3'] + + assert_select 'input[type=checkbox][value=1][checked=checked]' + assert_select 'input[type=checkbox][value=3][checked=checked]' + assert_no_select 'input[type=checkbox][value=2][checked=checked]' + end + test 'collection check boxes accepts a single checked value' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3 diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index a714264909..beb3ea752a 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -350,36 +350,52 @@ class FormHelperTest < ActionView::TestCase text_field("user", "email", :type => "email") end - def test_check_box + def test_check_box_is_html_safe assert check_box("post", "secret").html_safe? + end + + def test_check_box_checked_if_object_value_is_same_that_check_value assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret") ) + end + + def test_check_box_not_checked_if_object_value_is_same_that_unchecked_value @post.secret = 0 assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret") ) + end + + def test_check_box_checked_if_option_checked_is_present assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret" ,{"checked"=>"checked"}) ) + end + + def test_check_box_checked_if_object_value_is_true @post.secret = true assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret") ) + assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret?") ) + end + def test_check_box_checked_if_object_value_includes_checked_value @post.secret = ['0'] assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret") ) + @post.secret = ['1'] assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', @@ -392,12 +408,92 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal('<input id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret", :include_hidden => false)) end - def test_check_box_with_explicit_checked_and_unchecked_values + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_string @post.secret = "on" assert_dom_equal( '<input name="post[secret]" type="hidden" value="off" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />', check_box("post", "secret", {}, "on", "off") ) + + @post.secret = "off" + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="off" /><input id="post_secret" name="post[secret]" type="checkbox" value="on" />', + check_box("post", "secret", {}, "on", "off") + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_boolean + @post.secret = false + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="true" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="false" />', + check_box("post", "secret", {}, false, true) + ) + + @post.secret = true + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="true" /><input id="post_secret" name="post[secret]" type="checkbox" value="false" />', + check_box("post", "secret", {}, false, true) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_integer + @post.secret = 0 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 1 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 2 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_float + @post.secret = 0.0 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 1.1 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = 2.2 + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + end + + def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal + @post.secret = BigDecimal.new(0) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = BigDecimal.new(1) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) + + @post.secret = BigDecimal.new(2.2, 1) + assert_dom_equal( + '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />', + check_box("post", "secret", {}, 0, 1) + ) end def test_check_box_with_nil_unchecked_value @@ -2169,6 +2265,23 @@ class FormHelperTest < ActionView::TestCase ActionView::Base.default_form_builder = old_default_form_builder end + def test_lazy_loading_default_form_builder + old_default_form_builder, ActionView::Base.default_form_builder = + ActionView::Base.default_form_builder, "FormHelperTest::LabelledFormBuilder" + + form_for(@post) do |f| + concat f.text_field(:title) + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + end + + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_form_builder = old_default_form_builder + end + def test_fields_for_with_labelled_builder output_buffer = fields_for(:post, @post, :builder => LabelledFormBuilder) do |f| concat f.text_field(:title) diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb index 0b96985002..fe7607ee26 100644 --- a/actionpack/test/template/javascript_helper_test.rb +++ b/actionpack/test/template/javascript_helper_test.rb @@ -42,36 +42,6 @@ class JavaScriptHelperTest < ActionView::TestCase assert_instance_of ActiveSupport::SafeBuffer, escape_javascript(ActiveSupport::SafeBuffer.new(given)) end - def test_button_to_function - assert_dom_equal %(<input type="button" onclick="alert('Hello world!');" value="Greeting" />), - button_to_function("Greeting", "alert('Hello world!')") - end - - def test_button_to_function_with_onclick - assert_dom_equal "<input onclick=\"alert('Goodbye World :('); alert('Hello world!');\" type=\"button\" value=\"Greeting\" />", - button_to_function("Greeting", "alert('Hello world!')", :onclick => "alert('Goodbye World :(')") - end - - def test_button_to_function_without_function - assert_dom_equal "<input onclick=\";\" type=\"button\" value=\"Greeting\" />", - button_to_function("Greeting") - end - - def test_link_to_function - assert_dom_equal %(<a href="#" onclick="alert('Hello world!'); return false;">Greeting</a>), - link_to_function("Greeting", "alert('Hello world!')") - end - - def test_link_to_function_with_existing_onclick - assert_dom_equal %(<a href="#" onclick="confirm('Sanity!'); alert('Hello world!'); return false;">Greeting</a>), - link_to_function("Greeting", "alert('Hello world!')", :onclick => "confirm('Sanity!')") - end - - def test_function_with_href - assert_dom_equal %(<a href="http://example.com/" onclick="alert('Hello world!'); return false;">Greeting</a>), - link_to_function("Greeting", "alert('Hello world!')", :href => 'http://example.com/') - end - def test_javascript_tag self.output_buffer = 'foo' diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 43b176df3c..e7f5f100bf 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -153,15 +153,15 @@ module RenderTestCases def test_render_partial_with_invalid_name e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") } assert_equal "The partial name (test/200) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.", e.message + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_missing_filename e = assert_raises(ArgumentError) { @view.render(:partial => "test/") } assert_equal "The partial name (test/) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.", e.message + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_incompatible_object @@ -233,11 +233,26 @@ module RenderTestCases def test_render_partial_with_nil_values_in_collection assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ]) end - + def test_render_partial_with_layout_using_collection_and_template assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) end + def test_render_partial_with_layout_using_collection_and_template_makes_current_item_available_in_layout + assert_equal '<b class="amazon">Hello: Amazon</b><b class="yahoo">Hello: Yahoo</b>', + @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) + end + + def test_render_partial_with_layout_using_collection_and_template_makes_current_item_counter_available_in_layout + assert_equal '<b data-counter="0">Hello: Amazon</b><b data-counter="1">Hello: Yahoo</b>', + @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object_counter', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) + end + + def test_render_partial_with_layout_using_object_and_template_makes_object_available_in_layout + assert_equal '<b class="amazon">Hello: Amazon</b>', + @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :object => Customer.new("Amazon")) + end + def test_render_partial_with_empty_array_should_return_nil assert_nil @view.render(:partial => []) end @@ -310,7 +325,7 @@ module RenderTestCases ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) end - + def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization ActionView::Template::Handlers.extensions ActionView::Template.register_template_handler :foo, CustomHandler diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index 32b33b4a55..7161d107b3 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -91,6 +91,11 @@ class TagHelperTest < ActionView::TestCase assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>") end + def test_cdata_section_splitted + assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world") + assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again") + end + def test_escape_once assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') end diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb index 397de9c2ce..97777ccff0 100644 --- a/actionpack/test/template/translation_helper_test.rb +++ b/actionpack/test/template/translation_helper_test.rb @@ -11,7 +11,8 @@ class TranslationHelperTest < ActiveSupport::TestCase :translations => { :templates => { :found => { :foo => 'Foo' }, - :array => { :foo => { :bar => 'Foo Bar' } } + :array => { :foo => { :bar => 'Foo Bar' } }, + :default => { :foo => 'Foo' } }, :foo => 'Foo', :hello => '<a>Hello World</a>', @@ -71,6 +72,10 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip end + def test_default_lookup_scoped_by_partial + assert_equal 'Foo', view.render(:file => 'translations/templates/default').strip + end + def test_missing_translation_scoped_by_partial expected = '<span class="translation_missing" title="translation missing: en.translations.templates.missing.missing">Missing</span>' assert_equal expected, view.render(:file => 'translations/templates/missing').strip @@ -102,4 +107,22 @@ class TranslationHelperTest < ActiveSupport::TestCase def test_translation_returning_an_array_ignores_html_suffix assert_equal ["foo", "bar"], translate(:'translations.array_html') end + + def test_translate_with_default_named_html + translation = translate(:'translations.missing', :default => :'translations.hello_html') + assert_equal '<a>Hello World</a>', translation + assert translation.html_safe? + end + + def test_translate_with_two_defaults_named_html + translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html']) + assert_equal '<a>Hello World</a>', translation + assert translation.html_safe? + end + + def test_translate_with_last_default_named_html + translation = translate(:'translations.missing', :default => [:'translations.missing', :'translations.hello_html']) + assert_equal '<a>Hello World</a>', translation + assert translation.html_safe? + end end diff --git a/actionpack/test/ts_isolated.rb b/actionpack/test/ts_isolated.rb index ae2a0c95f6..595b4018e9 100644 --- a/actionpack/test/ts_isolated.rb +++ b/actionpack/test/ts_isolated.rb @@ -1,6 +1,3 @@ -$:.unshift(File.dirname(__FILE__)) -$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib') - require 'minitest/autorun' require 'rbconfig' require 'abstract_unit' diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index c6d8eae46c..789cff0673 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,7 @@ ## Rails 4.0.0 (unreleased) ## +* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella* + * Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran* * `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev* diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 9b05384792..0c7089598c 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -25,7 +25,7 @@ to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>. person = Person.new(:name => 'bob', :age => '18') person.name # => 'bob' - person.age # => 18 + person.age # => '18' person.valid? # => true It includes model name introspections, conversions, translations and @@ -135,6 +135,16 @@ behavior out of the box: pattern in a Rails App and take advantage of all the standard observer functions. + class PersonObserver < ActiveModel::Observer + def after_create(person) + person.logger.info("New person added!") + end + + def after_destroy(person) + person.logger.warn("Person with an id of #{person.id} was destroyed!") + end + end + {Learn more}[link:classes/ActiveModel/Observer.html] * Making objects serializable diff --git a/activemodel/Rakefile b/activemodel/Rakefile index fc5aaf9f8f..fc5aaf9f8f 100755..100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 2586147a20..ded1b752df 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -21,8 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require 'active_support' require 'active_model/version' @@ -59,5 +57,6 @@ module ActiveModel end end -require 'active_support/i18n' -I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 97a83e58af..25d5d84ce6 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -180,6 +180,18 @@ module ActiveModel undefine_attribute_methods end + + # Allows you to make aliases for attributes. + # + # class Person + # attr_accessor :name + # alias_attribute :nickname, :name + # end + # + # person = Person.new + # person.nickname = "Bob" + # person.nickname # => "Bob" + # person.name # => "Bob" def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index d327913824..3d35b5bb6f 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -83,6 +83,7 @@ module ActiveModel # in-place attributes. # # person.name_will_change! + # person.name_change # => ['Bill', 'Bill'] # person.name << 'y' # person.name_change # => ['Bill', 'Billy'] module Dirty @@ -151,13 +152,15 @@ module ActiveModel # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) + return if attribute_changed?(attr) + begin value = __send__(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end - changed_attributes[attr] = value unless changed_attributes.include?(attr) + changed_attributes[attr] = value end # Handle <tt>reset_*!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 0c628c33c2..aba6618b56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -3,7 +3,6 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/reverse_merge' module ActiveModel # == Active Model Errors @@ -202,12 +201,12 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) end # Returns an Hash that can be used as the JSON representation for this object. # Options: - # * <tt>:full_messages</tt> - determines if json object should contain + # * <tt>:full_messages</tt> - determines if json object should contain # full messages or not. Default: <tt>false</tt>. def as_json(options=nil) to_hash(options && options[:full_messages]) @@ -217,7 +216,7 @@ module ActiveModel if full_messages messages = {} self.messages.each do |attribute, array| - messages[attribute] = array.map{|message| full_message(attribute, message) } + messages[attribute] = array.map { |message| full_message(attribute, message) } end messages else @@ -347,7 +346,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value - }.merge(options) + }.merge!(options) I18n.translate(key, options) end @@ -356,9 +355,10 @@ module ActiveModel def normalize_message(attribute, message, options) message ||= :invalid - if message.is_a?(Symbol) + case message + when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - elsif message.is_a?(Proc) + when Proc message.call else message diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index ba49c6beaa..d17848c861 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -9,7 +9,7 @@ en: inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" - confirmation: "doesn't match confirmation" + confirmation: "doesn't match %{attribute}" accepted: "must be accepted" empty: "can't be empty" blank: "can't be blank" diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 5665e10002..2b5fc57a3a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -54,7 +54,7 @@ module ActiveModel defaults << options[:default] if options[:default] defaults << @human - options = {:scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults}.merge(options.except(:default)) + options = { :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults }.merge!(options.except(:default)) I18n.translate(defaults.shift, options) end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 35b1a1f0c7..f5ea285ccb 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -90,14 +90,15 @@ module ActiveModel def instantiate_observer(observer) #:nodoc: # string/symbol if observer.respond_to?(:to_sym) - observer.to_s.camelize.constantize.instance - elsif observer.respond_to?(:instance) + observer = observer.to_s.camelize.constantize + end + if observer.respond_to?(:instance) observer.instance else raise ArgumentError, - "#{observer} must be a lowercase, underscored class name (or an " + - "instance of the class itself) responding to the instance " + - "method. Example: Person.observers = :big_brother # calls " + + "#{observer} must be a lowercase, underscored class name (or " + + "the class itself) responding to the method :instance. " + + "Example: Person.observers = :big_brother # calls " + "BigBrother.instance" end end @@ -109,17 +110,24 @@ module ActiveModel end end - private - # Fires notifications to model's observers - # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) - # end - def notify_observers(method) - self.class.notify_observers(method, self) - end + # Fires notifications to model's observers + # + # def save + # notify_observers(:before_save) + # ... + # notify_observers(:after_save) + # end + # + # Custom notifications can be sent in a similar fashion: + # + # notify_observers(:custom_notification, :foo) + # + # This will call +custom_notification+, passing as arguments + # the current object and :foo. + # + def notify_observers(method, *extra_args) + self.class.notify_observers(method, self, *extra_args) + end end # == Active Model Observers @@ -194,7 +202,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - redefine_method(:observed_classes) { models } + singleton_class.redefine_method(:observed_classes) { models } end # Returns an array of Classes to observe. @@ -229,10 +237,10 @@ module ActiveModel # Send observed_method(object) if the method exists and # the observer is enabled for the given object's class. - def update(observed_method, object, &block) #:nodoc: + def update(observed_method, object, *extra_args, &block) #:nodoc: return unless respond_to?(observed_method) return if disabled_for?(object) - send(observed_method, object, &block) + send(observed_method, object, *extra_args, &block) end # Special method sent by the observed class when it is inherited. diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index a5828476b1..00cd05b7ef 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -34,7 +34,7 @@ module ActiveModel # # Most of the time though, you will want to include the JSON or XML # serializations. Both of these modules automatically include the - # +ActiveModel::Serialization+ module, so there is no need to explicitly + # <tt>ActiveModel::Serialization</tt> module, so there is no need to explicitly # include it. # # A minimal implementation including XML and JSON would be: @@ -87,8 +87,8 @@ module ActiveModel Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } serializable_add_includes(options) do |association, records, opts| - hash[association.to_s] = if records.is_a?(Enumerable) - records.map { |a| a.serializable_hash(opts) } + hash[association.to_s] = if records.respond_to?(:to_ary) + records.to_ary.map { |a| a.serializable_hash(opts) } else records.serializable_hash(opts) end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 5084298210..b78f1ff3f3 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -115,7 +115,9 @@ module ActiveModel merged_options = opts.merge(options.slice(:builder, :indent)) merged_options[:skip_instruct] = true - if records.is_a?(Enumerable) + if records.respond_to?(:to_ary) + records = records.to_ary + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) type = options[:skip_types] ? { } : {:type => "array"} association_name = association.to_s.singularize diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 02b7c54d61..6f0ca92e2a 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/reverse_merge' - module ActiveModel # == Active Model Translation @@ -43,19 +41,20 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - defaults = [] + options = { :count => 1 }.merge!(options) parts = attribute.to_s.split(".", 2) attribute = parts.pop namespace = parts.pop + attributes_scope = "#{self.i18n_scope}.attributes" if namespace - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" end - defaults << :"#{self.i18n_scope}.attributes.#{namespace}.#{attribute}" + defaults << :"#{attributes_scope}.#{namespace}.#{attribute}" else - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}" end end @@ -63,7 +62,7 @@ module ActiveModel defaults << options.delete(:default) if options[:default] defaults << attribute.humanize - options.reverse_merge! :count => 1, :default => defaults + options[:default] = defaults I18n.translate(defaults.shift, options) end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 0e15155b85..3ed72bae3b 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -65,7 +65,7 @@ module ActiveModel # # attr_accessor :first_name, :last_name # - # validates_each :first_name, :last_name do |record, attr, value| + # validates_each :first_name, :last_name, :allow_blank => true do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end @@ -128,6 +128,19 @@ module ActiveModel # end # end # + # Options: + # * <tt>:on</tt> - Specifies the context where this validation is active + # (e.g. <tt>:on => :create</tt> or <tt>:on => :custom_validation_context</tt>) + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <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. + # * <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. def validate(*args, &block) options = args.extract_options! if options.key?(:on) @@ -145,7 +158,7 @@ module ActiveModel _validators.values.flatten.uniq end - # List all validators that being used to validate a specific attribute. + # List all validators that are being used to validate a specific attribute. def validators_on(*attributes) attributes.map do |attribute| _validators[attribute.to_sym] diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index e8526303e2..69ab74734d 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -5,7 +5,8 @@ module ActiveModel class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - record.errors.add(attribute, :confirmation, options) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name)) end end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 72b8562b93..991c5f7b82 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -126,12 +126,12 @@ module ActiveModel # end # # Standard configuration options (:on, :if and :unless), which are - # available on the class version of validates_with, should instead be - # placed on the <tt>validates</tt> method as these are applied and tested + # available on the class version of +validates_with+, should instead be + # placed on the +validates+ method as these are applied and tested # in the callback # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>, please refer to the + # to the class and available as +options+, please refer to the # class version of this method for more information # def validates_with(*args, &block) diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 4347b17cbc..7d6f11b5a5 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,8 +1,5 @@ require File.expand_path('../../../../load_paths', __FILE__) -lib = File.expand_path("#{File.dirname(__FILE__)}/../../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - require 'config' require 'active_model' require 'active_support/core_ext/string/access' diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb index df070d2d8e..ade6026602 100644 --- a/activemodel/test/cases/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -14,8 +14,8 @@ class FooObserver < ActiveModel::Observer attr_accessor :stub - def on_spec(record) - stub.event_with(record) if stub + def on_spec(record, *args) + stub.event_with(record, *args) if stub end def around_save(record) @@ -70,6 +70,21 @@ class ObservingTest < ActiveModel::TestCase ObservedModel.instantiate_observers end + test "raises an appropriate error when a developer accidentally adds the wrong class (i.e. Widget instead of WidgetObserver)" do + assert_raise ArgumentError do + ObservedModel.observers = ['string'] + ObservedModel.instantiate_observers + end + assert_raise ArgumentError do + ObservedModel.observers = [:string] + ObservedModel.instantiate_observers + end + assert_raise ArgumentError do + ObservedModel.observers = [String] + ObservedModel.instantiate_observers + end + end + test "passes observers to subclasses" do FooObserver.instance bar = Class.new(Foo) @@ -80,13 +95,13 @@ end class ObserverTest < ActiveModel::TestCase def setup ObservedModel.observers = :foo_observer - FooObserver.instance_eval do + FooObserver.singleton_class.instance_eval do alias_method :original_observed_classes, :observed_classes end end def teardown - FooObserver.instance_eval do + FooObserver.singleton_class.instance_eval do undef_method :observed_classes alias_method :observed_classes, :original_observed_classes end @@ -98,44 +113,51 @@ class ObserverTest < ActiveModel::TestCase test "tracks implicit observable models" do instance = FooObserver.new - assert instance.send(:observed_classes).include?(Foo), "Foo not in #{instance.send(:observed_classes).inspect}" - assert !instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{instance.send(:observed_classes).inspect}" + assert_equal [Foo], instance.observed_classes end test "tracks explicit observed model class" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe ObservedModel instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "tracks explicit observed model as string" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe 'observed_model' instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "tracks explicit observed model as symbol" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe :observed_model instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "calls existing observer event" do foo = Foo.new FooObserver.instance.stub = stub FooObserver.instance.stub.expects(:event_with).with(foo) - Foo.send(:notify_observers, :on_spec, foo) + Foo.notify_observers(:on_spec, foo) + end + + test "calls existing observer event from the instance" do + foo = Foo.new + FooObserver.instance.stub = stub + FooObserver.instance.stub.expects(:event_with).with(foo) + foo.notify_observers(:on_spec) + end + + test "passes extra arguments" do + foo = Foo.new + FooObserver.instance.stub = stub + FooObserver.instance.stub.expects(:event_with).with(foo, :bar) + Foo.send(:notify_observers, :on_spec, foo, :bar) end test "skips nonexistent observer event" do foo = Foo.new - Foo.send(:notify_observers, :whatever, foo) + Foo.notify_observers(:whatever, foo) end test "update passes a block on to the observer" do @@ -145,4 +167,15 @@ class ObserverTest < ActiveModel::TestCase end assert_equal :in_around_save, yielded_value end + + test "observe redefines observed_classes class method" do + class BarObserver < ActiveModel::Observer + observe :foo + end + + assert_equal [Foo], BarObserver.observed_classes + + BarObserver.observe(ObservedModel) + assert_equal [ObservedModel], BarObserver.observed_classes + end end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 66b18d65e5..d2ba9fd95d 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -105,6 +105,24 @@ class SerializationTest < ActiveModel::TestCase assert_equal expected, @user.serializable_hash(:include => :friends) end + class FriendList + def initialize(friends) + @friends = friends + end + + def to_ary + @friends + end + end + + def test_include_option_with_ary + @user.friends = FriendList.new(@user.friends) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected, @user.serializable_hash(:include => :friends) + end + def test_multiple_includes expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}, diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 38aecf51ff..5fa227e0e0 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -188,6 +188,23 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<friend type="Contact">}, xml end + class FriendList + def initialize(friends) + @friends = friends + end + + def to_ary + @friends + end + end + + test "include option with ary" do + @contact.friends = FriendList.new(@contact.friends) + xml = @contact.to_xml :include => :friends, :indent => 0 + assert_match %r{<friends type="array">}, xml + assert_match %r{<friend type="Contact">}, xml + end + test "multiple includes" do xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => [ :address, :friends ] assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true)) diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 54e86d48db..4999583802 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -82,9 +82,15 @@ class ActiveModelI18nTests < ActiveModel::TestCase end def test_human_does_not_modify_options - options = {:default => 'person model'} + options = { :default => 'person model' } Person.model_name.human(options) - assert_equal({:default => 'person model'}, options) + assert_equal({ :default => 'person model' }, options) + end + + def test_human_attribute_name_does_not_modify_options + options = { :default => 'Cool gender' } + Person.human_attribute_name('gender', options) + assert_equal({ :default => 'Cool gender' }, options) end end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index d0418170fa..f7556a249f 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -44,7 +44,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase p.karma_confirmation = "None" assert p.invalid? - assert_equal ["doesn't match confirmation"], p.errors[:karma] + assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation] p.karma = "None" assert p.valid? @@ -52,4 +52,23 @@ class ConfirmationValidationTest < ActiveModel::TestCase Person.reset_callbacks(:validate) end + def test_title_confirmation_with_i18n_attribute + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations('en', { + :errors => {:messages => {:confirmation => "doesn't match %{attribute}"}}, + :activemodel => {:attributes => {:topic => {:title => 'Test Title'}}} + }) + + Topic.validates_confirmation_of(:title) + + t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") + assert t.invalid? + assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] + + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + end diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 0679e67f84..df0fcd243a 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -37,7 +37,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase # validates_confirmation_of: generate_message(attr_name, :confirmation, :message => custom_message) def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation) + assert_equal "doesn't match Title", @person.errors.generate_message(:title, :confirmation) end def test_generate_message_confirmation_with_custom_message diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index e9f0e430fe..6b6aad3bd1 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -81,7 +81,7 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title, :confirmation, generate_message_options) + @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(:attribute => 'Title')) @person.valid? end end @@ -217,24 +217,29 @@ class I18nValidationTest < ActiveModel::TestCase # To make things DRY this macro is defined to define 3 tests for every validation case. def self.set_expectations_for_validation(validation, error_type, &block_that_sets_validation) + if error_type == :confirmation + attribute = :title_confirmation + else + attribute = :title + end # test "validates_confirmation_of finds custom model key translation when blank" test "#{validation} finds custom model key translation when #{error_type}" do - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message'}}}}}} I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} yield(@person, {}) @person.valid? - assert_equal ['custom message'], @person.errors[:title] + assert_equal ['custom message'], @person.errors[attribute] end # test "validates_confirmation_of finds custom model key translation with interpolation when blank" test "#{validation} finds custom model key translation with interpolation when #{error_type}" do - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message with %{extra}'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message with %{extra}'}}}}}} I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} yield(@person, {:extra => "extra information"}) @person.valid? - assert_equal ['custom message with extra information'], @person.errors[:title] + assert_equal ['custom message with extra information'], @person.errors[attribute] end # test "validates_confirmation_of finds global default key translation when blank" @@ -243,7 +248,7 @@ class I18nValidationTest < ActiveModel::TestCase yield(@person, {}) @person.valid? - assert_equal ['global message'], @person.errors[:title] + assert_equal ['global message'], @person.errors[attribute] end end diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 575154ffbd..90bc018ae1 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -154,6 +154,6 @@ class ValidatesTest < ActiveModel::TestCase topic.title = "What's happening" topic.title_confirmation = "Not this" assert !topic.valid? - assert_equal ['Y U NO CONFIRM'], topic.errors[:title] + assert_equal ['Y U NO CONFIRM'], topic.errors[:title_confirmation] end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index aa84dacf07..31886c8212 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,17 @@ ## Rails 4.0.0 (unreleased) ## +* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by + default to avoid silent data loss. This can be disabled by specifying + `strict: false` in your `database.yml`. + + *Michael Pearson* + +* Added default order to `first` to assure consistent results among + diferent database engines. Introduced `take` as a replacement to + the old behavior of `first`. + + *Marcelo Silveira* + * Added an :index option to automatically create indexes for references and belongs_to statements in migrations. @@ -580,19 +592,6 @@ a URI that specifies the connection configuration. For example: ActiveRecord::Base.establish_connection 'postgres://localhost/foo' -* Active Record's dynamic finder will now raise the error if you passing in less number of arguments than what you call in method signature. - - So if you were doing this and expecting the second argument to be nil: - - User.find_by_username_and_group("sikachu") - - You'll now get `ArgumentError: wrong number of arguments (1 for 2).` You'll then have to do this: - - User.find_by_username_and_group("sikachu", nil) - - *Prem Sichanugrist* - - ## Rails 3.1.0 (August 30, 2011) ## * Add a proxy_association method to association proxies, which can be called by association diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 4090293b56..7feb0b75a0 100755..100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c4b10a8dae..210820062b 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,7 +22,6 @@ #++ require 'active_support' -require 'active_support/i18n' require 'active_model' require 'arel' require 'active_record_deprecated_finders' @@ -63,8 +62,6 @@ module ActiveRecord autoload :CounterCache autoload :ConnectionHandling autoload :DynamicMatchers - autoload :DynamicFinderMatch - autoload :DynamicScopeMatch autoload :Explain autoload :Inheritance autoload :Integration @@ -147,4 +144,6 @@ ActiveSupport.on_load(:active_record) do Arel::Table.engine = self end -I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index c39284539c..c7a329d74d 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -71,7 +71,7 @@ module ActiveRecord # 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: + # objects just like you would with any other attribute: # # customer.balance = Money.new(20) # sets the Money value object and the attribute # customer.balance # => Money value object @@ -86,6 +86,12 @@ module ActiveRecord # customer.address_street = "Hyancintvej" # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address_street = "Vesterbrogade" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # customer.clear_aggregation_cache + # customer.address # => Address.new("Vesterbrogade", "Copenhagen") + # # customer.address = Address.new("May Street", "Chicago") # customer.address_street # => "May Street" # customer.address_city # => "Chicago" @@ -101,8 +107,8 @@ module ActiveRecord # 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 amount changed after creation. Create a new Money object with the new value instead. The + # Money#exchange_to method is an example of this. It 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. # @@ -119,7 +125,7 @@ module ActiveRecord # 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 + # 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. # diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b901f06ca4..c30e8e08b8 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1129,7 +1129,7 @@ module ActiveRecord # 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 + # you want to do a join but not include the joined columns, for example. 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>). @@ -1264,8 +1264,8 @@ 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 + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you want to do a join but not include the joined columns, for example. 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>, @@ -1355,7 +1355,7 @@ module ActiveRecord # 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 + # if you want to do a join but not include the joined columns, for example. 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 @@ -1382,7 +1382,7 @@ module ActiveRecord # 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) - that is the migration for + # is used on the associate class (such as a Post class) - that is the migration for # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will # return the count cached, see note below). You can also specify a custom counter # cache column by providing a column name instead of a +true+/+false+ value to this @@ -1432,7 +1432,7 @@ 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". + # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. # 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 @@ -1576,8 +1576,8 @@ module ActiveRecord # 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 + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you want to do a join but exclude the joined columns, for example. 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. @@ -1596,7 +1596,7 @@ module ActiveRecord # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" + # proc { |record| "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" } def has_and_belongs_to_many(name, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, options, &extension) end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 14aa557b6c..00321ec860 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -16,12 +16,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - attr_reader :proxy - - def initialize(owner, reflection) - super - @proxy = CollectionProxy.new(self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -31,7 +25,7 @@ module ActiveRecord reload end - proxy + CollectionProxy.new(self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 44cbf473a1..cf4cc98f38 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,14 +33,7 @@ module ActiveRecord # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy # :nodoc: - alias :proxy_extend :extend - - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } - - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :pluck, :to => :scoped - + class CollectionProxy < Relation # :nodoc: delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, @@ -52,7 +45,8 @@ module ActiveRecord def initialize(association) @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + super association.klass, association.klass.arel_table + merge! association.scoped end alias_method :new, :build @@ -61,50 +55,28 @@ module ActiveRecord @association end - def scoped - association = @association - association.scoped.extending do - define_method(:proxy_association) { association } - end + # We don't want this object to be put on the scoping stack, because + # that could create an infinite loop where we call an @association + # method, which gets the current scope, which is this object, which + # delegates to @association, and so on. + def scoping + @association.scoped.scoping { yield } end - def respond_to?(name, include_private = false) - super || - (load_target && target.respond_to?(name, include_private)) || - proxy_association.klass.respond_to?(name, include_private) + def spawn + scoped end - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.instantiator? - send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| - proxy_association.send :set_owner_attributes, r - proxy_association.send :add_to_target, r - yield(r) if block_given? - end - - elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) - if load_target - if target.respond_to?(method) - target.send(method, *args, &block) - else - begin - super - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") - end - end - end + def scoped(options = nil) + association = @association - else - scoped.readonly(nil).send(method, *args, &block) + super.extending! do + define_method(:proxy_association) { association } end end - # Forwards <tt>===</tt> explicitly to the \target because the instance method - # removal above doesn't catch it. Loads the \target if needed. - def ===(other) - other === load_target + def ==(other) + load_target == other end def to_ary @@ -126,19 +98,6 @@ module ActiveRecord proxy_association.reload self end - - # Define array public methods because we know it should be invoked over - # the target, so we can have a performance improvement using those methods - # in association collections - Array.public_instance_methods.each do |m| - unless method_defined?(m) - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{m}(*args, &block) - target.public_send(:#{m}, *args, &block) if load_target - end - RUBY - end - end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 3005bef092..d545e7799d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -350,7 +350,7 @@ module ActiveRecord end records_to_destroy.each do |record| - association.proxy.destroy(record) + association.destroy(record) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index fc3906d395..189985b671 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -304,21 +304,22 @@ module ActiveRecord #:nodoc: # (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. - # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * 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. # * 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 # 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. + # * RecordInvalid - raised by save! and create! when the record is invalid. + # * 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. + # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # # *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 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 a609867898..46c7fc71ac 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -286,12 +286,10 @@ module ActiveRecord private def release(conn) - thread_id = nil - - if @reserved_connections[current_connection_id] == conn - thread_id = current_connection_id + thread_id = if @reserved_connections[current_connection_id] == conn + current_connection_id else - thread_id = @reserved_connections.keys.find { |k| + @reserved_connections.keys.find { |k| @reserved_connections[k] == conn } end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index be712f2334..7b2961a04a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -59,7 +59,7 @@ module ActiveRecord # Executes insert +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) exec_query(sql, name, binds) end @@ -87,7 +87,7 @@ module ActiveRecord # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 44ac37c498..6f9f0399db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -34,6 +34,7 @@ module ActiveRecord when Numeric then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" else "'#{quote_string(YAML.dump(value))}'" 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 3546873550..f17baec722 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -262,7 +262,7 @@ module ActiveRecord # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table - # === Examples + # # index(:account_id, :name => 'index_projects_on_account_id') def index(column_name, options = {}) indexes[column_name] = options @@ -348,7 +348,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. - # ===== Example + # # ====== Creating a simple column # t.column(:name, :string) def column(column_name, type, options = {}) @@ -363,7 +363,6 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # - # ===== Examples # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index @@ -380,7 +379,7 @@ module ActiveRecord end # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps - # ===== Example + # # t.timestamps def timestamps @base.add_timestamps(@table_name) @@ -388,7 +387,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # t.change(:name, :string, :limit => 80) # t.change(:description, :text) def change(column_name, type, options = {}) @@ -396,7 +395,7 @@ module ActiveRecord end # Sets a new default value for a column. See SchemaStatements#change_column_default - # ===== Examples + # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) def change_default(column_name, default) @@ -404,16 +403,15 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. # - # ===== Examples # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table @@ -427,14 +425,14 @@ module ActiveRecord end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. - # ===== Example + # # t.remove_timestamps def remove_timestamps @base.remove_timestamps(@table_name) end # Renames a column. - # ===== Example + # # t.rename(:description, :name) def rename(column_name, new_column_name) @base.rename_column(@table_name, column_name, new_column_name) @@ -442,7 +440,7 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. - # ===== Examples + # # t.references(:goat) # t.references(:goat, :polymorphic => true) # t.belongs_to(:goat) @@ -460,7 +458,7 @@ module ActiveRecord # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. - # ===== Examples + # # t.remove_references(:goat) # t.remove_references(:goat, :polymorphic => true) # t.remove_belongs_to(:goat) @@ -475,7 +473,7 @@ module ActiveRecord alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type - # ===== Examples + # # t.string(:goat) # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| 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 30a4f9aa35..62b0f51bb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -21,7 +21,6 @@ module ActiveRecord # Checks to see if the table +table_name+ exists on the database. # - # === Example # table_exists?(:developers) def table_exists?(table_name) tables.include?(table_name.to_s) @@ -32,7 +31,6 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # === Examples # # Check an index exists # index_exists?(:suppliers, :company_id) # @@ -126,7 +124,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -193,7 +190,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -215,7 +211,6 @@ module ActiveRecord # A block for changing columns in +table+. # - # === Example # # change_table() yields a Table instance # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -229,7 +224,6 @@ module ActiveRecord # # Defaults to false. # - # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -288,7 +282,7 @@ module ActiveRecord end # Renames a table. - # ===== Example + # # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" @@ -308,7 +302,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) @@ -318,7 +312,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # change_column(:suppliers, :name, :string, :limit => 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) @@ -326,7 +320,7 @@ module ActiveRecord end # Sets a new default value for a column. - # ===== Examples + # # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) # change_column_default(:users, :email, nil) @@ -335,7 +329,7 @@ module ActiveRecord end # Renames a column. - # ===== Example + # # rename_column(:suppliers, :description, :name) def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" @@ -347,8 +341,6 @@ module ActiveRecord # The index will be named after the table and the column name(s), unless # you pass <tt>:name</tt> as an option. # - # ===== Examples - # # ====== Creating a simple index # add_index(:suppliers, :name) # generates @@ -537,7 +529,7 @@ module ActiveRecord end # Adds timestamps (created_at and updated_at) columns to the named table. - # ===== Examples + # # add_timestamps(:suppliers) def add_timestamps(table_name) add_column table_name, :created_at, :datetime @@ -545,7 +537,7 @@ module ActiveRecord end # Removes the timestamp columns (created_at and updated_at) from the table definition. - # ===== Examples + # # remove_timestamps(:suppliers) def remove_timestamps(table_name) remove_column table_name, :updated_at @@ -618,8 +610,6 @@ module ActiveRecord end def columns_for_remove(table_name, *column_names) - column_names = column_names.flatten - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? column_names.map {|column_name| quote_column_name(column_name) } end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index b7e1513422..1933ce2b46 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -95,7 +95,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) @@ -158,7 +158,7 @@ module ActiveRecord def value_to_date(value) if value.is_a?(String) - return nil if value.empty? + return nil if value.blank? fast_string_to_date(value) || fallback_string_to_date(value) elsif value.respond_to?(:to_date) value.to_date @@ -169,14 +169,14 @@ module ActiveRecord def string_to_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? fast_string_to_time(string) || fallback_string_to_time(string) end def string_to_dummy_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? string_to_time "2000-01-01 #{string}" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 3f45f23de8..350ccce03d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -226,7 +226,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) execute to_sql(sql, binds), name end @@ -253,6 +253,14 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 variable_assignments = ['SQL_AUTO_IS_NULL=0'] + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" + end + encoding = @config[:encoding] # make sure we set the encoding diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 724dbff1f0..0b6734b010 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -53,6 +53,7 @@ module ActiveRecord # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html) # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -404,6 +405,13 @@ module ActiveRecord # 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) + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) + end end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 37d9ef1bf1..4d5459939b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -17,7 +17,7 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, :schema_order, :adapter, :pool, :wait_timeout, :template, - :reaping_frequency].each do |key| + :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end conn_params.delete_if { |k,v| v.nil? } @@ -258,6 +258,8 @@ module ActiveRecord # <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>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # defaults to true. # # Any further options are used as connection parameters to libpq. See # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the @@ -405,6 +407,7 @@ module ActiveRecord initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] + @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true end # Clears the prepared statements cache. @@ -508,8 +511,13 @@ module ActiveRecord else super end when Float - return super unless value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end when Numeric return super unless column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. @@ -666,8 +674,11 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - if pk + if pk && use_insert_returning? select_value("#{sql} RETURNING #{quote_column_name(pk)}") + elsif pk + super + last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) else super end @@ -782,11 +793,27 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end [sql, binds] end + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -1027,7 +1054,9 @@ module ActiveRecord # Returns the sequence name for a table's primary key or some other specified key. def default_sequence_name(table_name, pk = nil) #:nodoc: - serial_sequence(table_name, pk || 'id').split('.').last + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last rescue ActiveRecord::StatementInvalid "#{table_name}_#{pk || 'id'}_seq" end @@ -1209,7 +1238,10 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') } + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } @@ -1235,6 +1267,10 @@ module ActiveRecord end end + def use_insert_returning? + @use_insert_returning + end + protected # Returns the version of the connected PostgreSQL server. def postgresql_version @@ -1364,8 +1400,15 @@ module ActiveRecord # Returns the current ID of a table's sequence. def last_insert_id(sequence_name) #:nodoc: - r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) - Integer(r.rows.first.first) + Integer(last_insert_id_value(sequence_name)) + end + + def last_insert_id_value(sequence_name) + last_insert_id_result(sequence_name).rows.first.first + end + + def last_insert_id_result(sequence_name) #:nodoc: + exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) end # Executes a SELECT query and returns the results, performing any data type diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ee5d10859c..44e407a561 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,8 @@ -require 'active_record/connection_adapters/sqlite_adapter' +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'arel/visitors/bind_visitor' -gem 'sqlite3', '~> 1.3.5' +gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord @@ -35,7 +37,184 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Adapter < SQLiteAdapter # :nodoc: + class SQLite3Column < Column #:nodoc: + class << self + def binary_to_string(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + end + + # The SQLite3 adapter works SQLite 3.6.16 or newer + # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). + # + # Options: + # + # * <tt>:database</tt> - Path to the database file. + class SQLite3Adapter < AbstractAdapter + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map { |v| v.to_i } + end + + def <=>(version_string) + @version <=> version_string.split('.').map { |v| v.to_i } + end + end + + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last[:stmt]) + end + cache[sql] = key + end + + def clear + cache.values.each do |hash| + dealloc hash[:stmt] + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + + class BindSubstitution < Arel::Visitors::SQLite # :nodoc: + include Arel::Visitors::BindVisitor + end + + def initialize(connection, logger, config) + super(connection, logger) + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) + @config = config + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::SQLite.new self + else + @visitor = BindSubstitution.new self + end + end + + def adapter_name #:nodoc: + 'SQLite' + end + + # Returns true + def supports_ddl_transactions? + true + end + + # Returns true if SQLite version is '3.6.8' or greater, false otherwise. + def supports_savepoints? + sqlite_version >= '3.6.8' + end + + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? #:nodoc: + true + end + + # Returns true. + def supports_primary_key? #:nodoc: + true + end + + def requires_reloading? + true + end + + # Returns true + def supports_add_column? + true + end + + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. + def disconnect! + super + clear_cache! + @connection.close rescue nil + end + + # Clears the prepared statements cache. + def clear_cache! + @statements.clear + end + + # Returns true + def supports_count_distinct? #:nodoc: + true + end + + # Returns true + def supports_autoincrement? #:nodoc: + true + end + + def supports_index_sort_order? + true + end + + def native_database_types #:nodoc: + { + :primary_key => default_primary_key_type, + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "boolean" } + } + end + + # Returns the current database encoding format as a string, eg: 'UTF-8' + def encoding + @connection.encoding.to_s + end + + # Returns true. + def supports_explain? + true + 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] @@ -45,10 +224,387 @@ module ActiveRecord end end - # Returns the current database encoding format as a string, eg: 'UTF-8' - def encoding - @connection.encoding.to_s + + def quote_string(s) #:nodoc: + @connection.class.quote(s) + end + + def quote_column_name(name) #:nodoc: + %Q("#{name.to_s.gsub('"', '""')}") + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value + + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger + value.encode! 'utf-8' + end + value + end + + # DATABASE STATEMENTS ====================================== + + def explain(arel, binds = []) + sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter + # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) # :nodoc: + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + + def exec_query(sql, name = nil, binds = []) + log(sql, name, binds) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + type_cast(val, col) + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + exec_query(sql, name, binds) + @connection.changes end + alias :exec_update :exec_delete + + def last_inserted_id(result) + @connection.last_insert_row_id + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.execute(sql) } + end + + def update_sql(sql, name = nil) #:nodoc: + super + @connection.changes + end + + def delete_sql(sql, name = nil) #:nodoc: + sql += " WHERE 1=1" unless sql =~ /WHERE/i + super sql, name + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + super + id_value || @connection.last_insert_row_id + end + alias :create :insert_sql + + def select_rows(sql, name = nil) + exec_query(sql, name).rows + 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 begin_db_transaction #:nodoc: + log('begin transaction',nil) { @connection.transaction } + end + + def commit_db_transaction #:nodoc: + log('commit transaction',nil) { @connection.commit } + end + + def rollback_db_transaction #:nodoc: + log('rollback transaction',nil) { @connection.rollback } + end + + # SCHEMA STATEMENTS ======================================== + + def tables(name = 'SCHEMA', table_name = nil) #:nodoc: + sql = <<-SQL + SELECT name + FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name + + exec_query(sql, name).map do |row| + row['name'] + end + end + + def table_exists?(name) + name && tables('SCHEMA', name).any? + end + + # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + def columns(table_name) #:nodoc: + table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/ + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/ + field["dflt_value"] = $1.gsub('""', '"') + end + + SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + end + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + IndexDefinition.new( + table_name, + row['name'], + row['unique'] != 0, + exec_query("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'] == 1 + } + column && column['name'] + end + + def remove_index!(table_name, index_name) #:nodoc: + exec_query "DROP INDEX #{quote_column_name(index_name)}" + end + + # Renames a table. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + end + + # See: http://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + end + + def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? + column_names.each do |column_name| + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + end + alias :remove_columns :remove_column + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + alter_table(table_name) do |definition| + definition[column_name].null = null + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + include_default = options_include_default?(options) + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options.include?(:limit) + self.default = options[:default] if include_default + self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end + alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + end + + def empty_insert_statement_value + "VALUES(NULL)" + end + + protected + def select(sql, name = nil, binds = []) #:nodoc: + exec_query(sql, name, binds) + end + + def table_structure(table_name) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? + structure + end + + def alter_table(table_name, options = {}) #:nodoc: + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + move_table(table_name, altered_table_name, + options.merge(:temporary => true)) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) + create_table(to, options) do |definition| + @definition = definition + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :precision => column.precision, :scale => column.scale, + :null => column.null) + end + @definition.primary_key(primary_key(from)) if primary_key(from) + yield @definition if block_given? + end + + copy_table_indexes(from, to, options[:rename] || {}) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to, rename = {}) #:nodoc: + indexes(from).each do |index| + name = index.name + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + to_column_names = columns(to).map { |c| c.name } + columns = index.columns.map {|c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end + + unless columns.empty? + # index name can't be the same + opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } + opts[:unique] = true if index.unique + add_index(to, columns, opts) + end + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + column_mappings = Hash[columns.map {|name| [name, name]}] + rename.each { |a| column_mappings[a.last] = a.first } + from_columns = columns(from).collect {|col| col.name} + columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + + quoted_to = quote_table_name(to) + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| + sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" + sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + sql << ')' + exec_query sql + end + end + + def sqlite_version + @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) + end + + def default_primary_key_type + if supports_autoincrement? + 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' + else + 'INTEGER PRIMARY KEY NOT NULL' + end + end + + def translate_exception(exception, message) + case exception.message + when /column(s)? .* (is|are) not unique/ + RecordNotUnique.new(message, exception) + else + super + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb deleted file mode 100644 index 91e1482ffd..0000000000 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ /dev/null @@ -1,563 +0,0 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'arel/visitors/bind_visitor' - -module ActiveRecord - module ConnectionAdapters #:nodoc: - class SQLiteColumn < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - 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/). - # - # Options: - # - # * <tt>:database</tt> - Path to the database file. - class SQLiteAdapter < AbstractAdapter - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } - end - - def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } - end - end - - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.values.each do |hash| - dealloc hash[:stmt] - end - cache.clear - end - - private - def cache - @cache[$$] - end - - def dealloc(stmt) - stmt.close unless stmt.closed? - end - end - - class BindSubstitution < Arel::Visitors::SQLite # :nodoc: - include Arel::Visitors::BindVisitor - end - - def initialize(connection, logger, config) - super(connection, logger) - @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) - @config = config - - if config.fetch(:prepared_statements) { true } - @visitor = Arel::Visitors::SQLite.new self - else - @visitor = BindSubstitution.new self - end - end - - def adapter_name #:nodoc: - 'SQLite' - end - - # Returns true if SQLite version is '2.0.0' or greater, false otherwise. - def supports_ddl_transactions? - sqlite_version >= '2.0.0' - end - - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. - def supports_savepoints? - sqlite_version >= '3.6.8' - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - # Returns true. - def supports_primary_key? #:nodoc: - true - end - - # Returns true. - def supports_explain? - true - end - - def requires_reloading? - true - end - - # Returns true if SQLite version is '3.1.6' or greater, false otherwise. - def supports_add_column? - sqlite_version >= '3.1.6' - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - clear_cache! - @connection.close rescue nil - end - - # Clears the prepared statements cache. - def clear_cache! - @statements.clear - end - - # Returns true if SQLite version is '3.2.6' or greater, false otherwise. - def supports_count_distinct? #:nodoc: - sqlite_version >= '3.2.6' - end - - # Returns true if SQLite version is '3.1.0' or greater, false otherwise. - def supports_autoincrement? #:nodoc: - sqlite_version >= '3.1.0' - end - - def supports_index_sort_order? - sqlite_version >= '3.3.0' - end - - def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } - end - - - # QUOTING ================================================== - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value.encode! 'utf-8' - end - value - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end - end - - def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do - - # Don't cache statements without bind values - if binds.empty? - stmt = @connection.prepare(sql) - cols = stmt.columns - records = stmt.to_a - stmt.close - stmt = records - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } - end - - ActiveRecord::Result.new(cols, stmt.to_a) - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - exec_query(sql, name, binds) - @connection.changes - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_insert_row_id - end - - def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } - end - - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - - def select_rows(sql, name = nil) - exec_query(sql, name).rows - 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 begin_db_transaction #:nodoc: - log('begin transaction',nil) { @connection.transaction } - end - - def commit_db_transaction #:nodoc: - log('commit transaction',nil) { @connection.commit } - end - - def rollback_db_transaction #:nodoc: - log('rollback transaction',nil) { @connection.rollback } - end - - # SCHEMA STATEMENTS ======================================== - - def tables(name = 'SCHEMA', table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, name).map do |row| - row['name'] - end - end - - def table_exists?(name) - name && tables('SCHEMA', name).any? - end - - # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. - def columns(table_name) #:nodoc: - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/ - field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/ - field["dflt_value"] = $1.gsub('""', '"') - end - - SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) - end - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| - IndexDefinition.new( - table_name, - row['name'], - row['unique'] != 0, - exec_query("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'] == 1 - } - column && column['name'] - end - - def remove_index!(table_name, index_name) #:nodoc: - exec_query "DROP INDEX #{quote_column_name(index_name)}" - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - end - - # See: http://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) - type.to_sym != :primary_key - end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) - super(table_name, column_name, type, options) - else - alter_table(table_name) do |definition| - definition.column(column_name, type, options) - end - end - end - - def remove_column(table_name, *column_names) #:nodoc: - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) - end - end - end - alias :remove_columns :remove_column - - def change_column_default(table_name, column_name, default) #:nodoc: - alter_table(table_name) do |definition| - definition[column_name].default = default - end - end - - def change_column_null(table_name, column_name, null, default = nil) - unless null || default.nil? - exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - alter_table(table_name) do |definition| - definition[column_name].null = null - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - alter_table(table_name) do |definition| - include_default = options_include_default?(options) - definition[column_name].instance_eval do - self.type = type - self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default - self.null = options[:null] if options.include?(:null) - self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) - end - end - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - end - - def empty_insert_statement_value - "VALUES(NULL)" - end - - protected - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) - end - - def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash - raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure - end - - def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" - caller = lambda {|definition| yield definition if block_given?} - - transaction do - move_table(table_name, altered_table_name, - options.merge(:temporary => true)) - move_table(altered_table_name, table_name, &caller) - end - end - - def move_table(from, to, options = {}, &block) #:nodoc: - copy_table(from, to, options, &block) - drop_table(from) - end - - def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) - create_table(to, options) do |definition| - @definition = definition - columns(from).each do |column| - column_name = options[:rename] ? - (options[:rename][column.name] || - options[:rename][column.name.to_sym] || - column.name) : column.name - - @definition.column(column_name, column.type, - :limit => column.limit, :default => column.default, - :precision => column.precision, :scale => column.scale, - :null => column.null) - end - @definition.primary_key(primary_key(from)) if primary_key(from) - yield @definition if block_given? - end - - copy_table_indexes(from, to, options[:rename] || {}) - copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, - options[:rename] || {}) - end - - def copy_table_indexes(from, to, rename = {}) #:nodoc: - indexes(from).each do |index| - name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] - end - - to_column_names = columns(to).map { |c| c.name } - columns = index.columns.map {|c| rename[c] || c }.select do |column| - to_column_names.include?(column) - end - - unless columns.empty? - # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } - opts[:unique] = true if index.unique - add_index(to, columns, opts) - end - end - end - - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[columns.map {|name| [name, name]}] - rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} - columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} - quoted_columns = columns.map { |col| quote_column_name(col) } * ',' - - quoted_to = quote_table_name(to) - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' - sql << ')' - exec_query sql - end - end - - def sqlite_version - @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) - end - - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - - def translate_exception(exception, message) - case exception.message - when /column(s)? .* (is|are) not unique/ - RecordNotUnique.new(message, exception) - else - super - end - end - - end - end -end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index eb8f4ad669..f5349b0bea 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/deep_dup' require 'thread' module ActiveRecord @@ -76,7 +77,7 @@ module ActiveRecord ## # :singleton-method: - # Specifies wether or not has_many or has_one association option + # Specifies whether or not has_many or has_one association option # :dependent => :restrict raises an exception. If set to true, the # ActiveRecord::DeleteRestrictionError exception will be raised # along with a DEPRECATION WARNING. If set to false, an error would @@ -165,7 +166,7 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup) @columns_hash = self.class.column_types.dup init_internals @@ -203,13 +204,35 @@ module ActiveRecord self end - + + ## + # :method: clone + # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. + # That means that modifying attributes of the clone will modify the original, since they will both point to the + # same attributes hash. If you need a copy of your attributes hash, please use the #dup method. + # + # user = User.first + # new_user = user.clone + # user.name # => "Bob" + # new_user.name = "Joe" + # user.name # => "Joe" + # + # user.object_id == new_user.object_id # => false + # user.name.object_id == new_user.name.object_id # => true + # + # user.name.object_id == user.dup.name.object_id # => false + + ## + # :method: dup # Duped objects have no id assigned and are treated as new records. Note # that this is a "shallow" copy as it copies the object's attributes # only, not its associations. The extent of a "deep" copy is application # specific and is therefore left to the application to implement according # to its need. # The dup method does not preserve the timestamps (created|updated)_(at|on). + + ## + # :nodoc: def initialize_dup(other) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) self.class.initialize_attributes(cloned_attributes) diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index f52979ebd9..b163ef3c12 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -69,7 +69,7 @@ module ActiveRecord "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" end - update_all(updates.join(', '), primary_key => id) + where(primary_key => id).update_all updates.join(', ') end # Increment a number field by one, usually representing a count. diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 0473d6aafc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -1,108 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Finder Match - # - # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info - # - class DynamicFinderMatch - def self.match(method) - method = method.to_s - klass = klasses.find do |_klass| - _klass.matches?(method) - end - klass.new(method) if klass - end - - def self.matches?(method) - method =~ self::METHOD_PATTERN - end - - def self.klasses - [FindBy, FindByBang, FindOrInitializeCreateBy, FindOrCreateByBang] - end - - def initialize(method) - @finder = :first - @instantiator = nil - match_data = method.match(self.class::METHOD_PATTERN) - @attribute_names = match_data[-1].split("_and_") - initialize_from_match_data(match_data) - end - - attr_reader :finder, :attribute_names, :instantiator - - def finder? - @finder && !@instantiator - end - - def creator? - @finder == :first && @instantiator == :create - end - - def instantiator? - @instantiator - end - - def bang? - false - end - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - - def save_record? - @instantiator == :create - end - - def save_method - bang? ? :save! : :save - end - - private - - def initialize_from_match_data(match_data) - end - end - - class FindBy < DynamicFinderMatch - METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @finder = :last if match_data[1] == 'last_' - @finder = :all if match_data[1] == 'all_' - end - end - - class FindByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/ - - def bang? - true - end - end - - class FindOrInitializeCreateBy < DynamicFinderMatch - METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @instantiator = match_data[1] == 'initialize' ? :new : :create - end - - def valid_arguments?(arguments) - arguments.size == 1 && arguments.first.is_a?(Hash) || super - end - end - - class FindOrCreateByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_or_create_by_([_a-zA-Z]\w*)\!$/ - - def initialize_from_match_data(match_data) - @instantiator = :create - end - - def bang? - true - end - end -end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e35b1c91a0..e278e62ce7 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,83 +1,130 @@ module ActiveRecord - module DynamicMatchers - def respond_to?(method_id, include_private = false) - match = find_dynamic_match(method_id) - valid_match = match && all_attributes_exists?(match.attribute_names) + module DynamicMatchers #:nodoc: + # This code in this file seems to have a lot of indirection, but the indirection + # is there to provide extension points for the active_record_deprecated_finders + # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # then we can remove the indirection. - valid_match || super + def respond_to?(name, include_private = false) + match = Method.match(self, name) + match && match.valid? || super end private - # 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>. - # - # 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 = find_dynamic_match(method_id) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - - unless match.valid_arguments?(arguments) - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end + def method_missing(name, *arguments, &block) + match = Method.match(self, name) - if match.respond_to?(:scope?) && match.scope? - define_scope_method(method_id, attribute_names) - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end + if match && match.valid? + match.define + send(name, *arguments, &block) else super end end - def define_scope_method(method_id, attribute_names) #:nodoc - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - conditions = Hash[[:#{attribute_names.join(',:')}].zip(args)] # conditions = Hash[[:user_name, :password].zip(args)] - where(conditions) # where(conditions) - end # end - METHOD - end + class Method + @matchers = [] - def find_dynamic_match(method_id) #:nodoc: - DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id) - end + class << self + attr_reader :matchers - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map do |attribute_name| - if aggregation = reflect_on_aggregation(attribute_name.to_sym) - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym + def match(model, name) + klass = matchers.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end + + def pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end + + def prefix + raise NotImplementedError + end + + def suffix + '' end - end.flatten + end + + attr_reader :model, :name, :attribute_names + + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + end + + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end + + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end + + def body + raise NotImplementedError + end end - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? + module Finder + # Extended in active_record_deprecated_finders + def body + result + end + + # Extended in active_record_deprecated_finders + def result + "#{finder}(#{attributes_hash})" + end + + # Extended in active_record_deprecated_finders + def signature + attribute_names.join(', ') + end + + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + end + + def finder + raise NotImplementedError + end end - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] + class FindBy < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def finder + "find_by" + end + end + + class FindByBang < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def self.suffix + "!" + end + + def finder + "find_by!" + end end end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index 6c043d29c4..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Scope Match - # - # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> - # if, for example, the <tt>Product</tt> has an attribute with that name. You can - # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named - # scope except that it's dynamic. - class DynamicScopeMatch - METHOD_PATTERN = /^scoped_by_([_a-zA-Z]\w*)$/ - - def self.match(method) - if method.to_s =~ METHOD_PATTERN - new(true, $1 && $1.split('_and_')) - end - end - - def initialize(scope, attribute_names) - @scope = scope - @attribute_names = attribute_names - end - - attr_reader :scope, :attribute_names - alias :scope? :scope - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - end -end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 01cacf6153..313fdb3487 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -70,7 +70,7 @@ module ActiveRecord # the threshold is set to 0. # # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. def silence_auto_explain current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 9796b0a321..c630af59f0 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -372,19 +372,23 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures + #-- + # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + #++ MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_name) # :nodoc: + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - fixture_name.singularize.camelize : - fixture_name.camelize + fixture_set_name.singularize.camelize : + fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_name) # :nodoc: - "#{ActiveRecord::Base.table_name_prefix}"\ - "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym + def self.default_fixture_table_name(fixture_set_name) # :nodoc: + "#{ ActiveRecord::Base.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ ActiveRecord::Base.table_name_suffix }".to_sym end def self.reset_cache @@ -411,11 +415,11 @@ module ActiveRecord cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + def self.instantiate_fixtures(object, fixture_set, load_instances = true) if load_instances - fixtures.each do |name, fixture| + fixture_set.each do |fixture_name, fixture| begin - object.instance_variable_set "@#{name}", fixture.find + object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end @@ -424,61 +428,59 @@ module ActiveRecord end def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) end end cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = Array(table_names).map(&:to_s) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) + files_to_read = fixture_set_names.reject { |fs_name| + fixture_is_cached?(connection, fs_name) } unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixture_files = files_to_read.map do |path| - fixture_name = path - - fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new + fixture_sets = files_to_read.map do |fs_name| + fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new connection, - fixture_name, - class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), - ::File.join(fixtures_directory, path)) + fs_name, + class_names[fs_name] || default_fixture_model_name(fs_name), + ::File.join(fixtures_directory, fs_name)) end all_loaded_fixtures.update(fixtures_map) connection.transaction(:requires_new => true) do - fixture_files.each do |ff| - conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection - table_rows = ff.table_rows + fixture_sets.each do |fs| + conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection + table_rows = fs.table_rows table_rows.keys.each do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end - table_rows.each do |table_name,rows| + table_rows.each do |fixture_set_name, rows| rows.each do |row| - conn.insert_fixture(row, table_name) + conn.insert_fixture(row, fixture_set_name) end end end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - fixture_files.each do |ff| - connection.reset_pk_sequence!(ff.table_name) + fixture_sets.each do |fs| + connection.reset_pk_sequence!(fs.table_name) end end end @@ -486,7 +488,7 @@ module ActiveRecord cache_fixtures(connection, fixtures_map) end end - cached_fixtures(connection, table_names) + cached_fixtures(connection, fixture_set_names) end # Returns a consistent, platform-independent identifier for +label+. @@ -497,26 +499,23 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, fixture_name, class_name, fixture_path) - @connection = connection - @fixture_path = fixture_path - @name = fixture_name - @class_name = class_name - - @fixtures = {} + def initialize(connection, name, class_name, path) + @fixtures = {} # Ordered hash + @name = name + @path = path - # Should be an AR::Base type class - if class_name.is_a?(Class) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @model_class = class_name.constantize rescue nil end - @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + @connection = ( model_class.respond_to?(:connection) ? + model_class.connection : connection ) @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(fixture_name) ) + self.class.default_fixture_table_name(name) ) read_fixture_files end @@ -555,8 +554,8 @@ module ActiveRecord if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps - timestamp_column_names.each do |name| - row[name] = now unless row.key?(name) + timestamp_column_names.each do |c_name| + row[c_name] = now unless row.key?(c_name) end end @@ -634,26 +633,23 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] yaml_files.each do |file| Fixtures::File.open(file) do |fh| - fh.each do |name, row| - fixtures[name] = ActiveRecord::Fixture.new(row, model_class) + fh.each do |fixture_name, row| + fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end end end end def yaml_file_path - "#{@fixture_path}.yml" + "#{@path}.yml" end - def yaml_fixtures_key(path) - ::File.basename(@fixture_path).split(".").first - end end class Fixture #:nodoc: @@ -708,7 +704,7 @@ module ActiveRecord class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_fixtures - class_attribute :use_instantiated_fixtures # true, false, or :no_instances + class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures self.fixture_table_names = [] @@ -716,9 +712,8 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, fixture_name| - fixture_name = fixture_name.to_s - h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) + self.fixture_class_names = Hash.new do |h, fixture_set_name| + h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) end end @@ -742,18 +737,18 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end - def fixtures(*fixture_names) - if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f| File.basename f, '.yml' } else - fixture_names = fixture_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= fixture_names - require_fixture_classes(fixture_names) - setup_fixture_accessors(fixture_names) + self.fixture_table_names |= fixture_set_names + require_fixture_classes(fixture_set_names) + setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) @@ -768,33 +763,39 @@ module ActiveRecord end end - def require_fixture_classes(fixture_names = nil) - (fixture_names || fixture_table_names).each do |fixture_name| - file_name = fixture_name.to_s + def require_fixture_classes(fixture_set_names = nil) + if fixture_set_names + fixture_set_names = fixture_set_names.map { |n| n.to_s } + else + fixture_set_names = fixture_table_names + end + + fixture_set_names.each do |file_name| file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array(fixture_names || fixture_table_names) + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do - fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s - accessor_name = fixture_name.tr('/', '_').to_sym + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr('/', '_').to_sym - define_method(accessor_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - @fixture_cache[fixture_name] ||= {} + @fixture_cache[fs_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[fixture_name].delete(fixture) if force_reload + instances = fixture_names.map do |f_name| + f_name = f_name.to_s + @fixture_cache[fs_name].delete(f_name) if force_reload - if @loaded_fixtures[fixture_name][fixture.to_s] - @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find else - raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" end end @@ -823,7 +824,7 @@ module ActiveRecord end def setup_fixtures - return unless !ActiveRecord::Base.configurations.blank? + return if ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' @@ -901,8 +902,8 @@ module ActiveRecord ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? - @loaded_fixtures.each do |fixture_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 32a1dae6bc..95a2ddcc11 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -19,10 +19,10 @@ module ActiveRecord # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records - # through the parent. By default nested attribute updating is turned off, - # you can enable it using the accepts_nested_attributes_for class method. - # When you enable nested attributes an attribute writer is defined on - # the model. + # through the parent. By default nested attribute updating is turned off + # and you can enable it using the accepts_nested_attributes_for class + # method. When you enable nested attributes an attribute writer is + # defined on the model. # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index bb504ae90f..4a987c2343 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -178,7 +178,7 @@ module ActiveRecord verify_readonly_attribute(name) raise ActiveRecordError, "can not update on a new record object" unless persisted? raw_write_attribute(name, value) - self.class.update_all({ name => value }, self.class.primary_key => id) == 1 + self.class.where(self.class.primary_key => id).update_all(name => value) == 1 end # Updates the attributes of the model from the passed-in hash and saves the @@ -268,7 +268,13 @@ module ActiveRecord clear_aggregation_cache clear_association_cache - fresh_object = self.class.unscoped { self.class.find(id, options) } + fresh_object = + if options && options[:lock] + self.class.unscoped { self.class.lock.find(id) } + else + self.class.unscoped { self.class.find(id) } + end + @attributes.update(fresh_object.instance_variable_get('@attributes')) @columns_hash = fresh_object.instance_variable_get('@columns_hash') @@ -313,7 +319,7 @@ module ActiveRecord @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key - self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1 + self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 95565b503a..4d8283bcff 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -3,7 +3,7 @@ require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :find, :take, :take!, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped delegate :find_by, :find_by!, :to => :scoped delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped @@ -11,7 +11,7 @@ module ActiveRecord delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :references, :none, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index d4f4d593c6..c380b5c029 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -153,6 +153,10 @@ module ActiveRecord # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: + def mapping + mapping = options[:mapping] || [name, name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end end # Holds all the meta-data about an association as it was specified in the diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 333d31d8a3..779e052e3c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -390,6 +390,8 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index db894c8aa9..3ce9995031 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -139,6 +139,16 @@ module ActiveRecord end end + # Pluck all the ID's for the relation using the table's primary key + # + # Examples: + # + # Person.ids # SELECT people.id FROM people + # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id + def ids + pluck primary_key + end + private def perform_calculation(operation, column_name, options = {}) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 416b55f5c5..4fedd33d64 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -51,15 +51,37 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago # def find_by(*args) - where(*args).first + where(*args).take end # Like <tt>find_by</tt>, except that if no record is found, raises # an <tt>ActiveRecord::RecordNotFound</tt> error. def find_by!(*args) - where(*args).first! + where(*args).take! end + # Gives a record (or N records if a parameter is supplied) without any implied + # order. The order will depend on the database implementation. + # If an order is supplied it will be respected. + # + # Examples: + # + # Person.take # returns an object fetched by SELECT * FROM people + # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 + # Person.where(["name LIKE '%?'", name]).take + def take(limit = nil) + limit ? limit(limit).to_a : find_take + end + + # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. Note that <tt>take!</tt> accepts no arguments. + def take! + take or raise RecordNotFound + end + + # Find the first record (or first N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # # Examples: # # Person.first # returns the first object fetched by SELECT * FROM people @@ -67,7 +89,15 @@ module ActiveRecord # Person.where(["user_name = :u", { :u => user_name }]).first # Person.order("created_on DESC").offset(5).first def first(limit = nil) - limit ? limit(limit).to_a : find_first + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a + else + limit(limit).to_a + end + else + find_first + end end # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -76,6 +106,9 @@ module ActiveRecord first or raise RecordNotFound end + # Find the last record (or last N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # # Examples: # # Person.last # returns the last object fetched by SELECT * FROM people @@ -83,8 +116,8 @@ module ActiveRecord # Person.order("created_on DESC").offset(5).last def last(limit = nil) if limit - if order_values.empty? - order("#{primary_key} DESC").limit(limit).reverse + if order_values.empty? && primary_key + order(arel_table[primary_key].desc).limit(limit).reverse else to_a.last(limit) end @@ -137,9 +170,8 @@ module ActiveRecord # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? def exists?(id = false) - return false if id.nil? - id = id.id if ActiveRecord::Model === id + return false if id.nil? join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) @@ -210,44 +242,6 @@ module ActiveRecord ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end - def find_by_attributes(match, attributes, *args) - conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] - result = where(conditions).send(match.finder) - - if match.bang? && result.blank? - raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" - else - yield(result) if block_given? - result - end - end - - def find_or_instantiator_by_attributes(match, attributes, *args) - options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {} - protected_attributes_for_create, unprotected_attributes_for_create = {}, {} - args.each_with_index do |arg, i| - if arg.is_a?(Hash) - protected_attributes_for_create = args[i].with_indifferent_access - else - unprotected_attributes_for_create[attributes[i]] = args[i] - end - end - - conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys - - record = where(conditions).first - - unless record - record = @klass.new(protected_attributes_for_create, options) do |r| - r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) - end - yield(record) if block_given? - record.send(match.save_method) if match.save_record? - end - - record - end - def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? @@ -274,7 +268,7 @@ module ActiveRecord substitute = connection.substitute_at(column, bind_values.length) relation = where(table[primary_key].eq(substitute)) relation.bind_values += [[column, id]] - record = relation.first + record = relation.take unless record conditions = arel.where_sql @@ -312,11 +306,24 @@ module ActiveRecord end end + def find_take + if loaded? + @records.first + else + @take ||= limit(1).to_a.first + end + end + def find_first if loaded? @records.first else - @first ||= limit(1).to_a[0] + @first ||= + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(1).to_a.first + else + limit(1).to_a.first + end end end @@ -328,7 +335,7 @@ module ActiveRecord if offset_value || limit_value to_a.last else - reverse_order.limit(1).to_a[0] + reverse_order.limit(1).to_a.first end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b40bf2b3cf..6a0cdd5917 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -34,9 +34,6 @@ module ActiveRecord private def self.build(attribute, value) case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range)} @@ -59,6 +56,9 @@ module ActiveRecord array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) when Range attribute.in(value) when ActiveRecord::Model diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 855477eaed..29536b16c4 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -40,7 +40,7 @@ module ActiveRecord alias extensions extending_values def includes(*args) - args.empty? ? self : clone.includes!(*args) + args.empty? ? self : spawn.includes!(*args) end def includes!(*args) @@ -51,7 +51,7 @@ module ActiveRecord end def eager_load(*args) - args.blank? ? self : clone.eager_load!(*args) + args.blank? ? self : spawn.eager_load!(*args) end def eager_load!(*args) @@ -60,7 +60,7 @@ module ActiveRecord end def preload(*args) - args.blank? ? self : clone.preload!(*args) + args.blank? ? self : spawn.preload!(*args) end def preload!(*args) @@ -79,7 +79,7 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - args.blank? ? self : clone.references!(*args) + args.blank? ? self : spawn.references!(*args) end def references!(*args) @@ -120,7 +120,7 @@ module ActiveRecord if block_given? to_a.select { |*block_args| value.call(*block_args) } else - clone.select!(value) + spawn.select!(value) end end @@ -130,7 +130,7 @@ module ActiveRecord end def group(*args) - args.blank? ? self : clone.group!(*args) + args.blank? ? self : spawn.group!(*args) end def group!(*args) @@ -139,7 +139,7 @@ module ActiveRecord end def order(*args) - args.blank? ? self : clone.order!(*args) + args.blank? ? self : spawn.order!(*args) end def order!(*args) @@ -165,7 +165,7 @@ module ActiveRecord # generates a query with 'ORDER BY id ASC, name ASC'. # def reorder(*args) - args.blank? ? self : clone.reorder!(*args) + args.blank? ? self : spawn.reorder!(*args) end def reorder!(*args) @@ -175,7 +175,7 @@ module ActiveRecord end def joins(*args) - args.compact.blank? ? self : clone.joins!(*args) + args.compact.blank? ? self : spawn.joins!(*args) end def joins!(*args) @@ -186,7 +186,7 @@ module ActiveRecord end def bind(value) - clone.bind!(value) + spawn.bind!(value) end def bind!(value) @@ -195,7 +195,7 @@ module ActiveRecord end def where(opts, *rest) - opts.blank? ? self : clone.where!(opts, *rest) + opts.blank? ? self : spawn.where!(opts, *rest) end def where!(opts, *rest) @@ -206,7 +206,7 @@ module ActiveRecord end def having(opts, *rest) - opts.blank? ? self : clone.having!(opts, *rest) + opts.blank? ? self : spawn.having!(opts, *rest) end def having!(opts, *rest) @@ -217,7 +217,7 @@ module ActiveRecord end def limit(value) - clone.limit!(value) + spawn.limit!(value) end def limit!(value) @@ -226,7 +226,7 @@ module ActiveRecord end def offset(value) - clone.offset!(value) + spawn.offset!(value) end def offset!(value) @@ -235,7 +235,7 @@ module ActiveRecord end def lock(locks = true) - clone.lock!(locks) + spawn.lock!(locks) end def lock!(locks = true) @@ -283,7 +283,7 @@ module ActiveRecord end def readonly(value = true) - clone.readonly!(value) + spawn.readonly!(value) end def readonly!(value = true) @@ -292,7 +292,7 @@ module ActiveRecord end def create_with(value) - clone.create_with!(value) + spawn.create_with!(value) end def create_with!(value) @@ -301,7 +301,7 @@ module ActiveRecord end def from(value) - clone.from!(value) + spawn.from!(value) end def from!(value) @@ -320,7 +320,7 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - clone.uniq!(value) + spawn.uniq!(value) end def uniq!(value = true) @@ -366,7 +366,7 @@ module ActiveRecord # end def extending(*modules, &block) if modules.any? || block - clone.extending!(*modules, &block) + spawn.extending!(*modules, &block) else self end @@ -382,7 +382,7 @@ module ActiveRecord end def reverse_order - clone.reverse_order! + spawn.reverse_order! end def reverse_order! diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 41e55dfd0e..80d087a9ea 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -5,9 +5,14 @@ require 'active_record/relation/merger' module ActiveRecord module SpawnMethods - + + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. - # Returns an array representing the union of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. # # ==== Examples # @@ -16,14 +21,14 @@ module ActiveRecord # # recent_posts = Post.order('created_at DESC').first(5) # Post.where(:published => true).merge(recent_posts) - # # Returns the union of all published posts with the 5 most recently created posts. + # # Returns the intersection of all published posts with the 5 most recently created posts. # # (This is just an example. You'd probably want to do this with a single query!) # def merge(other) if other.is_a?(Array) to_a & other elsif other - clone.merge!(other) + spawn.merge!(other) else self end @@ -42,7 +47,7 @@ module ActiveRecord # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order # def except(*skips) - result = self.class.new(@klass, table, values.except(*skips)) + result = Relation.new(klass, table, values.except(*skips)) result.default_scoped = default_scoped result.extend(*extending_values) if extending_values.any? result @@ -56,7 +61,7 @@ module ActiveRecord # Post.order('id asc').only(:where, :order) # uses the specified order # def only(*onlies) - result = self.class.new(@klass, table, values.slice(*onlies)) + result = Relation.new(klass, table, values.slice(*onlies)) result.default_scoped = default_scoped result.extend(*extending_values) if extending_values.any? result diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fb4b89b87b..fd276ccf5d 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -28,6 +28,7 @@ module ActiveRecord alias :map! :map alias :collect! :map + # Returns true if there are no records. def empty? rows.empty? end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 81b13fe529..5530be3219 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -58,7 +58,7 @@ module ActiveRecord expanded_attrs = {} attrs.each do |attr, value| if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregate_mapping(aggregation) + mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| if mapping.size == 1 && !value.respond_to?(aggregate_attr) expanded_attrs[field_attr] = value diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index ce43ae8066..ed47a26749 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -119,7 +119,7 @@ module ActiveRecord class << self; remove_possible_method :find_by_session_id; end def self.find_by_session_id(session_id) - find :first, :conditions => {:session_id=>session_id} + where(session_id: session_id).first end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 743dfc5a38..64e5640791 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -302,12 +302,8 @@ module ActiveRecord def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index db618f617f..9e4b588ac2 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -196,7 +196,6 @@ module ActiveRecord # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # * ActiveRecord::ConnectionAdapters::MysqlAdapter # * ActiveRecord::ConnectionAdapters::Mysql2Adapter - # * ActiveRecord::ConnectionAdapters::SQLiteAdapter # * ActiveRecord::ConnectionAdapters::SQLite3Adapter # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter # diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index fa2ba8d592..5e1c52c9ba 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -120,6 +120,19 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + def test_mysql_strict_mode_disabled + run_without_connection do |orig_connection| + ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) + result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 292c7efebb..6faceaf7c0 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.find(:all, :include => [:groups]) } + assert_nothing_raised { Select.scoped(:includes => [:groups]).all } end #the following functions were added to DRY test cases diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 29f885c6e7..d94bb629a7 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_schema - assert @omgpost.find(:first) + assert @omgpost.first end def test_primary_key diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index cd9c1041dc..5e8065d80d 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -9,7 +9,7 @@ module ActiveRecord def test_update_question_marks str = "foo?bar" - x = Topic.find :first + x = Topic.first x.title = str x.content = str x.save! @@ -28,7 +28,7 @@ module ActiveRecord def test_update_null_bytes str = "foo\0bar" - x = Topic.find :first + x = Topic.first x.title = str x.content = str x.save! diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8e2b9ca9a5..684c7f5929 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -29,6 +29,22 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + # TODO: Below is a straight up copy/paste from mysql/connection_test.rb + # I'm not sure what the correct way is to share these tests between + # adapters in minitest. + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + def test_mysql_strict_mode_disabled + run_without_connection do |orig_connection| + ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) + result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 3a9744e78f..32d4282623 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.find(:all, :include => [:groups]) } + assert_nothing_raised { Select.scoped(:includes => [:groups]).all } end #the following functions were added to DRY test cases diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index de4d9c2b33..2c0ed73c92 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_schema - assert @omgpost.find(:first) + assert @omgpost.first end def test_primary_key diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 05d38ec1f1..23bafde17b 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -90,7 +90,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_rewrite @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.find :first + x = Hstore.first x.tags = { '"a\'' => 'b' } assert x.save! end @@ -98,13 +98,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_select @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.find :first + x = Hstore.first assert_equal({'1' => '2'}, x.tags) end def test_select_multikey @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" - x = Hstore.find :first + x = Hstore.first assert_equal({'1' => '2', '2' => '3'}, x.tags) end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a71d0bb848..92e31a3e44 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -49,6 +49,33 @@ module ActiveRecord assert_equal expect, id end + def test_insert_sql_with_returning_disabled + connection = connection_without_insert_returning + id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, id + end + + def test_exec_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, result.rows.first.first + end + + def test_exec_insert_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, result.rows.first.first + end + + def test_sql_for_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') + assert_equal ['sql', 'binds'], result + end + def test_serial_sequence assert_equal 'public.accounts_id_seq', @connection.serial_sequence('accounts', 'id') @@ -204,6 +231,10 @@ module ActiveRecord ctx.exec_insert(sql, 'SQL', binds) end + + def connection_without_insert_returning + ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 172055f15c..f8a605b67c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -19,6 +19,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end + + def test_quote_float_nan + nan = 0.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'NaN'", @conn.quote(nan, c) + end + + def test_quote_float_infinity + infinity = 1.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'Infinity'", @conn.quote(infinity, c) + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 1dbeb66af6..2ba9143cd5 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -5,7 +5,7 @@ require 'securerandom' module ActiveRecord module ConnectionAdapters - class SQLiteAdapter + class SQLite3Adapter class QuotingTest < ActiveRecord::TestCase def setup @conn = Base.sqlite3_connection :database => ':memory:', diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index ae272e2c4b..2f04c60a9a 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters - class SQLiteAdapter + class SQLite3Adapter class StatementPoolTest < ActiveRecord::TestCase def test_cache_is_per_pid return skip('must support fork') unless Process.respond_to?(:fork) diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 05ab86105c..b351196fbd 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -73,14 +73,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_eager_loading_with_primary_key Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key) + citibank_result = Client.scoped(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first assert citibank_result.association_cache.key?(:firm_with_primary_key) end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols) + citibank_result = Client.scoped(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first assert citibank_result.association_cache.key?(:firm_with_primary_key_symbols) end @@ -182,7 +182,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_with_select assert_equal Company.find(2).firm_with_select.attributes.size, 1 - assert_equal Company.find(2, :include => :firm_with_select ).firm_with_select.attributes.size, 1 + assert_equal Company.scoped(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size, 1 end def test_belongs_to_counter @@ -334,7 +334,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_new_record_with_foreign_key_but_no_object c = Client.new("firm_id" => 1) # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.find(:first, :order => "id"), c.firm_with_basic_id + assert_equal Firm.scoped(:order => "id").first, c.firm_with_basic_id end def test_setting_foreign_key_after_nil_target_loaded @@ -394,9 +394,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_association_assignment_sticks - post = Post.find(:first) + post = Post.first - author1, author2 = Author.find(:all, :limit => 2) + author1, author2 = Author.scoped(:limit => 2).all assert_not_nil author1 assert_not_nil author2 @@ -498,14 +498,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Account.find(@account.id).save! - Account.find(@account.id, :include => :firm).save! + Account.scoped(:includes => :firm).find(@account.id).save! end @account.firm.delete assert_nothing_raised do Account.find(@account.id).save! - Account.find(@account.id, :include => :firm).save! + Account.scoped(:includes => :firm).find(@account.id).save! end end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 6733f3e889..01f7f18397 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -16,7 +16,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels - authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>:comments}, :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -24,7 +24,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_one_level - authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id") + authors = Author.scoped(:includes=>[{:posts=>:comments}, :categorizations], :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -84,7 +84,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase 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") + authors = Author.scoped(:includes=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -92,7 +92,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference - authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>[:comments, :author]}, :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal authors(:david).name, authors[0].name @@ -100,13 +100,13 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_with_condition - authors = Author.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>:comments}, :where=>"authors.id=1", :order=>"authors.id").all assert_equal 1, authors.size assert_equal 5, authors[0].posts.size end def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong - firms = Firm.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id") + firms = Firm.scoped(:includes=>{:account=>{:firm=>:account}}, :order=>"companies.id").all assert_equal 2, firms.size assert_equal firms.first.account, firms.first.account.firm.account assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account } @@ -114,7 +114,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_has_many_sti - topics = Topic.find(:all, :include => :replies, :order => 'topics.id') + topics = Topic.scoped(:includes => :replies, :order => 'topics.id').all first, second, = topics(:first).replies.size, topics(:second).replies.size assert_no_queries do assert_equal first, topics[0].replies.size @@ -127,7 +127,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase silly.parent_id = 1 assert silly.save - topics = Topic.find(:all, :include => :replies, :order => ['topics.id', 'replies_topics.id']) + topics = Topic.scoped(:includes => :replies, :order => ['topics.id', 'replies_topics.id']).all assert_no_queries do assert_equal 2, topics[0].replies.size assert_equal 0, topics[1].replies.size @@ -135,14 +135,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_sti - replies = Reply.find(:all, :include => :topic, :order => 'topics.id') + replies = Reply.scoped(:includes => :topic, :order => 'topics.id').all assert replies.include?(topics(:second)) assert !replies.include?(topics(:first)) assert_equal topics(:first), assert_no_queries { replies.first.topic } end def test_eager_association_loading_with_multiple_stis_and_order - author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :conditions => 'posts.id = 4') + author = Author.scoped(:includes => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :where => 'posts.id = 4').first assert_equal authors(:david), author assert_no_queries do author.posts.first.special_comments @@ -151,7 +151,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_of_stis_with_multiple_references - authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4') + authors = Author.scoped(:includes => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :where => 'posts.id = 4').all assert_equal [authors(:david)], authors assert_no_queries do authors.first.posts.first.special_comments.first.post.special_comments @@ -160,7 +160,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_where_first_level_returns_nil - authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC') + authors = Author.scoped(:includes => {:post_about_thinking => :comments}, :order => 'authors.id DESC').all assert_equal [authors(:bob), authors(:mary), authors(:david)], authors assert_no_queries do authors[2].post_about_thinking.comments.first @@ -168,12 +168,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through - source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id') + source = Vertex.scoped(:includes=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id').first assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first } end def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many - sink = Vertex.find(:first, :include=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC') + sink = Vertex.scoped(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first } end end diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 3044a8c312..75a6295350 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -24,11 +24,11 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = false - post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff') assert_nil post.tagging ActiveRecord::Base.store_full_sti_class = true - post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff') assert_instance_of Tagging, post.tagging ensure ActiveRecord::Base.store_full_sti_class = old diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 1e1958410c..bb0d6bc70b 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -92,8 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase end def test_include_query - res = 0 - res = ShapeExpression.find :all, :include => [ :shape, { :paint => :non_poly } ] + res = ShapeExpression.scoped(:includes => [ :shape, { :paint => :non_poly } ]).all assert_equal NUM_SHAPE_EXPRESSIONS, res.size assert_queries(0) do res.each do |se| @@ -123,7 +122,7 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase assert_nothing_raised do # @davey_mcdave doesn't have any author_favorites includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author } - Author.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name' + Author.scoped(:includes => includes, :where => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name').to_a end end end diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 07d0b24613..5805e71249 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -103,43 +103,43 @@ class EagerSingularizationTest < ActiveRecord::TestCase def test_eager_no_extra_singularization_belongs_to return unless @have_tables assert_nothing_raised do - Virus.find(:all, :include => :octopus) + Virus.scoped(:includes => :octopus).all end end def test_eager_no_extra_singularization_has_one return unless @have_tables assert_nothing_raised do - Octopus.find(:all, :include => :virus) + Octopus.scoped(:includes => :virus).all end end def test_eager_no_extra_singularization_has_many return unless @have_tables assert_nothing_raised do - Bus.find(:all, :include => :passes) + Bus.scoped(:includes => :passes).all end end def test_eager_no_extra_singularization_has_and_belongs_to_many return unless @have_tables assert_nothing_raised do - Crisis.find(:all, :include => :messes) - Mess.find(:all, :include => :crises) + Crisis.scoped(:includes => :messes).all + Mess.scoped(:includes => :crises).all end end def test_eager_no_extra_singularization_has_many_through_belongs_to return unless @have_tables assert_nothing_raised do - Crisis.find(:all, :include => :successes) + Crisis.scoped(:includes => :successes).all end end def test_eager_no_extra_singularization_has_many_through_has_many return unless @have_tables assert_nothing_raised do - Crisis.find(:all, :include => :compresses) + Crisis.scoped(:includes => :compresses).all end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index d700554af7..08467900f9 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -35,42 +35,42 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_one_through_join_model_with_conditions_on_the_through - member = Member.find(members(:some_other_guy).id, :include => :favourite_club) + member = Member.scoped(:includes => :favourite_club).find(members(:some_other_guy).id) assert_nil member.favourite_club end def test_loading_with_one_association - posts = Post.find(:all, :include => :comments) + posts = Post.scoped(:includes => :comments).all post = posts.find { |p| p.id == 1 } assert_equal 2, post.comments.size assert post.comments.include?(comments(:greetings)) - post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'") + post = Post.scoped(:includes => :comments, :where => "posts.title = 'Welcome to the weblog'").first assert_equal 2, post.comments.size assert post.comments.include?(comments(:greetings)) - posts = Post.find(:all, :include => :last_comment) + posts = Post.scoped(:includes => :last_comment).all post = posts.find { |p| p.id == 1 } assert_equal Post.find(1).last_comment, post.last_comment end def test_loading_with_one_association_with_non_preload - posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC') + posts = Post.scoped(:includes => :last_comment, :order => 'comments.id DESC').all post = posts.find { |p| p.id == 1 } assert_equal Post.find(1).last_comment, post.last_comment end def test_loading_conditions_with_or - posts = authors(:david).posts.references(:comments).find( - :all, :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" - ) + posts = authors(:david).posts.references(:comments).scoped( + :includes => :comments, + :where => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" + ).all assert_nil posts.detect { |p| p.author_id != authors(:david).id }, "expected to find only david's posts" end def test_with_ordering - list = Post.find(:all, :include => :comments, :order => "posts.id DESC") + list = Post.scoped(:includes => :comments, :order => "posts.id DESC").all [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome ].each_with_index do |post, index| @@ -84,14 +84,14 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_with_multiple_associations - posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id") + posts = Post.scoped(:includes => [ :comments, :author, :categories ], :order => "posts.id").all assert_equal 2, posts.first.comments.size assert_equal 2, posts.first.categories.size assert posts.first.comments.include?(comments(:greetings)) end def test_duplicate_middle_objects - comments = Comment.find :all, :conditions => 'post_id = 1', :include => [:post => :author] + comments = Comment.scoped(:where => 'post_id = 1', :includes => [:post => :author]).all assert_no_queries do comments.each {|comment| comment.post.author.name} end @@ -99,25 +99,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.find(:all, :include=>:comments) + posts = Post.scoped(:includes=>:comments).all assert_equal 11, posts.size end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.find(:all, :include=>:comments) + posts = Post.scoped(:includes=>:comments).all assert_equal 11, posts.size end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.find(:all, :include=>:categories) + posts = Post.scoped(:includes=>:categories).all assert_equal 11, posts.size end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.find(:all, :include=>:categories) + posts = Post.scoped(:includes=>:categories).all assert_equal 11, posts.size end @@ -154,8 +154,8 @@ class EagerAssociationTest < ActiveRecord::TestCase popular_post.readers.create!(:person => people(:michael)) popular_post.readers.create!(:person => people(:david)) - readers = Reader.find(:all, :conditions => ["post_id = ?", popular_post.id], - :include => {:post => :comments}) + readers = Reader.scoped(:where => ["post_id = ?", popular_post.id], + :includes => {:post => :comments}).all readers.each do |reader| assert_equal [comment], reader.post.comments end @@ -167,8 +167,8 @@ class EagerAssociationTest < ActiveRecord::TestCase car_post.categories << categories(:technology) comment = car_post.comments.create!(:body => "hmm") - categories = Category.find(:all, :conditions => { 'posts.id' => car_post.id }, - :include => {:posts => :comments}) + categories = Category.scoped(:where => { 'posts.id' => car_post.id }, + :includes => {:posts => :comments}).all categories.each do |category| assert_equal [comment], category.posts[0].comments end @@ -186,10 +186,10 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once author_id = authors(:david).id - author = assert_queries(3) { Author.find(author_id, :include => {:posts_with_comments => :comments}) } # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.scoped(:includes => {:posts_with_comments => :comments}).find(author_id) } # find the author, then find the posts, then find the comments author.posts_with_comments.each do |post_with_comments| assert_equal post_with_comments.comments.length, post_with_comments.comments.count - assert_nil post_with_comments.comments.uniq! + assert_nil post_with_comments.comments.to_a.uniq! end end @@ -197,7 +197,7 @@ class EagerAssociationTest < ActiveRecord::TestCase author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment - author = assert_queries(3) { Author.find(author.id, :include => {:post_about_thinking_with_last_comment => :last_comment})} # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.scoped(:includes => {:post_about_thinking_with_last_comment => :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments assert_no_queries do assert_equal post, author.post_about_thinking_with_last_comment assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment @@ -208,7 +208,7 @@ class EagerAssociationTest < ActiveRecord::TestCase post = posts(:welcome) author = post.author author_address = author.author_address - post = assert_queries(3) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author, then find the address + post = assert_queries(3) { Post.scoped(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author, then find the address assert_no_queries do assert_equal author, post.author_with_address assert_equal author_address, post.author_with_address.author_address @@ -218,7 +218,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once post = posts(:welcome) post.update_attributes!(:author => nil) - post = assert_queries(1) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the author or address + post = assert_queries(1) { Post.scoped(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address assert_no_queries do assert_equal nil, post.author_with_address end @@ -227,85 +227,85 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_polymorphic_association sponsor = sponsors(:moustache_club_sponsor_for_groucho) sponsor.update_attributes!(:sponsorable => nil) - sponsor = assert_queries(1) { Sponsor.find(sponsor.id, :include => :sponsorable) } + sponsor = assert_queries(1) { Sponsor.scoped(:includes => :sponsorable).find(sponsor.id) } assert_no_queries do assert_equal nil, sponsor.sponsorable end end def test_loading_from_an_association - posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id") + posts = authors(:david).posts.scoped(:includes => :comments, :order => "posts.id").all assert_equal 2, posts.first.comments.size end def test_loading_from_an_association_that_has_a_hash_of_conditions assert_nothing_raised do - Author.find(:all, :include => :hello_posts_with_hash_conditions) + Author.scoped(:includes => :hello_posts_with_hash_conditions).all end - assert !Author.find(authors(:david).id, :include => :hello_posts_with_hash_conditions).hello_posts.empty? + assert !Author.scoped(:includes => :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? end def test_loading_with_no_associations - assert_nil Post.find(posts(:authorless).id, :include => :author).author + assert_nil Post.scoped(:includes => :author).find(posts(:authorless).id).author end def test_nested_loading_with_no_associations assert_nothing_raised do - Post.find(posts(:authorless).id, :include => {:author => :author_addresss}) + Post.scoped(:includes => {:author => :author_addresss}).find(posts(:authorless).id) end end def test_nested_loading_through_has_one_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}) + aa = AuthorAddress.scoped(:includes => {:author => :posts}).find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'author_addresses.id') + aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'author_addresses.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'authors.id') + aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'authors.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_nested_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'posts.id') + aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'posts.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions - aa = AuthorAddress.references(:author_addresses).find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "author_addresses.id > 0" - ) + aa = AuthorAddress.references(:author_addresses).scoped( + :includes => {:author => :posts}, + :where => "author_addresses.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions_on_association - aa = AuthorAddress.references(:authors).find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "authors.id > 0" - ) + aa = AuthorAddress.references(:authors).scoped( + :includes => {:author => :posts}, + :where => "authors.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions_on_nested_association - aa = AuthorAddress.references(:posts).find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "posts.id > 0" - ) + aa = AuthorAddress.references(:posts).scoped( + :includes => {:author => :posts}, + :where => "posts.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_eager_association_loading_with_belongs_to_and_foreign_keys - pets = Pet.find(:all, :include => :owner) + pets = Pet.scoped(:includes => :owner).all assert_equal 3, pets.length end def test_eager_association_loading_with_belongs_to - comments = Comment.find(:all, :include => :post) + comments = Comment.scoped(:includes => :post).all assert_equal 11, comments.length titles = comments.map { |c| c.post.title } assert titles.include?(posts(:welcome).title) @@ -313,31 +313,31 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_and_limit - comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :limit => 5, :order => 'comments.id').all assert_equal 5, comments.length assert_equal [1,2,3,5,6], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset - comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [3,5,6], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array - comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end @@ -345,7 +345,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4]) + Comment.scoped(:includes => :post, :where => ['posts.id = ?',4]).all end end end @@ -353,7 +353,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_hash comments = [] assert_nothing_raised do - comments = Comment.find(:all, :include => :post, :conditions => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').all end assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } @@ -366,14 +366,14 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4]) + Comment.scoped(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).all end end end def test_eager_association_loading_with_belongs_to_and_order_string_with_unquoted_table_name assert_nothing_raised do - Comment.find(:all, :include => :post, :order => 'posts.id') + Comment.scoped(:includes => :post, :order => 'posts.id').all end end @@ -381,55 +381,55 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :order => quoted_posts_id) + Comment.scoped(:includes => :post, :order => quoted_posts_id).all end end end def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations - posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id') + posts = Post.scoped(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').all assert_equal 1, posts.length assert_equal [1], posts.collect { |p| p.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations - posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id') + posts = Post.scoped(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').all assert_equal 1, posts.length assert_equal [2], posts.collect { |p| p.id } end def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name - author_favorite = AuthorFavorite.find(:first, :include => :favorite_author) + author_favorite = AuthorFavorite.scoped(:includes => :favorite_author).first assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author } end def test_eager_load_belongs_to_quotes_table_and_column_names - job = Job.find jobs(:unicyclist).id, :include => :ideal_reference + job = Job.includes(:ideal_reference).find jobs(:unicyclist).id references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), job.ideal_reference} end def test_eager_load_has_one_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :favourite_reference) + michael = Person.scoped(:includes => :favourite_reference).find(people(:michael)) references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference} end def test_eager_load_has_many_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :references) + michael = Person.scoped(:includes => :references).find(people(:michael)) references(:michael_magician,:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) } end def test_eager_load_has_many_through_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :jobs) + michael = Person.scoped(:includes => :jobs).find(people(:michael)) jobs(:magician, :unicyclist) assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) } end def test_eager_load_has_many_with_string_keys subscriptions = subscriptions(:webster_awdr, :webster_rfr) - subscriber =Subscriber.find(subscribers(:second).id, :include => :subscriptions) + subscriber =Subscriber.scoped(:includes => :subscriptions).find(subscribers(:second).id) assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end @@ -447,25 +447,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_has_many_through_with_string_keys books = books(:awdr, :rfr) - subscriber = Subscriber.find(subscribers(:second).id, :include => :books) + subscriber = Subscriber.scoped(:includes => :books).find(subscribers(:second).id) assert_equal books, subscriber.books.sort_by(&:id) end def test_eager_load_belongs_to_with_string_keys subscriber = subscribers(:second) - subscription = Subscription.find(subscriptions(:webster_awdr).id, :include => :subscriber) + subscription = Subscription.scoped(:includes => :subscriber).find(subscriptions(:webster_awdr).id) assert_equal subscriber, subscription.subscriber end def test_eager_association_loading_with_explicit_join - posts = Post.find(:all, :include => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id') + posts = Post.scoped(:includes => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id').all assert_equal 1, posts.length end def test_eager_with_has_many_through - posts_with_comments = people(:michael).posts.find(:all, :include => :comments, :order => 'posts.id') - posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id') - posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id') + posts_with_comments = people(:michael).posts.scoped(:includes => :comments, :order => 'posts.id').all + posts_with_author = people(:michael).posts.scoped(:includes => :author, :order => 'posts.id').all + posts_with_comments_and_author = people(:michael).posts.scoped(:includes => [ :comments, :author ], :order => 'posts.id').all assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size } assert_equal authors(:david), assert_no_queries { posts_with_author.first.author } assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } @@ -476,32 +476,32 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.create!(:author => author, :title => "TITLE", :body => "BODY") author.author_favorites.create(:favorite_author_id => 1) author.author_favorites.create(:favorite_author_id => 2) - posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites) + posts_with_author_favorites = author.posts.scoped(:includes => :author_favorites).all assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id } end def test_eager_with_has_many_through_an_sti_join_model - author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id') + author = Author.scoped(:includes => :special_post_comments, :order => 'authors.id').first assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments } end def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both - author = Author.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id') + author = Author.scoped(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first assert_equal [], author.special_nonexistant_post_comments end def test_eager_with_has_many_through_join_model_with_conditions - assert_equal Author.find(:first, :include => :hello_post_comments, - :order => 'authors.id').hello_post_comments.sort_by(&:id), - Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id) + assert_equal Author.scoped(:includes => :hello_post_comments, + :order => 'authors.id').first.hello_post_comments.sort_by(&:id), + Author.scoped(:order => 'authors.id').first.hello_post_comments.sort_by(&:id) end def test_eager_with_has_many_through_join_model_with_conditions_on_top_level - assert_equal comments(:more_greetings), Author.find(authors(:david).id, :include => :comments_with_order_and_conditions).comments_with_order_and_conditions.first + assert_equal comments(:more_greetings), Author.scoped(:includes => :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first end def test_eager_with_has_many_through_join_model_with_include - author_comments = Author.find(authors(:david).id, :include => :comments_with_include).comments_with_include.to_a + author_comments = Author.scoped(:includes => :comments_with_include).find(authors(:david).id).comments_with_include.to_a assert_no_queries do author_comments.first.post.title end @@ -509,7 +509,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_through_with_conditions_join_model_with_include post_tags = Post.find(posts(:welcome).id).misc_tags - eager_post_tags = Post.find(1, :include => :misc_tags).misc_tags + eager_post_tags = Post.scoped(:includes => :misc_tags).find(1).misc_tags assert_equal post_tags, eager_post_tags end @@ -520,16 +520,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit - posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2) + posts = Post.scoped(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).all assert_equal 2, posts.size assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size } end def test_eager_with_has_many_and_limit_and_conditions if current_adapter?(:OpenBaseAdapter) - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").all else - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").all end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -537,9 +537,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array if current_adapter?(:OpenBaseAdapter) - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").all else - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").all end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -547,7 +547,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers posts = ActiveSupport::Deprecation.silence do - Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).all end assert_equal 2, posts.size @@ -558,34 +558,34 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_high_offset - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).all assert_equal 0, posts.size end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions assert_queries(1) do posts = Post.references(:authors, :comments). - find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]) + scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, + :where => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]).all assert_equal 0, posts.size end end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions assert_queries(1) do - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }) + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, + :where => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }).all assert_equal 0, posts.size end end def test_count_eager_with_has_many_and_limit_and_high_offset - posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).count(:all) assert_equal 0, posts end def test_eager_with_has_many_and_limit_with_no_results - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "posts.title = 'magic forest'").all assert_equal 0, posts.size end @@ -602,7 +602,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_and_belongs_to_many_and_limit - posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3) + posts = Post.scoped(:includes => :categories, :order => "posts.id", :limit => 3).all assert_equal 3, posts.size assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size @@ -668,7 +668,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_habtm - posts = Post.find(:all, :include => :categories, :order => "posts.id") + posts = Post.scoped(:includes => :categories, :order => "posts.id").all assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size assert_equal 0, posts[2].categories.size @@ -677,23 +677,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_inheritance - SpecialPost.find(:all, :include => [ :comments ]) + SpecialPost.scoped(:includes => [ :comments ]).all end def test_eager_has_one_with_association_inheritance - post = Post.find(4, :include => [ :very_special_comment ]) + post = Post.scoped(:includes => [ :very_special_comment ]).find(4) assert_equal "VerySpecialComment", post.very_special_comment.class.to_s end def test_eager_has_many_with_association_inheritance - post = Post.find(4, :include => [ :special_comments ]) + post = Post.scoped(:includes => [ :special_comments ]).find(4) post.special_comments.each do |special_comment| assert special_comment.is_a?(SpecialComment) end end def test_eager_habtm_with_association_inheritance - post = Post.find(6, :include => [ :special_categories ]) + post = Post.scoped(:includes => [ :special_categories ]).find(6) assert_equal 1, post.special_categories.size post.special_categories.each do |special_category| assert_equal "SpecialCategory", special_category.class.to_s @@ -702,8 +702,8 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_one_dependent_does_not_destroy_dependent assert_not_nil companies(:first_firm).account - f = Firm.find(:first, :include => :account, - :conditions => ["companies.name = ?", "37signals"]) + f = Firm.scoped(:includes => :account, + :where => ["companies.name = ?", "37signals"]).first assert_not_nil f.account assert_equal companies(:first_firm, :reload).account, f.account end @@ -717,16 +717,16 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_invalid_association_reference assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=> :monkeys ) + Post.scoped(:includes=> :monkeys ).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=>[ :monkeys ]) + Post.scoped(:includes=>[ :monkeys ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=>[ 'monkeys' ]) + Post.scoped(:includes=>[ 'monkeys' ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { - Post.find(6, :include=>[ :monkeys, :elephants ]) + Post.scoped(:includes=>[ :monkeys, :elephants ]).find(6) } end @@ -771,51 +771,51 @@ class EagerAssociationTest < ActiveRecord::TestCase end def find_all_ordered(className, include=nil) - className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include) + className.scoped(:order=>"#{className.table_name}.#{className.primary_key}", :includes=>include).all end def test_limited_eager_with_order assert_equal( posts(:thinking, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title)', :limit => 2, :offset => 1 - ) + ).all ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1 - ) + ).all ) end def test_limited_eager_with_multiple_order_columns assert_equal( posts(:thinking, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1 - ) + ).all ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1 - ) + ).all ) end def test_limited_eager_with_numeric_in_association assert_equal( people(:david, :susan), - Person.references(:number1_fans_people).find( - :all, :include => [:readers, :primary_contact, :number1_fan], - :conditions => "number1_fans_people.first_name like 'M%'", + Person.references(:number1_fans_people).scoped( + :includes => [:readers, :primary_contact, :number1_fan], + :where => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0 - ) + ).all ) end @@ -828,9 +828,9 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_polymorphic_type_condition - post = Post.find(posts(:thinking).id, :include => :taggings) + post = Post.scoped(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) - post = SpecialPost.find(posts(:thinking).id, :include => :taggings) + post = SpecialPost.scoped(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) end @@ -881,13 +881,13 @@ class EagerAssociationTest < ActiveRecord::TestCase end end def test_eager_with_valid_association_as_string_not_symbol - assert_nothing_raised { Post.find(:all, :include => 'comments') } + assert_nothing_raised { Post.scoped(:includes => 'comments').all } end def test_eager_with_floating_point_numbers assert_queries(2) do # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query - Comment.find :all, :conditions => "123.456 = 123.456", :include => :post + Comment.scoped(:where => "123.456 = 123.456", :includes => :post).all end end @@ -941,21 +941,21 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_load_with_sti_sharing_association assert_queries(2) do #should not do 1 query per subclass - Comment.find :all, :include => :post + Comment.includes(:post).all end end def test_conditions_on_join_table_with_include_and_limit - assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => { 'developers_projects.access_level' => 1 }, :limit => 5).size + assert_equal 3, Developer.scoped(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).all.size end def test_order_on_join_table_with_include_and_limit - assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size + assert_equal 5, Developer.scoped(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).all.size end def test_eager_loading_with_order_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC') + Post.scoped(:joins => :comments, :includes => :author, :order => 'comments.id DESC').all end assert_equal posts(:eager_other), posts[1] assert_equal authors(:mary), assert_no_queries { posts[1].author} @@ -963,24 +963,24 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id') + Post.scoped(:includes => :author, :joins => {:taggings => :tag}, :where => "tags.name = 'General'", :order => 'posts.id').all end assert_equal posts(:welcome, :thinking), posts posts = assert_queries(2) do - Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id') + Post.scoped(:includes => :author, :joins => {:taggings => {:tag => :taggings}}, :where => "taggings_tags.super_tag_id=2", :order => 'posts.id').all end assert_equal posts(:welcome, :thinking), posts @@ -988,13 +988,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_string_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} @@ -1003,7 +1003,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_select_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'posts.*, authors.name as author_name', :include => :comments, :joins => :author, :order => 'posts.id') + Post.scoped(:select => 'posts.*, authors.name as author_name', :includes => :comments, :joins => :author, :order => 'posts.id').all end assert_equal 'David', posts[0].author_name assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments} @@ -1013,14 +1013,14 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.columns authors = assert_queries(2) do - Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'") + Author.scoped(:includes => :author_address, :joins => :comments, :where => "posts.title like 'Welcome%'").all end assert_equal authors(:david), authors[0] assert_equal author_addresses(:david_address), authors[0].author_address end def test_preload_belongs_to_uses_exclusive_scope - people = Person.males.find(:all, :include => :primary_contact) + people = Person.males.scoped(:includes => :primary_contact).all assert_not_equal people.length, 0 people.each do |person| assert_no_queries {assert_not_nil person.primary_contact} @@ -1029,15 +1029,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_has_many_uses_exclusive_scope - people = Person.males.find :all, :include => :agents + people = Person.males.includes(:agents).all people.each do |person| assert_equal Person.find(person.id).agents, person.agents end end def test_preload_has_many_using_primary_key - expected = Firm.find(:first).clients_using_primary_key.to_a - firm = Firm.find :first, :include => :clients_using_primary_key + expected = Firm.first.clients_using_primary_key.to_a + firm = Firm.includes(:clients_using_primary_key).first assert_no_queries do assert_equal expected, firm.clients_using_primary_key end @@ -1047,9 +1047,9 @@ class EagerAssociationTest < ActiveRecord::TestCase expected = Firm.find(1).clients_using_primary_key.sort_by(&:name) # Oracle adapter truncates alias to 30 characters if current_adapter?(:OracleAdapter) - firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name' + firm = Firm.scoped(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name').find(1) else - firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name' + firm = Firm.scoped(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name').find(1) end assert_no_queries do assert_equal expected, firm.clients_using_primary_key @@ -1058,7 +1058,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preload_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.find :first, :include => :account_using_primary_key, :order => 'companies.id' + firm = Firm.scoped(:includes => :account_using_primary_key, :order => 'companies.id').first assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1066,7 +1066,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_include_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').detect {|f| f.id == 1} + firm = Firm.scoped(:includes => :account_using_primary_key, :order => 'accounts.id').all.detect {|f| f.id == 1} assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1130,7 +1130,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm - posts = Post.find(:all, :include => {:categories => :categorizations}, :order => "posts.id") + posts = Post.scoped(:includes => {:categories => :categorizations}, :order => "posts.id").all assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } 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 f5f5b96aa7..470f0c3fd3 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 @@ -123,11 +123,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert active_record.developers.include?(david) end - def test_triple_equality - assert !(Array === Developer.find(1).projects) - assert Developer.find(1).projects === Array - end - def test_adding_single jamis = Developer.find(2) jamis.projects.reload # causing the collection to load @@ -355,7 +350,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_deleting_array david = Developer.find(1) david.projects.reload - david.projects.delete(Project.find(:all)) + david.projects.delete(Project.all) assert_equal 0, david.projects.size assert_equal 0, david.projects(true).size end @@ -375,7 +370,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase active_record.developers.reload assert_equal 3, active_record.developers_by_sql.size - active_record.developers_by_sql.delete(Developer.find(:all)) + active_record.developers_by_sql.delete(Developer.all) assert_equal 0, active_record.developers_by_sql(true).size end @@ -497,7 +492,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded project = projects(:active_record) - project.developers.class # force load target + project.developers.load_target developer = project.developers.first @@ -548,15 +543,15 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_find_with_merged_options assert_equal 1, projects(:active_record).limited_developers.size - assert_equal 1, projects(:active_record).limited_developers.find(:all).size - assert_equal 3, projects(:active_record).limited_developers.find(:all, :limit => nil).size + assert_equal 1, projects(:active_record).limited_developers.all.size + assert_equal 3, projects(:active_record).limited_developers.limit(nil).all.size end def test_dynamic_find_should_respect_association_order # Developers are ordered 'name DESC, id DESC' high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') - assert_equal high_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'") + assert_equal high_id_jamis, projects(:active_record).developers.scoped(:where => "name = 'Jamis'").first assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis') end @@ -566,7 +561,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase middle_id_jamis = developers(:poor_jamis) high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') - assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'") + assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.scoped(:where => "name = 'Jamis'").all assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis') end @@ -576,12 +571,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_all_should_respect_association_limit - assert_equal 1, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'").length + assert_equal 1, projects(:active_record).limited_developers.scoped(:where => "name = 'Jamis'").all.length assert_equal 1, projects(:active_record).limited_developers.find_all_by_name('Jamis').length end def test_dynamic_find_all_order_should_override_association_limit - assert_equal 2, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'", :limit => 9_000).length + assert_equal 2, projects(:active_record).limited_developers.scoped(:where => "name = 'Jamis'", :limit => 9_000).all.length assert_equal 2, projects(:active_record).limited_developers.find_all_by_name('Jamis', :limit => 9_000).length end @@ -632,7 +627,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_consider_type - developer = Developer.find(:first) + developer = Developer.first special_project = SpecialProject.create("name" => "Special Project") other_project = developer.projects.first @@ -667,7 +662,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase categories(:technology).select_testing_posts(true).each do |o| assert_respond_to o, :correctness_marker end - assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker + assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker end def test_habtm_selects_all_columns_by_default @@ -681,10 +676,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_join_table_alias assert_equal( 3, - Developer.references(:developers_projects_join).find( - :all, :include => {:projects => :developers}, - :conditions => 'developers_projects_join.joined_on IS NOT NULL' - ).size + Developer.references(:developers_projects_join).scoped( + :includes => {:projects => :developers}, + :where => 'developers_projects_join.joined_on IS NOT NULL' + ).to_a.size ) end @@ -697,16 +692,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, - Developer.references(:developers_projects_join).find( - :all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', + Developer.references(:developers_projects_join).scoped( + :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",") - ).size + ).to_a.size ) end def test_find_grouped - all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories) - grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories) + all_posts_from_category1 = Post.scoped(:where => "category_id = 1", :joins => :categories).all + grouped_posts_of_category1 = Post.scoped(:where => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories).all assert_equal 5, all_posts_from_category1.size assert_equal 2, grouped_posts_of_category1.size end @@ -780,8 +775,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, project.developers.size assert_equal 1, developer.projects.size - assert_equal developer, project.developers.find(:first) - assert_equal project, developer.projects.find(:first) + assert_equal developer, project.developers.first + assert_equal project, developer.projects.first end def test_self_referential_habtm_without_foreign_key_set_should_raise_exception @@ -798,13 +793,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog') end - def test_counting_on_habtm_association_and_not_array - david = Developer.find(1) - # Extra parameter just to make sure we aren't falling back to - # Array#count in Ruby >=1.8.7, which would raise an ArgumentError - assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') } - end - def test_count david = Developer.find(1) assert_equal 2, david.projects.count @@ -827,7 +815,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Post.expects(:transaction) - Category.find(:first).posts.transaction do + Category.first.posts.transaction do # nothing end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 6a4f972356..13d8c68b33 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -242,27 +242,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 2, Firm.find(:first, :order => "id").clients.count + assert_equal 2, Firm.scoped(:order => "id").first.clients.count end def test_counting - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count - end - - def test_counting_with_empty_hash_conditions - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {}) - end - - def test_counting_with_single_conditions - assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => ['name=?', "Microsoft"]) + assert_equal 2, Firm.scoped(:order => "id").first.plain_clients.count end def test_counting_with_single_hash - assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {:name => "Microsoft"}) + assert_equal 1, Firm.scoped(:order => "id").first.plain_clients.where(:name => "Microsoft").count end def test_counting_with_column_name_and_hash - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:name) + assert_equal 2, Firm.scoped(:order => "id").first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -272,7 +264,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.find(:first, :order => "id").clients.length + assert_equal 2, Firm.scoped(:order => "id").first.clients.length end def test_finding_array_compatibility @@ -281,14 +273,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_with_blank_conditions [[], {}, nil, ""].each do |blank| - assert_equal 2, Firm.find(:first, :order => "id").clients.find(:all, :conditions => blank).size + assert_equal 2, Firm.scoped(:order => "id").first.clients.where(blank).all.size end end def test_find_many_with_merged_options assert_equal 1, companies(:first_firm).limited_clients.size - assert_equal 1, companies(:first_firm).limited_clients.find(:all).size - assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size + assert_equal 1, companies(:first_firm).limited_clients.all.size + assert_equal 2, companies(:first_firm).limited_clients.limit(nil).all.size end def test_find_should_append_to_association_order @@ -301,30 +293,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'") + assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.scoped(:where => "type = 'Client'").first assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end def test_dynamic_find_all_should_respect_association_order - assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'") + assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.scoped(:where => "type = 'Client'").all assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client') end def test_dynamic_find_all_should_respect_association_limit - assert_equal 1, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'").length + assert_equal 1, companies(:first_firm).limited_clients.scoped(:where => "type = 'Client'").all.length assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length end def test_dynamic_find_all_limit_should_override_association_limit - assert_equal 2, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'", :limit => 9_000).length + assert_equal 2, companies(:first_firm).limited_clients.scoped(:where => "type = 'Client'", :limit => 9_000).all.length assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length end - def test_dynamic_find_all_should_respect_readonly_access - companies(:first_firm).readonly_clients.find(:all).each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } - 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 @@ -339,61 +326,55 @@ class HasManyAssociationsTest < ActiveRecord::TestCase authors(:david).readonly_comments.each { |c| assert c.readonly? } end - def test_triple_equality - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert !(Array === Firm.find(:first, :order => "id").clients) - assert Firm.find(:first, :order => "id").clients === Array - end - def test_finding_default_orders - assert_equal "Summit", Firm.find(:first, :order => "id").clients.first.name + assert_equal "Summit", Firm.scoped(:order => "id").first.clients.first.name end def test_finding_with_different_class_name_and_order - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_sorted_desc.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_sorted_desc.first.name end def test_finding_with_foreign_key - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_of_firm.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_of_firm.first.name end def test_finding_with_condition - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_like_ms.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_like_ms.first.name end def test_finding_with_condition_hash - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_like_ms_with_hash_conditions.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_like_ms_with_hash_conditions.first.name end def test_finding_using_primary_key - assert_equal "Summit", Firm.find(:first, :order => "id").clients_using_primary_key.first.name + assert_equal "Summit", Firm.scoped(:order => "id").first.clients_using_primary_key.first.name end def test_finding_using_sql - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first first_client = firm.clients_using_sql.first assert_not_nil first_client assert_equal "Microsoft", first_client.name assert_equal 1, firm.clients_using_sql.size - assert_equal 1, Firm.find(:first, :order => "id").clients_using_sql.size + assert_equal 1, Firm.scoped(:order => "id").first.clients_using_sql.size end def test_finding_using_sql_take_into_account_only_uniq_ids - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client = firm.clients_using_sql.first assert_equal client, firm.clients_using_sql.find(client.id, client.id) assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s) end def test_counting_using_sql - assert_equal 1, Firm.find(:first, :order => "id").clients_using_counter_sql.size - assert Firm.find(:first, :order => "id").clients_using_counter_sql.any? - assert_equal 0, Firm.find(:first, :order => "id").clients_using_zero_counter_sql.size - assert !Firm.find(:first, :order => "id").clients_using_zero_counter_sql.any? + assert_equal 1, Firm.scoped(:order => "id").first.clients_using_counter_sql.size + assert Firm.scoped(:order => "id").first.clients_using_counter_sql.any? + assert_equal 0, Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.size + assert !Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.any? end def test_counting_non_existant_items_using_sql - assert_equal 0, Firm.find(:first, :order => "id").no_clients_using_counter_sql.size + assert_equal 0, Firm.scoped(:order => "id").first.no_clients_using_counter_sql.size end def test_counting_using_finder_sql @@ -408,7 +389,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_ids - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find } @@ -428,7 +409,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_string_ids_when_using_finder_sql - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client = firm.clients_using_finder_sql.find("2") assert_kind_of Client, client @@ -444,9 +425,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all - firm = Firm.find(:first, :order => "id") - assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length - assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length + firm = Firm.scoped(:order => "id").first + assert_equal 2, firm.clients.scoped(:where => "#{QUOTED_TYPE} = 'Client'").all.length + assert_equal 1, firm.clients.scoped(:where => "name = 'Summit'").all.length end def test_find_each @@ -465,7 +446,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_queries(2) do - firm.clients.find_each(:batch_size => 1, :conditions => {:name => "Microsoft"}) do |c| + firm.clients.where(name: 'Microsoft').find_each(batch_size: 1) do |c| assert_equal firm.id, c.firm_id assert_equal "Microsoft", c.name end @@ -490,29 +471,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_all_sanitized # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.find(:first, :order => "id") - summit = firm.clients.find(:all, :conditions => "name = 'Summit'") - assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"]) - assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }]) + firm = Firm.scoped(:order => "id").first + summit = firm.clients.scoped(:where => "name = 'Summit'").all + assert_equal summit, firm.clients.scoped(:where => ["name = ?", "Summit"]).all + assert_equal summit, firm.clients.scoped(:where => ["name = :name", { :name => "Summit" }]).all end def test_find_first - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client2 = Client.find(2) - assert_equal firm.clients.first, firm.clients.find(:first, :order => "id") - assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'", :order => "id") + assert_equal firm.clients.first, firm.clients.scoped(:order => "id").first + assert_equal client2, firm.clients.scoped(:where => "#{QUOTED_TYPE} = 'Client'", :order => "id").first end def test_find_first_sanitized - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client2 = Client.find(2) - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id") - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id") + assert_equal client2, firm.clients.scoped(:where => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id").first + assert_equal client2, firm.clients.scoped(:where => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id").first end def test_find_all_with_include_and_conditions assert_nothing_raised do - Developer.find(:all, :joins => :audit_logs, :conditions => {'audit_logs.message' => nil, :name => 'Smith'}) + Developer.scoped(:joins => :audit_logs, :where => {'audit_logs.message' => nil, :name => 'Smith'}).all end end @@ -522,8 +503,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_grouped - all_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1") - grouped_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count') + all_clients_of_firm1 = Client.scoped(:where => "firm_id = 1").all + grouped_clients_of_firm1 = Client.scoped(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').all assert_equal 2, all_clients_of_firm1.size assert_equal 1, grouped_clients_of_firm1.size end @@ -581,7 +562,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_with_bang_on_has_many_raises_when_record_not_saved assert_raise(ActiveRecord::RecordInvalid) do - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first firm.plain_clients.create! end end @@ -813,7 +794,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_deleting_updates_counter_cache - topic = Topic.first(:order => "id ASC") + topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count topic.replies.delete(topic.replies.first) @@ -905,12 +886,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client_id = firm.clients_of_firm.first.id assert_equal 1, firm.clients_of_firm.size - cleared = firm.clients_of_firm.clear + firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size assert_equal 0, firm.clients_of_firm(true).size assert_equal [], Client.destroyed_client_ids[firm.id] - assert_equal firm.clients_of_firm.object_id, cleared.object_id # Should not be destroyed since the association is not dependent. assert_nothing_raised do @@ -1008,14 +988,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_delete_all_association_with_primary_key_deletes_correct_records - firm = Firm.find(:first) + firm = Firm.first # break the vanilla firm_id foreign key assert_equal 2, firm.clients.count firm.clients.first.update_column(:firm_id, nil) assert_equal 1, firm.clients(true).count assert_equal 1, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first - firm = Firm.find(:first) + firm = Firm.first firm.destroy assert_nil Client.find_by_id(old_record.id) end @@ -1123,7 +1103,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal 2, firm.clients.size firm.destroy - assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty? + assert Client.scoped(:where => "firm_id=#{firm.id}").all.empty? end def test_dependence_for_associations_with_hash_condition @@ -1133,7 +1113,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_dependent_when_deleted_from_association # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first assert_equal 2, firm.clients.size client = firm.clients.first @@ -1161,7 +1141,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.destroy rescue "do nothing" - assert_equal 2, Client.find(:all, :conditions => "firm_id=#{firm.id}").size + assert_equal 2, Client.scoped(:where => "firm_id=#{firm.id}").all.size end def test_dependence_on_account @@ -1224,16 +1204,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_adding_array_and_collection - assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients } - end - - def test_find_all_without_conditions - firm = companies(:first_firm) - assert_equal 2, firm.clients.find(:all).length + assert_nothing_raised { Firm.first.clients + Firm.all.last.clients } end def test_replace_with_less - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first firm.clients = [companies(:first_client)] assert firm.save, "Could not save firm" firm.reload @@ -1247,7 +1222,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_new - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first firm.clients = [companies(:second_client), Client.new("name" => "New Client")] firm.save firm.reload @@ -1347,27 +1322,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order_for_through - assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'") + assert_equal Comment.find(10), authors(:david).comments_desc.scoped(:where => "comments.type = 'SpecialComment'").first assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment') end def test_dynamic_find_all_should_respect_association_order_for_through - assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find(:all, :conditions => "comments.type = 'SpecialComment'") + assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.scoped(:where => "comments.type = 'SpecialComment'").all assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment') end def test_dynamic_find_all_should_respect_association_limit_for_through - assert_equal 1, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'").length + assert_equal 1, authors(:david).limited_comments.scoped(:where => "comments.type = 'SpecialComment'").all.length assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length end def test_dynamic_find_all_order_should_override_association_limit_for_through - assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length + assert_equal 4, authors(:david).limited_comments.scoped(:where => "comments.type = 'SpecialComment'", :limit => 9_000).all.length assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length end def test_find_all_include_over_the_same_table_for_through - assert_equal 2, people(:michael).posts.find(:all, :include => :people).length + assert_equal 2, people(:michael).posts.scoped(:includes => :people).all.length end def test_has_many_through_respects_hash_conditions @@ -1377,7 +1352,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target client = firm.clients.first @@ -1427,7 +1402,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target assert firm.clients.loaded? assert_no_queries do @@ -1499,17 +1474,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_first_or_last_with_find_options_on_loaded_association_should_fetch_with_query - firm = companies(:first_firm) - firm.clients.class # force load target - - assert_queries 2 do - assert firm.clients.loaded? - firm.clients.first(:order => 'name') - firm.clients.last(:order => 'name') - end - end - def test_calling_first_or_last_with_integer_on_association_should_load_association firm = companies(:first_firm) @@ -1567,11 +1531,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = Namespaced::Firm.create({ :name => 'Some Company' }) firm.clients.create({ :name => 'Some Client' }) - stats = Namespaced::Firm.find(firm.id, { + stats = Namespaced::Firm.scoped( :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", :joins => :clients, :group => "#{Namespaced::Firm.table_name}.id" - }) + ).find firm.id assert_equal 1, stats.num_clients.to_i ensure @@ -1580,7 +1544,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Comment.expects(:transaction) - Post.find(:first).comments.transaction do + Post.first.comments.transaction do # nothing end end @@ -1597,7 +1561,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_creating_using_primary_key - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client = firm.clients_using_primary_key.create!(:name => 'test') assert_equal firm.name, client.firm_name end @@ -1732,4 +1696,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.dependent_restrict_raises = option_before end + + def test_collection_association_with_private_kernel_method + firm = companies(:first_firm) + assert_equal [accounts(:signals37)], firm.accounts.open + end + + test "first_or_initialize adds the record to the association" do + firm = Firm.create! name: 'omg' + client = firm.clients_of_firm.first_or_initialize + assert_equal [client], firm.clients_of_firm + end + + test "first_or_create adds the record to the association" do + firm = Firm.create! name: 'omg' + firm.clients_of_firm.load_target + client = firm.clients_of_firm.first_or_create name: 'lol' + assert_equal [client], firm.clients_of_firm + assert_equal [client], firm.reload.clients_of_firm + end end 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 e9b930204f..1c06007d86 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -533,7 +533,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_count_with_include_should_alias_join_table - assert_equal 2, people(:michael).posts.count(:include => :readers) + assert_equal 2, people(:michael).posts.includes(:readers).count end def test_inner_join_with_quoted_table_name @@ -568,7 +568,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Tag.expects(:transaction) - Post.find(:first).tags.transaction do + Post.first.tags.transaction do # nothing end end @@ -651,7 +651,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter company = companies(:rails_core) - dev = Developer.find(:first) + dev = Developer.first company.developer_ids = [dev.id] assert_equal [dev], company.developers @@ -671,7 +671,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) - ids = [Developer.find(:first).id, -9999] + ids = [Developer.first.id, -9999] assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 9c05b36426..88ec65706c 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -25,13 +25,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_queries(1) { assert_nil firm.account } assert_queries(0) { assert_nil firm.account } - firms = Firm.find(:all, :include => :account) + firms = Firm.scoped(:includes => :account).all assert_queries(0) { firms.each(&:account) } end def test_with_select assert_equal Firm.find(1).account_with_select.attributes.size, 2 - assert_equal Firm.find(1, :include => :account_with_select).account_with_select.attributes.size, 2 + assert_equal Firm.scoped(:includes => :account_with_select).find(1).account_with_select.attributes.size, 2 end def test_finding_using_primary_key @@ -294,13 +294,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_dependence_with_missing_association_and_nullify Account.destroy_all - firm = DependentFirm.find(:first) + firm = DependentFirm.first assert_nil firm.account firm.destroy end def test_finding_with_interpolated_condition - firm = Firm.find(:first) + firm = Firm.first superior = firm.clients.create(:name => 'SuperiorCo') superior.rating = 10 superior.save @@ -346,14 +346,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Firm.find(@firm.id).save! - Firm.find(@firm.id, :include => :account).save! + Firm.scoped(:includes => :account).find(@firm.id).save! end @firm.account.destroy assert_nothing_raised do Firm.find(@firm.id).save! - Firm.find(@firm.id, :include => :account).save! + Firm.scoped(:includes => :account).find(@firm.id).save! 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 2503349c08..94b9639e57 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -73,7 +73,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_eager_loading members = assert_queries(3) do #base table, through table, clubs table - Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"]) + Member.scoped(:includes => :club, :where => ["name = ?", "Groucho Marx"]).all end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].club} @@ -81,7 +81,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_eager_loading_through_polymorphic members = assert_queries(3) do #base table, through table, clubs table - Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"]) + Member.scoped(:includes => :sponsor_club, :where => ["name = ?", "Groucho Marx"]).all end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].sponsor_club} @@ -89,14 +89,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_with_conditions_eager_loading # conditions on the through table - assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :favourite_club).favourite_club + assert_equal clubs(:moustache_club), Member.scoped(:includes => :favourite_club).find(@member.id).favourite_club memberships(:membership_of_favourite_club).update_column(:favourite, false) - assert_equal nil, Member.find(@member.id, :include => :favourite_club).reload.favourite_club + assert_equal nil, Member.scoped(:includes => :favourite_club).find(@member.id).reload.favourite_club # conditions on the source table - assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :hairy_club).hairy_club + assert_equal clubs(:moustache_club), Member.scoped(:includes => :hairy_club).find(@member.id).hairy_club clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons") - assert_equal nil, Member.find(@member.id, :include => :hairy_club).reload.hairy_club + assert_equal nil, Member.scoped(:includes => :hairy_club).find(@member.id).reload.hairy_club end def test_has_one_through_polymorphic_with_source_type @@ -104,14 +104,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_eager_has_one_through_polymorphic_with_source_type - clubs = Club.find(:all, :include => :sponsored_member, :conditions => ["name = ?","Moustache and Eyebrow Fancier Club"]) + clubs = Club.scoped(:includes => :sponsored_member, :where => ["name = ?","Moustache and Eyebrow Fancier Club"]).all # Only the eyebrow fanciers club has a sponsored_member assert_not_nil assert_no_queries {clubs[0].sponsored_member} end def test_has_one_through_nonpreload_eagerloading members = assert_queries(1) do - Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback + Member.scoped(:includes => :club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').all #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].club} @@ -119,7 +119,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic members = assert_queries(1) do - Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback + Member.scoped(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').all #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].sponsor_club} @@ -128,7 +128,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save! members = assert_queries(1) do - Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback + Member.scoped(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC').all #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } @@ -197,7 +197,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase @member.member_detail = @member_detail @member.organization = @organization @member_details = assert_queries(3) do - MemberDetail.find(:all, :include => :member_type) + MemberDetail.scoped(:includes => :member_type).all end @new_detail = @member_details[0] assert @new_detail.send(:association, :member_type).loaded? @@ -210,14 +210,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Club.find(@club.id).save! - Club.find(@club.id, :include => :sponsored_member).save! + Club.scoped(:includes => :sponsored_member).find(@club.id).save! end @club.sponsor.destroy assert_nothing_raised do Club.find(@club.id).save! - Club.find(@club.id, :include => :sponsored_member).save! + Club.scoped(:includes => :sponsored_member).find(@club.id).save! end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 68a1e62328..1d61d5c474 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -72,17 +72,17 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_count_honors_implicit_inner_joins real_count = Author.scoped.to_a.sum{|a| a.posts.count } - assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records" + assert_equal real_count, Author.joins(:posts).count, "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins real_count = Author.scoped.to_a.sum{|a| a.posts.count } - assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records" + assert_equal real_count, Author.joins(:posts).calculate(:count, 'authors.id'), "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions real_count = Author.scoped.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length - authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'") + authors_with_welcoming_post_titles = Author.scoped(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 76282213d8..f35ffb2994 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -96,7 +96,7 @@ class InverseHasOneTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face) + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :face).first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -104,7 +104,7 @@ class InverseHasOneTests < ActiveRecord::TestCase f.man.name = 'Mungo' assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id') + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :face, :order => 'faces.id').first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -114,7 +114,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_built_child - m = Man.find(:first) + m = Man.first f = m.build_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -125,7 +125,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_child - m = Man.find(:first) + m = Man.first f = m.create_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -136,7 +136,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method - m = Man.find(:first) + m = Man.first f = m.create_face!(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -147,7 +147,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_replaced_via_accessor_child - m = Man.find(:first) + m = Man.first f = Face.new(:description => 'haunted') m.face = f assert_not_nil f.man @@ -159,7 +159,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.dirty_face } end end @@ -179,7 +179,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests) + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :interests).first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -189,7 +189,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" end - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id') + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :interests, :order => 'interests.id').first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -201,7 +201,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_block_style_built_child - m = Man.find(:first) + m = Man.first i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'} assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated" assert_not_nil i.man @@ -213,7 +213,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child - m = Man.find(:first) + m = Man.first i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment') assert_not_nil i.man assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -224,7 +224,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_block_style_created_child - m = Man.find(:first) + m = Man.first i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'} assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated" assert_not_nil i.man @@ -248,7 +248,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_replaced_via_accessor_children - m = Man.find(:first) + m = Man.first i = Interest.new(:topic => 'Industrial Revolution Re-enactment') m.interests = [i] assert_not_nil i.man @@ -260,7 +260,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end end @@ -278,7 +278,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.find(:first, :include => :man, :conditions => {:description => 'trusting'}) + f = Face.scoped(:includes => :man, :where => {:description => 'trusting'}).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -286,7 +286,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase m.face.description = 'pleasing' assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'}) + f = Face.scoped(:includes => :man, :order => 'men.id', :where => {:description => 'trusting'}).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -331,7 +331,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_child_instance_should_be_shared_with_replaced_via_accessor_parent - f = Face.find(:first) + f = Face.first m = Man.new(:name => 'Charles') f.man = m assert_not_nil m.face @@ -343,7 +343,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_man } end end @@ -351,7 +351,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase fixtures :men, :faces, :interests def test_child_instance_should_be_shared_with_parent_on_find - f = Face.find(:first, :conditions => {:description => 'confused'}) + f = Face.scoped(:where => {:description => 'confused'}).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -361,7 +361,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man) + f = Face.scoped(:where => {:description => 'confused'}, :includes => :man).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -369,7 +369,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase m.polymorphic_face.description = 'pleasing' assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id') + f = Face.scoped(:where => {:description => 'confused'}, :includes => :man, :order => 'men.id').first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -421,19 +421,19 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man } + assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error # fails because no class has the correct inverse_of for horrible_polymorphic_man - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man = Man.first } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error # passes because Man does have the correct inverse_of - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first } + assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first } # fails because Interest does have the correct inverse_of - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end end @@ -444,7 +444,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do - i = Interest.find(:first) + i = Interest.first i.zine i.man end @@ -452,7 +452,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do - i = Interest.find(:first) + i = Interest.first i.build_zine(:title => 'Get Some in Winter! 2008') i.build_man(:name => 'Gordon') i.save! diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index d5d1d3405d..20b0eeb5ea 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -46,12 +46,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert !authors(:mary).unique_categorized_posts.loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) } - assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, :conditions => "title is NULL") } + assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) } assert !authors(:mary).unique_categorized_posts.loaded? end def test_has_many_uniq_through_find - assert_equal 1, authors(:mary).unique_categorized_posts.find(:all).size + assert_equal 1, authors(:mary).unique_categorized_posts.all.size end def test_has_many_uniq_through_dynamic_find @@ -71,7 +71,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_going_through_join_model_with_find - assert_equal tags(:general), tag = posts(:welcome).tags.find(:first) + assert_equal tags(:general), tag = posts(:welcome).tags.first assert_no_queries do tag.tagging end @@ -85,7 +85,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find - assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first) + assert_equal tags(:general), tag = posts(:welcome).funky_tags.first assert_no_queries do tag.tagging end @@ -237,8 +237,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_authors = Post.scoped(:includes => :authors, :order => 'posts.id').all assert_equal posts.length, posts_with_authors.length posts.length.times do |i| assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length } @@ -246,7 +246,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_one - post = Post.find_by_id(posts(:welcome).id, :include => :tagging) + post = Post.includes(:tagging).find posts(:welcome).id tagging = taggings(:welcome_general) assert_no_queries do assert_equal tagging, post.tagging @@ -254,7 +254,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_one_defined_in_abstract_parent - item = Item.find_by_id(items(:dvd).id, :include => :tagging) + item = Item.includes(:tagging).find items(:dvd).id tagging = taggings(:godfather) assert_no_queries do assert_equal tagging, item.tagging @@ -262,8 +262,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_tags = Post.scoped(:includes => :tags, :order => 'posts.id').all assert_equal posts.length, posts_with_tags.length posts.length.times do |i| assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } @@ -271,8 +271,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many - posts = Post.find(:all, :order => 'posts.id') - posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_taggings = Post.scoped(:includes => :taggings, :order => 'posts.id').all assert_equal posts.length, posts_with_taggings.length posts.length.times do |i| assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } @@ -280,20 +280,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_find_all - assert_equal [categories(:general)], authors(:david).categories.find(:all) + assert_equal [categories(:general)], authors(:david).categories.all end def test_has_many_find_first - assert_equal categories(:general), authors(:david).categories.find(:first) + assert_equal categories(:general), authors(:david).categories.first end def test_has_many_with_hash_conditions - assert_equal categories(:general), authors(:david).categories_like_general.find(:first) + assert_equal categories(:general), authors(:david).categories_like_general.first end def test_has_many_find_conditions - assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'") - assert_nil authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'") + assert_equal categories(:general), authors(:david).categories.scoped(:where => "categories.name = 'General'").first + assert_nil authors(:david).categories.scoped(:where => "categories.name = 'Technology'").first end def test_has_many_class_methods_called_by_method_missing @@ -364,7 +364,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_has_many_polymorphic_with_source_type - tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts) + tag_with_include = Tag.scoped(:includes => :tagged_posts).find(tags(:general).id) desired = posts(:welcome, :thinking) assert_no_queries do # added sort by ID as otherwise test using JRuby was failing as array elements were in different order @@ -374,20 +374,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_has_many_find_all - assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first + assert_equal comments(:greetings), authors(:david).comments.scoped(:order => 'comments.id').all.first end def test_has_many_through_has_many_find_all_with_custom_class - assert_equal comments(:greetings), authors(:david).funky_comments.find(:all, :order => 'comments.id').first + assert_equal comments(:greetings), authors(:david).funky_comments.scoped(:order => 'comments.id').all.first end def test_has_many_through_has_many_find_first - assert_equal comments(:greetings), authors(:david).comments.find(:first, :order => 'comments.id') + assert_equal comments(:greetings), authors(:david).comments.scoped(:order => 'comments.id').first end def test_has_many_through_has_many_find_conditions - options = { :conditions => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } - assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, options) + options = { :where => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } + assert_equal comments(:does_it_hurt), authors(:david).comments.scoped(options).first end def test_has_many_through_has_many_find_by_id @@ -403,7 +403,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through_polymorphic_has_many - author = Author.find_by_id(authors(:david).id, :include => :taggings) + author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } @@ -411,7 +411,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many - author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' + author = Author.scoped(:where => ['name = ?', 'David'], :includes => :comments, :order => 'comments.id').first SpecialComment.new; VerySpecialComment.new assert_no_queries do assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) @@ -419,7 +419,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many_with_conditions - post = Post.find(:first, :include => :invalid_tags) + post = Post.scoped(:includes => :invalid_tags).first assert_no_queries do post.invalid_tags end @@ -427,8 +427,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_eager_belongs_to_and_has_one_not_singularized assert_nothing_raised do - Author.find(:first, :include => :author_address) - AuthorAddress.find(:first, :include => :author) + Author.scoped(:includes => :author_address).first + AuthorAddress.scoped(:includes => :author).first end end @@ -444,7 +444,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_uses_conditions_specified_on_the_has_many_association - author = Author.find(:first) + author = Author.first assert_present author.comments assert_blank author.nonexistant_comments end @@ -614,7 +614,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many expected = taggings(:welcome_general) - p = Post.find(posts(:welcome).id, :include => :taggings) + p = Post.scoped(:includes => :taggings).find(posts(:welcome).id) assert_no_queries {assert p.taggings.include?(expected)} assert posts(:welcome).taggings.include?(taggings(:welcome_general)) end @@ -622,18 +622,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_one expected = posts(:welcome) - tagging = Tagging.find(taggings(:welcome_general).id, :include => :taggable) + tagging = Tagging.scoped(:includes => :taggable).find(taggings(:welcome_general).id) assert_no_queries { assert_equal expected, tagging.taggable} end def test_polymorphic_belongs_to - p = Post.find(posts(:welcome).id, :include => {:taggings => :taggable}) + p = Post.scoped(:includes => {:taggings => :taggable}).find(posts(:welcome).id) assert_no_queries {assert_equal posts(:welcome), p.taggings.first.taggable} end def test_preload_polymorphic_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_tags = Post.scoped(:includes => :tags, :order => 'posts.id').all assert_equal posts.length, posts_with_tags.length posts.length.times do |i| assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } @@ -641,7 +641,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_preload_polymorph_many_types - taggings = Tagging.find :all, :include => :taggable, :conditions => ['taggable_type != ?', 'FakeModel'] + taggings = Tagging.scoped(:includes => :taggable, :where => ['taggable_type != ?', 'FakeModel']).all assert_no_queries do taggings.first.taggable.id taggings[1].taggable.id @@ -654,13 +654,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_preload_nil_polymorphic_belongs_to assert_nothing_raised do - Tagging.find(:all, :include => :taggable, :conditions => ['taggable_type IS NULL']) + Tagging.scoped(:includes => :taggable, :where => ['taggable_type IS NULL']).all end end def test_preload_polymorphic_has_many - posts = Post.find(:all, :order => 'posts.id') - posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_taggings = Post.scoped(:includes => :taggings, :order => 'posts.id').all assert_equal posts.length, posts_with_taggings.length posts.length.times do |i| assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } @@ -668,7 +668,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_shared_parent - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 1') + comments = Comment.scoped(:includes => :post, :where => 'post_id = 1').all assert_no_queries do assert_equal comments.first.post, comments[1].post end @@ -676,7 +676,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_include_uses_array_include_after_loaded david = authors(:david) - david.categories.class # force load target + david.categories.load_target category = david.categories.first diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 017905e0ac..1d0550afaf 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -74,8 +74,8 @@ class AssociationsTest < ActiveRecord::TestCase 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)} + assert_nothing_raised {Account.scoped(:order => 'id', :includes => :firm).first} + assert_nothing_raised {Account.scoped(:order => :id, :includes => :firm).first} end def test_bad_collection_keys @@ -214,6 +214,10 @@ class AssociationProxyTest < ActiveRecord::TestCase david = developers(:david) assert_equal david.association(:projects), david.projects.proxy_association end + + def test_scoped_allows_conditions + assert developers(:david).projects.scoped(where: 'foo').where_values.include?('foo') + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ef01476ae4..1093fedea1 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -244,7 +244,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase # DB2 is not case-sensitive return true if current_adapter?(:DB2Adapter) - assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes + assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end def test_hashes_not_mangled @@ -481,23 +481,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_typecast_attribute_from_select_to_false - topic = Topic.create(:title => 'Budget') + Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.find(:first, :select => "topics.*, 0 as is_test") + topic = Topic.scoped(:select => "topics.*, 0 as is_test").first else - topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test") + topic = Topic.scoped(:select => "topics.*, 1=2 as is_test").first end assert !topic.is_test? end def test_typecast_attribute_from_select_to_true - topic = Topic.create(:title => 'Budget') + Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.find(:first, :select => "topics.*, 1 as is_test") + topic = Topic.scoped(:select => "topics.*, 1 as is_test").first else - topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test") + topic = Topic.scoped(:select => "topics.*, 2=2 as is_test").first end assert topic.is_test? end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index d555c1699e..8ef3bfef15 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -87,7 +87,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_save_fails_for_invalid_has_one - firm = Firm.find(:first) + firm = Firm.first assert firm.valid? firm.build_account @@ -99,7 +99,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_save_succeeds_for_invalid_has_one_with_validate_false - firm = Firm.find(:first) + firm = Firm.first assert firm.valid? firm.build_unvalidated_account @@ -155,20 +155,20 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_not_resaved_when_unchanged - firm = Firm.find(:first, :include => :account) + firm = Firm.scoped(:includes => :account).first firm.name += '-changed' assert_queries(1) { firm.save! } - firm = Firm.find(:first) - firm.account = Account.find(:first) + firm = Firm.first + firm.account = Account.first assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } - firm = Firm.find(:first).dup - firm.account = Account.find(:first) + firm = Firm.first.dup + firm.account = Account.first assert_queries(2) { firm.save! } - firm = Firm.find(:first).dup - firm.account = Account.find(:first).dup + firm = Firm.first.dup + firm.account = Account.first.dup assert_queries(2) { firm.save! } end @@ -228,7 +228,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_assignment_before_parent_saved - client = Client.find(:first) + client = Client.first apple = Firm.new("name" => "Apple") client.firm = apple assert_equal apple, client.firm @@ -342,7 +342,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_build_and_then_save_parent_should_not_reload_target - client = Client.find(:first) + client = Client.first apple = client.build_firm(:name => "Apple") client.save! assert_no_queries { assert_equal apple, client.firm } @@ -384,7 +384,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa end def test_invalid_adding_with_validate_false - firm = Firm.find(:first) + firm = Firm.first client = Client.new firm.unvalidated_clients_of_firm << client @@ -397,7 +397,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_valid_adding_with_validate_false no_of_clients = Client.count - firm = Firm.find(:first) + firm = Firm.first client = Client.new("name" => "Apple") assert firm.valid? @@ -872,7 +872,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } before = @pirate.parrots.map { |c| c.mark_for_destruction ; c } - class << @pirate.parrots + class << @pirate.association(:parrots) def destroy(*args) super raise 'Oh noes!' @@ -1277,7 +1277,7 @@ module AutosaveAssociationOnACollectionAssociationTests def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } - @pirate.send(@association_name).class # hack to load the target + @pirate.send(@association_name).load_target assert_queries(3) do @pirate.catchphrase = 'Yarr' diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index eadc7e1701..c1b0cb8886 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -125,20 +125,13 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter) def test_limit_with_comma - assert_nothing_raised do - Topic.limit("1,2").all - end + assert Topic.limit("1,2").all end end def test_limit_without_comma - assert_nothing_raised do - assert_equal 1, Topic.limit("1").all.length - end - - assert_nothing_raised do - assert_equal 1, Topic.limit(1).all.length - end + assert_equal 1, Topic.limit("1").all.length + assert_equal 1, Topic.limit(1).all.length end def test_invalid_limit @@ -175,6 +168,24 @@ class BasicsTest < ActiveRecord::TestCase assert Topic.table_exists? end + def test_finder_block + t = Topic.first + found = nil + Topic.find_by_id(t.id) { |f| found = f } + assert_equal t, found + end + + def test_finder_block_nothing_found + bad_id = Topic.maximum(:id) + 1 + assert_nil Topic.find_by_id(bad_id) { |f| raise } + end + + def test_find_returns_block_value + t = Topic.first + x = Topic.find_by_id(t.id) { |f| "hi mom!" } + assert_equal "hi mom!", x + end + def test_preserving_date_objects if current_adapter?(:SybaseAdapter) # Sybase ctlib does not (yet?) support the date type; use datetime instead. @@ -192,7 +203,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_previously_changed - topic = Topic.find :first + topic = Topic.first topic.title = '<3<3<3' assert_equal({}, topic.previous_changes) @@ -202,7 +213,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_previously_changed_dup - topic = Topic.find :first + topic = Topic.first topic.title = '<3<3<3' topic.save! @@ -228,7 +239,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLiteAdapter) + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter) assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec @@ -350,13 +361,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_load - topics = Topic.find(:all, :order => 'id') + topics = Topic.scoped(:order => 'id').all assert_equal(4, topics.size) assert_equal(topics(:first).title, topics.first.title) end def test_load_with_condition - topics = Topic.find(:all, :conditions => "author_name = 'Mary'") + topics = Topic.scoped(:where => "author_name = 'Mary'").all assert_equal(1, topics.size) assert_equal(topics(:second).title, topics.first.title) @@ -478,7 +489,7 @@ class BasicsTest < ActiveRecord::TestCase 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') + assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!') end end @@ -1264,10 +1275,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_quoting_arrays - replies = Reply.find(:all, :conditions => [ "id IN (?)", topics(:first).replies.collect(&:id) ]) + replies = Reply.scoped(:where => [ "id IN (?)", topics(:first).replies.collect(&:id) ]).all assert_equal topics(:first).replies.size, replies.size - replies = Reply.find(:all, :conditions => [ "id IN (?)", [] ]) + replies = Reply.scoped(:where => [ "id IN (?)", [] ]).all assert_equal 0, replies.size end @@ -1568,22 +1579,19 @@ class BasicsTest < ActiveRecord::TestCase def test_count_with_join res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'" - res2 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + res2 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count assert_equal res, res2 res3 = nil assert_nothing_raised do - res3 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", - :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + res3 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count end assert_equal res, res3 res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res5 = nil assert_nothing_raised do - res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", - :joins => "p, comments co", - :select => "p.id") + res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count end assert_equal res4, res5 @@ -1591,67 +1599,64 @@ class BasicsTest < ActiveRecord::TestCase res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", - :joins => "p, comments co", - :select => "p.id", - :distinct => true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) end assert_equal res6, res7 end def test_no_limit_offset assert_nothing_raised do - Developer.find(:all, :offset => 2) + Developer.scoped(:offset => 2).all end end def test_find_last - last = Developer.find :last - assert_equal last, Developer.find(:first, :order => 'id desc') + last = Developer.last + assert_equal last, Developer.scoped(:order => 'id desc').first end def test_last - assert_equal Developer.find(:first, :order => 'id desc'), Developer.last + assert_equal Developer.scoped(:order => 'id desc').first, Developer.last end def test_all developers = Developer.all assert_kind_of Array, developers - assert_equal Developer.find(:all), developers + assert_equal Developer.all, developers end def test_all_with_conditions - assert_equal Developer.find(:all, :order => 'id desc'), Developer.order('id desc').all + assert_equal Developer.scoped(:order => 'id desc').all, Developer.order('id desc').all end def test_find_ordered_last - last = Developer.find :last, :order => 'developers.salary ASC' - assert_equal last, Developer.find(:all, :order => 'developers.salary ASC').last + last = Developer.scoped(:order => 'developers.salary ASC').last + assert_equal last, Developer.scoped(:order => 'developers.salary ASC').all.last end def test_find_reverse_ordered_last - last = Developer.find :last, :order => 'developers.salary DESC' - assert_equal last, Developer.find(:all, :order => 'developers.salary DESC').last + last = Developer.scoped(:order => 'developers.salary DESC').last + assert_equal last, Developer.scoped(:order => 'developers.salary DESC').all.last end def test_find_multiple_ordered_last - last = Developer.find :last, :order => 'developers.name, developers.salary DESC' - assert_equal last, Developer.find(:all, :order => 'developers.name, developers.salary DESC').last + last = Developer.scoped(:order => 'developers.name, developers.salary DESC').last + assert_equal last, Developer.scoped(:order => 'developers.name, developers.salary DESC').all.last end def test_find_keeps_multiple_order_values - combined = Developer.find(:all, :order => 'developers.name, developers.salary') - assert_equal combined, Developer.find(:all, :order => ['developers.name', 'developers.salary']) + combined = Developer.scoped(:order => 'developers.name, developers.salary').all + assert_equal combined, Developer.scoped(:order => ['developers.name', 'developers.salary']).all end def test_find_keeps_multiple_group_values - combined = Developer.find(:all, :group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at') - assert_equal combined, Developer.find(:all, :group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']) + combined = Developer.scoped(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').all + assert_equal combined, Developer.scoped(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).all end def test_find_symbol_ordered_last - last = Developer.find :last, :order => :salary - assert_equal last, Developer.find(:all, :order => :salary).last + last = Developer.scoped(:order => :salary).last + assert_equal last, Developer.scoped(:order => :salary).all.last end def test_abstract_class @@ -1726,7 +1731,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_to_param_should_return_string - assert_kind_of String, Client.find(:first).to_param + assert_kind_of String, Client.first.to_param end def test_inspect_class @@ -1745,8 +1750,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_inspect_limited_select_instance - assert_equal %(#<Topic id: 1>), Topic.find(:first, :select => 'id', :conditions => 'id = 1').inspect - assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.find(:first, :select => 'id, title', :conditions => 'id = 1').inspect + assert_equal %(#<Topic id: 1>), Topic.scoped(:select => 'id', :where => 'id = 1').first.inspect + assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.scoped(:select => 'id, title', :where => 'id = 1').first.inspect end def test_inspect_class_without_table @@ -1904,7 +1909,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], + assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end @@ -1996,6 +2001,12 @@ class BasicsTest < ActiveRecord::TestCase assert_nil hash['firm_name'] end + def test_default_values_are_deeply_dupped + company = Company.new + company.description << "foo" + assert_equal "", Company.new.description + end + ["find_by", "find_by!"].each do |meth| test "#{meth} delegates to scoped" do record = stub diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 660098b9ad..cdd4b49042 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -27,30 +27,18 @@ class EachTest < ActiveRecord::TestCase def test_each_should_raise_if_select_is_set_without_id assert_raise(RuntimeError) do - Post.find_each(:select => :title, :batch_size => 1) { |post| post } + Post.select(:title).find_each(:batch_size => 1) { |post| post } end end def test_each_should_execute_if_id_is_in_select assert_queries(6) do - Post.find_each(:select => "id, title, type", :batch_size => 2) do |post| + Post.select("id, title, type").find_each(:batch_size => 2) do |post| assert_kind_of Post, post end end end - def test_each_should_raise_if_the_order_is_set - assert_raise(RuntimeError) do - Post.find_each(:order => "title") { |post| post } - end - end - - def test_each_should_raise_if_the_limit_is_set - assert_raise(RuntimeError) do - Post.find_each(:limit => 1) { |post| post } - end - end - def test_warn_if_limit_scope_is_set ActiveRecord::Base.logger.expects(:warn) Post.limit(1).find_each { |post| post } diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index af1e689eff..e096585f62 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -57,12 +57,12 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_field - c = Account.sum(:credit_limit, :group => :firm_id) + c = Account.group(:firm_id).sum(:credit_limit) [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } end def test_should_group_by_multiple_fields - c = Account.count(:all, :group => ['firm_id', :credit_limit]) + c = Account.group('firm_id', :credit_limit).count(:all) [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) } end @@ -75,32 +75,32 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field - c = Account.sum(:credit_limit, :group => :firm_id) + c = Account.group(:firm_id).sum(:credit_limit) assert_equal 50, c[1] assert_equal 105, c[6] assert_equal 60, c[2] end def test_should_order_by_grouped_field - c = Account.sum(:credit_limit, :group => :firm_id, :order => "firm_id") + c = Account.scoped(:group => :firm_id, :order => "firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact end def test_should_order_by_calculation - c = Account.sum(:credit_limit, :group => :firm_id, :order => "sum_credit_limit desc, firm_id") + c = Account.scoped(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit) assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] } assert_equal [6, 2, 9, 1], c.keys.compact end def test_should_limit_calculation - c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2) + c = Account.scoped(:where => "firm_id IS NOT NULL", + :group => :firm_id, :order => "firm_id", :limit => 2).sum(:credit_limit) assert_equal [1, 2], c.keys.compact end def test_should_limit_calculation_with_offset - c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1) + c = Account.scoped(:where => "firm_id IS NOT NULL", :group => :firm_id, + :order => "firm_id", :limit => 2, :offset => 1).sum(:credit_limit) assert_equal [2, 6], c.keys.compact end @@ -150,16 +150,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition - c = Account.sum(:credit_limit, :group => :firm_id, - :having => 'sum(credit_limit) > 50') - assert_nil c[1] - assert_equal 105, c[6] - assert_equal 60, c[2] - end - - def test_should_group_by_summed_field_having_sanitized_condition - c = Account.sum(:credit_limit, :group => :firm_id, - :having => ['sum(credit_limit) > ?', 50]) + c = Account.scoped(:group => :firm_id, + :having => 'sum(credit_limit) > 50').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_equal 60, c[2] @@ -173,19 +165,19 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_association - c = Account.sum(:credit_limit, :group => :firm) + c = Account.group(:firm).sum(:credit_limit) assert_equal 50, c[companies(:first_firm)] assert_equal 105, c[companies(:rails_core)] assert_equal 60, c[companies(:first_client)] end def test_should_sum_field_with_conditions - assert_equal 105, Account.sum(:credit_limit, :conditions => 'firm_id = 6') + assert_equal 105, Account.where('firm_id = 6').sum(:credit_limit) end def test_should_return_zero_if_sum_conditions_return_nothing - assert_equal 0, Account.sum(:credit_limit, :conditions => '1 = 2') - assert_equal 0, companies(:rails_core).companies.sum(:id, :conditions => '1 = 2') + assert_equal 0, Account.where('1 = 2').sum(:credit_limit) + assert_equal 0, companies(:rails_core).companies.where('1 = 2').sum(:id) end def test_sum_should_return_valid_values_for_decimals @@ -194,24 +186,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_with_conditions - c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', - :group => :firm_id) + c = Account.scoped(:where => 'firm_id > 1', + :group => :firm_id).sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_equal 60, c[2] end def test_should_group_by_summed_field_with_conditions_and_having - c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', - :group => :firm_id, - :having => 'sum(credit_limit) > 60') + c = Account.scoped(:where => 'firm_id > 1', + :group => :firm_id, + :having => 'sum(credit_limit) > 60').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_nil c[2] end def test_should_group_by_fields_with_table_alias - c = Account.sum(:credit_limit, :group => 'accounts.firm_id') + c = Account.group('accounts.firm_id').sum(:credit_limit) assert_equal 50, c[1] assert_equal 105, c[6] assert_equal 60, c[2] @@ -223,14 +215,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_calculate_grouped_with_invalid_field - c = Account.count(:all, :group => 'accounts.firm_id') + c = Account.group('accounts.firm_id').count(:all) assert_equal 1, c[1] assert_equal 2, c[6] assert_equal 1, c[2] end def test_should_calculate_grouped_association_with_invalid_field - c = Account.count(:all, :group => :firm) + c = Account.group(:firm).count(:all) assert_equal 1, c[companies(:first_firm)] assert_equal 2, c[companies(:rails_core)] assert_equal 1, c[companies(:first_client)] @@ -249,7 +241,7 @@ class CalculationsTest < ActiveRecord::TestCase column.expects(:type_cast).with("ABC").returns("ABC") Account.expects(:columns).at_least_once.returns([column]) - c = Account.count(:all, :group => :firm) + c = Account.group(:firm).count(:all) first_key = c.keys.first assert_equal Firm, first_key.class assert_equal 1, c[first_key] @@ -257,22 +249,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_calculate_grouped_association_with_foreign_key_option Account.belongs_to :another_firm, :class_name => 'Firm', :foreign_key => 'firm_id' - c = Account.count(:all, :group => :another_firm) + c = Account.group(:another_firm).count(:all) assert_equal 1, c[companies(:first_firm)] assert_equal 2, c[companies(:rails_core)] assert_equal 1, c[companies(:first_client)] end - def test_should_not_modify_options_when_using_includes - options = {:conditions => 'companies.id > 1', :include => :firm} - options_copy = options.dup - - Account.references(:companies).count(:all, options) - assert_equal options_copy, options - end - def test_should_calculate_grouped_by_function - c = Company.count(:all, :group => "UPPER(#{QUOTED_TYPE})") + c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] assert_equal 4, c['CLIENT'] @@ -280,7 +264,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_calculate_grouped_by_function_with_table_alias - c = Company.count(:all, :group => "UPPER(companies.#{QUOTED_TYPE})") + c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] assert_equal 4, c['CLIENT'] @@ -300,25 +284,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_scoped_field_with_conditions - assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7') + assert_equal 8, companies(:rails_core).companies.where('id > 7').sum(:id) end def test_should_group_by_scoped_field - c = companies(:rails_core).companies.sum(:id, :group => :name) + c = companies(:rails_core).companies.group(:name).sum(:id) assert_equal 7, c['Leetsoft'] assert_equal 8, c['Jadedpixel'] end def test_should_group_by_summed_field_through_association_and_having - c = companies(:rails_core).companies.sum(:id, :group => :name, - :having => 'sum(id) > 7') + c = companies(:rails_core).companies.group(:name).having('sum(id) > 7').sum(:id) assert_nil c['Leetsoft'] assert_equal 8, c['Jadedpixel'] end def test_should_count_selected_field_with_include - assert_equal 6, Account.count(:distinct => true, :include => :firm) - assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit) + assert_equal 6, Account.includes(:firm).count(:distinct => true) + assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) end def test_should_not_perform_joined_include_by_default @@ -342,11 +325,11 @@ class CalculationsTest < ActiveRecord::TestCase Account.last.update_column('credit_limit', 49) Account.first.update_column('credit_limit', 51) - assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50']) + assert_equal 1, Account.scoped(:select => "credit_limit").where('credit_limit >= 50').count end def test_should_count_manual_select_with_include - assert_equal 6, Account.count(:select => "DISTINCT accounts.id", :include => :firm) + assert_equal 6, Account.scoped(:select => "DISTINCT accounts.id", :includes => :firm).count end def test_count_with_column_parameter @@ -354,16 +337,16 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_column_and_options_parameter - assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50 AND firm_id IS NOT NULL") + assert_equal 2, Account.where("credit_limit = 50 AND firm_id IS NOT NULL").count(:firm_id) end def test_should_count_field_in_joined_table - assert_equal 5, Account.count('companies.id', :joins => :firm) - assert_equal 4, Account.count('companies.id', :joins => :firm, :distinct => true) + assert_equal 5, Account.joins(:firm).count('companies.id') + assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) end def test_should_count_field_in_joined_table_with_group_by - c = Account.count('companies.id', :group => 'accounts.firm_id', :joins => :firm) + c = Account.scoped(:group => 'accounts.firm_id', :joins => :firm).count('companies.id') [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } end @@ -386,17 +369,17 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_from_option - assert_equal Company.count(:all), Company.count(:all, :from => 'companies') - assert_equal Account.count(:all, :conditions => "credit_limit = 50"), - Account.count(:all, :from => 'accounts', :conditions => "credit_limit = 50") - assert_equal Company.count(:type, :conditions => {:type => "Firm"}), - Company.count(:type, :conditions => {:type => "Firm"}, :from => 'companies') + assert_equal Company.count(:all), Company.from('companies').count(:all) + assert_equal Account.where("credit_limit = 50").count(:all), + Account.from('accounts').where("credit_limit = 50").count(:all) + assert_equal Company.where(:type => "Firm").count(:type), + Company.where(:type => "Firm").from('companies').count(:type) end def test_sum_with_from_option - assert_equal Account.sum(:credit_limit), Account.sum(:credit_limit, :from => 'accounts') - assert_equal Account.sum(:credit_limit, :conditions => "credit_limit > 50"), - Account.sum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit) + assert_equal Account.where("credit_limit > 50").sum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').sum(:credit_limit) end def test_sum_array_compatibility @@ -404,33 +387,33 @@ class CalculationsTest < ActiveRecord::TestCase end def test_average_with_from_option - assert_equal Account.average(:credit_limit), Account.average(:credit_limit, :from => 'accounts') - assert_equal Account.average(:credit_limit, :conditions => "credit_limit > 50"), - Account.average(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit) + assert_equal Account.where("credit_limit > 50").average(:credit_limit), + Account.where("credit_limit > 50").from('accounts').average(:credit_limit) end def test_minimum_with_from_option - assert_equal Account.minimum(:credit_limit), Account.minimum(:credit_limit, :from => 'accounts') - assert_equal Account.minimum(:credit_limit, :conditions => "credit_limit > 50"), - Account.minimum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.minimum(:credit_limit), Account.from('accounts').minimum(:credit_limit) + assert_equal Account.where("credit_limit > 50").minimum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').minimum(:credit_limit) end def test_maximum_with_from_option - assert_equal Account.maximum(:credit_limit), Account.maximum(:credit_limit, :from => 'accounts') - assert_equal Account.maximum(:credit_limit, :conditions => "credit_limit > 50"), - Account.maximum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.maximum(:credit_limit), Account.from('accounts').maximum(:credit_limit) + assert_equal Account.where("credit_limit > 50").maximum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').maximum(:credit_limit) end def test_from_option_with_specified_index 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') + assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) + assert_equal Edge.where('sink_id < 5').count(:all), + Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all) end end def test_from_option_with_table_different_than_class - assert_equal Account.count(:all), Company.count(:all, :from => 'accounts') + assert_equal Account.count(:all), Company.from('accounts').count(:all) end def test_distinct_is_honored_when_used_with_count_operation_after_group @@ -482,4 +465,8 @@ class CalculationsTest < ActiveRecord::TestCase Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) assert_equal [7], Company.joins(:contracts).pluck(:developer_id) end + + def test_plucks_with_ids + assert_equal Company.all.map(&:id).sort, Company.ids.sort + end end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index ccc57cb876..4111a5f808 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -24,6 +24,58 @@ module ActiveRecord assert !column.type_cast('off') assert !column.type_cast('OFF') end + + def test_type_cast_integer + column = Column.new("field", nil, "integer") + assert_equal 1, column.type_cast(1) + assert_equal 1, column.type_cast('1') + assert_equal 1, column.type_cast('1ignore') + assert_equal 0, column.type_cast('bad1') + assert_equal 0, column.type_cast('bad') + assert_equal 1, column.type_cast(1.7) + assert_nil column.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + column = Column.new("field", nil, "integer") + assert_raises(NoMethodError) do + column.type_cast([]) + end + assert_raises(NoMethodError) do + column.type_cast(true) + end + assert_raises(NoMethodError) do + column.type_cast(false) + end + end + + def test_type_cast_time + column = Column.new("field", nil, "time") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, column.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column| + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") + end + end + + def test_type_cast_date + column = Column.new("field", nil, "date") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, column.type_cast(date_string).strftime("%F") + end end end end diff --git a/activerecord/test/cases/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb new file mode 100644 index 0000000000..59dcb96ebc --- /dev/null +++ b/activerecord/test/cases/connection_adapters/quoting_test.rb @@ -0,0 +1,13 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + module Quoting + class QuotingTest < ActiveRecord::TestCase + def test_quoting_classes + assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object) + end + end + end + end +end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index d63ecdbcc5..42ef51ef3e 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -9,7 +9,7 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql assert_sql(/LOCK IN SHARE MODE/) do - Person.find(1, :lock => 'LOCK IN SHARE MODE') + Person.scoped(:lock => 'LOCK IN SHARE MODE').find(1) end end end diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb deleted file mode 100644 index 2afc91b769..0000000000 --- a/activerecord/test/cases/deprecated_finder_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" -require 'models/entrant' - -class DeprecatedFinderTest < ActiveRecord::TestCase - fixtures :entrants - - def test_deprecated_find_all_was_removed - assert_raise(NoMethodError) { Entrant.find_all } - end - - def test_deprecated_find_first_was_removed - assert_raise(NoMethodError) { Entrant.find_first } - end - - def test_deprecated_find_on_conditions_was_removed - assert_raise(NoMethodError) { Entrant.find_on_conditions } - end - - def test_count - assert_equal(0, Entrant.count(:conditions => "id > 3")) - assert_equal(1, Entrant.count(:conditions => ["id > ?", 2])) - assert_equal(2, Entrant.count(:conditions => ["id > ?", 1])) - end - - def test_count_by_sql - assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) - assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) - assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])) - end -end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 54e0b40b4f..2650040a80 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -288,7 +288,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates Pirate, false do assert_queries(2) { 2.times { pirate.save! } } - Pirate.update_all({ :updated_on => old_updated_on }, :id => pirate.id) + Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on) end with_partial_updates Pirate, true do @@ -306,7 +306,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates Person, false do assert_queries(2) { 2.times { person.save! } } - Person.update_all({ :first_name => 'baz' }, :id => person.id) + Person.where(id: person.id).update_all(:first_name => 'baz') end with_partial_updates Person, true do diff --git a/activerecord/test/cases/dynamic_finder_match_test.rb b/activerecord/test/cases/dynamic_finder_match_test.rb deleted file mode 100644 index db619faa83..0000000000 --- a/activerecord/test/cases/dynamic_finder_match_test.rb +++ /dev/null @@ -1,106 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class DynamicFinderMatchTest < ActiveRecord::TestCase - def test_find_or_create_by - match = DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :create, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_or_initialize_by - match = DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :new, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_no_match - assert_nil DynamicFinderMatch.match("not_a_finder") - end - - def find_by_bang - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location!") - assert_not_nil match - assert match.finder? - assert match.bang? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by_with_symbol - m = DynamicFinderMatch.match(:find_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by_with_symbol - m = DynamicFinderMatch.match(:find_all_by_foo) - assert_equal :all, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by - match = DynamicFinderMatch.match("find_all_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :all, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_last_by - m = DynamicFinderMatch.match(:find_last_by_foo) - assert_equal :last, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_by! - m = DynamicFinderMatch.match(:find_by_foo!) - assert_equal :first, m.finder - assert m.bang?, 'should be banging' - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_or_create - m = DynamicFinderMatch.match(:find_or_create_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :create, m.instantiator - end - - def test_find_or_create! - m = DynamicFinderMatch.match(:find_or_create_by_foo!) - assert_equal :first, m.finder - assert m.bang?, 'should be banging' - assert_equal %w{ foo }, m.attribute_names - assert_equal :create, m.instantiator - end - - def test_find_or_initialize - m = DynamicFinderMatch.match(:find_or_initialize_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :new, m.instantiator - end - - def test_garbage - assert !DynamicFinderMatch.match(:fooo), 'should be false' - assert !DynamicFinderMatch.match(:find_by), 'should be false' - end - end -end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 36ec459ca1..c960773284 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -32,6 +32,7 @@ class FinderTest < ActiveRecord::TestCase assert Topic.exists?(:author_name => "Mary", :approved => true) assert Topic.exists?(["parent_id = ?", 1]) assert !Topic.exists?(45) + assert !Topic.exists?(Topic.new) begin assert !Topic.exists?("foo") @@ -98,14 +99,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.find([1,3,2], :limit => 2).size - assert_equal 1, Entrant.find([1,3,2], :limit => 3, :offset => 2).size + assert_equal 2, Entrant.scoped(:limit => 2).find([1,3,2]).size + assert_equal 1, Entrant.scoped(:limit => 3, :offset => 2).find([1,3,2]).size # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there # will be only 2 results, regardless of the limit. - devs = Developer.find :all - last_devs = Developer.find devs.map(&:id), :limit => 3, :offset => 9 + devs = Developer.all + last_devs = Developer.scoped(:limit => 3, :offset => 9).find devs.map(&:id) assert_equal 2, last_devs.size end @@ -113,58 +114,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal [], Topic.find([]) end - def test_find_by_ids_missing_one - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } - end - - def test_find_all_with_limit - assert_equal(2, Entrant.find(:all, :limit => 2).size) - assert_equal(0, Entrant.find(:all, :limit => 0).size) - end - - def test_find_all_with_prepared_limit_and_offset - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 1) - - assert_equal(2, entrants.size) - assert_equal(entrants(:second).name, entrants.first.name) - - assert_equal 3, Entrant.count - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2) - assert_equal(1, entrants.size) - assert_equal(entrants(:third).name, entrants.first.name) - end - - def test_find_all_with_limit_and_offset_and_multiple_order_clauses - first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 - second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 - third_three_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 - last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 9 - - assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[2,7],[2,9],[2,11]], third_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[3,8],[3,10]], last_posts.map { |p| [p.author_id, p.id] } - end - - - def test_find_with_group - developers = Developer.find(:all, :group => "salary", :select => "salary") - assert_equal 4, developers.size - assert_equal 4, developers.map(&:salary).uniq.size - end - - def test_find_with_group_and_having - developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary") - assert_equal 3, developers.size - assert_equal 3, developers.map(&:salary).uniq.size - assert developers.all? { |developer| developer.salary > 10000 } + def test_find_doesnt_have_implicit_ordering + assert_sql(/^((?!ORDER).)*$/) { Topic.find(1) } end - def test_find_with_group_and_sanitized_having - developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary") - assert_equal 3, developers.size - assert_equal 3, developers.map(&:salary).uniq.size - assert developers.all? { |developer| developer.salary > 10000 } + def test_find_by_ids_missing_one + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } end def test_find_with_group_and_sanitized_having_method @@ -193,14 +148,24 @@ class FinderTest < ActiveRecord::TestCase assert_equal [Account], accounts.collect(&:class).uniq end - def test_find_first - first = Topic.find(:first, :conditions => "title = 'The First Topic'") - assert_equal(topics(:first).title, first.title) + def test_take + assert_equal topics(:first), Topic.take end - def test_find_first_failing - first = Topic.find(:first, :conditions => "title = 'The First Topic!'") - assert_nil(first) + def test_take_failing + assert_nil Topic.where("title = 'This title does not exist'").take + end + + def test_take_bang_present + assert_nothing_raised do + assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").take! + end + end + + def test_take_bang_missing + assert_raises ActiveRecord::RecordNotFound do + Topic.where("title = 'This title does not exist'").take! + end end def test_first @@ -223,6 +188,12 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_have_primary_key_order_by_default + expected = topics(:first) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.first + end + def test_model_class_responds_to_first_bang assert Topic.first! Topic.delete_all @@ -251,7 +222,8 @@ class FinderTest < ActiveRecord::TestCase end end - def test_first_and_last_with_integer_should_use_sql_limit + def test_take_and_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } end @@ -272,7 +244,8 @@ class FinderTest < ActiveRecord::TestCase assert_no_match(/LIMIT/, query.first) end - def test_first_and_last_with_integer_should_return_an_array + def test_take_and_first_and_last_with_integer_should_return_an_array + assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) assert_kind_of Array, Topic.last(5) end @@ -286,7 +259,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_only_some_columns - topic = Topic.find(1, :select => "author_name") + topic = Topic.scoped(:select => "author_name").find(1) assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name @@ -296,36 +269,24 @@ class FinderTest < ActiveRecord::TestCase assert_respond_to topic, "author_name" end - def test_find_on_blank_conditions - [nil, " ", [], {}].each do |blank| - assert_nothing_raised { Topic.find(:first, :conditions => blank) } - end - end - - def test_find_on_blank_bind_conditions - [ [""], ["",{}] ].each do |blank| - assert_nothing_raised { Topic.find(:first, :conditions => blank) } - end - end - def test_find_on_array_conditions - assert Topic.find(1, :conditions => ["approved = ?", false]) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) } + assert Topic.scoped(:where => ["approved = ?", false]).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => ["approved = ?", true]).find(1) } end def test_find_on_hash_conditions - assert Topic.find(1, :conditions => { :approved => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) } + assert Topic.scoped(:where => { :approved => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :approved => true }).find(1) } end def test_find_on_hash_conditions_with_explicit_table_name - assert Topic.find(1, :conditions => { 'topics.approved' => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) } + assert Topic.scoped(:where => { 'topics.approved' => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { 'topics.approved' => true }).find(1) } end def test_find_on_hash_conditions_with_hashed_table_name - assert Topic.find(1, :conditions => {:topics => { :approved => false }}) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) } + assert Topic.scoped(:where => {:topics => { :approved => false }}).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => {:topics => { :approved => true }}).find(1) } end def test_find_with_hash_conditions_on_joined_table @@ -335,16 +296,16 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_hash_conditions_on_joined_table_and_with_range - firms = DependentFirm.all :joins => :account, :conditions => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }} + firms = DependentFirm.scoped :joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }} assert_equal 1, firms.size assert_equal companies(:rails_core), firms.first end def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate david = customers(:david) - assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address }) + assert Customer.scoped(:where => { 'customers.name' => david.name, :address => david.address }).find(david.id) assert_raise(ActiveRecord::RecordNotFound) { - Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address }) + Customer.scoped(:where => { 'customers.name' => david.name + "1", :address => david.address }).find(david.id) } end @@ -353,71 +314,71 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) } + assert_equal [1,2], Topic.scoped(:where => { :id => 1..2 }).all.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :id => 2..3 }).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.find(:all, :conditions => { :id => 1..3 }).map(&:id).sort - assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1...3 }).map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) } + assert_equal [1,2,3], Topic.scoped(:where => { :id => 1..3 }).all.map(&:id).sort + assert_equal [1,2], Topic.scoped(:where => { :id => 1...3 }).all.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :id => 2...3 }).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.find(:all, :conditions => { :id => 1..3, :post_id => 1..2 }).map(&:id).sort - assert_equal [1], Comment.find(:all, :conditions => { :id => 1..1, :post_id => 1..10 }).map(&:id).sort + assert_equal [1,2,3], Comment.scoped(:where => { :id => 1..3, :post_id => 1..2 }).all.map(&:id).sort + assert_equal [1], Comment.scoped(:where => { :id => 1..1, :post_id => 1..10 }).all.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.find(:all, :conditions => {:id => [1..2, 3, 5, 6..8, 9]}).map(&:id).sort + assert_equal [1,2,3,5,6,7,8,9], Comment.scoped(:where => {:id => [1..2, 3, 5, 6..8, 9]}).all.map(&:id).sort end def test_find_on_multiple_hash_conditions - assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } + assert Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } end def test_condition_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on + assert_kind_of Firm, Company.where("name = '%s'", "37signals").first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!"]).first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.scoped(:where => ["id = %d", 1]).first.written_on end def test_condition_array_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on + assert_kind_of Firm, Company.scoped(:where => ["name = '%s'", "37signals"]).first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!"]).first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.scoped(:where => ["id = %d", 1]).first.written_on end def test_condition_hash_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => { :name => "37signals"}) - assert_nil Company.find(:first, :conditions => { :name => "37signals!"}) - assert_kind_of Time, Topic.find(:first, :conditions => {:id => 1}).written_on + assert_kind_of Firm, Company.scoped(:where => { :name => "37signals"}).first + assert_nil Company.scoped(:where => { :name => "37signals!"}).first + assert_kind_of Time, Topic.scoped(:where => {:id => 1}).first.written_on end def test_hash_condition_find_malformed assert_raise(ActiveRecord::StatementInvalid) { - Company.find(:first, :conditions => { :id => 2, :dhh => true }) + Company.scoped(:where => { :id => 2, :dhh => true }).first } end def test_hash_condition_find_with_escaped_characters Company.create("name" => "Ain't noth'n like' \#stuff") - assert Company.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff" }) + assert Company.scoped(:where => { :name => "Ain't noth'n like' \#stuff" }).first end def test_hash_condition_find_with_array - p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2] }, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2.id] }, :order => 'id asc') + p1, p2 = Post.scoped(:limit => 2, :order => 'id asc').all + assert_equal [p1, p2], Post.scoped(:where => { :id => [p1, p2] }, :order => 'id asc').all + assert_equal [p1, p2], Post.scoped(:where => { :id => [p1, p2.id] }, :order => 'id asc').all end def test_hash_condition_find_with_nil - topic = Topic.find(:first, :conditions => { :last_read => nil } ) + topic = Topic.scoped(:where => { :last_read => nil } ).first assert_not_nil topic assert_nil topic.last_read end @@ -425,42 +386,42 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_find_with_aggregate_having_one_mapping balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.find(:first, :conditions => {:balance => balance}) + found_customer = Customer.scoped(:where => {:balance => balance}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location - found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location}) + found_customer = Customer.scoped(:where => {:gps_location => gps_location}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.find(:first, :conditions => {:balance => balance.amount}) + found_customer = Customer.scoped(:where => {:balance => balance.amount}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location - found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location.gps_location}) + found_customer = Customer.scoped(:where => {:gps_location => gps_location.gps_location}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_having_three_mappings address = customers(:david).address assert_kind_of Address, address - found_customer = Customer.find(:first, :conditions => {:address => address}) + found_customer = Customer.scoped(:where => {:address => address}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not address = customers(:david).address assert_kind_of Address, address - found_customer = Customer.find(:first, :conditions => {:address => address, :name => customers(:david).name}) + found_customer = Customer.scoped(:where => {:address => address, :name => customers(:david).name}).first assert_equal customers(:david), found_customer end @@ -468,7 +429,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getutc]) + assert_equal topic, Topic.scoped(:where => ['written_on = ?', topic.written_on.getutc]).first end end end @@ -477,7 +438,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getutc}) + assert_equal topic, Topic.scoped(:where => {:written_on => topic.written_on.getutc}).first end end end @@ -486,7 +447,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :utc do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getlocal]) + assert_equal topic, Topic.scoped(:where => ['written_on = ?', topic.written_on.getlocal]).first end end end @@ -495,32 +456,32 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :utc do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getlocal}) + assert_equal topic, Topic.scoped(:where => {:written_on => topic.written_on.getlocal}).first end end end def test_bind_variables - assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on + assert_kind_of Firm, Company.scoped(:where => ["name = ?", "37signals"]).first + assert_nil Company.scoped(:where => ["name = ?", "37signals!"]).first + assert_nil Company.scoped(:where => ["name = ?", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.scoped(:where => ["id = ?", 1]).first.written_on assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.find(:first, :conditions => ["id=? AND name = ?", 2]) + Company.scoped(:where => ["id=? AND name = ?", 2]).first } assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.find(:first, :conditions => ["id=?", 2, 3, 4]) + Company.scoped(:where => ["id=?", 2, 3, 4]).first } end def test_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.find(:first, :conditions => ["name = ?", "37signals' go'es agains"]) + assert Company.scoped(:where => ["name = ?", "37signals' go'es agains"]).first end def test_named_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.find(:first, :conditions => ["name = :name", {:name => "37signals' go'es agains"}]) + assert Company.scoped(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first end def test_bind_arity @@ -538,10 +499,10 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }]) - assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!" }]) - assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!' OR 1=1" }]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = :id", { :id => 1 }]).written_on + assert_kind_of Firm, Company.scoped(:where => ["name = :name", { :name => "37signals" }]).first + assert_nil Company.scoped(:where => ["name = :name", { :name => "37signals!" }]).first + assert_nil Company.scoped(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first + assert_kind_of Time, Topic.scoped(:where => ["id = :id", { :id => 1 }]).first.written_on end class SimpleEnumerable @@ -612,12 +573,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") end - def test_count - assert_equal(0, Entrant.count(:conditions => "id > 3")) - assert_equal(1, Entrant.count(:conditions => ["id > ?", 2])) - assert_equal(2, Entrant.count(:conditions => ["id > ?", 1])) - end - def test_count_by_sql assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) @@ -640,7 +595,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_one_attribute_with_conditions - assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) + assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50) end def test_find_by_one_attribute_that_is_an_aggregate @@ -680,12 +635,12 @@ class FinderTest < ActiveRecord::TestCase def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching # ensure this test can run independently of order class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' } - a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) - assert_equal a, Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) # find_by_credit_limit has been cached + a = Account.where('firm_id = ?', 6).find_by_credit_limit(50) + assert_equal a, Account.where('firm_id = ?', 6).find_by_credit_limit(50) # find_by_credit_limit has been cached end def test_find_by_one_attribute_with_several_options - assert_equal accounts(:unknown), Account.find_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + assert_equal accounts(:unknown), Account.order('id DESC').where('id != ?', 3).find_by_credit_limit(50) end def test_find_by_one_missing_attribute @@ -719,7 +674,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_last_by_one_attribute_with_several_options - assert_equal accounts(:signals37), Account.find_last_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + assert_equal accounts(:signals37), Account.order('id DESC').where('id != ?', 3).find_last_by_credit_limit(50) end def test_find_last_by_one_missing_attribute @@ -1054,21 +1009,15 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" } end - def test_find_with_invalid_params - assert_raise(ArgumentError) { Topic.find :first, :join => "It should be `joins'" } - assert_raise(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" } - end - def test_dynamic_finder_with_invalid_params assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" } end def test_find_all_with_join - developers_on_project_one = Developer.find( - :all, + developers_on_project_one = Developer.scoped( :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1' - ) + :where => 'project_id=1' + ).all assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map { |d| d.name } assert developer_names.include?('David') @@ -1076,17 +1025,15 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_dont_clobber_id - first = Firm.find( - :first, + first = Firm.scoped( :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', - :conditions => 'companies.id = 1' - ) + :where => 'companies.id = 1' + ).first assert_equal 1, first.id end def test_joins_with_string_array - person_with_reader_and_post = Post.find( - :all, + person_with_reader_and_post = Post.scoped( :joins => [ "INNER JOIN categorizations ON categorizations.post_id = posts.id", "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" @@ -1097,8 +1044,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_id_with_conditions_with_or assert_nothing_raised do - Post.find([1,2,3], - :conditions => "posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'") + Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1,2,3]) end end @@ -1113,13 +1059,13 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_empty_in_condition - assert_equal [], Post.find(:all, :conditions => ['id in (?)', []]) + assert_equal [], Post.where('id in (?)', []) end def test_find_by_records - p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2]], :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2.id]], :order => 'id asc') + p1, p2 = Post.scoped(:limit => 2, :order => 'id asc').all + assert_equal [p1, p2], Post.scoped(:where => ['id in (?)', [p1, p2]], :order => 'id asc') + assert_equal [p1, p2], Post.scoped(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc') end def test_select_value @@ -1146,16 +1092,15 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).size + assert_equal 2, Post.scoped(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).all.size - assert_equal 3, Post.find(:all, :include => { :author => :author_address, :authors => :author_address}, - :order => 'author_addresses_authors.id DESC ', :limit => 3).size + assert_equal 3, Post.scoped(:includes => { :author => :author_address, :authors => :author_address}, + :order => 'author_addresses_authors.id DESC ', :limit => 3).all.size end def test_find_with_nil_inside_set_passed_for_one_attribute - client_of = Company.find( - :all, - :conditions => { + client_of = Company.scoped( + :where => { :client_of => [2, 1, nil], :name => ['37signals', 'Summit', 'Microsoft'] }, :order => 'client_of DESC' @@ -1166,9 +1111,8 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_nil_inside_set_passed_for_attribute - client_of = Company.find( - :all, - :conditions => { :client_of => [nil] }, + client_of = Company.scoped( + :where => { :client_of => [nil] }, :order => 'client_of DESC' ).map { |x| x.client_of } @@ -1176,10 +1120,10 @@ class FinderTest < ActiveRecord::TestCase end def test_with_limiting_with_custom_select - posts = Post.references(:authors).find( - :all, :include => :author, :select => ' posts.*, authors.id as "author_id"', + posts = Post.references(:authors).scoped( + :includes => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id' - ) + ).all assert_equal 3, posts.size assert_equal [0, 1, 1], posts.map(&:author_id).sort end @@ -1194,7 +1138,7 @@ class FinderTest < ActiveRecord::TestCase end def test_finder_with_offset_string - assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.find(:all, :offset => "3") } + assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.scoped(:offset => "3").all } end protected diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index e660b2ca35..c28f8de682 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -716,7 +716,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_only_generates_a_pk_if_necessary - m = Matey.find(:first) + m = Matey.first m.pirate = pirates(:blackbeard) m.target = pirates(:redbeard) end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 5c3560a33b..20279f814b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -36,7 +36,7 @@ def current_adapter?(*types) end def in_memory_db? - current_adapter?(:SQLiteAdapter) && + current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end @@ -80,8 +80,8 @@ class ActiveSupport::TestCase self.use_instantiated_fixtures = false self.use_transactional_fixtures = true - def create_fixtures(*table_names, &block) - ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) + def create_fixtures(*fixture_set_names, &block) + ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 02df464469..06de51f5cd 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -15,7 +15,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_with_blank_sti_name - company = Company.find(:first) + company = Company.first company = company.dup company.extend(Module.new { def read_attribute(name) @@ -24,7 +24,7 @@ class InheritanceTest < ActiveRecord::TestCase end }) company.save! - company = Company.find(:all).find { |x| x.id == company.id } + company = Company.all.find { |x| x.id == company.id } assert_equal ' ', company.type end @@ -98,7 +98,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_inheritance_find_all - companies = Company.find(:all, :order => 'id') + companies = Company.scoped(:order => 'id').all assert_kind_of Firm, companies[0], "37signals should be a firm" assert_kind_of Client, companies[1], "Summit should be a client" end @@ -149,9 +149,9 @@ class InheritanceTest < ActiveRecord::TestCase def test_update_all_within_inheritance Client.update_all "name = 'I am a client'" - assert_equal "I am a client", Client.find(:all).first.name + assert_equal "I am a client", Client.all.first.name # Order by added as otherwise Oracle tests were failing because of different order of results - assert_equal "37signals", Firm.find(:all, :order => "id").first.name + assert_equal "37signals", Firm.scoped(:order => "id").all.first.name end def test_alt_update_all_within_inheritance @@ -173,9 +173,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_find_first_within_inheritance - assert_kind_of Firm, Company.find(:first, :conditions => "name = '37signals'") - assert_kind_of Firm, Firm.find(:first, :conditions => "name = '37signals'") - assert_nil Client.find(:first, :conditions => "name = '37signals'") + assert_kind_of Firm, Company.scoped(:where => "name = '37signals'").first + assert_kind_of Firm, Firm.scoped(:where => "name = '37signals'").first + assert_nil Client.scoped(:where => "name = '37signals'").first end def test_alt_find_first_within_inheritance @@ -187,10 +187,10 @@ class InheritanceTest < ActiveRecord::TestCase def test_complex_inheritance very_special_client = VerySpecialClient.create("name" => "veryspecial") assert_equal very_special_client, VerySpecialClient.where("name = 'veryspecial'").first - assert_equal very_special_client, SpecialClient.find(:first, :conditions => "name = 'veryspecial'") - assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'") - assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'") - assert_equal 1, Client.find(:all, :conditions => "name = 'Summit'").size + assert_equal very_special_client, SpecialClient.scoped(:where => "name = 'veryspecial'").first + assert_equal very_special_client, Company.scoped(:where => "name = 'veryspecial'").first + assert_equal very_special_client, Client.scoped(:where => "name = 'veryspecial'").first + assert_equal 1, Client.scoped(:where => "name = 'Summit'").all.size assert_equal very_special_client, Client.find(very_special_client.id) end @@ -201,14 +201,14 @@ class InheritanceTest < ActiveRecord::TestCase end def test_eager_load_belongs_to_something_inherited - account = Account.find(1, :include => :firm) + account = Account.scoped(:includes => :firm).find(1) assert account.association_cache.key?(:firm), "nil proves eager load failed" end def test_eager_load_belongs_to_primary_key_quoting con = Account.connection assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do - Account.find(1, :include => :firm) + Account.scoped(:includes => :firm).find(1) end end @@ -230,7 +230,7 @@ class InheritanceTest < ActiveRecord::TestCase private def switch_to_alt_inheritance_column # we don't want misleading test results, so get rid of the values in the type column - Company.find(:all, :order => 'id').each do |c| + Company.scoped(:order => 'id').all.each do |c| c['type'] = nil c.save end @@ -259,7 +259,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase def test_instantiation_doesnt_try_to_require_corresponding_file ActiveRecord::Base.store_full_sti_class = false - foo = Firm.find(:first).clone + foo = Firm.first.clone foo.ruby_type = foo.type = 'FirmOnTheFly' foo.save! diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index 98cda010ae..426a350379 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -7,8 +7,6 @@ class InvalidDateTest < ActiveRecord::TestCase invalid_dates = [[2007, 11, 31], [1993, 2, 29], [2007, 2, 29]] - topic = Topic.new - valid_dates.each do |date_src| topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s) # Oracle DATE columns are datetime columns and Oracle adapter returns Time value diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 75e5dfa49b..0b78f2e46b 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -137,7 +137,7 @@ class LifecycleTest < ActiveRecord::TestCase def test_auto_observer topic_observer = TopicaAuditor.instance assert_nil TopicaAuditor.observed_class - assert_equal [Topic], TopicaAuditor.instance.observed_classes.to_a + assert_equal [Topic], TopicaAuditor.observed_classes.to_a topic = Topic.find(1) assert_equal topic.title, topic_observer.topic.title diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 48ce7ea2d3..afb0bd6fd9 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -323,7 +323,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase def counter_test(model, expected_count) add_counter_column_to(model) - object = model.find(:first) + object = model.first assert_equal 0, object.test_count assert_equal 0, object.send(model.locking_column) yield object.id @@ -358,7 +358,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? def test_sane_find_with_lock assert_nothing_raised do Person.transaction do - Person.find 1, :lock => true + Person.lock.find(1) end end end @@ -369,7 +369,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? def test_eager_find_with_lock assert_nothing_raised do Person.transaction do - Person.find 1, :include => :readers, :lock => true + Person.includes(:readers).lock.find(1) end end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 040445ef12..409a558f5c 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -64,7 +64,7 @@ module ActiveRecord end # SELECT - row = TestModel.find(:first) + row = TestModel.first assert_kind_of BigDecimal, row.wealth # If this assert fails, that means the SELECT is broken! @@ -79,7 +79,7 @@ module ActiveRecord TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") # SELECT - row = TestModel.find(:first) + row = TestModel.first assert_kind_of BigDecimal, row.wealth # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! @@ -132,7 +132,7 @@ module ActiveRecord :birthday => 18.years.ago, :favorite_day => 10.days.ago, :moment_of_truth => "1782-10-10 21:40:18", :male => true - bob = TestModel.find(:first) + bob = TestModel.first assert_equal 'bob', bob.first_name assert_equal 'bobsen', bob.last_name assert_equal "I was born ....", bob.bio diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb index 16e09fd80e..d1a85ee5e4 100644 --- a/activerecord/test/cases/migration/rename_column_test.rb +++ b/activerecord/test/cases/migration/rename_column_test.rb @@ -18,7 +18,7 @@ module ActiveRecord rename_column "test_models", "girlfriend", "exgirlfriend" TestModel.reset_column_information - bob = TestModel.find(:first) + bob = TestModel.first assert_equal "bobette", bob.exgirlfriend end @@ -33,7 +33,7 @@ module ActiveRecord rename_column :test_models, :first_name, :nick_name TestModel.reset_column_information assert TestModel.column_names.include?("nick_name") - assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + assert_equal ['foo'], TestModel.all.map(&:nick_name) end # FIXME: another integration test. We should decouple this from the @@ -46,7 +46,7 @@ module ActiveRecord rename_column "test_models", "first_name", "nick_name" TestModel.reset_column_information assert TestModel.column_names.include?("nick_name") - assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + assert_equal ['foo'], TestModel.all.map(&:nick_name) end def test_rename_column_preserves_default_value_not_null diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index e14c2d072c..f788690b0d 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -109,7 +109,7 @@ class MigrationTest < ActiveRecord::TestCase :value_of_e => BigDecimal("2.7182818284590452353602875") ) - b = BigNumber.find(:first) + b = BigNumber.first assert_not_nil b assert_not_nil b.bank_balance @@ -153,7 +153,7 @@ class MigrationTest < ActiveRecord::TestCase end GiveMeBigNumbers.down - assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { BigNumber.first } end def test_filtering_migrations @@ -165,13 +165,13 @@ class MigrationTest < ActiveRecord::TestCase Person.reset_column_information assert Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) Person.reset_column_information assert !Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end class MockMigration < ActiveRecord::Migration @@ -285,12 +285,12 @@ class MigrationTest < ActiveRecord::TestCase WeNeedThings.up assert Thing.create("content" => "hello world") - assert_equal "hello world", Thing.find(:first).content + assert_equal "hello world", Thing.first.content RenameThings.up Thing.table_name = "p_awesome_things_s" - assert_equal "hello world", Thing.find(:first).content + assert_equal "hello world", Thing.first.content ensure ActiveRecord::Base.table_name_prefix = '' ActiveRecord::Base.table_name_suffix = '' @@ -306,10 +306,10 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_sequence_name WeNeedReminders.up assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content + assert_equal "hello world", Reminder.first.content WeNeedReminders.down - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ensure ActiveRecord::Base.table_name_prefix = '' ActiveRecord::Base.table_name_suffix = '' @@ -588,14 +588,14 @@ class ChangeTableMigrationsTest < ActiveRecord::TestCase def test_remove_drops_single_column with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar]) + @connection.expects(:remove_column).with(:delete_me, :bar) t.remove :bar end end def test_remove_drops_multiple_columns with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar, :baz]) + @connection.expects(:remove_column).with(:delete_me, :bar, :baz) t.remove :bar, :baz end end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index e0fdd2871c..a03c4f552e 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -27,19 +27,19 @@ class ModulesTest < ActiveRecord::TestCase end def test_module_spanning_associations - firm = MyApplication::Business::Firm.find(:first) + firm = MyApplication::Business::Firm.first assert !firm.clients.empty?, "Firm should have clients" assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name" end def test_module_spanning_has_and_belongs_to_many_associations - project = MyApplication::Business::Project.find(:first) + project = MyApplication::Business::Project.first project.developers << MyApplication::Business::Developer.create("name" => "John") assert_equal "John", project.developers.last.name end def test_associations_spanning_cross_modules - account = MyApplication::Billing::Account.find(:first, :order => 'id') + account = MyApplication::Billing::Account.scoped(:order => 'id').first assert_kind_of MyApplication::Business::Firm, account.firm assert_kind_of MyApplication::Billing::Firm, account.qualified_billing_firm assert_kind_of MyApplication::Billing::Firm, account.unqualified_billing_firm @@ -48,7 +48,7 @@ class ModulesTest < ActiveRecord::TestCase end def test_find_account_and_include_company - account = MyApplication::Billing::Account.find(1, :include => :firm) + account = MyApplication::Billing::Account.scoped(:includes => :firm).find(1) assert_kind_of MyApplication::Business::Firm, account.firm end @@ -72,8 +72,8 @@ class ModulesTest < ActiveRecord::TestCase clients = [] assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do - clients << MyApplication::Business::Client.references(:accounts).find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL') - clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}) + clients << MyApplication::Business::Client.references(:accounts).scoped(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) + clients << MyApplication::Business::Client.scoped(:includes => {:firm => :account}).find(3) end clients.each do |client| @@ -123,7 +123,7 @@ class ModulesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true - collection = Shop::Collection.find(:first) + collection = Shop::Collection.first assert !collection.products.empty?, "Collection should have products" assert_nothing_raised { collection.destroy } ensure @@ -134,7 +134,7 @@ class ModulesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true - product = Shop::Product.find(:first) + product = Shop::Product.first assert !product.variants.empty?, "Product should have variants" assert_nothing_raised { product.destroy } ensure diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 06d076943e..2e87c5be2f 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -10,12 +10,12 @@ class NamedScopeTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base - assert_equal Topic.find(:all), Topic.base.to_a - assert_equal Topic.find(:first), Topic.base.first - assert_equal Topic.find(:all), Topic.base.map { |i| i } + assert_equal Topic.all, Topic.base + assert_equal Topic.all, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.all, Topic.base.map { |i| i } end def test_found_items_are_cached @@ -38,10 +38,10 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_delegates_finds_and_calculations_to_the_base_class - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base.find(:all) - assert_equal Topic.find(:first), Topic.base.find(:first) + assert_equal Topic.all, Topic.base.all + assert_equal Topic.first, Topic.base.first assert_equal Topic.count, Topic.base.count assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end @@ -58,10 +58,10 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.find(:all, :conditions => {:approved => true}).empty? + assert !Topic.scoped(:where => {:approved => true}).all.empty? - assert_equal Topic.find(:all, :conditions => {:approved => true}), Topic.approved - assert_equal Topic.count(:conditions => {:approved => true}), Topic.approved.count + assert_equal Topic.scoped(:where => {:approved => true}).all, Topic.approved + assert_equal Topic.where(:approved => true).count, Topic.approved.count end def test_scopes_with_string_name_can_be_composed @@ -70,13 +70,9 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.replied.approved, Topic.replied.approved_as_string end - def test_scopes_can_be_specified_with_deep_hash_conditions - assert_equal Topic.replied.approved, Topic.replied.approved_as_hash_condition - end - def test_scopes_are_composable - assert_equal((approved = Topic.find(:all, :conditions => {:approved => true})), Topic.approved) - assert_equal((replied = Topic.find(:all, :conditions => 'replies_count > 0')), Topic.replied) + assert_equal((approved = Topic.scoped(:where => {:approved => true}).all), Topic.approved) + assert_equal((replied = Topic.scoped(:where => 'replies_count > 0').all), Topic.replied) assert !(approved == replied) assert !(approved & replied).empty? @@ -84,8 +80,8 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes - topics_written_before_the_third = Topic.find(:all, :conditions => ['written_on < ?', topics(:third).written_on]) - topics_written_before_the_second = Topic.find(:all, :conditions => ['written_on < ?', topics(:second).written_on]) + topics_written_before_the_third = Topic.where('written_on < ?', topics(:third).written_on) + topics_written_before_the_second = Topic.where('written_on < ?', topics(:second).written_on) assert_not_equal topics_written_before_the_second, topics_written_before_the_third assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on) @@ -93,30 +89,11 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes_returning_nil - all_topics = Topic.find(:all) + all_topics = Topic.all assert_equal all_topics, Topic.written_before(nil) end - def test_scopes_with_joins - address = author_addresses(:david_address) - posts_with_authors_at_address = Post.find( - :all, :joins => 'JOIN authors ON authors.id = posts.author_id', - :conditions => [ 'authors.author_address_id = ?', address.id ] - ) - assert_equal posts_with_authors_at_address, Post.with_authors_at_address(address) - end - - def test_scopes_with_joins_respects_custom_select - address = author_addresses(:david_address) - posts_with_authors_at_address_titles = Post.find(:all, - :select => 'title', - :joins => 'JOIN authors ON authors.id = posts.author_id', - :conditions => [ 'authors.author_address_id = ?', address.id ] - ) - assert_equal posts_with_authors_at_address_titles.map(&:title), Post.with_authors_at_address(address).find(:all, :select => 'title').map(&:title) - end - def test_scope_with_object objects = Topic.with_object assert_operator objects.length, :>, 0 @@ -153,20 +130,16 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_active_records_have_scope_named__all__ - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base + assert_equal Topic.all, Topic.base end def test_active_records_have_scope_named__scoped__ - assert !Topic.find(:all, scope = {:conditions => "content LIKE '%Have%'"}).empty? + scope = Topic.where("content LIKE '%Have%'") + assert !scope.empty? - assert_equal Topic.find(:all, scope), Topic.scoped(scope) - end - - def test_first_and_last_should_support_find_options - assert_equal Topic.base.first(:order => 'title'), Topic.base.find(:first, :order => 'title') - assert_equal Topic.base.last(:order => 'title'), Topic.base.find(:last, :order => 'title') + assert_equal scope, Topic.scoped(where: "content LIKE '%Have%'") end def test_first_and_last_should_allow_integers_for_limit @@ -183,15 +156,6 @@ class NamedScopeTest < ActiveRecord::TestCase end end - def test_first_and_last_find_options_should_use_query_when_results_are_loaded - topics = Topic.base - topics.reload # force load - assert_queries(2) do - topics.first(:order => 'title') - topics.last(:order => 'title') - end - end - def test_empty_should_not_load_results topics = Topic.base assert_queries(2) do @@ -254,9 +218,9 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_many_should_return_false_if_none_or_one - topics = Topic.base.scoped(:conditions => {:id => 0}) + topics = Topic.base.where(:id => 0) assert !topics.many? - topics = Topic.base.scoped(:conditions => {:id => 1}) + topics = Topic.base.where(:id => 1) assert !topics.many? end @@ -334,7 +298,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_chaining_with_duplicate_joins join = "INNER JOIN comments ON comments.post_id = posts.id" post = Post.find(1) - assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size + assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size end def test_chaining_should_use_latest_conditions_when_creating @@ -465,16 +429,6 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title) end - def test_eager_default_scope_hashes_are_not_deprecated - klass = Class.new(ActiveRecord::Base) - klass.table_name = 'posts' - - assert_not_deprecated do - klass.send(:default_scope, :conditions => { :id => posts(:welcome).id }) - end - assert_equal [posts(:welcome).title], klass.all.map(&:title) - end - def test_eager_default_scope_relations_are_deprecated klass = Class.new(ActiveRecord::Base) klass.table_name = 'posts' @@ -488,13 +442,12 @@ end class DynamicScopeMatchTest < ActiveRecord::TestCase def test_scoped_by_no_match - assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all") + assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all") end def test_scoped_by - match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location") + match = ActiveRecord::DynamicMatchers::Method.match(nil, "scoped_by_age_and_sex_and_location") assert_not_nil match - assert match.scope? assert_equal %w(age sex location), match.attribute_names end end @@ -510,7 +463,7 @@ class DynamicScopeTest < ActiveRecord::TestCase def test_dynamic_scope assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1) - assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"}) + assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.scoped(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first end def test_dynamic_scope_should_create_methods_after_hitting_method_missing diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index bb60aa0a66..0933a4ff3d 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -55,7 +55,7 @@ class PersistencesTest < ActiveRecord::TestCase 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 2, author.posts_sorted_by_id_limited.scoped(:limit => 2).all.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 @@ -94,12 +94,6 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal count, Pet.joins(:toys).where(where_args).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 @@ -126,7 +120,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_destroy_all conditions = "author_name = 'Mary'" - topics_by_mary = Topic.all(:conditions => conditions, :order => 'id') + topics_by_mary = Topic.scoped(:where => conditions, :order => 'id').to_a assert ! topics_by_mary.empty? assert_difference('Topic.count', -topics_by_mary.size) do @@ -137,7 +131,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.find([2, 3], :order => 'id') + clients = Client.scoped(:order => 'id').find([2, 3]) assert_difference('Client.count', -2) do destroyed = Client.destroy([2, 3]).sort_by(&:id) @@ -338,7 +332,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_update_all_with_non_standard_table_name - assert_equal 1, WarehouseThing.update_all(['value = ?', 0], ['id = ?', 1]) + assert_equal 1, WarehouseThing.where(id: 1).update_all(['value = ?', 0]) assert_equal 0, WarehouseThing.find(1).value end @@ -411,7 +405,6 @@ class PersistencesTest < ActiveRecord::TestCase 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" diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index d93478513b..a712e5f689 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -96,7 +96,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_clear_after_close mw = ActiveRecord::QueryCache.new lambda { |env| - Post.find(:first) + Post.first [200, {}, nil] } body = mw.call({}).last @@ -233,8 +233,8 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_cache_is_expired_by_habtm_update ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) ActiveRecord::Base.cache do - c = Category.find(:first) - p = Post.find(:first) + c = Category.first + p = Post.first p.categories << c end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index b72b72ecf2..39b66b3399 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -27,7 +27,7 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_find_with_readonly_option - Developer.find(:all).each { |d| assert !d.readonly? } + Developer.all.each { |d| assert !d.readonly? } Developer.readonly(false).each { |d| assert !d.readonly? } Developer.readonly(true).each { |d| assert d.readonly? } Developer.readonly.each { |d| assert d.readonly? } @@ -48,7 +48,7 @@ class ReadOnlyTest < ActiveRecord::TestCase post = Post.find(1) assert !post.comments.empty? assert !post.comments.any?(&:readonly?) - assert !post.comments.find(:all).any?(&:readonly?) + assert !post.comments.all.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 16f05f2198..7dd5698dcf 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -63,7 +63,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_column_null_not_null - subscriber = Subscriber.find(:first) + subscriber = Subscriber.first assert subscriber.column_for_attribute("name").null assert !subscriber.column_for_attribute("nick").null end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 7216282892..342f7a86fa 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -53,8 +53,8 @@ class RelationScopingTest < ActiveRecord::TestCase end def test_scoped_find_last_preserves_scope - lowest_salary = Developer.first :order => "salary ASC" - highest_salary = Developer.first :order => "salary DESC" + lowest_salary = Developer.order("salary ASC").first + highest_salary = Developer.order("salary DESC").first Developer.order("salary").scoping do assert_equal highest_salary, Developer.last @@ -323,8 +323,8 @@ class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts def test_default_scope - expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + expected = Developer.scoped(:order => 'salary DESC').all.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary } assert_equal expected, received end @@ -362,12 +362,12 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort assert_equal nil, DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end @@ -396,8 +396,8 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_scope_overwrites_default - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + expected = Developer.scoped(:order => 'salary DESC, name DESC').all.collect { |dev| dev.name } + received = DeveloperOrderedBySalary.by_name.all.collect { |dev| dev.name } assert_equal expected, received end @@ -414,8 +414,8 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_order_in_default_scope_should_prevail - expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + expected = Developer.scoped(:order => 'salary desc').all.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.scoped(:order => 'salary').all.collect { |dev| dev.salary } assert_equal expected, received end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 3fe3122fa1..76b868c7b4 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -255,7 +255,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal nil, Developer.none.calculate(:average, 'salary') end end - + def test_null_relation_metadata_methods assert_equal "", Developer.none.to_sql assert_equal({}, Developer.none.where_values_hash) @@ -282,7 +282,7 @@ class RelationTest < ActiveRecord::TestCase end def test_find_on_hash_conditions - assert_equal Topic.find(:all, :conditions => {:approved => false}), Topic.where({ :approved => false }).to_a + assert_equal Topic.scoped(:where => {:approved => false}).all, Topic.where({ :approved => false }).to_a end def test_joins_with_string_array @@ -644,6 +644,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(authors(:mary).id) assert ! davids.exists?("42") assert ! davids.exists?(42) + assert ! davids.exists?(davids.new) fake = Author.where(:name => 'fake author') assert ! fake.exists? @@ -688,6 +689,10 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_delete_all_limit_error + assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } + end + def test_select_argument_error assert_raises(ArgumentError) { Developer.select } end @@ -1059,10 +1064,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.all, all_posts.all end - def test_extensions_with_except - assert_equal 2, Topic.named_extension.order(:author_name).except(:order).two - end - def test_only relation = Post.where(:author_id => 1).order('id ASC').limit(1) assert_equal [posts(:welcome)], relation.all @@ -1074,10 +1075,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.limit(1).all.first, all_posts.first end - def test_extensions_with_only - assert_equal 2, Topic.named_extension.order(:author_name).only(:order).two - end - def test_anonymous_extension relation = Post.where(:author_id => 1).order('id ASC').extending do def author @@ -1099,36 +1096,26 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.order(Post.arel_table[:title]).all, Post.order("title").all end - def test_order_with_find_with_order - assert_equal 'zyke', CoolCar.order('name desc').find(:first, :order => 'id').name - assert_equal 'zyke', FastCar.order('name desc').find(:first, :order => 'id').name - end - def test_default_scope_order_with_scope_order assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', CoolCar.order_using_old_style.limit(1).first.name assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', FastCar.order_using_old_style.limit(1).first.name end def test_order_using_scoping car1 = CoolCar.order('id DESC').scoping do - CoolCar.find(:first, :order => 'id asc') + CoolCar.scoped(:order => 'id asc').first end assert_equal 'zyke', car1.name car2 = FastCar.order('id DESC').scoping do - FastCar.find(:first, :order => 'id asc') + FastCar.scoped(:order => 'id asc').first end assert_equal 'zyke', car2.name end def test_unscoped_block_style assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name} - assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_old_style.limit(1).first.name} - assert_equal 'honda', FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name} - assert_equal 'honda', FastCar.unscoped { FastCar.order_using_old_style.limit(1).first.name} end def test_intersection_with_array @@ -1139,10 +1126,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal [rails_author], relation & [rails_author] end - def test_removing_limit_with_options - assert_not_equal 1, Post.limit(1).all(:limit => nil).count - end - def test_primary_key assert_equal "id", Post.scoped.primary_key end @@ -1305,6 +1288,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal nil, Post.scoped.find_by("1 = 0") end + test "find_by doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) } + end + test "find_by! with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) end @@ -1317,6 +1304,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:eager_other), Post.order(:id).find_by!('author_id = ?', 2) end + test "find_by! doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) } + end + test "find_by! raises RecordNotFound if the record is missing" do assert_raises(ActiveRecord::RecordNotFound) do Post.scoped.find_by!("1 = 0") diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f8b3e01a49..9846f5b12d 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -290,3 +290,34 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase assert_equal %w{ after_rollback }, topic.history end end + +class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class TopicWithSaveInCallback < ActiveRecord::Base + self.table_name = :topics + after_commit :cache_topic, :on => :create + after_commit :call_update, :on => :update + attr_accessor :cached, :record_updated + + def call_update + self.record_updated = true + end + + def cache_topic + unless cached + self.cached = true + self.save + else + self.cached = false + end + end + end + + def test_after_commit_in_save + topic = TopicWithSaveInCallback.new() + topic.save + assert_equal true, topic.cached + assert_equal true, topic.record_updated + end +end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index c72b7b35cd..7ac34bc71e 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -86,7 +86,7 @@ class AssociationValidationTest < ActiveRecord::TestCase assert !r.valid? assert r.errors[:topic].any? - r.topic = Topic.find :first + r.topic = Topic.first assert r.valid? end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 2b1f34a2d0..c173ee9a15 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -189,7 +189,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t_utf8.save, "Should save t_utf8 as unique" # If database hasn't UTF-8 character set, this test fails - if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" + if Topic.scoped(:select => 'LOWER(title) AS title').find(t_utf8).title == "я тоже уникальный!" t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") assert !t2_utf8.valid?, "Shouldn't be valid" assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 42ac81690f..b4bc0ad5fa 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -15,7 +15,6 @@ class Car < ActiveRecord::Base scope :incl_engines, -> { includes(:engines) } scope :order_using_new_style, -> { order('name asc') } - scope :order_using_old_style, -> { { :order => 'name asc' } } end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index cf73c28ded..3e9f1b0635 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -19,7 +19,7 @@ class Comment < ActiveRecord::Base end def self.search_by_type(q) - self.find(:all, :conditions => ["#{QUOTED_TYPE} = ?", q]) + self.scoped(:where => ["#{QUOTED_TYPE} = ?", q]).all end def self.all_as_method diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index fbdfaa2c29..7b993d5a2c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -198,6 +198,11 @@ class Account < ActiveRecord::Base @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } end + # Test private kernel method through collection proxy using has_many. + def self.open + where('firm_name = ?', '37signals') + end + before_destroy do |account| if account.firm Account.destroyed_account_ids[account.firm.id] << account.id diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 7133094f5d..83482f4d07 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -2,20 +2,20 @@ require 'ostruct' module DeveloperProjectsAssociationExtension def find_most_recent - find(:first, :order => "id DESC") + scoped(:order => "id DESC").first end end module DeveloperProjectsAssociationExtension2 def find_least_recent - find(:first, :order => "id ASC") + scoped(:order => "id ASC").first end end class Developer < ActiveRecord::Base has_and_belongs_to_many :projects do def find_most_recent - find(:first, :order => "id DESC") + scoped(:order => "id DESC").first end end @@ -37,7 +37,7 @@ class Developer < ActiveRecord::Base :association_foreign_key => "project_id", :extend => DeveloperProjectsAssociationExtension do def find_least_recent - find(:first, :order => "id ASC") + scoped(:order => "id ASC").first end end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 5002ab9ff8..1aaf9a1b82 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -9,11 +9,6 @@ class Post < ActiveRecord::Base scope :ranked_by_comments, -> { order("comments_count DESC") } scope :limit_by, lambda {|l| limit(l) } - scope :with_authors_at_address, lambda { |address| { - :conditions => [ 'authors.author_address_id = ?', address.id ], - :joins => 'JOIN authors ON authors.id = posts.author_id' - } - } belongs_to :author do def greeting @@ -32,13 +27,11 @@ class Post < ActiveRecord::Base scope :with_special_comments, -> { joins(:comments).where(:comments => {:type => 'SpecialComment'}) } scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) } - scope :with_post, lambda {|post_id| - { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } - } + scope :with_post, ->(post_id) { joins(:comments).where(:comments => { :post_id => post_id }) } has_many :comments do def find_most_recent - find(:first, :order => "id DESC") + scoped(:order => "id DESC").first end def newest @@ -71,8 +64,8 @@ class Post < ActiveRecord::Base has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select - find :all, :select => 'tags.*, authors.id as author_id', - :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' + scoped(:select => 'tags.*, authors.id as author_id', + :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id').all end end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 0625b8d296..079e403444 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -2,7 +2,7 @@ class Topic < ActiveRecord::Base scope :base, -> { scoped } scope :written_before, lambda { |time| if time - { :conditions => ['written_on < ?', time] } + where 'written_on < ?', time end } scope :approved, -> { where(:approved => true) } @@ -11,11 +11,7 @@ class Topic < ActiveRecord::Base scope :scope_with_lambda, lambda { scoped } scope :by_lifo, -> { where(:author_name => 'lifo') } - - ActiveSupport::Deprecation.silence do - scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}} - scope :replied, :conditions => ['replies_count > 0'] - end + scope :replied, -> { where 'replies_count > 0' } scope 'approved_as_string', -> { where(:approved => true) } scope :anonymous_extension, -> { scoped } do @@ -35,18 +31,6 @@ class Topic < ActiveRecord::Base 2 end end - module MultipleExtensionOne - def extension_one - 1 - end - end - module MultipleExtensionTwo - def extension_two - 2 - end - end - scope :named_extension, :extend => NamedExtension - scope :multiple_extensions, :extend => [MultipleExtensionTwo, MultipleExtensionOne] has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 25b416a906..e51db50ae3 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,8 +1,8 @@ ActiveRecord::Schema.define do %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones).each do |table_name| - execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| + execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE' @@ -10,6 +10,8 @@ ActiveRecord::Schema.define do execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')" execute 'DROP SEQUENCE IF EXISTS companies_id_seq' + execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' + %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -125,6 +127,37 @@ _SQL ); _SQL +begin + execute <<_SQL + CREATE TABLE postgresql_partitioned_table_parent ( + id SERIAL PRIMARY KEY, + number integer + ); + CREATE TABLE postgresql_partitioned_table ( ) + INHERITS (postgresql_partitioned_table_parent); + + CREATE OR REPLACE FUNCTION partitioned_insert_trigger() + RETURNS TRIGGER AS $$ + BEGIN + INSERT INTO postgresql_partitioned_table VALUES (NEW.*); + RETURN NULL; + END; + $$ + LANGUAGE plpgsql; + + CREATE TRIGGER insert_partitioning_trigger + BEFORE INSERT ON postgresql_partitioned_table_parent + FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger(); +_SQL +rescue ActiveRecord::StatementInvalid => e + if e.message =~ /language "plpgsql" does not exist/ + execute "CREATE LANGUAGE 'plpgsql';" + retry + else + raise e + end +end + begin execute <<_SQL CREATE TABLE postgresql_xml_data_type ( diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 377fde5c96..cef08cd99c 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -38,7 +38,9 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name t.text :settings, :null => true - t.text :preferences, :null => false, :default => "" + # MySQL does not allow default values for blobs. Fake it out with a + # big varchar below. + t.string :preferences, :null => false, :default => '', :limit => 1024 t.references :account end @@ -173,6 +175,7 @@ ActiveRecord::Schema.define do t.integer :client_of t.integer :rating, :default => 1 t.integer :account_id + t.string :description, :null => false, :default => "" end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index ea05b35fe0..e9ddeb32cf 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - # For sqlite 3.1.0+, make a table with a autoincrement column + # For sqlite 3.1.0+, make a table with an autoincrement column if supports_autoincrement? create_table :table_with_autoincrement, :force => true do |t| t.column :name, :string diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index b6c3e91db8..fe322b4865 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,32 +1,56 @@ ## Rails 4.0.0 (unreleased) ## -* Make Module#delegate stop using `send` - can no longer delegate to private methods. *dasch* +* `Object#try` can't call private methods . *Vasiliy Ermolovich* -* AS::Callbacks: deprecate `:rescuable` option. *Bogdan Gusiev* +* `AS::Callbacks#run_callbacks` remove `key` argument. *Francesco Rodriguez* -* Adds Integer#ordinal to get the ordinal suffix string of an integer. *Tim Gildea* +* `deep_dup` works more expectedly now and duplicates also values in +Hash+ instances and elements in +Array+ instances. *Alexey Gaziev* -* AS::Callbacks: `:per_key` option is no longer supported +* Inflector no longer applies ice -> ouse to words like slice, police, ets *Wes Morgan* -* `AS::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option. +* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations *twinturbo* -* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it. *Carlos Antonio da Silva* +* Make Module#delegate stop using `send` - can no longer delegate to private methods. *dasch* -* Remove ActiveSupport::TestCase#pending method, use `skip` instead. *Carlos Antonio da Silva* +* AS::Callbacks: deprecate `:rescuable` option. *Bogdan Gusiev* -* Deprecates the compatibility method Module#local_constant_names, - use Module#local_constants instead (which returns symbols). *fxn* +* Adds Integer#ordinal to get the ordinal suffix string of an integer. *Tim Gildea* -* Deletes the compatibility method Module#method_names, - use Module#methods from now on (which returns symbols). *fxn* +* AS::Callbacks: `:per_key` option is no longer supported -* Deletes the compatibility method Module#instance_method_names, - use Module#instance_methods from now on (which returns symbols). *fxn* +* `AS::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option. -* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger - from Ruby stdlib. +* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it. *Carlos Antonio da Silva* -* Unicode database updated to 6.1.0. +* Remove ActiveSupport::TestCase#pending method, use `skip` instead. *Carlos Antonio da Silva* + +* Deprecates the compatibility method Module#local_constant_names, + use Module#local_constants instead (which returns symbols). *fxn* + +* Deletes the compatibility method Module#method_names, + use Module#methods from now on (which returns symbols). *fxn* + +* Deletes the compatibility method Module#instance_method_names, + use Module#instance_methods from now on (which returns symbols). *fxn* + +* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger + from Ruby stdlib. + +* Unicode database updated to 6.1.0. + +* Adds `encode_big_decimal_as_string` option to force JSON serialization of BigDecimals as numeric instead + of wrapping them in strings for safety. + + +## Rails 3.2.4 (unreleased) ## + +* Added #beginning_of_hour and #end_of_hour to Time and DateTime core + extensions. *Mark J. Titorenko* + + +## Rails 3.2.3 (March 30, 2012) ## + +* No changes. ## Rails 3.2.2 (March 1, 2012) ## @@ -221,7 +245,7 @@ * Hash.from_xml no longer loses attributes on tags containing only whitespace *André Arko* -## Rails 3.0.6 (April 5, 2011) ## +## Rails 3.0.6 (April 5, 2011) ## * No changes. diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 822c9d98ae..822c9d98ae 100755..100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index dbf0c25c5c..8f018dcbc6 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -22,20 +22,6 @@ #++ require 'securerandom' - -module ActiveSupport - class << self - attr_accessor :load_all_hooks - def on_load_all(&hook) load_all_hooks << hook end - def load_all!; load_all_hooks.each { |hook| hook.call } end - end - self.load_all_hooks = [] - - on_load_all do - [Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte] - end -end - require "active_support/dependencies/autoload" require "active_support/version" require "active_support/logger" @@ -44,6 +30,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :Dependencies autoload :DescendantsTracker autoload :FileUpdateChecker autoload :LogSubscriber diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index b9f196d7a9..55791bfa56 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -280,7 +280,7 @@ module ActiveSupport end end if entry && entry.expired? - race_ttl = options[:race_condition_ttl].to_f + race_ttl = options[:race_condition_ttl].to_i if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl entry.expires_at = Time.now + race_ttl write_entry(key, entry, :expires_in => race_ttl * 2) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 8e6a3bc5a8..89bdb741d0 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/file/atomic' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/object/inclusion' -require 'rack/utils' +require 'uri/common' module ActiveSupport module Cache @@ -23,7 +23,7 @@ module ActiveSupport end def clear(options = nil) - root_dirs = Dir.entries(cache_path).reject{|f| f.in?(EXCLUDED_DIRS)} + root_dirs = Dir.entries(cache_path).reject{|f| f.in?(EXCLUDED_DIRS + [".gitkeep"])} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end @@ -126,7 +126,7 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) - fname = Rack::Utils.escape(key) + fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) @@ -144,7 +144,7 @@ module ActiveSupport # Translate a file path into a key. def file_path_key(path) fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last - Rack::Utils.unescape(fname) + URI.decode_www_form_component(fname, Encoding::UTF_8) end # Delete empty directories in the cache. diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index b15bb42c88..7fd5e3b53d 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -137,6 +137,7 @@ module ActiveSupport def write_entry(key, entry, options) # :nodoc: synchronize do old_entry = @data[key] + return false if @data.key?(key) && options[:unless_exist] @cache_size -= old_entry.size if old_entry @cache_size += entry.size @key_access[key] = Time.now.to_f diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 2a569d9a9b..c6c7e2821b 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -54,7 +54,6 @@ module ActiveSupport # saving... # - save # saved - # module Callbacks extend Concern @@ -73,10 +72,9 @@ module ActiveSupport # run_callbacks :save do # save # end - # - def run_callbacks(kind, key = nil, &block) - #TODO: deprecate key argument - self.class.__run_callbacks(kind, self, &block) + def run_callbacks(kind, &block) + runner_name = self.class.__define_callbacks(kind, self) + send(runner_name, &block) end private @@ -198,7 +196,6 @@ module ActiveSupport # yield self # end # end - # def define_conditional_callback name = "_conditional_callback_#{@kind}_#{next_id}" @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 @@ -252,7 +249,6 @@ module ActiveSupport # Objects:: # a method is created that calls the before_foo method # on the object. - # def _compile_filter(filter) method_name = "_callback_#{@kind}_#{next_id}" case filter @@ -323,18 +319,17 @@ module ActiveSupport method << callbacks method << "halted ? false : (block_given? ? value : true)" - method.flatten.compact.join("\n") + method.join("\n") end end module ClassMethods - # This method runs callback chain for the given kind. - # If this called first time it creates a new callback method for the kind. + # This method defines callback chain method for the given kind + # if it was not yet defined. # This generated method plays caching role. - # - def __run_callbacks(kind, object, &blk) #:nodoc: + def __define_callbacks(kind, object) #:nodoc: name = __callback_runner_name(kind) unless object.respond_to?(name, true) str = object.send("_#{kind}_callbacks").compile @@ -343,7 +338,7 @@ module ActiveSupport protected :#{name} RUBY_EVAL end - object.send(name, &blk) + name end def __reset_runner(symbol) @@ -405,7 +400,6 @@ module ActiveSupport # will be called only when it returns a false value. # * <tt>:prepend</tt> - If true, the callback will be prepended to the existing # chain rather than appended. - # def set_callback(name, *filter_list, &block) mapped = nil @@ -430,7 +424,6 @@ module ActiveSupport # 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 |target, chain, type, filters, options| filters.each do |filter| @@ -449,7 +442,6 @@ module ActiveSupport end # Remove all set callbacks for the given event. - # def reset_callbacks(symbol) callbacks = send("_#{symbol}_callbacks") @@ -530,7 +522,6 @@ module ActiveSupport # define_callbacks :save, :scope => [:name] # # would call <tt>Audit#save</tt>. - # def define_callbacks(*callbacks) config = callbacks.last.is_a?(Hash) ? callbacks.pop : {} callbacks.each do |callback| diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 6162f7af27..44d90ef732 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -16,7 +16,7 @@ class Array # %w( a b c d ).to(10) # => %w( a b c d ) # %w().to(0) # => %w() def to(position) - self.first position + 1 + first position + 1 end # Equal to <tt>self[1]</tt>. diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index f3d06ecb2f..24aa28b895 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -9,28 +9,32 @@ class Array # * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ") # * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ") def to_sentence(options = {}) + options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale) + + default_connectors = { + :words_connector => ', ', + :two_words_connector => ' and ', + :last_word_connector => ', and ' + } if defined?(I18n) - default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale]) - default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale]) - default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale]) - else - default_words_connector = ", " - default_two_words_connector = " and " - default_last_word_connector = ", and " + namespace = 'support.array.' + default_connectors.each_key do |name| + i18n_key = (namespace + name.to_s).to_sym + default_connectors[name] = I18n.translate i18n_key, :locale => options[:locale] + end end - options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale) - options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector + options.reverse_merge! default_connectors case length - when 0 - "" - when 1 - self[0].to_s.dup - when 2 - "#{self[0]}#{options[:two_words_connector]}#{self[1]}" - else - "#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}" + when 0 + '' + when 1 + self[0].to_s.dup + when 2 + "#{self[0]}#{options[:two_words_connector]}#{self[1]}" + else + "#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}" end end @@ -45,14 +49,14 @@ class Array # Blog.all.to_formatted_s(:db) # => "1,2,3" def to_formatted_s(format = :default) case format - when :db - if respond_to?(:empty?) && self.empty? - "null" - else - collect { |element| element.id }.join(",") - end + when :db + if empty? + 'null' else - to_default_s + collect { |element| element.id }.join(',') + end + else + to_default_s end end alias_method :to_default_s, :to_s @@ -86,20 +90,20 @@ class Array # </project> # </projects> # - # Otherwise the root element is "records": + # Otherwise the root element is "objects": # # [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml # # <?xml version="1.0" encoding="UTF-8"?> - # <records type="array"> - # <record> + # <objects type="array"> + # <object> # <bar type="integer">2</bar> # <foo type="integer">1</foo> - # </record> - # <record> + # </object> + # <object> # <baz type="integer">3</baz> - # </record> - # </records> + # </object> + # </objects> # # If the collection is empty the root element is "nil-classes" by default: # @@ -139,26 +143,28 @@ class Array options = options.dup options[:indent] ||= 2 options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) - options[:root] ||= if first.class.to_s != "Hash" && all? { |e| e.is_a?(first.class) } - underscored = ActiveSupport::Inflector.underscore(first.class.name) - ActiveSupport::Inflector.pluralize(underscored).tr('/', '_') - else - "objects" - end + options[:root] ||= \ + if first.class != Hash && all? { |e| e.is_a?(first.class) } + underscored = ActiveSupport::Inflector.underscore(first.class.name) + ActiveSupport::Inflector.pluralize(underscored).tr('/', '_') + else + 'objects' + end builder = options[:builder] builder.instruct! unless options.delete(:skip_instruct) root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options) children = options.delete(:children) || root.singularize + attributes = options[:skip_types] ? {} : {:type => 'array'} - attributes = options[:skip_types] ? {} : {:type => "array"} - return builder.tag!(root, attributes) if empty? - - builder.__send__(:method_missing, root, attributes) do - each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) } - yield builder if block_given? + if empty? + builder.tag!(root, attributes) + else + builder.__send__(:method_missing, root, attributes) do + each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) } + yield builder if block_given? + end end end - end diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 2b3f639cb1..ac1ae53db0 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -82,11 +82,9 @@ class Array # # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] - def split(value = nil) - using_block = block_given? - + def split(value = nil, &block) inject([[]]) do |results, element| - if (using_block && yield(element)) || (value == element) + if block && block.call(element) || value == element results << [] else results.last << element 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 ac3dedc0e3..3bedfa9a61 100644 --- a/activesupport/lib/active_support/core_ext/array/uniq_by.rb +++ b/activesupport/lib/active_support/core_ext/array/uniq_by.rb @@ -6,8 +6,7 @@ class Array # [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2] # def uniq_by(&block) - ActiveSupport::Deprecation.warn "uniq_by " \ - "is deprecated. Use Array#uniq instead", caller + ActiveSupport::Deprecation.warn 'uniq_by is deprecated. Use Array#uniq instead', caller uniq(&block) end @@ -15,8 +14,7 @@ class Array # # Same as +uniq_by+, but modifies +self+. def uniq_by!(&block) - ActiveSupport::Deprecation.warn "uniq_by! " \ - "is deprecated. Use Array#uniq! instead", caller + ActiveSupport::Deprecation.warn 'uniq_by! is deprecated. Use Array#uniq! instead', caller uniq!(&block) end end diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index 4834eca8b1..9ea93d7226 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -25,9 +25,6 @@ class Array # 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] diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 305ed4964b..c64685a694 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -109,7 +109,7 @@ class Class end private - def singleton_class? - ancestors.first != self - end + def singleton_class? + ancestors.first != self + end end 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 95eb94fdf6..fa1dbfdf06 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -2,34 +2,37 @@ 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. -# To opt out of both instance methods, pass :instance_accessor => 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 + # Defines a class attribute if it's not defined and creates a reader method that + # returns the attribute value. + # + # class Person + # cattr_reader :hair_colors + # end + # + # Person.class_variable_set("@@hair_colors", [:brown, :black]) + # Person.hair_colors # => [:brown, :black] + # Person.new.hair_colors # => [:brown, :black] + # + # The attribute name must be a valid method name in Ruby. + # + # class Person + # cattr_reader :"1_Badname " + # end + # # => NameError: invalid attribute name + # + # If you want to opt out the instance reader method, you can pass <tt>instance_reader: false</tt> + # or <tt>instance_accessor: false</tt>. + # + # class Person + # cattr_reader :hair_colors, instance_reader: false + # end + # + # Person.new.hair_colors # => NoMethodError def cattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) unless defined? @@#{sym} @@#{sym} = nil @@ -50,10 +53,47 @@ class Class end end + # Defines a class attribute if it's not defined and creates a writer method to allow + # assignment to the attribute. + # + # class Person + # cattr_writer :hair_colors + # end + # + # Person.hair_colors = [:brown, :black] + # Person.class_variable_get("@@hair_colors") # => [:brown, :black] + # Person.new.hair_colors = [:blonde, :red] + # Person.class_variable_get("@@hair_colors") # => [:blonde, :red] + # + # The attribute name must be a valid method name in Ruby. + # + # class Person + # cattr_writer :"1_Badname " + # end + # # => NameError: invalid attribute name + # + # If you want to opt out the instance writer method, pass <tt>instance_writer: false</tt> + # or <tt>instance_accessor: false</tt>. + # + # class Person + # cattr_writer :hair_colors, instance_writer: false + # end + # + # Person.new.hair_colors = [:blonde, :red] # => NoMethodError + # + # Also, you can pass a block to set up the attribute with a default value. + # + # class Person + # cattr_writer :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def cattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) unless defined? @@#{sym} @@#{sym} = nil @@ -71,10 +111,58 @@ class Class end EOS end - self.send("#{sym}=", yield) if block_given? + send("#{sym}=", yield) if block_given? end end + # Defines both class and instance accessors for class attributes. + # + # 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] + # + # 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 Male < Person + # end + # + # Male.hair_colors << :blue + # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] + # + # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. + # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. + # + # class Person + # cattr_accessor :hair_colors, instance_writer: false, instance_reader: false + # end + # + # Person.new.hair_colors = [:brown] # => NoMethodError + # Person.new.hair_colors # => NoMethodError + # + # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # + # class Person + # cattr_accessor :hair_colors, instance_accessor: false + # end + # + # Person.new.hair_colors = [:brown] # => NoMethodError + # Person.new.hair_colors # => NoMethodError + # + # Also you can pass a block to set up the attribute with a default value. + # + # class Person + # cattr_accessor :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] def cattr_accessor(*syms, &blk) cattr_reader(*syms) cattr_writer(*syms, &blk) diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index 0634f20e3c..ff870f5fd1 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -20,23 +20,21 @@ class Class define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false end -private - - # Take the object being set and store it in a method. This gives us automatic - # inheritance behavior, without having to store the object in an instance - # variable and look up the superclass chain manually. - def _stash_object_in_method(object, method, instance_reader = true) - singleton_class.remove_possible_method(method) - singleton_class.send(:define_method, method) { object } - remove_possible_method(method) - define_method(method) { object } if instance_reader - end - - def _superclass_delegating_accessor(name, options = {}) - singleton_class.send(:define_method, "#{name}=") do |value| - _stash_object_in_method(value, name, options[:instance_reader] != false) + private + # Take the object being set and store it in a method. This gives us automatic + # inheritance behavior, without having to store the object in an instance + # variable and look up the superclass chain manually. + def _stash_object_in_method(object, method, instance_reader = true) + singleton_class.remove_possible_method(method) + singleton_class.send(:define_method, method) { object } + remove_possible_method(method) + define_method(method) { object } if instance_reader end - send("#{name}=", nil) - end + def _superclass_delegating_accessor(name, options = {}) + singleton_class.send(:define_method, "#{name}=") do |value| + _stash_object_in_method(value, name, options[:instance_reader] != false) + end + send("#{name}=", nil) + end end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 6d4270f8b0..323126fcfa 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -5,7 +5,15 @@ require 'active_support/core_ext/date/zones' require 'active_support/core_ext/time/zones' class Date - DAYS_INTO_WEEK = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6 } + DAYS_INTO_WEEK = { + :monday => 0, + :tuesday => 1, + :wednesday => 2, + :thursday => 3, + :friday => 4, + :saturday => 5, + :sunday => 6 + } class << self # Returns a new Date representing the date 1 day ago (i.e. yesterday's date). @@ -31,7 +39,7 @@ class Date # 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 + to_date == ::Date.current # we need the to_date because of DateTime end # Returns true if the Date object's date lies in the future. @@ -105,9 +113,9 @@ class Date # Date.new(2007, 5, 12).change(:year => 2005, :month => 1) # => Date.new(2005, 1, 12) def change(options) ::Date.new( - options[:year] || self.year, - options[:month] || self.month, - options[:day] || self.day + options.fetch(:year, year), + options.fetch(:month, month), + options.fetch(:day, day) ) end @@ -166,7 +174,7 @@ class Date def end_of_week(start_day = :monday) days_to_end = 6 - days_to_week_start(start_day) result = self + days_to_end.days - self.acts_like?(:time) ? result.end_of_day : result + acts_like?(:time) ? result.end_of_day : result end alias :at_end_of_week :end_of_week @@ -180,7 +188,7 @@ class Date # week. Default is +:monday+. +DateTime+ objects have their time set to 0:00. def prev_week(day = :monday) result = (self - 7).beginning_of_week + DAYS_INTO_WEEK[day] - self.acts_like?(:time) ? result.change(:hour => 0) : result + acts_like?(:time) ? result.change(:hour => 0) : result end alias :last_week :prev_week @@ -193,43 +201,57 @@ class Date # Returns a new Date/DateTime representing the start of the given day in next week (default is :monday). def next_week(day = :monday) result = (self + 7).beginning_of_week + DAYS_INTO_WEEK[day] - self.acts_like?(:time) ? result.change(:hour => 0) : result + acts_like?(:time) ? result.change(:hour => 0) : result end # Returns a new ; DateTime objects will have time set to 0:00DateTime representing the start of the month (1st of the month; DateTime objects will have time set to 0:00) def beginning_of_month - self.acts_like?(:time) ? change(:day => 1, :hour => 0) : change(:day => 1) + acts_like?(:time) ? change(:day => 1, :hour => 0) : change(:day => 1) end alias :at_beginning_of_month :beginning_of_month # Returns a new Date/DateTime representing the end of the month (last day of the month; DateTime objects will have time set to 0:00) def end_of_month - last_day = ::Time.days_in_month( self.month, self.year ) - self.acts_like?(:time) ? change(:day => last_day, :hour => 23, :min => 59, :sec => 59) : change(:day => last_day) + last_day = ::Time.days_in_month(month, year) + if acts_like?(:time) + change(:day => last_day, :hour => 23, :min => 59, :sec => 59) + else + change(:day => last_day) + end end alias :at_end_of_month :end_of_month # Returns a new Date/DateTime representing the start of the quarter (1st of january, april, july, october; DateTime objects will have time set to 0:00) def beginning_of_quarter - beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month }) + first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } + beginning_of_month.change(:month => first_quarter_month) end alias :at_beginning_of_quarter :beginning_of_quarter # Returns a new Date/DateTime representing the end of the quarter (last day of march, june, september, december; DateTime objects will have time set to 23:59:59) def end_of_quarter - beginning_of_month.change(:month => [3, 6, 9, 12].detect { |m| m >= self.month }).end_of_month + last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } + beginning_of_month.change(:month => last_quarter_month).end_of_month end alias :at_end_of_quarter :end_of_quarter # Returns a new Date/DateTime representing the start of the year (1st of january; DateTime objects will have time set to 0:00) def beginning_of_year - self.acts_like?(:time) ? change(:month => 1, :day => 1, :hour => 0) : change(:month => 1, :day => 1) + if acts_like?(:time) + change(:month => 1, :day => 1, :hour => 0) + else + change(:month => 1, :day => 1) + end end alias :at_beginning_of_year :beginning_of_year # Returns a new Time representing the end of the year (31st of december; DateTime objects will have time set to 23:59:59) def end_of_year - self.acts_like?(:time) ? change(:month => 12, :day => 31, :hour => 23, :min => 59, :sec => 59) : change(:month => 12, :day => 31) + if acts_like?(:time) + change(:month => 12, :day => 31, :hour => 23, :min => 59, :sec => 59) + else + change(:month => 12, :day => 31) + end end alias :at_end_of_year :end_of_year diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 3262c254f7..97e3c71992 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -5,12 +5,15 @@ require 'active_support/core_ext/module/remove_method' class Date DATE_FORMATS = { - :short => "%e %b", - :long => "%B %e, %Y", - :db => "%Y-%m-%d", - :number => "%Y%m%d", - :long_ordinal => lambda { |date| date.strftime("%B #{ActiveSupport::Inflector.ordinalize(date.day)}, %Y") }, # => "April 25th, 2007" - :rfc822 => "%e %b %Y" + :short => '%e %b', + :long => '%B %e, %Y', + :db => '%Y-%m-%d', + :number => '%Y%m%d', + :long_ordinal => lambda { |date| + day_format = ActiveSupport::Inflector.ordinalize(date.day) + date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007" + }, + :rfc822 => '%e %b %Y' } # Ruby 1.9 has Date#to_time which converts to localtime only. @@ -40,7 +43,7 @@ class Date # or Proc instance that takes a date argument as the value. # # # config/initializers/time_formats.rb - # Date::DATE_FORMATS[:month_and_year] = "%B %Y" + # Date::DATE_FORMATS[:month_and_year] = '%B %Y' # Date::DATE_FORMATS[:short_ordinal] = lambda { |date| date.strftime("%B #{date.day.ordinalize}") } def to_formatted_s(format = :default) if formatter = DATE_FORMATS[format] @@ -58,7 +61,7 @@ class Date # Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005" def readable_inspect - strftime("%a, %d %b %Y") + strftime('%a, %d %b %Y') end alias_method :default_inspect, :inspect alias_method :inspect, :readable_inspect diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 6f730e4b6f..020fa1a06d 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -4,8 +4,8 @@ class DateTime class << self # *DEPRECATED*: Use +DateTime.civil_from_format+ directly. def local_offset - ActiveSupport::Deprecation.warn 'DateTime.local_offset is deprecated. ' \ - 'Use DateTime.civil_from_format directly.', caller + ActiveSupport::Deprecation.warn 'DateTime.local_offset is deprecated. Use DateTime.civil_from_format directly.', caller + ::Time.local(2012).utc_offset.to_r / 86400 end @@ -35,14 +35,14 @@ class DateTime # minute is passed, then sec is set to 0. def change(options) ::DateTime.civil( - options[:year] || year, - options[:month] || month, - options[:day] || day, - options[:hour] || hour, - options[:min] || (options[:hour] ? 0 : min), - options[:sec] || ((options[:hour] || options[:min]) ? 0 : sec), - options[:offset] || offset, - options[:start] || start + options.fetch(:year, year), + options.fetch(:month, month), + options.fetch(:day, day), + options.fetch(:hour, hour), + options.fetch(:min, options[:hour] ? 0 : min), + options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec), + options.fetch(:offset, offset), + options.fetch(:start, start) ) end @@ -53,8 +53,16 @@ class DateTime def advance(options) d = to_date.advance(options) datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) - seconds_to_advance = (options[:seconds] || 0) + (options[:minutes] || 0) * 60 + (options[:hours] || 0) * 3600 - seconds_to_advance == 0 ? datetime_advanced_by_date : datetime_advanced_by_date.since(seconds_to_advance) + seconds_to_advance = \ + options.fetch(:seconds, 0) + + options.fetch(:minutes, 0) * 60 + + options.fetch(:hours, 0) * 3600 + + if seconds_to_advance.zero? + datetime_advanced_by_date + else + datetime_advanced_by_date.since seconds_to_advance + end end # Returns a new DateTime representing the time a number of seconds ago @@ -83,6 +91,17 @@ class DateTime change(:hour => 23, :min => 59, :sec => 59) end + # Returns a new DateTime representing the start of the hour (hh:00:00) + def beginning_of_hour + change(:min => 0) + end + alias :at_beginning_of_hour :beginning_of_hour + + # Returns a new DateTime representing the end of the hour (hh:59:59) + def end_of_hour + change(:min => 59, :sec => 59) + end + # Adjusts DateTime to UTC by adding its offset value; offset is set to 0 # # Example: @@ -108,4 +127,5 @@ class DateTime def <=>(other) super other.to_datetime end + end diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index dc55e9c33c..6338dc6397 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -30,7 +30,7 @@ class DateTime # datetime argument as the value. # # # config/initializers/time_formats.rb - # Time::DATE_FORMATS[:month_and_year] = "%B %Y" + # Time::DATE_FORMATS[:month_and_year] = '%B %Y' # Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") } def to_formatted_s(format = :default) if formatter = ::Time::DATE_FORMATS[format] @@ -61,7 +61,11 @@ class DateTime # Attempts to convert self to a Ruby Time object; returns self if out of range of Ruby Time class. # If self has an offset other than 0, self will just be returned unaltered, since there's no clean way to map it to a Time. def to_time - self.offset == 0 ? ::Time.utc_time(year, month, day, hour, min, sec, sec_fraction * 1000000) : self + if offset == 0 + ::Time.utc_time(year, month, day, hour, min, sec, sec_fraction * 1000000) + else + self + end end # Returns DateTime with local offset for given year if format is local else offset is zero diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb index 6fa55a9255..823735d3e2 100644 --- a/activesupport/lib/active_support/core_ext/date_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb @@ -14,8 +14,10 @@ class DateTime # # DateTime.new(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 def in_time_zone(zone = ::Time.zone) - return self unless zone - - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) + if zone + ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) + else + self + end end end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 77a5087981..b9c632e4f5 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -11,7 +11,7 @@ module Enumerable # It can also calculate the sum without the use of a block. # # [5, 15, 10].sum # => 30 - # ["foo", "bar"].sum # => "foobar" + # ['foo', 'bar'].sum # => "foobar" # [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5] # # The default sum of an empty list is zero. You can override this default: @@ -34,8 +34,11 @@ module Enumerable # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} # def index_by - return to_enum :index_by unless block_given? - Hash[map { |elem| [yield(elem), elem] }] + if block_given? + Hash[map { |elem| [yield(elem), elem] }] + else + to_enum :index_by + end end # Returns true if the enumerable has more than 1 element. Functionally equivalent to enum.to_a.size > 1. @@ -48,7 +51,7 @@ module Enumerable cnt > 1 end else - any?{ (cnt += 1) > 1 } + any? { (cnt += 1) > 1 } end end @@ -62,8 +65,11 @@ class Range #:nodoc: # Optimize range sum to use arithmetic progression if a block is not given and # we have a range of numeric values. def sum(identity = 0) - return super if block_given? || !(first.instance_of?(Integer) && last.instance_of?(Integer)) - actual_last = exclude_end? ? (last - 1) : last - (actual_last - first + 1) * (actual_last + first) / 2 + if block_given? || !(first.instance_of?(Integer) && last.instance_of?(Integer)) + super + else + actual_last = exclude_end? ? (last - 1) : last + (actual_last - first + 1) * (actual_last + first) / 2 + end end end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index fc3277f4d2..9e504851e7 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -2,15 +2,15 @@ class File # Write to a file atomically. Useful for situations where you don't # want other processes or threads to see half-written files. # - # File.atomic_write("important.file") do |file| - # file.write("hello") + # File.atomic_write('important.file') do |file| + # file.write('hello') # end # # If your temp directory is not on the same filesystem as the file you're # trying to write, you can provide a different temporary directory. # - # File.atomic_write("/data/something.important", "/data/tmp") do |file| - # file.write("hello") + # File.atomic_write('/data/something.important', '/data/tmp') do |file| + # file.write('hello') # end def self.atomic_write(file_name, temp_dir = Dir.tmpdir) require 'tempfile' unless defined?(Tempfile) @@ -26,8 +26,14 @@ class File old_stat = stat(file_name) rescue Errno::ENOENT # No old permissions, write a temp file to determine the defaults - check_name = join(dirname(file_name), ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}") - open(check_name, "w") { } + temp_file_name = [ + '.permissions_check', + Thread.current.object_id, + Process.pid, + rand(1000000) + ].join('.') + check_name = join(dirname(file_name), temp_file_name) + open(check_name, 'w') { } old_stat = stat(check_name) unlink(check_name) end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index fd1cda991e..501483498d 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/deep_dup' require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/indifferent_access' diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 5f07bb4f5a..469dc41f2d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -8,7 +8,7 @@ require 'active_support/core_ext/string/inflections' class Hash # Returns a string containing an XML representation of its receiver: # - # {"foo" => 1, "bar" => 2}.to_xml + # {'foo' => 1, 'bar' => 2}.to_xml # # => # # <?xml version="1.0" encoding="UTF-8"?> # # <hash> @@ -26,20 +26,20 @@ class Hash # # * If +value+ is a callable object it must expect one or two arguments. Depending # on the arity, the callable is invoked with the +options+ hash as first argument - # with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The + # with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The # callable can add nodes by using <tt>options[:builder]</tt>. # - # "foo".to_xml(lambda { |options, key| options[:builder].b(key) }) + # 'foo'.to_xml(lambda { |options, key| options[:builder].b(key) }) # # => "<b>foo</b>" # # * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>. - # + # # class Foo # def to_xml(options) - # options[:builder].bar "fooing!" + # options[:builder].bar 'fooing!' # end # end - # + # # {:foo => Foo.new}.to_xml(:skip_instruct => true) # # => "<hash><bar>fooing!</bar></hash>" # @@ -71,7 +71,7 @@ class Hash options = options.dup options[:indent] ||= 2 - options[:root] ||= "hash" + options[:root] ||= 'hash' options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) builder = options[:builder] @@ -100,24 +100,24 @@ class Hash [] else case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a? - when "Array" + when 'Array' entries.collect { |v| typecast_xml_value(v) } - when "Hash" + when 'Hash' [typecast_xml_value(entries)] else raise "can't typecast #{entries.inspect}" end end - elsif value['type'] == 'file' || - (value["__content__"] && (value.keys.size == 1 || value["__content__"].present?)) - content = value["__content__"] - if parser = ActiveSupport::XmlMini::PARSING[value["type"]] + elsif value['type'] == 'file' || + (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?)) + content = value['__content__'] + if parser = ActiveSupport::XmlMini::PARSING[value['type']] parser.arity == 1 ? parser.call(content) : parser.call(content, value) else content end elsif value['type'] == 'string' && value['nil'] != 'true' - "" + '' # blank or nil parsed values are represented by nil elsif value.blank? || value['nil'] == 'true' nil @@ -131,7 +131,7 @@ class Hash # Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with # how multipart uploaded files from HTML appear - xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value + xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value end when 'Array' value.map! { |i| typecast_xml_value(i) } @@ -145,9 +145,9 @@ class Hash def unrename_keys(params) case params.class.to_s - when "Hash" - Hash[params.map { |k,v| [k.to_s.tr("-", "_"), unrename_keys(v)] } ] - when "Array" + when 'Hash' + Hash[params.map { |k,v| [k.to_s.tr('-', '_'), unrename_keys(v)] } ] + when 'Array' params.map { |v| unrename_keys(v) } else params diff --git a/activesupport/lib/active_support/core_ext/hash/deep_dup.rb b/activesupport/lib/active_support/core_ext/hash/deep_dup.rb deleted file mode 100644 index 9ab179c566..0000000000 --- a/activesupport/lib/active_support/core_ext/hash/deep_dup.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Hash - # Returns a deep copy of hash. - def deep_dup - duplicate = self.dup - duplicate.each_pair do |k,v| - duplicate[k] = v.is_a?(Hash) ? v.deep_dup : v - end - duplicate - end -end diff --git a/activesupport/lib/active_support/core_ext/hash/diff.rb b/activesupport/lib/active_support/core_ext/hash/diff.rb index b904f49fa8..855dcb38bc 100644 --- a/activesupport/lib/active_support/core_ext/hash/diff.rb +++ b/activesupport/lib/active_support/core_ext/hash/diff.rb @@ -7,7 +7,9 @@ class Hash # {1 => 2}.diff(1 => 3) # => {1 => 2} # {}.diff(1 => 2) # => {1 => 2} # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4} - def diff(h2) - dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) }) + def diff(other) + dup. + delete_if { |k, v| other[k] == v }. + merge!(other.dup.delete_if { |k, v| has_key?(k) }) end end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 89729df258..5a61906222 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -9,7 +9,7 @@ class Hash # for instance: # # {:a => 1}.with_indifferent_access.except(:a) # => {} - # {:a => 1}.with_indifferent_access.except("a") # => {} + # {:a => 1}.with_indifferent_access.except('a') # => {} # def except(*keys) dup.except!(*keys) diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 65c3736593..230a84dabc 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -25,6 +25,7 @@ class Hash end result end + alias_method :to_options, :symbolize_keys # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. @@ -34,8 +35,6 @@ class Hash end self end - - alias_method :to_options, :symbolize_keys alias_method :to_options!, :symbolize_keys! # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch. @@ -43,13 +42,13 @@ class Hash # as keys, this will fail. # # ==== Examples - # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years" - # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key: name" - # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing + # { :name => 'Rob', :years => '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years" + # { :name => 'Rob', :age => '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: name" + # { :name => 'Rob', :age => '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing def assert_valid_keys(*valid_keys) valid_keys.flatten! each_key do |k| - raise(ArgumentError, "Unknown key: #{k}") unless valid_keys.include?(k) + raise ArgumentError.new("Unknown key: #{k}") unless valid_keys.include?(k) end end end diff --git a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb index 01863a162b..6074103484 100644 --- a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb @@ -18,6 +18,5 @@ class Hash # right wins if there is no left merge!( other_hash ){|key,left,right| left } end - alias_method :reverse_update, :reverse_merge! end diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 45181f0e16..b862b5ae2a 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -18,7 +18,7 @@ class Hash end # Replaces the hash with only the given keys. - # Returns a hash contained the removed key/value pairs + # Returns a hash containing the removed key/value pairs. # {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d => 4} def slice!(*keys) keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) @@ -31,6 +31,6 @@ class Hash # Removes and returns the key/value pairs matching the given keys. # {:a => 1, :b => 2, :c => 3, :d => 4}.extract!(:a, :b) # => {:a => 1, :b => 2} def extract!(*keys) - keys.each_with_object({}) {|key, result| result[key] = delete(key) } + keys.each_with_object({}) { |key, result| result[key] = delete(key) } end end diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb index 8dff217ddc..7c6c2f1ca7 100644 --- a/activesupport/lib/active_support/core_ext/integer/multiple.rb +++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb @@ -1,5 +1,9 @@ class Integer # Check whether the integer is evenly divisible by the argument. + # + # 0.multiple_of?(0) #=> true + # 6.multiple_of?(5) #=> false + # 10.multiple_of?(2) #=> true def multiple_of?(number) number != 0 ? self % number == 0 : zero? end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 526b8378a5..ad3f9ebec9 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,4 +1,5 @@ require 'rbconfig' + module Kernel # Sets $VERBOSE to nil for the duration of the block and back to its original value afterwards. # @@ -49,10 +50,10 @@ module Kernel # # suppress(ZeroDivisionError) do # 1/0 - # puts "This code is NOT reached" + # puts 'This code is NOT reached' # end # - # puts "This code gets executed and nothing related to ZeroDivisionError was seen" + # puts 'This code gets executed and nothing related to ZeroDivisionError was seen' def suppress(*exception_classes) begin yield rescue Exception => e @@ -62,7 +63,7 @@ module Kernel # Captures the given stream and returns it: # - # stream = capture(:stdout) { puts "Cool" } + # stream = capture(:stdout) { puts 'Cool' } # stream # => "Cool\n" # def capture(stream) diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb index a51818d2b2..16fce81445 100644 --- a/activesupport/lib/active_support/core_ext/logger.rb +++ b/activesupport/lib/active_support/core_ext/logger.rb @@ -56,8 +56,8 @@ class Logger alias :old_datetime_format= :datetime_format= # Logging date-time format (string passed to +strftime+). Ignored if the formatter # does not respond to datetime_format=. - def datetime_format=(datetime_format) - formatter.datetime_format = datetime_format if formatter.respond_to?(:datetime_format=) + def datetime_format=(format) + formatter.datetime_format = format if formatter.respond_to?(:datetime_format=) end alias :old_datetime_format :datetime_format diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index ce481f0e84..382156ecd8 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -26,18 +26,19 @@ class Module aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 yield(aliased_target, punctuation) if block_given? - with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}" + with_method = "#{aliased_target}_with_#{feature}#{punctuation}" + without_method = "#{aliased_target}_without_#{feature}#{punctuation}" alias_method without_method, target alias_method target, with_method case - when public_method_defined?(without_method) - public target - when protected_method_defined?(without_method) - protected target - when private_method_defined?(without_method) - private target + when public_method_defined?(without_method) + public target + when protected_method_defined?(without_method) + protected target + when private_method_defined?(without_method) + private target end end diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb index 00db75bfec..db07d549b0 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb @@ -15,7 +15,6 @@ class Module attr_internal_reader(*attrs) attr_internal_writer(*attrs) end - alias_method :attr_internal, :attr_internal_accessor class << self; attr_accessor :attr_internal_naming_format 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 84acb629ad..f914425827 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -4,7 +4,7 @@ class Module def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -26,7 +26,7 @@ class Module def mattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) def self.#{sym}=(obj) @@#{sym} = obj diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index ee8adae1cb..fbef27c76a 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -8,11 +8,11 @@ class Module # # class Greeter < ActiveRecord::Base # def hello - # "hello" + # 'hello' # end # # def goodbye - # "goodbye" + # 'goodbye' # end # end # @@ -62,7 +62,7 @@ class Module # delegate :name, :address, :to => :client, :prefix => true # end # - # john_doe = Person.new("John Doe", "Vimmersvej 13") + # john_doe = Person.new('John Doe', 'Vimmersvej 13') # invoice = Invoice.new(john_doe) # invoice.client_name # => "John Doe" # invoice.client_address # => "Vimmersvej 13" @@ -74,8 +74,8 @@ class Module # end # # invoice = Invoice.new(john_doe) - # invoice.customer_name # => "John Doe" - # invoice.customer_address # => "Vimmersvej 13" + # invoice.customer_name # => 'John Doe' + # invoice.customer_address # => 'Vimmersvej 13' # # If the delegate object is +nil+ an exception is raised, and that happens # no matter whether +nil+ responds to the delegated method. You can get a @@ -104,17 +104,17 @@ class Module def delegate(*methods) options = methods.pop unless options.is_a?(Hash) && to = options[:to] - raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)." + raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter).' end to = to.to_s prefix, allow_nil = options.values_at(:prefix, :allow_nil) if prefix == true && to =~ /^[^a-z_]/ - raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method." + raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.' end - method_prefix = + method_prefix = \ if prefix "#{prefix == true ? to : prefix}_" else @@ -129,7 +129,7 @@ class Module # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. - definition = (method =~ /[^\]]=$/) ? "arg" : "*args, &block" + definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' if allow_nil module_eval(<<-EOS, file, line - 2) diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index 743db47bac..3c8e811fa4 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -5,10 +5,11 @@ class Module # # M::N.parent_name # => "M" def parent_name - unless defined? @parent_name + if defined? @parent_name + @parent_name + else @parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil end - @parent_name end # Returns the module which contains this one according to its name. @@ -73,7 +74,7 @@ class Module # This method is useful for forward compatibility, since Ruby 1.8 returns # constant names as strings, whereas 1.9 returns them as symbols. def local_constant_names - ActiveSupport::Deprecation.warn('Module#local_constant_names is deprecated, use Module#local_constants instead', caller) + ActiveSupport::Deprecation.warn 'Module#local_constant_names is deprecated, use Module#local_constants instead', caller local_constants.map { |c| c.to_s } end end diff --git a/activesupport/lib/active_support/core_ext/module/qualified_const.rb b/activesupport/lib/active_support/core_ext/module/qualified_const.rb index 8adf050b6b..65525013db 100644 --- a/activesupport/lib/active_support/core_ext/module/qualified_const.rb +++ b/activesupport/lib/active_support/core_ext/module/qualified_const.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/string/inflections' #++ module QualifiedConstUtils def self.raise_if_absolute(path) - raise NameError, "wrong constant name #$&" if path =~ /\A::[^:]+/ + raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/ end def self.names(path) @@ -20,7 +20,7 @@ end #-- # Qualified names are required to be relative because we are extending existing # methods that expect constant names, ie, relative paths of length 1. For example, -# Object.const_get("::String") raises NameError and so does qualified_const_get. +# Object.const_get('::String') raises NameError and so does qualified_const_get. #++ class Module def qualified_const_defined?(path, search_parents=true) diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 822f766af7..2bf3d1f278 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -8,13 +8,13 @@ class Numeric # These methods use Time#advance for precise date calculations when using from_now, ago, etc. # as well as adding or subtracting their results from a Time object. For example: # - # # equivalent to Time.now.advance(:months => 1) + # # equivalent to Time.current.advance(:months => 1) # 1.month.from_now # - # # equivalent to Time.now.advance(:years => 2) + # # equivalent to Time.current.advance(:years => 2) # 2.years.from_now # - # # equivalent to Time.now.advance(:months => 4, :years => 5) + # # equivalent to Time.current.advance(:months => 4, :years => 5) # (4.months + 5.years).from_now # # While these methods provide precise calculation when used as in the examples above, care diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 9ad1e12699..ec2157221f 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -1,6 +1,7 @@ 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/deep_dup' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/inclusion' diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index d67711f3b8..e238fef5a2 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -2,7 +2,7 @@ class Object # An object is blank if it's false, empty, or a whitespace string. - # For example, "", " ", +nil+, [], and {} are all blank. + # For example, '', ' ', +nil+, [], and {} are all blank. # # This simplifies: # @@ -90,10 +90,10 @@ end class String # A string is blank if it's empty or contains whitespaces only: # - # "".blank? # => true - # " ".blank? # => true - # " ".blank? # => true - # " something here ".blank? # => false + # ''.blank? # => true + # ' '.blank? # => true + # ' '.blank? # => true + # ' something here '.blank? # => false # def blank? self !~ /[^[:space:]]/ diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb new file mode 100644 index 0000000000..2c4383ac94 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -0,0 +1,22 @@ +class Object + # Returns a deep copy of object if it's duplicable. + def deep_dup + duplicable? ? dup : self + end +end + +class Array + # Returns a deep copy of array. + def deep_dup + map { |it| it.deep_dup } + end +end + +class Hash + # Returns a deep copy of hash. + def deep_dup + each_with_object(dup) do |(key, value), hash| + hash[key.deep_dup] = value.deep_dup + end + end +end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index f611cdd606..3fec465ec0 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -2,11 +2,11 @@ class Object # Returns true if this object is included in the argument(s). Argument must be # any object which responds to +#include?+ or optionally, multiple arguments can be passed in. Usage: # - # characters = ["Konata", "Kagami", "Tsukasa"] - # "Konata".in?(characters) # => true - # - # character = "Konata" - # character.in?("Konata", "Kagami", "Tsukasa") # => true + # characters = ['Konata', 'Kagami', 'Tsukasa'] + # 'Konata'.in?(characters) # => true + # + # character = 'Konata' + # character.in?('Konata', 'Kagami', 'Tsukasa') # => true # # This will throw an ArgumentError if a single argument is passed in and it doesn't respond # to +#include?+. @@ -18,7 +18,7 @@ class Object if another_object.respond_to? :include? another_object.include? self else - raise ArgumentError.new("The single parameter passed to #in? must respond to #include?") + raise ArgumentError.new 'The single parameter passed to #in? must respond to #include?' end end end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index e77a9da0ec..40a8101ca3 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -1,5 +1,5 @@ class Object - # Invokes the method identified by the symbol +method+, passing it any arguments + # Invokes the public method identified by the symbol +method+, passing it any arguments # and/or the block specified, just like the regular Ruby <tt>Object#send</tt> does. # # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised @@ -29,7 +29,7 @@ class Object if a.empty? && block_given? yield self else - __send__(*a, &b) + public_send(*a, &b) end end end diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 1397142c04..e058367111 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -29,7 +29,7 @@ class Object # # It can also be used with an explicit receiver: # - # I18n.with_options :locale => user.locale, :scope => "newsletter" do |i18n| + # I18n.with_options :locale => user.locale, :scope => 'newsletter' do |i18n| # subject i18n.t :subject # body i18n.t :body, :user_name => user.name # end diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb index 684b7cbc4a..3af66aaf2f 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -5,7 +5,7 @@ class Range # (1..5).include?(2..6) # => false # # The native Range#include? behavior is untouched. - # ("a".."f").include?("c") # => true + # ('a'..'f').include?('c') # => true # (5..9).include?(11) # => false def include_with_range?(value) if value.is_a?(::Range) diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 9b5266c58c..5c32a2453d 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -1,18 +1,79 @@ -require "active_support/multibyte" +require 'active_support/multibyte' class String + # If you pass a single Fixnum, returns a substring of one character at that + # position. The first character of the string is at position 0, the next at + # position 1, and so on. If a range is supplied, a substring containing + # characters at offsets given by the range is returned. In both cases, if an + # offset is negative, it is counted from the end of the string. Returns nil + # if the initial offset falls outside the string. Returns an empty string if + # the beginning of the range is greater than the end of the string. + # + # str = "hello" + # str.at(0) #=> "h" + # str.at(1..3) #=> "ell" + # str.at(-2) #=> "l" + # str.at(-2..-1) #=> "lo" + # str.at(5) #=> nil + # str.at(5..-1) #=> "" + # + # If a Regexp is given, the matching portion of the string is returned. + # If a String is given, that given string is returned if it occurs in + # the string. In both cases, nil is returned if there is no match. + # + # str = "hello" + # str.at(/lo/) #=> "lo" + # str.at(/ol/) #=> nil + # str.at("lo") #=> "lo" + # str.at("ol") #=> nil def at(position) self[position] end + # Returns a substring from the given position to the end of the string. + # If the position is negative, it is counted from the end of the string. + # + # str = "hello" + # str.from(0) #=> "hello" + # str.from(3) #=> "lo" + # str.from(-2) #=> "lo" + # + # You can mix it with +to+ method and do fun things like: + # + # str = "hello" + # str.from(0).to(-1) #=> "hello" + # str.from(1).to(-2) #=> "ell" def from(position) self[position..-1] end + # Returns a substring from the beginning of the string to the given position. + # If the position is negative, it is counted from the end of the string. + # + # str = "hello" + # str.to(0) #=> "h" + # str.to(3) #=> "hell" + # str.to(-2) #=> "hell" + # + # You can mix it with +from+ method and do fun things like: + # + # str = "hello" + # str.from(0).to(-1) #=> "hello" + # str.from(1).to(-2) #=> "ell" def to(position) self[0..position] end + # Returns the first character. If a limit is supplied, returns a substring + # from the beginning of the string until it reaches the limit value. If the + # given limit is greater than or equal to the string length, returns self. + # + # str = "hello" + # str.first #=> "h" + # str.first(1) #=> "h" + # str.first(2) #=> "he" + # str.first(0) #=> "" + # str.first(6) #=> "hello" def first(limit = 1) if limit == 0 '' @@ -23,6 +84,16 @@ class String end end + # Returns the last character of the string. If a limit is supplied, returns a substring + # from the end of the string until it reaches the limit value (counting backwards). If + # the given limit is greater than or equal to the string length, returns self. + # + # str = "hello" + # str.last #=> "o" + # str.last(1) #=> "o" + # str.last(2) #=> "lo" + # str.last(0) #=> "" + # str.last(6) #=> "hello" def last(limit = 1) if limit == 0 '' diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 541f969faa..022b376aec 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -4,21 +4,45 @@ require 'active_support/core_ext/time/calculations' class String # Form can be either :utc (default) or :local. def to_time(form = :utc) - return nil if self.blank? - d = ::Date._parse(self, false).values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset).map { |arg| arg || 0 } - d[6] *= 1000000 - ::Time.send("#{form}_time", *d[0..6]) - d[7] + unless blank? + date_values = ::Date._parse(self, false). + values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset). + map! { |arg| arg || 0 } + date_values[6] *= 1000000 + offset = date_values.pop + + ::Time.send("#{form}_time", *date_values) - offset + end end + # Converts a string to a Date value. + # + # "1-1-2012".to_date #=> Sun, 01 Jan 2012 + # "01/01/2012".to_date #=> Sun, 01 Jan 2012 + # "2012-12-13".to_date #=> Thu, 13 Dec 2012 + # "12/13/2012".to_date #=> ArgumentError: invalid date def to_date - return nil if self.blank? - ::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday)) + unless blank? + date_values = ::Date._parse(self, false).values_at(:year, :mon, :mday) + + ::Date.new(*date_values) + end end + # Converts a string to a DateTime value. + # + # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000 + # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000 + # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000 + # "12/13/2012".to_datetime #=> ArgumentError: invalid date def to_datetime - return nil if self.blank? - d = ::Date._parse(self, false).values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :sec_fraction).map { |arg| arg || 0 } - d[5] += d.pop - ::DateTime.civil(*d) + unless blank? + date_values = ::Date._parse(self, false). + values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :sec_fraction). + map! { |arg| arg || 0 } + date_values[5] += date_values.pop + + ::DateTime.civil(*date_values) + end end end diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb index 5e184ec1b3..114bcb87f0 100644 --- a/activesupport/lib/active_support/core_ext/string/exclude.rb +++ b/activesupport/lib/active_support/core_ext/string/exclude.rb @@ -1,5 +1,10 @@ class String - # The inverse of <tt>String#include?</tt>. Returns true if the string does not include the other string. + # The inverse of <tt>String#include?</tt>. Returns true if the string + # does not include the other string. + # + # "hello".exclude? "lo" #=> false + # "hello".exclude? "ol" #=> true + # "hello".exclude? ?h #=> false def exclude?(string) !include?(string) end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 1a34e88a87..2478f42290 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -5,7 +5,6 @@ class String # the string, and then changing remaining consecutive whitespace # groups into one space each. # - # Examples: # %{ Multi-line # string }.squish # => "Multi-line string" # " foo bar \n \t boo".squish # => "foo bar boo" @@ -22,26 +21,33 @@ class String # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>: # - # "Once upon a time in a world far far away".truncate(27) + # 'Once upon a time in a world far far away'.truncate(27) # # => "Once upon a time in a wo..." # - # Pass a <tt>:separator</tt> to truncate +text+ at a natural break: + # Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break: # - # "Once upon a time in a world far far away".truncate(27, :separator => ' ') + # 'Once upon a time in a world far far away'.truncate(27, :separator => ' ') + # # => "Once upon a time in a..." + # + # 'Once upon a time in a world far far away'.truncate(27, :separator => /\s/) # # => "Once upon a time in a..." # # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...") # for a total length not exceeding <tt>:length</tt>: # - # "And they found that many people were sleeping better.".truncate(25, :omission => "... (continued)") + # 'And they found that many people were sleeping better.'.truncate(25, :omission => '... (continued)') # # => "And they f... (continued)" - def truncate(length, options = {}) - return self.dup unless self.length > length + def truncate(truncate_at, options = {}) + return dup unless length > truncate_at - options[:omission] ||= "..." - length_with_room_for_omission = length - options[:omission].length - stop = options[:separator] ? - (rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission) : length_with_room_for_omission + options[:omission] ||= '...' + length_with_room_for_omission = truncate_at - options[:omission].length + stop = \ + if options[:separator] + rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission + else + length_with_room_for_omission + end self[0...stop] + options[:omission] end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 2194dafe4d..070bfd7af6 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -4,7 +4,7 @@ require 'active_support/inflector/transliterate' # 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 table from the name of a class. # -# "ScaleScore".tableize # => "scale_scores" +# 'ScaleScore'.tableize # => "scale_scores" # class String # Returns the plural form of the word in the string. @@ -13,15 +13,14 @@ class String # the singular form will be returned if <tt>count == 1</tt>. # For any other value of +count+ the plural will be returned. # - # ==== Examples - # "post".pluralize # => "posts" - # "octopus".pluralize # => "octopi" - # "sheep".pluralize # => "sheep" - # "words".pluralize # => "words" - # "the blue mailman".pluralize # => "the blue mailmen" - # "CamelOctopus".pluralize # => "CamelOctopi" - # "apple".pluralize(1) # => "apple" - # "apple".pluralize(2) # => "apples" + # 'post'.pluralize # => "posts" + # 'octopus'.pluralize # => "octopi" + # 'sheep'.pluralize # => "sheep" + # 'words'.pluralize # => "words" + # 'the blue mailman'.pluralize # => "the blue mailmen" + # 'CamelOctopus'.pluralize # => "CamelOctopi" + # 'apple'.pluralize(1) # => "apple" + # 'apple'.pluralize(2) # => "apples" def pluralize(count = nil) if count == 1 self @@ -32,12 +31,12 @@ class String # The reverse of +pluralize+, returns the singular form of a word in a string. # - # "posts".singularize # => "post" - # "octopi".singularize # => "octopus" - # "sheep".singularize # => "sheep" - # "word".singularize # => "word" - # "the blue mailmen".singularize # => "the blue mailman" - # "CamelOctopi".singularize # => "CamelOctopus" + # 'posts'.singularize # => "post" + # 'octopi'.singularize # => "octopus" + # 'sheep'.singularize # => "sheep" + # 'word'.singularize # => "word" + # 'the blue mailmen'.singularize # => "the blue mailman" + # 'CamelOctopi'.singularize # => "CamelOctopus" def singularize ActiveSupport::Inflector.singularize(self) end @@ -46,10 +45,9 @@ class String # in the string. It raises a NameError when the name is not in CamelCase # or is not initialized. See ActiveSupport::Inflector.constantize # - # Examples - # "Module".constantize # => Module - # "Class".constantize # => Class - # "blargle".constantize # => NameError: wrong constant name blargle + # 'Module'.constantize # => Module + # 'Class'.constantize # => Class + # 'blargle'.constantize # => NameError: wrong constant name blargle def constantize ActiveSupport::Inflector.constantize(self) end @@ -58,10 +56,9 @@ class String # in the string. It returns nil when the name is not in CamelCase # or is not initialized. See ActiveSupport::Inflector.safe_constantize # - # Examples - # "Module".safe_constantize # => Module - # "Class".safe_constantize # => Class - # "blargle".safe_constantize # => nil + # 'Module'.safe_constantize # => Module + # 'Class'.safe_constantize # => Class + # 'blargle'.safe_constantize # => nil def safe_constantize ActiveSupport::Inflector.safe_constantize(self) end @@ -71,14 +68,16 @@ class String # # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces. # - # "active_record".camelize # => "ActiveRecord" - # "active_record".camelize(:lower) # => "activeRecord" - # "active_record/errors".camelize # => "ActiveRecord::Errors" - # "active_record/errors".camelize(:lower) # => "activeRecord::Errors" + # 'active_record'.camelize # => "ActiveRecord" + # 'active_record'.camelize(:lower) # => "activeRecord" + # 'active_record/errors'.camelize # => "ActiveRecord::Errors" + # 'active_record/errors'.camelize(:lower) # => "activeRecord::Errors" def camelize(first_letter = :upper) case first_letter - when :upper then ActiveSupport::Inflector.camelize(self, true) - when :lower then ActiveSupport::Inflector.camelize(self, false) + when :upper + ActiveSupport::Inflector.camelize(self, true) + when :lower + ActiveSupport::Inflector.camelize(self, false) end end alias_method :camelcase, :camelize @@ -89,8 +88,8 @@ class String # # +titleize+ is also aliased as +titlecase+. # - # "man from the boondocks".titleize # => "Man From The Boondocks" - # "x-men: the last stand".titleize # => "X Men: The Last Stand" + # 'man from the boondocks'.titleize # => "Man From The Boondocks" + # 'x-men: the last stand'.titleize # => "X Men: The Last Stand" def titleize ActiveSupport::Inflector.titleize(self) end @@ -100,23 +99,23 @@ class String # # +underscore+ will also change '::' to '/' to convert namespaces to paths. # - # "ActiveModel".underscore # => "active_model" - # "ActiveModel::Errors".underscore # => "active_model/errors" + # 'ActiveModel'.underscore # => "active_model" + # 'ActiveModel::Errors'.underscore # => "active_model/errors" def underscore ActiveSupport::Inflector.underscore(self) end # Replaces underscores with dashes in the string. # - # "puni_puni" # => "puni-puni" + # 'puni_puni' # => "puni-puni" def dasherize ActiveSupport::Inflector.dasherize(self) end # Removes the module part from the constant expression in the string. # - # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections" - # "Inflections".demodulize # => "Inflections" + # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" + # 'Inflections'.demodulize # => "Inflections" # # See also +deconstantize+. def demodulize @@ -125,11 +124,11 @@ class String # Removes the rightmost segment from the constant expression in the string. # - # "Net::HTTP".deconstantize # => "Net" - # "::Net::HTTP".deconstantize # => "::Net" - # "String".deconstantize # => "" - # "::String".deconstantize # => "" - # "".deconstantize # => "" + # 'Net::HTTP'.deconstantize # => "Net" + # '::Net::HTTP'.deconstantize # => "::Net" + # 'String'.deconstantize # => "" + # '::String'.deconstantize # => "" + # ''.deconstantize # => "" # # See also +demodulize+. def deconstantize @@ -138,8 +137,6 @@ class String # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. # - # ==== Examples - # # class Person # def to_param # "#{id}-#{name.parameterize}" @@ -158,9 +155,9 @@ class String # Creates the name of a table like Rails does for models to table names. This method # uses the +pluralize+ method on the last word in the string. # - # "RawScaledScorer".tableize # => "raw_scaled_scorers" - # "egg_and_ham".tableize # => "egg_and_hams" - # "fancyCategory".tableize # => "fancy_categories" + # 'RawScaledScorer'.tableize # => "raw_scaled_scorers" + # 'egg_and_ham'.tableize # => "egg_and_hams" + # 'fancyCategory'.tableize # => "fancy_categories" def tableize ActiveSupport::Inflector.tableize(self) end @@ -169,12 +166,12 @@ class String # Note that this returns a string and not a class. (To convert to an actual class # follow +classify+ with +constantize+.) # - # "egg_and_hams".classify # => "EggAndHam" - # "posts".classify # => "Post" + # 'egg_and_hams'.classify # => "EggAndHam" + # 'posts'.classify # => "Post" # # Singular names are not handled correctly. # - # "business".classify # => "Busines" + # 'business'.classify # => "Busines" def classify ActiveSupport::Inflector.classify(self) end @@ -182,8 +179,8 @@ class String # Capitalizes the first word, turns underscores into spaces, and strips '_id'. # Like +titleize+, this is meant for creating pretty output. # - # "employee_salary" # => "Employee salary" - # "author_id" # => "Author" + # 'employee_salary' # => "Employee salary" + # 'author_id' # => "Author" def humanize ActiveSupport::Inflector.humanize(self) end @@ -192,10 +189,9 @@ class String # +separate_class_name_and_id_with_underscore+ sets whether # the method should put '_' between the name and 'id'. # - # Examples - # "Message".foreign_key # => "message_id" - # "Message".foreign_key(false) # => "messageid" - # "Admin::Post".foreign_key # => "post_id" + # 'Message'.foreign_key # => "message_id" + # 'Message'.foreign_key(false) # => "messageid" + # 'Admin::Post'.foreign_key # => "post_id" def foreign_key(separate_class_name_and_id_with_underscore = true) ActiveSupport::Inflector.foreign_key(self, separate_class_name_and_id_with_underscore) end diff --git a/activesupport/lib/active_support/core_ext/string/inquiry.rb b/activesupport/lib/active_support/core_ext/string/inquiry.rb index 5f0a017de6..2562a7cef6 100644 --- a/activesupport/lib/active_support/core_ext/string/inquiry.rb +++ b/activesupport/lib/active_support/core_ext/string/inquiry.rb @@ -4,7 +4,7 @@ class String # Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class, # which gives you a prettier way to test for equality. Example: # - # env = "production".inquiry + # env = 'production'.inquiry # env.production? # => true # env.development? # => false def inquiry diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 4903687b73..6bda970e40 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -14,8 +14,7 @@ class ERB # In your ERB templates, use this method to escape any unsafe content. For example: # <%=h @person.name %> # - # ==== Example: - # puts html_escape("is a > 0 & a < 10?") + # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? def html_escape(s) s = s.to_s @@ -37,11 +36,10 @@ class ERB # A utility method for escaping HTML without affecting existing escaped entities. # - # ==== Examples - # html_escape_once("1 < 2 & 3") + # html_escape_once('1 < 2 & 3') # # => "1 < 2 & 3" # - # html_escape_once("<< Accept & Checkout") + # html_escape_once('<< Accept & Checkout') # # => "<< Accept & Checkout" def html_escape_once(s) result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP) { |special| HTML_ESCAPE[special] } @@ -53,7 +51,7 @@ class ERB # A utility method for escaping HTML entities in JSON strings # using \uXXXX JavaScript escape sequences for string literals: # - # json_escape("is a > 0 & a < 10?") + # json_escape('is a > 0 & a < 10?') # # => is a \u003E 0 \u0026 a \u003C 10? # # Note that after this operation is performed the output is not @@ -92,26 +90,31 @@ end module ActiveSupport #:nodoc: class SafeBuffer < String - UNSAFE_STRING_METHODS = ["capitalize", "chomp", "chop", "delete", "downcase", "gsub", "lstrip", "next", "reverse", "rstrip", "slice", "squeeze", "strip", "sub", "succ", "swapcase", "tr", "tr_s", "upcase", "prepend"].freeze + UNSAFE_STRING_METHODS = %w( + capitalize chomp chop delete downcase gsub lstrip next reverse rstrip + slice squeeze strip sub succ swapcase tr tr_s upcase prepend + ) alias_method :original_concat, :concat private :original_concat class SafeConcatError < StandardError def initialize - super "Could not concatenate to the buffer because it is not html safe." + super 'Could not concatenate to the buffer because it is not html safe.' end end def [](*args) - return super if args.size < 2 - - if html_safe? - new_safe_buffer = super - new_safe_buffer.instance_eval { @html_safe = true } - new_safe_buffer + if args.size < 2 + super else - to_str[*args] + if html_safe? + new_safe_buffer = super + new_safe_buffer.instance_eval { @html_safe = true } + new_safe_buffer + else + to_str[*args] + end end end diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 7add47be44..a0f610d60c 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -3,7 +3,15 @@ require 'active_support/core_ext/time/conversions' class Time COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - DAYS_INTO_WEEK = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6 } + DAYS_INTO_WEEK = { + :monday => 0, + :tuesday => 1, + :wednesday => 2, + :thursday => 3, + :friday => 4, + :saturday => 5, + :sunday => 6 + } class << self # Overriding case equality method so that it returns true for ActiveSupport::TimeWithZone instances @@ -14,8 +22,11 @@ class Time # Return the number of days in the given month. # If no year is specified, it will use the current year. def days_in_month(month, year = now.year) - return 29 if month == 2 && ::Date.gregorian_leap?(year) - COMMON_YEAR_DAYS_IN_MONTH[month] + if month == 2 && ::Date.gregorian_leap?(year) + 29 + else + COMMON_YEAR_DAYS_IN_MONTH[month] + end end # Returns a new Time if requested year can be accommodated by Ruby's Time class @@ -23,8 +34,13 @@ class Time # otherwise returns a DateTime. def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0) time = ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec) + # This check is needed because Time.utc(y) returns a time object in the 2000s for 0 <= y <= 138. - time.year == year ? time : ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) + if time.year == year + time + else + ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) + end rescue ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) end @@ -71,13 +87,13 @@ class Time def change(options) ::Time.send( utc? ? :utc_time : :local_time, - options[:year] || year, - options[:month] || month, - options[:day] || day, - options[:hour] || hour, - options[:min] || (options[:hour] ? 0 : min), - options[:sec] || ((options[:hour] || options[:min]) ? 0 : sec), - options[:usec] || ((options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + options.fetch(:year, year), + options.fetch(:month, month), + options.fetch(:day, day), + options.fetch(:hour, hour), + options.fetch(:min, options[:hour] ? 0 : min), + options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec), + options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) ) end @@ -88,18 +104,26 @@ class Time def advance(options) unless options[:weeks].nil? options[:weeks], partial_weeks = options[:weeks].divmod(1) - options[:days] = (options[:days] || 0) + 7 * partial_weeks + options[:days] = options.fetch(:days, 0) + 7 * partial_weeks end unless options[:days].nil? options[:days], partial_days = options[:days].divmod(1) - options[:hours] = (options[:hours] || 0) + 24 * partial_days + options[:hours] = options.fetch(:hours, 0) + 24 * partial_days end d = to_date.advance(options) time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) - seconds_to_advance = (options[:seconds] || 0) + (options[:minutes] || 0) * 60 + (options[:hours] || 0) * 3600 - seconds_to_advance == 0 ? time_advanced_by_date : time_advanced_by_date.since(seconds_to_advance) + seconds_to_advance = \ + options.fetch(:seconds, 0) + + options.fetch(:minutes, 0) * 60 + + options.fetch(:hours, 0) * 3600 + + if seconds_to_advance.zero? + time_advanced_by_date + else + time_advanced_by_date.since(seconds_to_advance) + end end # Returns a new Time representing the time a number of seconds ago, this is basically a wrapper around the Numeric extension @@ -167,6 +191,7 @@ class Time start_day_number = DAYS_INTO_WEEK[start_day] current_day_number = wday != 0 ? wday - 1 : 6 days_span = current_day_number - start_day_number + days_span >= 0 ? days_span : 7 + days_span end @@ -198,13 +223,19 @@ class Time # Returns a new Time representing the start of the given day in the previous week (default is :monday). def prev_week(day = :monday) - ago(1.week).beginning_of_week.since(DAYS_INTO_WEEK[day].day).change(:hour => 0) + ago(1.week). + beginning_of_week. + since(DAYS_INTO_WEEK[day].day). + change(:hour => 0) end alias_method :last_week, :prev_week # Returns a new Time representing the start of the given day in next week (default is :monday). def next_week(day = :monday) - since(1.week).beginning_of_week.since(DAYS_INTO_WEEK[day].day).change(:hour => 0) + since(1.week). + beginning_of_week. + since(DAYS_INTO_WEEK[day].day). + change(:hour => 0) end # Returns a new Time representing the start of the day (0:00) @@ -218,7 +249,27 @@ class Time # Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9) def end_of_day - change(:hour => 23, :min => 59, :sec => 59, :usec => 999999.999) + change( + :hour => 23, + :min => 59, + :sec => 59, + :usec => 999999.999 + ) + end + + # Returns a new Time representing the start of the hour (x:00) + def beginning_of_hour + change(:min => 0) + end + alias :at_beginning_of_hour :beginning_of_hour + + # Returns a new Time representing the end of the hour, x:59:59.999999 (.999999999 in ruby1.9) + def end_of_hour + change( + :min => 59, + :sec => 59, + :usec => 999999.999 + ) end # Returns a new Time representing the start of the month (1st of the month, 0:00) @@ -232,19 +283,27 @@ class Time def end_of_month #self - ((self.mday-1).days + self.seconds_since_midnight) last_day = ::Time.days_in_month(month, year) - change(:day => last_day, :hour => 23, :min => 59, :sec => 59, :usec => 999999.999) + change( + :day => last_day, + :hour => 23, + :min => 59, + :sec => 59, + :usec => 999999.999 + ) end alias :at_end_of_month :end_of_month # Returns a new Time representing the start of the quarter (1st of january, april, july, october, 0:00) def beginning_of_quarter - beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= month }) + first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } + beginning_of_month.change(:month => first_quarter_month) end alias :at_beginning_of_quarter :beginning_of_quarter # Returns a new Time representing the end of the quarter (end of the last day of march, june, september, december) def end_of_quarter - beginning_of_month.change(:month => [3, 6, 9, 12].detect { |m| m >= month }).end_of_month + last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } + beginning_of_month.change(:month => last_quarter_month).end_of_month end alias :at_end_of_quarter :end_of_quarter @@ -256,7 +315,14 @@ class Time # Returns a new Time representing the end of the year (end of the 31st of december) def end_of_year - change(:month => 12, :day => 31, :hour => 23, :min => 59, :sec => 59, :usec => 999999.999) + change( + :month => 12, + :day => 31, + :hour => 23, + :min => 59, + :sec => 59, + :usec => 999999.999 + ) end alias :at_end_of_year :end_of_year @@ -329,7 +395,11 @@ class Time # can be chronologically compared with a Time def compare_with_coercion(other) # we're avoiding Time#to_datetime cause it's expensive - other.is_a?(Time) ? compare_without_coercion(other.to_time) : to_datetime <=> other + if other.is_a?(Time) + compare_without_coercion(other.to_time) + else + to_datetime <=> other + end end alias_method :compare_without_coercion, :<=> alias_method :<=>, :compare_with_coercion @@ -343,4 +413,5 @@ class Time end alias_method :eql_without_coercion, :eql? alias_method :eql?, :eql_with_coercion + end diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 0fdcd383f0..4f852fd780 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -3,13 +3,19 @@ require 'active_support/values/time_zone' class Time DATE_FORMATS = { - :db => "%Y-%m-%d %H:%M:%S", - :number => "%Y%m%d%H%M%S", - :time => "%H:%M", - :short => "%d %b %H:%M", - :long => "%B %d, %Y %H:%M", - :long_ordinal => lambda { |time| time.strftime("%B #{ActiveSupport::Inflector.ordinalize(time.day)}, %Y %H:%M") }, - :rfc822 => lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S #{time.formatted_offset(false)}") } + :db => '%Y-%m-%d %H:%M:%S', + :number => '%Y%m%d%H%M%S', + :time => '%H:%M', + :short => '%d %b %H:%M', + :long => '%B %d, %Y %H:%M', + :long_ordinal => lambda { |time| + day_format = ActiveSupport::Inflector.ordinalize(time.day) + time.strftime("%B #{day_format}, %Y %H:%M") + }, + :rfc822 => lambda { |time| + offset_format = time.formatted_offset(false) + time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}") + } } # Converts to a formatted string. See DATE_FORMATS for builtin formats. @@ -34,7 +40,7 @@ class Time # or Proc instance that takes a time argument as the value. # # # config/initializers/time_formats.rb - # Time::DATE_FORMATS[:month_and_year] = "%B %Y" + # Time::DATE_FORMATS[:month_and_year] = '%B %Y' # Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") } def to_formatted_s(format = :default) if formatter = DATE_FORMATS[format] diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index b4ed74a7b0..e48866abe3 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -51,13 +51,21 @@ class Time # Returns a TimeZone instance or nil, or raises an ArgumentError for invalid timezones. def find_zone!(time_zone) - return time_zone if time_zone.nil? || time_zone.is_a?(ActiveSupport::TimeZone) - # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) - unless time_zone.respond_to?(:period_for_local) - time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) + if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) + time_zone + else + # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) + unless time_zone.respond_to?(:period_for_local) + time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) + end + + # Return if a TimeZone instance, or wrap in a TimeZone instance if a TZInfo::Timezone + if time_zone.is_a?(ActiveSupport::TimeZone) + time_zone + else + ActiveSupport::TimeZone.create(time_zone.name, nil, time_zone) + end end - # Return if a TimeZone instance, or wrap in a TimeZone instance if a TZInfo::Timezone - time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone.create(time_zone.name, nil, time_zone) rescue TZInfo::InvalidTimezoneIdentifier raise ArgumentError, "Invalid Timezone: #{time_zone}" end @@ -80,8 +88,10 @@ class Time # # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 def in_time_zone(zone = ::Time.zone) - return self unless zone - - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) + if zone + ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) + else + self + end end end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 94f8d7133e..9102537810 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -6,17 +6,31 @@ module ActiveSupport # Whether to print a backtrace along with the warning. attr_accessor :debug - # Returns the set behavior or if one isn't set, defaults to +:stderr+ + # Returns the current behavior or if one isn't set, defaults to +:stderr+ def behavior @behavior ||= [DEFAULT_BEHAVIORS[:stderr]] end - # Sets the behavior to the specified value. Can be a single value or an array. + # Sets the behavior to the specified value. Can be a single value, array, or + # an object that responds to +call+. # - # Examples + # Available behaviors: + # + # [+stderr+] Log all deprecation warnings to +$stderr+. + # [+log+] Log all deprecation warnings to +Rails.logger+. + # [+notify] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+. + # [+silence+] Do nothing. + # + # Setting behaviors only affects deprecations that happen after boot time. + # Deprecation warnings raised by gems are not affected by this setting because + # they happen before Rails boots up. # # ActiveSupport::Deprecation.behavior = :stderr # ActiveSupport::Deprecation.behavior = [:stderr, :log] + # ActiveSupport::Deprecation.behavior = MyCustomHandler + # ActiveSupport::Deprecation.behavior = proc { |message, callstack| + # # custom stuff + # } def behavior=(behavior) @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b } end @@ -41,8 +55,9 @@ module ActiveSupport }, :notify => Proc.new { |message, callstack| ActiveSupport::Notifications.instrument("deprecation.rails", - :message => message, :callstack => callstack) - } + :message => message, :callstack => callstack) + }, + :silence => Proc.new { |message, callstack| } } end end diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index f9c5e5e2f8..188653bd9b 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -6,4 +6,5 @@ rescue LoadError => e raise e end +ActiveSupport.run_load_hooks(:i18n) I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index b3eb1333ca..7eb61cd1a0 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -16,8 +16,8 @@ module ActiveSupport inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies') inflect.plural(/(x|ch|ss|sh)$/i, '\1es') inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices') - inflect.plural(/(m|l)ouse$/i, '\1ice') - inflect.plural(/(m|l)ice$/i, '\1ice') + inflect.plural(/^(m|l)ouse$/i, '\1ice') + inflect.plural(/^(m|l)ice$/i, '\1ice') inflect.plural(/^(ox)$/i, '\1en') inflect.plural(/^(oxen)$/i, '\1') inflect.plural(/(quiz)$/i, '\1zes') @@ -36,7 +36,7 @@ module ActiveSupport inflect.singular(/(s)eries$/i, '\1eries') inflect.singular(/(m)ovies$/i, '\1ovie') inflect.singular(/(x|ch|ss|sh)es$/i, '\1') - inflect.singular(/(m|l)ice$/i, '\1ouse') + inflect.singular(/^(m|l)ice$/i, '\1ouse') inflect.singular(/(bus)(es)?$/i, '\1') inflect.singular(/(o)es$/i, '\1') inflect.singular(/(shoe)s$/i, '\1') @@ -58,6 +58,6 @@ module ActiveSupport inflect.irregular('cow', 'kine') inflect.irregular('zombie', 'zombies') - inflect.uncountable(%w(equipment information rice money species series fish sheep jeans)) + inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police)) end end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 61876d89a9..4fcd32edf2 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -106,7 +106,7 @@ module ActiveSupport # a nicer looking title. +titleize+ is meant for creating pretty output. It is not # used in the Rails internals. # - # +titleize+ is also aliased as as +titlecase+. + # +titleize+ is also aliased as +titlecase+. # # Examples: # "man from the boondocks".titleize # => "Man From The Boondocks" diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index b2adfea273..ab12f3f454 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -17,6 +17,7 @@ module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, :escape_html_entities_in_json, :escape_html_entities_in_json=, + :encode_big_decimal_as_string, :encode_big_decimal_as_string=, :to => :'ActiveSupport::JSON::Encoding' end @@ -104,6 +105,9 @@ module ActiveSupport # If true, use ISO 8601 format for dates and times. Otherwise, fall back to the Active Support legacy format. attr_accessor :use_standard_json_time_format + # If false, serializes BigDecimal objects as numeric instead of wrapping them in a string + attr_accessor :encode_big_decimal_as_string + attr_accessor :escape_regex attr_reader :escape_html_entities_in_json @@ -133,6 +137,7 @@ module ActiveSupport self.use_standard_json_time_format = true self.escape_html_entities_in_json = false + self.encode_big_decimal_as_string = true end end end @@ -182,6 +187,12 @@ class Numeric def encode_json(encoder) to_s end #:nodoc: end +class Float + # Encoding Infinity or NaN to JSON should return "null". The default returns + # "Infinity" or "NaN" what breaks parsing the JSON. E.g. JSON.parse('[NaN]'). + def as_json(options = nil) finite? ? self : NilClass::AS_JSON end #:nodoc: +end + class BigDecimal # A BigDecimal would be naturally represented as a JSON number. Most libraries, # however, parse non-integer JSON numbers directly as floats. Clients using @@ -191,7 +202,15 @@ class BigDecimal # That's why a JSON string is returned. The JSON literal is not numeric, but if # the other end knows by contract that the data is supposed to be a BigDecimal, # it still has the chance to post-process the string and get the real value. - def as_json(options = nil) to_s end #:nodoc: + # + # Use ActiveSupport.use_standard_json_big_decimal_format = true to override this behaviour + def as_json(options = nil) #:nodoc: + if finite? + ActiveSupport.encode_big_decimal_as_string ? to_s : self + else + NilClass::AS_JSON + end + end end class Regexp diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 9a748dfa60..b20c980f36 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -62,8 +62,8 @@ module ActiveSupport #:nodoc: # Returns +true+ if _obj_ responds to the given method. Private methods are included in the search # only if the optional second parameter evaluates to +true+. - def respond_to?(method, include_private=false) - super || @wrapped_string.respond_to?(method, include_private) + def respond_to_missing?(method, include_private) + @wrapped_string.respond_to?(method, include_private) end # Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise. diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 538e41e0eb..60e6cd55ad 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -36,7 +36,7 @@ module ActiveSupport #:nodoc: end end - def respond_to?(name) + def respond_to_missing?(name, include_private) true end end diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 85e84bc203..7aecdd11d3 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -48,6 +48,7 @@ module ActiveSupport # end # end # + # Exceptions raised inside exception handlers are not propagated up. def rescue_from(*klasses, &block) options = klasses.extract_options! diff --git a/activesupport/lib/active_support/ruby/shim.rb b/activesupport/lib/active_support/ruby/shim.rb deleted file mode 100644 index 13e96b3596..0000000000 --- a/activesupport/lib/active_support/ruby/shim.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Backported Ruby builtins so you can code with the latest & greatest -# but still run on any Ruby 1.8.x. -# -# Date next_year, next_month -# DateTime to_date, to_datetime, xmlschema -# Enumerable group_by, none? -# String ord -# Time to_date, to_time, to_datetime -require 'active_support' -require 'active_support/core_ext/date/calculations' -require 'active_support/core_ext/date_time/conversions' -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/string/conversions' -require 'active_support/core_ext/string/interpolation' -require 'active_support/core_ext/time/conversions' diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb index 244ee1a224..f6bf0318f7 100644 --- a/activesupport/lib/active_support/testing/performance.rb +++ b/activesupport/lib/active_support/testing/performance.rb @@ -61,7 +61,7 @@ module ActiveSupport ensure begin teardown - run_callbacks :teardown, :enumerator => :reverse_each + run_callbacks :teardown rescue Exception => e result = @runner.puke(self.class, method_name, e) end diff --git a/activesupport/lib/active_support/testing/performance/jruby.rb b/activesupport/lib/active_support/testing/performance/jruby.rb index b347539f13..34e3f9f45f 100644 --- a/activesupport/lib/active_support/testing/performance/jruby.rb +++ b/activesupport/lib/active_support/testing/performance/jruby.rb @@ -42,7 +42,7 @@ module ActiveSupport klasses.each do |klass| fname = output_filename(klass) FileUtils.mkdir_p(File.dirname(fname)) - file = File.open(fname, 'wb') do |file| + File.open(fname, 'wb') do |file| klass.new(@data).printProfile(file) end end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 9634b52ecf..bcd5d78b54 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -4,10 +4,6 @@ module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' autoload :TimeZone, 'active_support/values/time_zone' - - on_load_all do - [Duration, TimeWithZone, TimeZone] - end end require 'date' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 1cb71012ef..cd07c24257 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -35,8 +35,10 @@ module ActiveSupport # t.is_a?(ActiveSupport::TimeWithZone) # => true # class TimeWithZone + + # Report class name as 'Time' to thwart type checking def self.name - 'Time' # Report class name as 'Time' to thwart type checking + 'Time' end include Comparable @@ -311,10 +313,10 @@ module ActiveSupport end # Ensure proxy class responds to all methods that underlying time instance responds to. - def respond_to?(sym, include_priv = false) + def respond_to_missing?(sym, include_priv) # consistently respond false to acts_like?(:date), regardless of whether #time is a Time or DateTime - return false if sym.to_s == 'acts_like_date?' - super || time.respond_to?(sym, include_priv) + return false if sym.to_sym == :acts_like_date? + time.respond_to?(sym, include_priv) end # Send the missing method to +time+ instance, and wrap result in a new TimeWithZone with the existing +time_zone+. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index ce46c46092..9543e50395 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -28,7 +28,7 @@ module ActiveSupport MAPPING = { "International Date Line West" => "Pacific/Midway", "Midway Island" => "Pacific/Midway", - "Samoa" => "Pacific/Pago_Pago", + "American Samoa" => "Pacific/Pago_Pago", "Hawaii" => "Pacific/Honolulu", "Alaska" => "America/Juneau", "Pacific Time (US & Canada)" => "America/Los_Angeles", @@ -167,7 +167,9 @@ module ActiveSupport "Marshall Is." => "Pacific/Majuro", "Auckland" => "Pacific/Auckland", "Wellington" => "Pacific/Auckland", - "Nuku'alofa" => "Pacific/Tongatapu" + "Nuku'alofa" => "Pacific/Tongatapu", + "Tokelau Is." => "Pacific/Fakaofo", + "Samoa" => "Pacific/Apia" } UTC_OFFSET_WITH_COLON = '%s%02d:%02d' @@ -202,6 +204,7 @@ module ActiveSupport @current_period = nil end + # Returns the offset of this time zone from UTC in seconds. def utc_offset if @utc_offset @utc_offset diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 40e25ce0cd..57ed4a6b60 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -7,9 +7,6 @@ ensure $VERBOSE = old end -lib = File.expand_path("#{File.dirname(__FILE__)}/../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/string/encoding' diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 5365713e6e..d62b782e2d 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -568,6 +568,13 @@ class FileStoreTest < ActiveSupport::TestCase include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior + def test_clear + filepath = File.join(cache_dir, ".gitkeep") + FileUtils.touch(filepath) + @cache.clear + assert File.exist?(filepath) + end + def test_key_transformation key = @cache.send(:key_file_path, "views/index?id=1") assert_equal "views/index?id=1", @cache.send(:file_path_key, key) @@ -677,6 +684,13 @@ class MemoryStoreTest < ActiveSupport::TestCase assert @cache.exist?(2) assert !@cache.exist?(1) end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", :unless_exist => true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", :unless_exist => true) + end end uses_memcached 'memcached backed store' do diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb index e5ac9511df..6be8ea8b84 100644 --- a/activesupport/test/callback_inheritance_test.rb +++ b/activesupport/test/callback_inheritance_test.rb @@ -29,7 +29,7 @@ class GrandParent end def dispatch - run_callbacks(:dispatch, action_name) do + run_callbacks :dispatch do @log << action_name end self diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 25688a9da5..b7c3b130c3 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -112,7 +112,7 @@ module CallbacksTest end def dispatch - run_callbacks :dispatch, action_name do + run_callbacks :dispatch do @logger << "Done" end self @@ -153,7 +153,7 @@ module CallbacksTest end def save - run_callbacks :save, :action + run_callbacks :save end end @@ -338,7 +338,7 @@ module CallbacksTest end def save - run_callbacks :save, "hyphen-ated" do + run_callbacks :save do @stuff end end diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index cd8cb5d18b..3da0825489 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -91,6 +91,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day end + def test_beginning_of_hour + assert_equal DateTime.civil(2005,2,4,19,0,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_hour + end + + def test_end_of_hour + assert_equal DateTime.civil(2005,2,4,19,59,59), DateTime.civil(2005,2,4,19,30,10).end_of_hour + end + def test_beginning_of_month assert_equal DateTime.civil(2005,2,1,0,0,0), DateTime.civil(2005,2,22,10,10,10).beginning_of_month end diff --git a/activesupport/test/core_ext/deep_dup_test.rb b/activesupport/test/core_ext/deep_dup_test.rb new file mode 100644 index 0000000000..91d558dbb5 --- /dev/null +++ b/activesupport/test/core_ext/deep_dup_test.rb @@ -0,0 +1,53 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class DeepDupTest < ActiveSupport::TestCase + + def test_array_deep_dup + array = [1, [2, 3]] + dup = array.deep_dup + dup[1][2] = 4 + assert_equal nil, array[1][2] + assert_equal 4, dup[1][2] + end + + def test_hash_deep_dup + hash = { :a => { :b => 'b' } } + dup = hash.deep_dup + dup[:a][:c] = 'c' + assert_equal nil, hash[:a][:c] + assert_equal 'c', dup[:a][:c] + end + + def test_array_deep_dup_with_hash_inside + array = [1, { :a => 2, :b => 3 } ] + dup = array.deep_dup + dup[1][:c] = 4 + assert_equal nil, array[1][:c] + assert_equal 4, dup[1][:c] + end + + def test_hash_deep_dup_with_array_inside + hash = { :a => [1, 2] } + dup = hash.deep_dup + dup[:a][2] = 'c' + assert_equal nil, hash[:a][2] + assert_equal 'c', dup[:a][2] + end + + def test_deep_dup_initialize + zero_hash = Hash.new 0 + hash = { :a => zero_hash } + dup = hash.deep_dup + assert_equal 0, dup[:a][44] + end + + def test_object_deep_dup + object = Object.new + dup = object.deep_dup + dup.instance_variable_set(:@a, 1) + assert !object.instance_variable_defined?(:@a) + assert dup.instance_variable_defined?(:@a) + end + +end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 80b3c16328..8239054117 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -363,21 +363,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end - def test_deep_dup - hash = { :a => { :b => 'b' } } - dup = hash.deep_dup - dup[:a][:c] = 'c' - assert_equal nil, hash[:a][:c] - assert_equal 'c', dup[:a][:c] - end - - def test_deep_dup_initialize - zero_hash = Hash.new 0 - hash = { :a => zero_hash } - dup = hash.deep_dup - assert_equal 0, dup[:a][44] - end - def test_store_on_indifferent_access hash = HashWithIndifferentAccess.new hash.store(:test1, 1) @@ -506,15 +491,21 @@ class HashExtTest < ActiveSupport::TestCase original = { :a => 'x', :b => 'y', :c => 10 } expected = { :a => 'x', :b => 'y' } - # Should return a new hash with only the given keys. + # Should return a new hash without the given keys. assert_equal expected, original.except(:c) assert_not_equal expected, original - # Should replace the hash with only the given keys. + # Should replace the hash without the given keys. assert_equal expected, original.except!(:c) assert_equal expected, original end + def test_except_with_more_than_one_argument + original = { :a => 'x', :b => 'y', :c => 10 } + expected = { :a => 'x' } + assert_equal expected, original.except(:b, :c) + end + def test_except_with_original_frozen original = { :a => 'x', :b => 'y' } original.freeze diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index b027fccab3..98ab82609e 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -101,7 +101,7 @@ class ObjectTryTest < ActiveSupport::TestCase assert !@string.respond_to?(method) assert_raise(NoMethodError) { @string.try(method) } end - + def test_nonexisting_method_with_arguments method = :undefined_method assert !@string.respond_to?(method) @@ -138,4 +138,16 @@ class ObjectTryTest < ActiveSupport::TestCase nil.try { ran = true } assert_equal false, ran end + + def test_try_with_private_method + klass = Class.new do + private + + def private_method + 'private method' + end + end + + assert_raise(NoMethodError) { klass.new.try(:private_method) } + end end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index cf1ec448c2..9c3389ba82 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -41,6 +41,18 @@ class RangeTest < ActiveSupport::TestCase assert((1..10).include?(1...10)) end + def test_should_compare_identical_inclusive + assert((1..10) === (1..10)) + end + + def test_should_compare_identical_exclusive + assert((1...10) === (1...10)) + end + + def test_should_compare_other_with_exlusive_end + assert((1..10) === (1...10)) + end + def test_exclusive_end_should_not_include_identical_with_inclusive_end assert !(1...10).include?(1..10) end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 6c2828b74e..9010a4a716 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -279,6 +279,12 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ') end + def test_truncate_with_omission_and_regexp_seperator + assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/) + assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/) + assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/) + end + def test_truncate_multibyte assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding('UTF-8'), "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8').truncate(10) diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index c542acca68..4c1ed4b1ae 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -93,6 +93,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_beginning_of_hour + assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour + end + def test_beginning_of_month assert_equal Time.local(2005,2,1,0,0,0), Time.local(2005,2,22,10,10,10).beginning_of_month end @@ -127,6 +131,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2007,9,2,23,59,59,999999.999), Time.local(2007,9,02,0,0,0).end_of_week #sunday end + def test_end_of_hour + assert_equal Time.local(2005,2,4,19,59,59,999999.999), Time.local(2005,2,4,19,30,10).end_of_hour + end + def test_end_of_month assert_equal Time.local(2005,3,31,23,59,59,999999.999), Time.local(2005,3,20,10,10,10).end_of_month assert_equal Time.local(2005,2,28,23,59,59,999999.999), Time.local(2005,2,20,10,10,10).end_of_month diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 7cf3842a16..b62337e31b 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -191,7 +191,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end end - def future_with_time_current_as_time_with_zone + def test_future_with_time_current_as_time_with_zone twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) Time.stubs(:current).returns(twz) assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? @@ -450,6 +450,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_ruby_19_weekday_name_query_methods %w(sunday? monday? tuesday? wednesday? thursday? friday? saturday?).each do |name| assert_respond_to @twz, name + assert_equal @twz.send(name), @twz.method(name).call end end @@ -482,36 +483,50 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.advance(:seconds => 30).inspect end - def beginning_of_year + def test_beginning_of_year assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 01 Jan 1999 00:00:00 EST -05:00", @twz.beginning_of_year.inspect end - def end_of_year + def test_end_of_year assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 23:59:59 EST -05:00", @twz.end_of_year.inspect end - def beginning_of_month + def test_beginning_of_month assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect - assert_equal "Fri, 01 Dec 1999 00:00:00 EST -05:00", @twz.beginning_of_month.inspect + assert_equal "Wed, 01 Dec 1999 00:00:00 EST -05:00", @twz.beginning_of_month.inspect end - def end_of_month + def test_end_of_month assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 23:59:59 EST -05:00", @twz.end_of_month.inspect end - def beginning_of_day + def test_beginning_of_day assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 00:00:00 EST -05:00", @twz.beginning_of_day.inspect end - def end_of_day + def test_end_of_day assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Fri, 31 Dec 1999 23:59:59 EST -05:00", @twz.end_of_day.inspect end + def test_beginning_of_hour + utc = Time.utc(2000, 1, 1, 0, 30) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:00 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", twz.beginning_of_hour.inspect + end + + def test_end_of_hour + utc = Time.utc(2000, 1, 1, 0, 30) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:00 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:59:59 EST -05:00", twz.end_of_hour.inspect + end + def test_since assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.since(1).inspect end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index e821a285d7..e21f3efe36 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -93,6 +93,26 @@ class DeprecationTest < ActiveSupport::TestCase assert_match(/foo=nil/, @b) end + def test_default_stderr_behavior + ActiveSupport::Deprecation.behavior = :stderr + behavior = ActiveSupport::Deprecation.behavior.first + + content = capture(:stderr) { + assert_nil behavior.call('Some error!', ['call stack!']) + } + assert_match(/Some error!/, content) + assert_match(/call stack!/, content) + end + + def test_default_silence_behavior + ActiveSupport::Deprecation.behavior = :silence + behavior = ActiveSupport::Deprecation.behavior.first + + assert_blank capture(:stderr) { + assert_nil behavior.call('Some error!', ['call stack!']) + } + end + def test_deprecated_instance_variable_proxy assert_not_deprecated { @dtc.request.size } diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 86c5e1f135..4d10cfca25 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -109,7 +109,9 @@ module InflectorTestCases # regression tests against improper inflection regexes "|ice" => "|ices", - "|ouse" => "|ouses" + "|ouse" => "|ouses", + "slice" => "slices", + "police" => "police" } CamelToUnderscore = { diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index a2e61d88d5..0566ebf291 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -27,6 +27,10 @@ class TestJSONEncoding < ActiveSupport::TestCase NilTests = [[ nil, %(null) ]] NumericTests = [[ 1, %(1) ], [ 2.5, %(2.5) ], + [ 0.0/0.0, %(null) ], + [ 1.0/0.0, %(null) ], + [ -1.0/0.0, %(null) ], + [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]] StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")], @@ -270,6 +274,17 @@ class TestJSONEncoding < ActiveSupport::TestCase JSON.parse(json_string_and_date)) end + def test_opt_out_big_decimal_string_serialization + big_decimal = BigDecimal('2.5') + + begin + ActiveSupport.encode_big_decimal_as_string = false + assert_equal big_decimal.to_s, big_decimal.to_json + ensure + ActiveSupport.encode_big_decimal_as_string = true + end + end + protected def object_keys(json_object) diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 90aa13b3e6..a8d69d0ec3 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -458,6 +458,15 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert !''.mb_chars.respond_to?(:undefined_method) # Not defined end + def test_method_works_for_proxyed_methods + assert_equal 'll', 'hello'.mb_chars.method(:slice).call(2..3) # Defined on Chars + chars = 'hello'.mb_chars + assert_equal 'Hello', chars.method(:capitalize!).call # Defined on Chars + assert_equal 'Hello', chars + assert_equal 'jello', 'hello'.mb_chars.method(:gsub).call(/h/, 'j') # Defined on String + assert_raise(NameError){ ''.mb_chars.method(:undefined_method) } # Not defined + end + def test_acts_like_string assert 'Bambi'.mb_chars.acts_like_string? end diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index 3526c7a366..f60f9a58e3 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -77,4 +77,12 @@ class OrderedOptionsTest < ActiveSupport::TestCase assert copy.kind_of?(original.class) assert_not_equal copy.object_id, original.object_id end + + def test_introspection + a = ActiveSupport::OrderedOptions.new + assert a.respond_to?(:blah) + assert a.respond_to?(:blah=) + assert_equal 42, a.method(:blah=).call(42) + assert_equal 42, a.method(:blah).call + end end diff --git a/activesupport/test/ts_isolated.rb b/activesupport/test/ts_isolated.rb index 1d96c20bb6..938bb4ee99 100644 --- a/activesupport/test/ts_isolated.rb +++ b/activesupport/test/ts_isolated.rb @@ -1,5 +1,3 @@ -$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib') - require 'minitest/autorun' require 'active_support/test_case' require 'rbconfig' diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png Binary files differnew file mode 100644 index 0000000000..a26c09ef2d --- /dev/null +++ b/guides/assets/images/getting_started/confirm_dialog.png diff --git a/guides/assets/images/getting_started/post_with_comments.png b/guides/assets/images/getting_started/post_with_comments.png Binary files differnew file mode 100644 index 0000000000..bd9b2e10f5 --- /dev/null +++ b/guides/assets/images/getting_started/post_with_comments.png diff --git a/guides/assets/images/getting_started/undefined_method_post_path.png b/guides/assets/images/getting_started/undefined_method_post_path.png Binary files differnew file mode 100644 index 0000000000..f568bf315c --- /dev/null +++ b/guides/assets/images/getting_started/undefined_method_post_path.png diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb index 7447fd078b..cf3d1be42e 100644 --- a/guides/code/getting_started/app/controllers/comments_controller.rb +++ b/guides/code/getting_started/app/controllers/comments_controller.rb @@ -1,16 +1,17 @@ class CommentsController < ApplicationController http_basic_authenticate_with :name => "dhh", :password => "secret", :only => :destroy + def create @post = Post.find(params[:post_id]) @comment = @post.comments.create(params[:comment]) redirect_to post_path(@post) end - + def destroy @post = Post.find(params[:post_id]) @comment = @post.comments.find(params[:id]) @comment.destroy redirect_to post_path(@post) end - + end diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb index fc71e9b4e8..a8ac9aba5a 100644 --- a/guides/code/getting_started/app/controllers/posts_controller.rb +++ b/guides/code/getting_started/app/controllers/posts_controller.rb @@ -1,5 +1,7 @@ class PostsController < ApplicationController + http_basic_authenticate_with :name => "dhh", :password => "secret", :except => [:index, :show] + def index @posts = Post.all end @@ -35,4 +37,11 @@ class PostsController < ApplicationController render 'edit' end end + + def destroy + @post = Post.find(params[:id]) + @post.destroy + + redirect_to :action => :index + end end diff --git a/guides/code/getting_started/app/views/comments/_comment.html.erb b/guides/code/getting_started/app/views/comments/_comment.html.erb index 4c3fbf26cd..0cebe0bd96 100644 --- a/guides/code/getting_started/app/views/comments/_comment.html.erb +++ b/guides/code/getting_started/app/views/comments/_comment.html.erb @@ -1,13 +1,13 @@ <p> - <b>Commenter:</b> + <strong>Commenter:</strong> <%= comment.commenter %> </p> - + <p> - <b>Comment:</b> + <strong>Comment:</strong> <%= comment.body %> </p> - + <p> <%= link_to 'Destroy Comment', [comment.post, comment], :confirm => 'Are you sure?', diff --git a/guides/code/getting_started/app/views/comments/_form.html.erb b/guides/code/getting_started/app/views/comments/_form.html.erb index d15bdd6b59..00cb3a08f0 100644 --- a/guides/code/getting_started/app/views/comments/_form.html.erb +++ b/guides/code/getting_started/app/views/comments/_form.html.erb @@ -1,13 +1,13 @@ <%= form_for([@post, @post.comments.build]) do |f| %> - <div class="field"> + <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> - </div> - <div class="field"> + </p> + <p> <%= f.label :body %><br /> <%= f.text_area :body %> - </div> - <div class="actions"> + </p> + <p> <%= f.submit %> - </div> + </p> <% end %> diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/posts/_form.html.erb index 46ec257b91..f22139938c 100644 --- a/guides/code/getting_started/app/views/posts/_form.html.erb +++ b/guides/code/getting_started/app/views/posts/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_for :post, :url => { :action => :update, :id => @post.id }, :method => :put do |f| %> +<%= form_for @post do |f| %> <% if @post.errors.any? %> <div id="errorExplanation"> <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2> @@ -10,12 +10,12 @@ </div> <% end %> <p> - <%= f.label :title %><br> + <%= f.label :title %><br /> <%= f.text_field :title %> </p> <p> - <%= f.label :text %><br> + <%= f.label :text %><br /> <%= f.text_area :text %> </p> diff --git a/guides/code/getting_started/app/views/posts/index.html.erb b/guides/code/getting_started/app/views/posts/index.html.erb index 3ba7091c15..7b72720d50 100644 --- a/guides/code/getting_started/app/views/posts/index.html.erb +++ b/guides/code/getting_started/app/views/posts/index.html.erb @@ -8,6 +8,7 @@ <th>Text</th> <th></th> <th></th> + <th></th> </tr> <% @posts.each do |post| %> @@ -16,6 +17,7 @@ <td><%= post.text %></td> <td><%= link_to 'Show', :action => :show, :id => post.id %> <td><%= link_to 'Edit', :action => :edit, :id => post.id %> + <td><%= link_to 'Destroy', { :action => :destroy, :id => post.id }, :method => :delete, :confirm => 'Are you sure?' %> </tr> <% end %> </table> diff --git a/guides/code/getting_started/app/views/posts/show.html.erb b/guides/code/getting_started/app/views/posts/show.html.erb index aea28cd5a2..65809033ed 100644 --- a/guides/code/getting_started/app/views/posts/show.html.erb +++ b/guides/code/getting_started/app/views/posts/show.html.erb @@ -8,5 +8,11 @@ <%= @post.text %> </p> -<%= link_to 'Back', :action => :index %> -| <%= link_to 'Edit', :action => :edit, :id => @post.id %> +<h2>Comments</h2> +<%= render @post.comments %> + +<h2>Add a comment:</h2> +<%= render "comments/form" %> + +<%= link_to 'Edit Post', edit_post_path(@post) %> | +<%= link_to 'Back to Posts', posts_path %> diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb index b0973c62d5..04a6bd374e 100644 --- a/guides/code/getting_started/config/routes.rb +++ b/guides/code/getting_started/config/routes.rb @@ -1,14 +1,8 @@ Blog::Application.routes.draw do - # resources :posts do - # resources :comments - # end - get "posts" => "posts#index" - get "posts/new" - post "posts/create" - get "posts/:id" => "posts#show" - get "posts/:id/edit" => "posts#edit" - put "posts/:id/update" => "posts#update" + resources :posts do + resources :comments + end # The priority is based upon order of creation: # first created -> highest priority. diff --git a/guides/code/getting_started/public/robots.txt b/guides/code/getting_started/public/robots.txt index 085187fa58..1a3a5e4dd2 100644 --- a/guides/code/getting_started/public/robots.txt +++ b/guides/code/getting_started/public/robots.txt @@ -1,5 +1,5 @@ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: -# User-Agent: * +# User-agent: * # Disallow: / diff --git a/guides/code/getting_started/test/fixtures/tags.yml b/guides/code/getting_started/test/fixtures/tags.yml deleted file mode 100644 index 8485668908..0000000000 --- a/guides/code/getting_started/test/fixtures/tags.yml +++ /dev/null @@ -1,9 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html - -one: - name: MyString - post: - -two: - name: MyString - post: diff --git a/guides/code/getting_started/test/performance/browsing_test.rb b/guides/code/getting_started/test/performance/browsing_test.rb index 3fea27b916..2a849b7f2b 100644 --- a/guides/code/getting_started/test/performance/browsing_test.rb +++ b/guides/code/getting_started/test/performance/browsing_test.rb @@ -3,7 +3,7 @@ require 'rails/performance_test_help' class BrowsingTest < ActionDispatch::PerformanceTest # Refer to the documentation for all available options - # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] + # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory], # :output => 'tmp/performance', :formats => [:flat] } def test_homepage diff --git a/guides/rails_guides/textile_extensions.rb b/guides/rails_guides/textile_extensions.rb index 4677fae504..0a002a785f 100644 --- a/guides/rails_guides/textile_extensions.rb +++ b/guides/rails_guides/textile_extensions.rb @@ -1,5 +1,11 @@ require 'active_support/core_ext/object/inclusion' +module RedCloth::Formatters::HTML + def emdash(opts) + "--" + end +end + module RailsGuides module TextileExtensions def notestuff(body) diff --git a/guides/source/action_mailer_basics.textile b/guides/source/action_mailer_basics.textile index c277f764e7..ebe774fbef 100644 --- a/guides/source/action_mailer_basics.textile +++ b/guides/source/action_mailer_basics.textile @@ -4,7 +4,7 @@ This guide should provide you with all you need to get started in sending and re endprologue. -WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails. +WARNING. This Guide is based on Rails 3.2. Some of the code shown here will not work in earlier versions of Rails. h3. Introduction diff --git a/guides/source/action_view_overview.textile b/guides/source/action_view_overview.textile index 6649974eea..fd1b6c5fc2 100644 --- a/guides/source/action_view_overview.textile +++ b/guides/source/action_view_overview.textile @@ -833,7 +833,7 @@ Reports the approximate distance in time between two Time or Date objects or int <ruby> distance_of_time_in_words(Time.now, Time.now + 15.seconds) # => less than a minute -distance_of_time_in_words(Time.now, Time.now + 15.seconds, true) # => less than 20 seconds +distance_of_time_in_words(Time.now, Time.now + 15.seconds, :include_seconds => true) # => less than 20 seconds </ruby> h5. select_date diff --git a/guides/source/active_record_querying.textile b/guides/source/active_record_querying.textile index 98937266ba..a9cb424eaa 100644 --- a/guides/source/active_record_querying.textile +++ b/guides/source/active_record_querying.textile @@ -99,9 +99,28 @@ SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 <tt>Model.find(primary_key)</tt> will raise an +ActiveRecord::RecordNotFound+ exception if no matching record is found. +h5. +take+ + +<tt>Model.take</tt> retrieves a record without any implicit ordering. For example: + +<ruby> +client = Client.take +# => #<Client id: 1, first_name: "Lifo"> +</ruby> + +The SQL equivalent of the above is: + +<sql> +SELECT * FROM clients LIMIT 1 +</sql> + +<tt>Model.take</tt> returns +nil+ if no record is found and no exception will be raised. + +TIP: The retrieved record may vary depending on the database engine. + h5. +first+ -<tt>Model.first</tt> finds the first record matched by the supplied options, if any. For example: +<tt>Model.first</tt> finds the first record ordered by the primary key. For example: <ruby> client = Client.first @@ -111,14 +130,14 @@ client = Client.first The SQL equivalent of the above is: <sql> -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 </sql> -<tt>Model.first</tt> returns +nil+ if no matching record is found. No exception will be raised. +<tt>Model.first</tt> returns +nil+ if no matching record is found and no exception will be raised. h5. +last+ -<tt>Model.last</tt> finds the last record matched by the supplied options. For example: +<tt>Model.last</tt> finds the last record ordered by the primary key. For example: <ruby> client = Client.last @@ -131,7 +150,7 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 </sql> -<tt>Model.last</tt> returns +nil+ if no matching record is found. No exception will be raised. +<tt>Model.last</tt> returns +nil+ if no matching record is found and no exception will be raised. h5. +find_by+ @@ -148,12 +167,29 @@ Client.find_by first_name: 'Jon' It is equivalent to writing: <ruby> -Client.where(first_name: 'Lifo').first +Client.where(first_name: 'Lifo').take </ruby> +h5(#take_1). +take!+ + +<tt>Model.take!</tt> retrieves a record without any implicit ordering. For example: + +<ruby> +client = Client.take! +# => #<Client id: 1, first_name: "Lifo"> +</ruby> + +The SQL equivalent of the above is: + +<sql> +SELECT * FROM clients LIMIT 1 +</sql> + +<tt>Model.take!</tt> raises +ActiveRecord::RecordNotFound+ if no matching record is found. + h5(#first_1). +first!+ -<tt>Model.first!</tt> finds the first record. For example: +<tt>Model.first!</tt> finds the first record ordered by the primary key. For example: <ruby> client = Client.first! @@ -163,14 +199,14 @@ client = Client.first! The SQL equivalent of the above is: <sql> -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 </sql> -<tt>Model.first!</tt> raises +RecordNotFound+ if no matching record is found. +<tt>Model.first!</tt> raises +ActiveRecord::RecordNotFound+ if no matching record is found. h5(#last_1). +last!+ -<tt>Model.last!</tt> finds the last record. For example: +<tt>Model.last!</tt> finds the last record ordered by the primary key. For example: <ruby> client = Client.last! @@ -183,24 +219,24 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 </sql> -<tt>Model.last!</tt> raises +RecordNotFound+ if no matching record is found. +<tt>Model.last!</tt> raises +ActiveRecord::RecordNotFound+ if no matching record is found. h5(#find_by_1). +find_by!+ -<tt>Model.find_by!</tt> finds the first record matching some conditions. It raises +RecordNotFound+ if no matching record is found. For example: +<tt>Model.find_by!</tt> finds the first record matching some conditions. It raises +ActiveRecord::RecordNotFound+ if no matching record is found. For example: <ruby> Client.find_by! first_name: 'Lifo' # => #<Client id: 1, first_name: "Lifo"> Client.find_by! first_name: 'Jon' -# => RecordNotFound +# => ActiveRecord::RecordNotFound </ruby> It is equivalent to writing: <ruby> -Client.where(first_name: 'Lifo').first! +Client.where(first_name: 'Lifo').take! </ruby> h4. Retrieving Multiple Objects @@ -356,20 +392,6 @@ Client.where("created_at >= :start_date AND created_at <= :end_date", This makes for clearer readability if you have a large number of variable conditions. -h5(#array-range_conditions). Range Conditions - -If you're looking for a range inside of a table (for example, users created in a certain timeframe) you can use the conditions option coupled with the +IN+ SQL statement for this. If you had two dates coming in from a controller you could do something like this to look for a range: - -<ruby> -Client.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date)) -</ruby> - -This query will generate something similar to the following SQL: - -<sql> - SELECT "clients".* FROM "clients" WHERE ("clients"."created_at" BETWEEN '2010-09-29' AND '2010-11-30') -</sql> - h4. Hash Conditions 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: @@ -392,8 +414,6 @@ NOTE: The values cannot be symbols. For example, you cannot do +Client.where(:st h5(#hash-range_conditions). Range Conditions -The good thing about this is that we can pass in a range for our fields without it generating a large query as shown in the preamble of this section. - <ruby> Client.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight) </ruby> @@ -926,7 +946,7 @@ This code looks fine at the first sight. But the problem lies within the total n 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: +Revisiting the above case, we could rewrite +Client.limit(10)+ to use eager load addresses: <ruby> clients = Client.includes(:address).limit(10) diff --git a/guides/source/active_record_validations_callbacks.textile b/guides/source/active_record_validations_callbacks.textile index 88c4481e5e..f49d91fd3c 100644 --- a/guides/source/active_record_validations_callbacks.textile +++ b/guides/source/active_record_validations_callbacks.textile @@ -1064,6 +1064,7 @@ Additionally, the +after_find+ callback is triggered by the following finder met * +find_all_by_<em>attribute</em>+ * +find_by_<em>attribute</em>+ * +find_by_<em>attribute</em>!+ +* +find_by_sql+ * +last+ The +after_initialize+ callback is triggered every time a new object of the class is initialized. @@ -1076,7 +1077,6 @@ Just as with validations, it is also possible to skip callbacks. These methods s * +decrement_counter+ * +delete+ * +delete_all+ -* +find_by_sql+ * +increment+ * +increment_counter+ * +toggle+ diff --git a/guides/source/active_support_core_extensions.textile b/guides/source/active_support_core_extensions.textile index 5d0a3f82e8..8045316e98 100644 --- a/guides/source/active_support_core_extensions.textile +++ b/guides/source/active_support_core_extensions.textile @@ -154,6 +154,51 @@ WARNING. Any class can disallow duplication removing +dup+ and +clone+ or raisin NOTE: Defined in +active_support/core_ext/object/duplicable.rb+. +h4. +deep_dup+ + +The +deep_dup+ method returns deep copy of given object. Normally, when you +dup+ an object that contains other objects, ruby does not +dup+ them. If you have array with a string, for example, it will look like this: + +<ruby> +array = ['string'] +duplicate = array.dup + +duplicate.push 'another-string' + +# object was duplicated, element added only to duplicate +array #=> ['string'] +duplicate #=> ['string', 'another-string'] + +duplicate.first.gsub!('string', 'foo') + +# first element was not duplicated, it will be changed for both arrays +array #=> ['foo'] +duplicate #=> ['foo', 'another-string'] +</ruby> + +As you can see, after duplicating +Array+ instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since +dup+ does not make deep copy, the string inside array is still the same object. + +If you need a deep copy of an object, you should use +deep_dup+ in such situation: + +<ruby> +array = ['string'] +duplicate = array.deep_dup + +duplicate.first.gsub!('string', 'foo') + +array #=> ['string'] +duplicate #=> ['foo'] +</ruby> + +If object is not duplicable +deep_dup+ will just return this object: + +<ruby> +number = 1 +dup = number.deep_dup +number.object_id == dup.object_id # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/object/deep_dup.rb+. + h4. +try+ Sometimes you want to call a method provided the receiver object is not +nil+, which is something you usually check first. +try+ is like +Object#send+ except that it returns +nil+ if sent to +nil+. @@ -184,7 +229,7 @@ You can evaluate code in the context of any object's singleton class using +clas <ruby> class Proc def bind(object) - block, time = self, Time.now + block, time = self, Time.current object.class_eval do method_name = "__bind_#{time.to_i}_#{time.usec}" define_method(method_name, &block) @@ -1034,49 +1079,6 @@ A model may find it useful to set +:instance_accessor+ to +false+ as a way to pr NOTE: Defined in +active_support/core_ext/class/attribute_accessors.rb+. -h4. Class Inheritable Attributes - -WARNING: Class Inheritable Attributes are deprecated. It's recommended that you use +Class#class_attribute+ instead. - -Class variables are shared down the inheritance tree. Class instance variables are not shared, but they are not inherited either. The macros +class_inheritable_reader+, +class_inheritable_writer+, and +class_inheritable_accessor+ provide accessors for class-level data which is inherited but not shared with children: - -<ruby> -module ActionController - class Base - # FIXME: REVISE/SIMPLIFY THIS COMMENT. - # The value of allow_forgery_protection is inherited, - # but its value in a particular class does not affect - # the value in the rest of the controllers hierarchy. - class_inheritable_accessor :allow_forgery_protection - end -end -</ruby> - -They accomplish this with class instance variables and cloning on subclassing, there are no class variables involved. Cloning is performed with +dup+ as long as the value is duplicable. - -There are some variants specialised in arrays and hashes: - -<ruby> -class_inheritable_array -class_inheritable_hash -</ruby> - -Those writers take any inherited array or hash into account and extend them rather than overwrite them. - -As with vanilla class attribute accessors these macros create convenience instance methods for reading and writing. The generation of the writer instance method can be prevented setting +:instance_writer+ to +false+ (not any false value, but exactly +false+): - -<ruby> -module ActiveRecord - class Base - class_inheritable_accessor :default_scoping, :instance_writer => false - end -end -</ruby> - -Since values are copied when a subclass is defined, if the base class changes the attribute after that, the subclass does not see the new value. That's the point. - -NOTE: Defined in +active_support/core_ext/class/inheritable_attributes.rb+. - h4. Subclasses & Descendants h5. +subclasses+ @@ -1131,7 +1133,7 @@ h4. Output Safety h5. Motivation -Inserting data into HTML templates needs extra care. For example you can't just interpolate +@review.title+ verbatim into an HTML page. On one hand if the review title is "Flanagan & Matz rules!" the output won't be well-formed because an ampersand has to be escaped as "&amp;". On the other hand, depending on the application that may be a big security hole because users can inject malicious HTML setting a hand-crafted review title. Check out the "section about cross-site scripting in the Security guide":security.html#cross-site-scripting-xss for further information about the risks. +Inserting data into HTML templates needs extra care. For example, you can't just interpolate +@review.title+ verbatim into an HTML page. For one thing, if the review title is "Flanagan & Matz rules!" the output won't be well-formed because an ampersand has to be escaped as "&amp;". What's more, depending on the application, that may be a big security hole because users can inject malicious HTML setting a hand-crafted review title. Check out the "section about cross-site scripting in the Security guide":security.html#cross-site-scripting-xss for further information about the risks. h5. Safe Strings @@ -1255,9 +1257,14 @@ Pass a +:separator+ to truncate the string at a natural break: # => "Oh dear! Oh..." </ruby> -In the above example "dear" gets cut first, but then +:separator+ prevents it. +The option +:separator+ can be a regexp: + +<ruby> +"Oh dear! Oh dear! I shall be late!".truncate(18, :separator => /\s/) +# => "Oh dear! Oh..." +</ruby> -WARNING: The option +:separator+ can't be a regexp. +In above examples "dear" gets cut first, but then +:separator+ prevents it. NOTE: Defined in +active_support/core_ext/string/filters.rb+. @@ -1810,6 +1817,43 @@ Singular forms are aliased so you are able to say: NOTE: Defined in +active_support/core_ext/numeric/bytes.rb+. +h4. Time + +Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. + +These methods use Time#advance for precise date calculations when using from_now, ago, etc. +as well as adding or subtracting their results from a Time object. For example: + +<ruby> +# equivalent to Time.current.advance(:months => 1) +1.month.from_now + +# equivalent to Time.current.advance(:years => 2) +2.years.from_now + +# equivalent to Time.current.advance(:months => 4, :years => 5) +(4.months + 5.years).from_now +</ruby> + +While these methods provide precise calculation when used as in the examples above, care +should be taken to note that this is not true if the result of `months', `years', etc is +converted before use: + +<ruby> +# equivalent to 30.days.to_i.from_now +1.month.to_i.from_now + +# equivalent to 365.25.days.to_f.from_now +1.year.to_f.from_now +</ruby> + +In such cases, Ruby's core +Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and +Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision +date and time arithmetic. + +NOTE: Defined in +active_support/core_ext/numeric/time.rb+. + h3. Extensions to +Integer+ h4. +multiple_of?+ @@ -2103,20 +2147,20 @@ To do so it sends +to_xml+ to every item in turn, and collects the results under By default, the name of the root element is the underscorized and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with <tt>is_a?</tt>) and they are not hashes. In the example above that's "contributors". -If there's any element that does not belong to the type of the first one the root node becomes "records": +If there's any element that does not belong to the type of the first one the root node becomes "objects": <ruby> [Contributor.first, Commit.first].to_xml # => # <?xml version="1.0" encoding="UTF-8"?> -# <records type="array"> -# <record> +# <objects type="array"> +# <object> # <id type="integer">4583</id> # <name>Aaron Batalion</name> # <rank type="integer">53</rank> # <url-id>aaron-batalion</url-id> -# </record> -# <record> +# </object> +# <object> # <author>Joshua Peek</author> # <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp> # <branch>origin/master</branch> @@ -2127,30 +2171,30 @@ If there's any element that does not belong to the type of the first one the roo # <imported-from-svn type="boolean">false</imported-from-svn> # <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message> # <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1> -# </record> -# </records> +# </object> +# </objects> </ruby> -If the receiver is an array of hashes the root element is by default also "records": +If the receiver is an array of hashes the root element is by default also "objects": <ruby> [{:a => 1, :b => 2}, {:c => 3}].to_xml # => # <?xml version="1.0" encoding="UTF-8"?> -# <records type="array"> -# <record> +# <objects type="array"> +# <object> # <b type="integer">2</b> # <a type="integer">1</a> -# </record> -# <record> +# </object> +# <object> # <c type="integer">3</c> -# </record> -# </records> +# </object> +# </objects> </ruby> WARNING. If the collection is empty the root element is by default "nil-classes". That's a gotcha, for example the root element of the list of contributors above would not be "contributors" if the collection was empty, but "nil-classes". You may use the <tt>:root</tt> option to ensure a consistent root element. -The name of children nodes is by default the name of the root node singularized. In the examples above we've seen "contributor" and "record". The option <tt>:children</tt> allows you to set these node names. +The name of children nodes is by default the name of the root node singularized. In the examples above we've seen "contributor" and "object". The option <tt>:children</tt> allows you to set these node names. The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can configure your own builder via the <tt>:builder</tt> option. The method also accepts options like <tt>:dasherize</tt> and friends, they are forwarded to the builder: @@ -2217,6 +2261,19 @@ Thus, in this case the behavior is different for +nil+, and the differences with NOTE: Defined in +active_support/core_ext/array/wrap.rb+. +h4. Duplicating + +The method +Array.deep_dup+ duplicates itself and all objects inside recursively with ActiveSupport method +Object#deep_dup+. It works like +Array#map+ with sending +deep_dup+ method to each object inside. + +<ruby> +array = [1, [2, 3]] +dup = array.deep_dup +dup[1][2] = 4 +array[1][2] == nil # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/array/deep_dup.rb+. + h4. Grouping h5. +in_groups_of(number, fill_with = nil)+ @@ -2423,6 +2480,23 @@ The method +deep_merge!+ performs a deep merge in place. NOTE: Defined in +active_support/core_ext/hash/deep_merge.rb+. +h4. Deep duplicating + +The method +Hash.deep_dup+ duplicates itself and all keys and values inside recursively with ActiveSupport method +Object#deep_dup+. It works like +Enumerator#each_with_object+ with sending +deep_dup+ method to each pair inside. + +<ruby> +hash = { :a => 1, :b => { :c => 2, :d => [3, 4] } } + +dup = hash.deep_dup +dup[:b][:e] = 5 +dup[:b][:d] << 5 + +hash[:b][:e] == nil # => true +hash[:b][:d] == [3, 4] # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/hash/deep_dup.rb+. + h4. Diffing The method +diff+ returns a hash that represents a diff of the receiver and the argument with the following logic: @@ -2707,22 +2781,25 @@ NOTE: Defined in +active_support/core_ext/range/blockless_step.rb+. h4. +include?+ -The method +Range#include?+ says whether some value falls between the ends of a given instance: +The methods +Range#include?+ and +Range#===+ say whether some value falls between the ends of a given instance: <ruby> (2..3).include?(Math::E) # => true </ruby> -Active Support extends this method so that the argument may be another range in turn. In that case we test whether the ends of the argument range belong to the receiver themselves: +Active Support extends these methods so that the argument may be another range in turn. In that case we test whether the ends of the argument range belong to the receiver themselves: <ruby> (1..10).include?(3..7) # => true (1..10).include?(0..7) # => false (1..10).include?(3..11) # => false (1...9).include?(3..9) # => false -</ruby> -WARNING: The original +Range#include?+ is still the one aliased to +Range#===+. +(1..10) === (3..7) # => true +(1..10) === (0..7) # => false +(1..10) === (3..11) # => false +(1...9) === (3..9) # => false +</ruby> NOTE: Defined in +active_support/core_ext/range/include_range.rb+. @@ -3052,18 +3129,38 @@ The method +beginning_of_day+ returns a timestamp at the beginning of the day (0 <ruby> date = Date.new(2010, 6, 7) -date.beginning_of_day # => Sun Jun 07 00:00:00 +0200 2010 +date.beginning_of_day # => Mon Jun 07 00:00:00 +0200 2010 </ruby> The method +end_of_day+ returns a timestamp at the end of the day (23:59:59): <ruby> date = Date.new(2010, 6, 7) -date.end_of_day # => Sun Jun 06 23:59:59 +0200 2010 +date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010 </ruby> +beginning_of_day+ is aliased to +at_beginning_of_day+, +midnight+, +at_midnight+. +h6. +beginning_of_hour+, +end_of_hour+ + +The method +beginning_of_hour+ returns a timestamp at the beginning of the hour (hh:00:00): + +<ruby> +date = DateTime.new(2010, 6, 7, 19, 55, 25) +date.beginning_of_hour # => Mon Jun 07 19:00:00 +0200 2010 +</ruby> + +The method +end_of_hour+ returns a timestamp at the end of the hour (hh:59:59): + +<ruby> +date = DateTime.new(2010, 6, 7, 19, 55, 25) +date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010 +</ruby> + ++beginning_of_hour+ is aliased to +at_beginning_of_hour+. + +INFO: +beginning_of_hour+ and +end_of_hour+ are implemented for +Time+ and +DateTime+ but *not* +Date+ as it does not make sense to request the beginning or end of an hour on a +Date+ instance. + h6. +ago+, +since+ The method +ago+ receives a number of seconds as argument and returns a timestamp those many seconds ago from midnight: @@ -3131,6 +3228,13 @@ since (in) On the other hand, +advance+ and +change+ are also defined and support more options, they are documented below. +The following methods are only implemented in +active_support/core_ext/date_time/calculations.rb+ as they only make sense when used with a +DateTime+ instance: + +<ruby> +beginning_of_hour (at_beginning_of_hour) +end_of_hour +</ruby> + h5. Named Datetimes h6. +DateTime.current+ @@ -3273,6 +3377,8 @@ ago since (in) beginning_of_day (midnight, at_midnight, at_beginning_of_day) end_of_day +beginning_of_hour (at_beginning_of_hour) +end_of_hour beginning_of_week (at_beginning_of_week) end_of_week (at_end_of_week) monday diff --git a/guides/source/asset_pipeline.textile b/guides/source/asset_pipeline.textile index d79eb01ab2..010154f1d1 100644 --- a/guides/source/asset_pipeline.textile +++ b/guides/source/asset_pipeline.textile @@ -204,6 +204,8 @@ Images can also be organized into subdirectories if required, and they can be ac <%= image_tag "icons/rails.png" %> </erb> +WARNING: If you're precompiling your assets (see "In Production":#in-production below), linking to an asset that does not exist will raise an exception in the calling page. This includes linking to a blank string. As such, be careful using <tt>image_tag</tt> and the other helpers with user-supplied data. + h5. CSS and ERB The asset pipeline automatically evaluates ERB. This means that if you add an +erb+ extension to a CSS asset (for example, +application.css.erb+), then helpers like +asset_path+ are available in your CSS rules: diff --git a/guides/source/caching_with_rails.textile b/guides/source/caching_with_rails.textile index 12bc32f4e1..34a100cd3a 100644 --- a/guides/source/caching_with_rails.textile +++ b/guides/source/caching_with_rails.textile @@ -157,7 +157,7 @@ and you can expire it using the +expire_fragment+ method, like so: expire_fragment(:controller => 'products', :action => 'recent', :action_suffix => 'all_products') </ruby> -If you don't want the cache block to bind to the action that called it, You can also use globally keyed fragments by calling the +cache+ method with a key, like so: +If you don't want the cache block to bind to the action that called it, you can also use globally keyed fragments by calling the +cache+ method with a key: <ruby> <% cache('all_available_products') do %> @@ -229,6 +229,42 @@ class ProductsController < ActionController end </ruby> +Sometimes it is necessary to disambiguate the controller when you call +expire_action+, such as when there are two identically named controllers in separate namespaces: + +<ruby> +class ProductsController < ActionController + caches_action :index + + def index + @products = Product.all + end +end + +module Admin + class ProductsController < ActionController + cache_sweeper :product_sweeper + + def new + @product = Product.new + end + + def create + @product = Product.create(params[:product]) + end + end +end + +class ProductSweeper < ActionController::Caching::Sweeper + observe Product + + def after_create(product) + expire_action(:controller => '/products', :action => 'index') + end +end +</ruby> + +Note the use of '/products' here rather than 'products'. If you wanted to expire an action cache for the +Admin::ProductsController+, you would use 'admin/products' instead. + h4. SQL Caching Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again. diff --git a/guides/source/command_line.textile b/guides/source/command_line.textile index 858ce47db1..b656a0857a 100644 --- a/guides/source/command_line.textile +++ b/guides/source/command_line.textile @@ -12,7 +12,7 @@ endprologue. NOTE: This tutorial assumes you have basic Rails knowledge from reading the "Getting Started with Rails Guide":getting_started.html. -WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails. +WARNING. This Guide is based on Rails 3.2. Some of the code shown here will not work in earlier versions of Rails. h3. Command Line Basics @@ -31,7 +31,7 @@ h4. +rails new+ 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 can install the rails gem by typing +gem install rails+, if you don't have it already. Follow the instructions in the "Rails 3 Release Notes":/3_0_release_notes.html +TIP: You can install the rails gem by typing +gem install rails+, if you don't have it already. <shell> $ rails new commandsapp @@ -185,8 +185,6 @@ $ rails server => Booting WEBrick... </shell> -WARNING: Make sure that you do not have any "tilde backup" files in +app/views/(controller)+, or else WEBrick will _not_ show the expected output. This seems to be a *bug* in Rails 2.3.0. - The URL will be "http://localhost:3000/greetings/hello":http://localhost:3000/greetings/hello. INFO: With a normal, plain-old Rails application, your URLs will generally follow the pattern of http://(host)/(controller)/(action), and a URL like http://(host)/(controller) will hit the *index* action of that controller. @@ -446,6 +444,18 @@ app/model/post.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. +By default, +rake notes+ will look in the +app+, +config+, +lib+, +script+ and +test+ directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable +SOURCE_ANNOTATION_DIRECTORIES+. + +<shell> +$ export SOURCE_ANNOTATION_DIRECTORIES='rspec,vendor' +$ rake notes +(in /home/foobar/commandsapp) +app/model/user.rb: + * [ 35] [FIXME] User should have a subscription at this point +rspec/model/user_spec.rb: + * [122] [TODO] Verify the user that has a subscription works +</shell> + h4. +routes+ +rake routes+ will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with. diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index 1541428af9..b2c9300034 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -70,12 +70,23 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.action_view.cache_template_loading+ controls whether or not templates should be reloaded on each request. Defaults to whatever is set for +config.cache_classes+. -* +config.cache_store+ configures which cache store to use for Rails caching. Options include one of the symbols +:memory_store+, +:file_store+, +:mem_cache_store+, or an object that implements the cache API. Defaults to +:file_store+ if the directory +tmp/cache+ exists, and to +:memory_store+ otherwise. +* +config.cache_store+ configures which cache store to use for Rails caching. Options include one of the symbols +:memory_store+, +:file_store+, +:mem_cache_store+, +:null_store+, or an object that implements the cache API. Defaults to +:file_store+ if the directory +tmp/cache+ exists, and to +:memory_store+ otherwise. * +config.colorize_logging+ specifies whether or not to use ANSI color codes when logging information. Defaults to true. * +config.consider_all_requests_local+ is a flag. If true then any error will cause detailed debugging information to be dumped in the HTTP response, and the +Rails::Info+ controller will show the application runtime context in +/rails/info/properties+. True by default in development and test environments, and false in production mode. For finer-grained control, set this to false and implement +local_request?+ in controllers to specify which requests should provide debugging information on errors. +* +config.console+ allows you to set class that will be used as console you run +rails console+. It's best to run it in +console+ block: + +<ruby> +console do + # this block is called only when running console, + # so we can safely require pry here + require "pry" + config.console = Pry +end +</ruby> + * +config.dependency_loading+ is a flag that allows you to disable constant autoloading setting it to false. It only has effect if +config.cache_classes+ is true, which it is by default in production mode. This flag is set to false by +config.threadsafe!+. * +config.eager_load_paths+ accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the +app+ directory of the application. @@ -100,6 +111,10 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Enabled by +config.threadsafe!+. Defaults to +nil+, so is disabled. +* +config.queue+ configures a different queue implementation for the application. Defaults to +Rails::Queueing::Queue+. Note that, if the default queue is changed, the default +queue_consumer+ is not going to be initialized, it is up to the new queue implementation to handle starting and shutting down its own consumer(s). + +* +config.queue_consumer+ configures a different consumer implementation for the default queue. Defaults to +Rails::Queueing::ThreadedConsumer+. + * +config.reload_classes_only_on_change+ enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If +config.cache_classes+ is true, this option is ignored. * +config.secret_token+ used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get +config.secret_token+ initialized to a random key in +config/initializers/secret_token.rb+. @@ -122,17 +137,6 @@ WARNING: Threadsafe operation is incompatible with the normal workings of develo * +config.whiny_nils+ enables or disables warnings when a certain set of methods are invoked on +nil+ and it does not respond to them. Defaults to true in development and test environments. -* +config.console+ allows you to set class that will be used as console you run +rails console+. It's best to run it in +console+ block: - -<ruby> -console do - # this block is called only when running console, - # so we can safely require pry here - require "pry" - config.console = Pry -end -</ruby> - h4. Configuring Assets Rails 3.1, by default, is set up to use the +sprockets+ gem to manage assets within an application. This gem concatenates and compresses assets in order to make serving them much less painful. @@ -182,13 +186,13 @@ The full set of methods that can be used in this block are as follows: * +force_plural+ allows pluralized model names. Defaults to +false+. * +helper+ defines whether or not to generate helpers. Defaults to +true+. * +integration_tool+ defines which integration tool to use. Defaults to +nil+. -* +javascripts+ turns on the hook for javascripts in generators. Used in Rails for when the +scaffold+ generator is ran. Defaults to +true+. +* +javascripts+ turns on the hook for javascripts in generators. Used in Rails for when the +scaffold+ generator is run. Defaults to +true+. * +javascript_engine+ configures the engine to be used (for eg. coffee) when generating assets. Defaults to +nil+. * +orm+ defines which orm to use. Defaults to +false+ and will use Active Record by default. * +performance_tool+ defines which performance tool to use. Defaults to +nil+. * +resource_controller+ defines which generator to use for generating a controller when using +rails generate resource+. Defaults to +:controller+. * +scaffold_controller+ different from +resource_controller+, defines which generator to use for generating a _scaffolded_ controller when using +rails generate scaffold+. Defaults to +:scaffold_controller+. -* +stylesheets+ turns on the hook for stylesheets in generators. Used in Rails for when the +scaffold+ generator is ran, but this hook can be used in other generates as well. Defaults to +true+. +* +stylesheets+ turns on the hook for stylesheets in generators. Used in Rails for when the +scaffold+ generator is run, but this hook can be used in other generates as well. Defaults to +true+. * +stylesheet_engine+ configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to +:css+. * +test_framework+ defines which test framework to use. Defaults to +false+ and will use Test::Unit by default. * +template_engine+ defines which template engine to use, such as ERB or Haml. Defaults to +:erb+. @@ -248,14 +252,6 @@ They can also be removed from the stack completely: config.middleware.delete ActionDispatch::BestStandardsSupport </ruby> -In addition to these methods to handle the stack, if your application is going to be used as an API endpoint only, the middleware stack can be configured like this: - -<ruby> -config.middleware.http_only! -</ruby> - -By doing this, Rails will create a smaller middleware stack, by not adding some middlewares that are usually useful for browser access only, such as Cookies, Session and Flash, BestStandardsSupport, and MethodOverride. You can always add any of them later manually if you want. Refer to the "API App docs":api_app.html for more info on how to setup your application for API only apps. - h4. Configuring i18n * +config.i18n.default_locale+ sets the default locale of an application used for i18n. Defaults to +:en+. @@ -362,7 +358,7 @@ h4. Configuring Action View Proc.new { |html_tag, instance| %Q(<div class="field_with_errors">#{html_tag}</div>).html_safe } </ruby> -* +config.action_view.default_form_builder+ tells Rails which form builder to use by default. The default is +ActionView::Helpers::FormBuilder+. +* +config.action_view.default_form_builder+ tells Rails which form builder to use by default. The default is +ActionView::Helpers::FormBuilder+. If you want your form builder class to be loaded after initialization (so it's reloaded on each request in development), you can pass it as a +String+ * +config.action_view.logger+ accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to +nil+ to disable logging. @@ -452,9 +448,9 @@ There are a few configuration options available in Active Support: * +config.active_support.bare+ enables or disables the loading of +active_support/all+ when booting Rails. Defaults to +nil+, which means +active_support/all+ is loaded. -* +config.active_support.escape_html_entities_in_json+ enables or disables the escaping of HTML entities in JSON serialization. Defaults to +true+. +* +config.active_support.escape_html_entities_in_json+ enables or disables the escaping of HTML entities in JSON serialization. Defaults to +false+. -* +config.active_support.use_standard_json_time_format+ enables or disables serializing dates to ISO 8601 format. Defaults to +false+. +* +config.active_support.use_standard_json_time_format+ enables or disables serializing dates to ISO 8601 format. Defaults to +true+. * +ActiveSupport::BufferedLogger.silencer+ is set to +false+ to disable the ability to silence logging in a block. The default is +true+. @@ -525,7 +521,8 @@ development: password: </yaml> -If you use external connection pool manager, you can disable prepared statements in rails: +Prepared Statements can be disabled thus: + <yaml> production: adapter: postgresql @@ -592,13 +589,13 @@ TIP: If you have any ordering dependency in your initializers, you can control t h3. Initialization events -Rails has 5 initialization events which can be hooked into (listed in the order that they are ran): +Rails has 5 initialization events which can be hooked into (listed in the order that they are run): * +before_configuration+: This is run as soon as the application constant inherits from +Rails::Application+. The +config+ calls are evaluated before this happens. * +before_initialize+: This is run directly before the initialization process of the application occurs with the +:bootstrap_hook+ initializer near the beginning of the Rails initialization process. -* +to_prepare+: Run after the initializers are ran for all Railties (including the application itself), but before eager loading and the middleware stack is built. More importantly, will run upon every request in +development+, but only once (during boot-up) in +production+ and +test+. +* +to_prepare+: Run after the initializers are run for all Railties (including the application itself), but before eager loading and the middleware stack is built. More importantly, will run upon every request in +development+, but only once (during boot-up) in +production+ and +test+. * +before_eager_load+: This is run directly before eager loading occurs, which is the default behaviour for the _production_ environment and not for the +development+ environment. @@ -739,7 +736,7 @@ The error occurred while evaluating nil.each *+load_config_initializers+* Loads all Ruby files from +config/initializers+ in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. -*+engines_blank_point+* Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are ran. +*+engines_blank_point+* Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run. *+add_generator_templates+* Finds templates for generators at +lib/templates+ for the application, railities and engines and adds these to the +config.generators.templates+ setting, which will make the templates available for all generators to reference. diff --git a/guides/source/contributing_to_ruby_on_rails.textile b/guides/source/contributing_to_ruby_on_rails.textile index fbb3483dae..df475a2359 100644 --- a/guides/source/contributing_to_ruby_on_rails.textile +++ b/guides/source/contributing_to_ruby_on_rails.textile @@ -105,6 +105,13 @@ $ cd railties $ TEST_DIR=generators bundle exec rake test </shell> +You can run any single test separately too: + +<shell> +$ cd actionpack +$ ruby -Itest test/template/form_helper_test.rb +</shell> + h4. Warnings The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings. @@ -201,6 +208,12 @@ $ bundle exec rake test will now run the four of them in turn. +You can also run any single test separately: + +<shell> +$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb +</shell> + You can invoke +test_jdbcmysql+, +test_jdbcsqlite3+ or +test_jdbcpostgresql+ also. See the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/travis.rb+ for the test suite run by the continuous integration server. h4. Older Versions of Ruby on Rails diff --git a/guides/source/debugging_rails_applications.textile b/guides/source/debugging_rails_applications.textile index 903ed59e7b..45fa4ada78 100644 --- a/guides/source/debugging_rails_applications.textile +++ b/guides/source/debugging_rails_applications.textile @@ -124,7 +124,7 @@ h4. Log Levels When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the +Rails.logger.level+ method. -The available log levels are: +:debug+, +:info+, +:warn+, +:error+, and +:fatal+, corresponding to the log level numbers from 0 up to 4 respectively. To change the default log level, use +The available log levels are: +:debug+, +:info+, +:warn+, +:error+, +:fatal+, and +:unknown+, corresponding to the log level numbers from 0 up to 5 respectively. To change the default log level, use <ruby> config.log_level = :warn # In any environment initializer, or diff --git a/guides/source/engines.textile b/guides/source/engines.textile index 36210aedb0..880be57fb5 100644 --- a/guides/source/engines.textile +++ b/guides/source/engines.textile @@ -448,6 +448,8 @@ rake db:migrate SCOPE=blorgh VERSION=0 h4. Using a class provided by the application +h5. Using a model provided by the application + When an engine is created, it may want to use specific classes from an application to provide links between the pieces of the engine and the pieces of the application. In the case of the +blorgh+ engine, making posts and comments have authors would make a lot of sense. Usually, an application would have a +User+ class that would provide the objects that would represent the posts' and comments' authors, but there could be a case where the application calls this class something different, such as +Person+. It's because of this reason that the engine should not hardcode the associations to be exactly for a +User+ class, but should allow for some flexibility around what the class is called. @@ -544,6 +546,19 @@ end Now instead of the ugly Ruby object output the author's name will be displayed. +h5. Using a controller provided by the application + +Because Rails controllers generally share code for things like authentication and accessing session variables, by default they inherit from <tt>ApplicationController</tt>. Rails engines, however are scoped to run independently from the main application, so each engine gets a scoped +ApplicationController+. This namespace prevents code collisions, but often engine controllers should access methods in the main application's +ApplicationController+. An easy way to provide this access is to change the engine's scoped +ApplicationController+ to inherit from the main application's +ApplicationController+. For our Blorgh engine this would be done by changing +app/controllers/blorgh/application_controller.rb+ to look like: + +<ruby> +class Blorgh::ApplicationController < ApplicationController +end +</ruby> + +By default, the engine's controllers inherit from <tt>Blorgh::ApplicationController</tt>. So, after making this change they will have access to the main applications +ApplicationController+ as though they were part of the main application. + +This change does require that the engine is run from a Rails application that has an +ApplicationController+. + h4. Configuring an engine This section covers firstly how you can make the +user_class+ setting of the Blorgh engine configurable, followed by general configuration tips for the engine. @@ -695,7 +710,7 @@ If a template is rendered from within an engine and it's attempting to use one o h4. Assets -Assets within an engine work in an identical way to a full application. Because the engine class inherits from +Rails::Engine+, the application will know to look up in the engine's +app/assets+ directory for potential assets. +Assets within an engine work in an identical way to a full application. Because the engine class inherits from +Rails::Engine+, the application will know to look up in the engine's +app/assets+ and +lib/assets+ directories for potential assets. Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called +style.css+, it should be placed at +app/assets/stylesheets/[engine name]/style.css+, rather than +app/assets/stylesheets/style.css+. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence and the engine's one would be all but ignored. diff --git a/guides/source/generators.textile b/guides/source/generators.textile index e9d713d91d..2e9ab0526d 100644 --- a/guides/source/generators.textile +++ b/guides/source/generators.textile @@ -468,7 +468,7 @@ Replaces text inside a file. <ruby> gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code' -</ruby +</ruby> Regular Expressions can be used to make this method more precise. You can also use append_file and prepend_file in the same way to place code at the beginning and end of a file respectively. diff --git a/guides/source/getting_started.textile b/guides/source/getting_started.textile index 88d5ce6d9b..1e9bd1f144 100644 --- a/guides/source/getting_started.textile +++ b/guides/source/getting_started.textile @@ -45,10 +45,6 @@ internet for learning Ruby, including: h3. What is Rails? -TIP: This section goes into the background and philosophy of the Rails framework -in detail. You can safely skip this section and come back to it at a later time. -Section 3 starts you on the path to creating your first Rails application. - Rails is a web application development framework written in the Ruby language. It is designed to make programming web applications easier by making assumptions about what every developer needs to get started. It allows you to write less @@ -76,7 +72,8 @@ step needed to make this example application has been left out, so you can literally follow along step by step. You can get the complete code "here":https://github.com/lifo/docrails/tree/master/guides/code/getting_started. -By following along with this guide, you'll create a Rails project called <tt>blog</tt>, a +By following along with this guide, you'll create a Rails project called ++blog+, a (very) simple weblog. Before you can start building the application, you need to make sure that you have Rails itself installed. @@ -90,7 +87,10 @@ To install Rails, use the +gem install+ command provided by RubyGems: # gem install rails </shell> -TIP. If you're working on Windows, you can quickly install Ruby and Rails with "Rails Installer":http://railsinstaller.org. +TIP. A number of tools exist to help you quickly install Ruby and Ruby +on Rails on your system. Windows users can use "Rails +Installer":http://railsinstaller.org, while Mac OS X users can use +"Rails One Click":http://railsoneclick.com. To verify that you have everything installed correctly, you should be able to run the following: @@ -112,7 +112,8 @@ $ rails new blog This will create a Rails application called Blog in a directory called blog. -TIP: You can see all of the command line options that the Rails application builder accepts by running <tt>rails new -h</tt>. +TIP: You can see all of the command line options that the Rails +application builder accepts by running +rails new -h+. After you create the blog application, switch to its folder to continue work directly in that application: @@ -120,7 +121,10 @@ After you create the blog application, switch to its folder to continue work dir $ cd blog </shell> -The +rails new blog+ command we ran above created a folder in your working directory called <tt>blog</tt>. The <tt>blog</tt> directory has a number of auto-generated files and folders that make up the structure of a Rails application. Most of the work in this tutorial will happen in the <tt>app/</tt> folder, but here's a basic rundown on the function of each of the files and folders that Rails created by default: +The +rails new blog+ command we ran above created a folder in your +working directory called +blog+. The +blog+ directory has a number of +auto-generated files and folders that make up the structure of a Rails +application. Most of the work in this tutorial will happen in the +app/+ folder, but here's a basic rundown on the function of each of the files and folders that Rails created by default: |_.File/Folder|_.Purpose| |app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| @@ -141,7 +145,7 @@ The +rails new blog+ command we ran above created a folder in your working direc h3. Hello, Rails! -One of the traditional places to start with a new language is by getting some text up on screen quickly. To do this, you need to get your Rails application server running. +To begin with, let's get some text up on screen quickly. To do this, you need to get your Rails application server running. h4. Starting up the Web Server @@ -153,7 +157,7 @@ $ rails server TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the absence of a runtime will give you an +execjs+ error. Usually Mac OS X and Windows come with a JavaScript runtime installed. Rails adds the +therubyracer+ gem to Gemfile in a commented line for new apps and you can uncomment if you need it. +therubyrhino+ is the recommended runtime for JRuby users and is added by default to Gemfile in apps generated under JRuby. You can investigate about all the supported runtimes at "ExecJS":https://github.com/sstephenson/execjs#readme. -This will fire up an instance of a webserver built into Ruby called WEBrick by default. To see your application in action, open a browser window and navigate to "http://localhost:3000":http://localhost:3000. You should see the Rails default information page: +This will fire up WEBrick, a webserver built into Ruby by default. To see your application in action, open a browser window and navigate to "http://localhost:3000":http://localhost:3000. You should see the Rails default information page: !images/rails_welcome.png(Welcome Aboard screenshot)! @@ -165,7 +169,7 @@ h4. Say "Hello", Rails To get Rails saying "Hello", you need to create at minimum a _controller_ and a _view_. -A controller's purpose is to receive specific requests for the application. What controller receives what request is determined by the _routing_. There is very often more than one route to each controller, and different routes can be served by different _actions_. Each action's purpose is to collect information to provide it to a view. +A controller's purpose is to receive specific requests for the application. _Routing_ decides which controller receives which requests. Often, there is more than one route to each controller, and different routes can be served by different _actions_. Each action's purpose is to collect information to provide it to a view. A view's purpose is to display this information in a human readable format. An important distinction to make is that it is the _controller_, not the view, where information is collected. The view should just display that information. By default, view templates are written in a language called ERB (Embedded Ruby) which is converted by the request cycle in Rails before being sent to the user. @@ -389,11 +393,10 @@ This action is now displaying the parameters for the post that are coming in fro h4. Creating the Post model -Rails uses models to manage database objects, so if you want to save -data to the database you'll have to create a model. In our blog -application you want to save posts, so you'll create a +Post+ model. - -You can create a model with the following command: +Models in Rails use a singular name, and their corresponding database tables use +a plural name. Rails provides a generator for creating models, which +most Rails developers tend to use when creating new models. +To create the new model, run this command in your terminal: <shell> $ rails generate model Post title:string text:text @@ -401,10 +404,13 @@ $ rails generate model Post title:string text:text With that command we told Rails that we want a +Post+ model, which in turn should have a title attribute of type string, and a text attribute -of type text. Rails in turn responded by creating a bunch of files. For +of type text. Those attributes are automatically added to the +posts+ +table in the database and mapped to the +Post+ model. + +Rails in turn responded by creating a bunch of files. For now, we're only interested in +app/models/post.rb+ and +db/migrate/20120419084633_create_posts.rb+. The latter is responsible -for creating the dabase structure, which is what we'll look at next. +for creating the database structure, which is what we'll look at next. h4. Running a Migration @@ -416,7 +422,7 @@ and it's possible to undo a migration after it's been applied to your database. Migration filenames include a timestamp to ensure that they're processed in the order that they were created. -If you look in the +db/migrate/20100207214725_create_posts.rb+ file (remember, +If you look in the +db/migrate/20120419084633_create_posts.rb+ file (remember, yours will have a slightly different name), here's what you'll find: <ruby> @@ -461,11 +467,11 @@ NOTE. Because you're working in the development environment by default, this command will apply to the database defined in the +development+ section of your +config/database.yml+ file. If you would like to execute migrations in another environment, for instance in production, you must explicitly pass it when -invoking the command: <tt>rake db:migrate RAILS_ENV=production</tt>. +invoking the command: +rake db:migrate RAILS_ENV=production+. h4. Saving data in the controller -Back into +posts_controller+, we need to change the +create+ action +Back in +posts_controller+, we need to change the +create+ action to use the new +Post+ model to save data in the database. Open that file and change the +create+ action to look like the following: @@ -711,7 +717,7 @@ If you reload try to save a post without a title, Rails will send you back to the form, but that's not very useful. You need to tell the user that something went wrong. To do that, you'll modify -+app/views/posts/index.html.erb+ to check for error messages: ++app/views/posts/new.html.erb+ to check for error messages: <erb> <%= form_for :post, :url => { :action => :create } do |f| %> @@ -835,7 +841,7 @@ Moving on, we need to add the +update+ action. The file +config/routes.rb+ will need just one more line: <ruby> -put "posts/:id/update" +put "posts/:id" => "posts#update" </ruby> And the +update+ action in +posts_controller+ itself should not look too complicated by now: @@ -923,196 +929,18 @@ The +show+ view will automatically include the content of the as to not be confused with regular views. However, you don't include the underscore when including them with the +helper+ method. -TIP: You can red more about partials in the "Layouts and Rendering in +TIP: You can read more about partials in the "Layouts and Rendering in Rails":layouts_and_rendering.html guide. Our +edit+ action looks very similar to the +new+ action, in fact they both share the same code for displaying the form. Lets clean them up by using a +_form+ partial. -h4. Using the Console - -To see your validations in action, you can use the console. The console is a -command-line tool that lets you execute Ruby code in the context of your -application: - -<shell> -$ rails console -</shell> - -TIP: The default console will make changes to your database. You can instead -open a console that will roll back any changes you make by using <tt>rails console ---sandbox</tt>. - -After the console loads, you can use it to work with your application's models: - -<shell> ->> p = Post.new(:content => "A new post") -=> #<Post id: nil, name: nil, title: nil, - content: "A new post", created_at: nil, - updated_at: nil> ->> p.save -=> false ->> p.errors.full_messages -=> ["Name can't be blank", "Title can't be blank", "Title is too short (minimum is 5 characters)"] -</shell> - -This code shows creating a new +Post+ instance, attempting to save it and -getting +false+ for a return value (indicating that the save failed), and -inspecting the +errors+ of the post. - -When you're finished, type +exit+ and hit +return+ to exit the console. - -TIP: Unlike the development web server, the console does not automatically load -your code afresh for each line. If you make changes to your models (in your editor) -while the console is open, type +reload!+ at the console prompt to load them. - -h4. Listing All Posts - -Let's dive into the Rails code a little deeper to see how the application is -showing us the list of Posts. Open the file -+app/controllers/posts_controller.rb+ and look at the -+index+ action: - -<ruby> -def index - @posts = Post.all - - respond_to do |format| - format.html # index.html.erb - format.json { render :json => @posts } - end -end -</ruby> - -+Post.all+ returns all of the posts currently in the database as an array -of +Post+ records that we store in an instance variable called +@posts+. - -TIP: For more information on finding records with Active Record, see "Active -Record Query Interface":active_record_querying.html. - -The +respond_to+ block handles both HTML and JSON calls to this action. If you -browse to "http://localhost:3000/posts.json":http://localhost:3000/posts.json, -you'll see a JSON containing all of the posts. The HTML format looks for a view -in +app/views/posts/+ with a name that corresponds to the action name. Rails -makes all of the instance variables from the action available to the view. -Here's +app/views/posts/index.html.erb+: - -<erb> -<h1>Listing posts</h1> - -<table> - <tr> - <th>Name</th> - <th>Title</th> - <th>Content</th> - <th></th> - <th></th> - <th></th> - </tr> - -<% @posts.each do |post| %> - <tr> - <td><%= post.name %></td> - <td><%= post.title %></td> - <td><%= post.content %></td> - <td><%= link_to 'Show', post %></td> - <td><%= link_to 'Edit', edit_post_path(post) %></td> - <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', - :method => :delete %></td> - </tr> -<% end %> -</table> - -<br /> - -<%= link_to 'New post', new_post_path %> -</erb> - -This view iterates over the contents of the +@posts+ array to display content -and links. A few things to note in the view: - -* +link_to+ builds a hyperlink to a particular destination -* +edit_post_path+ and +new_post_path+ are helpers that Rails provides as part of RESTful routing. You'll see a variety of these helpers for the different actions that the controller includes. - -NOTE. In previous versions of Rails, you had to use +<%=h post.name %>+ so -that any HTML would be escaped before being inserted into the page. In Rails -3 and above, this is now the default. To get unescaped HTML, you now use <tt><%= raw post.name %></tt>. - -TIP: For more details on the rendering process, see "Layouts and Rendering in -Rails":layouts_and_rendering.html. - -h4. Customizing the Layout - -The view is only part of the story of how HTML is displayed in your web browser. -Rails also has the concept of +layouts+, which are containers for views. When -Rails renders a view to the browser, it does so by putting the view's HTML into -a layout's HTML. In previous versions of Rails, the +rails generate scaffold+ -command would automatically create a controller specific layout, like -+app/views/layouts/posts.html.erb+, for the posts controller. However this has -been changed in Rails 3. An application specific +layout+ is used for all the -controllers and can be found in +app/views/layouts/application.html.erb+. Open -this layout in your editor and modify the +body+ tag to include the style directive -below: - -<erb> -<!DOCTYPE html> -<html> -<head> - <title>Blog</title> - <%= stylesheet_link_tag "application" %> - <%= javascript_include_tag "application" %> - <%= csrf_meta_tags %> -</head> -<body style="background-color: #EEEEEE;"> - -<%= yield %> - -</body> -</html> -</erb> - -Now when you refresh the +/posts+ page, you'll see a gray background to the -page. This same gray background will be used throughout all the views. - -h4. Creating New Posts - -Creating a new post involves two actions. The first is the +new+ action, which -instantiates an empty +Post+ object: - -<ruby> -def new - @post = Post.new - - respond_to do |format| - format.html # new.html.erb - format.json { render :json => @post } - end -end -</ruby> - -The +new.html.erb+ view displays this empty Post to the user: - -<erb> -<h1>New post</h1> - -<%= render 'form' %> - -<%= link_to 'Back', posts_path %> -</erb> - -The +<%= render 'form' %>+ line is our first introduction to _partials_ in -Rails. A partial is a snippet of HTML and Ruby code that can be reused in -multiple locations. In this case, the form used to make a new post is basically -identical to the form used to edit a post, both having text fields for the name and -title, a text area for the content, and a button to create the new post or to update -the existing one. - -If you take a look at +views/posts/_form.html.erb+ file, you will see the -following: +Create a new file +app/views/posts/_form.html.erb+ with the following +content: <erb> -<%= form_for(@post) do |f| %> +<%= form_for @post do |f| %> <% if @post.errors.any? %> <div id="errorExplanation"> <h2><%= pluralize(@post.errors.count, "error") %> prohibited @@ -1124,230 +952,218 @@ following: </ul> </div> <% end %> - - <div class="field"> - <%= f.label :name %><br /> - <%= f.text_field :name %> - </div> - <div class="field"> - <%= f.label :title %><br /> + <p> + <%= f.label :title %><br> <%= f.text_field :title %> - </div> - <div class="field"> - <%= f.label :content %><br /> - <%= f.text_area :content %> - </div> - <div class="actions"> + </p> + + <p> + <%= f.label :text %><br> + <%= f.text_area :text %> + </p> + + <p> <%= f.submit %> - </div> + </p> <% end %> </erb> -This partial receives all the instance variables defined in the calling view -file. In this case, the controller assigned the new +Post+ object to +@post+, -which will thus be available in both the view and the partial as +@post+. +Everything except for the +form_for+ declaration remained the same. I'll +explain later how +form_for+ can figure out the right +action+ and ++method+ attributes when building the form, for now let's update the ++new+ and +edit+ views: -For more information on partials, refer to the "Layouts and Rendering in -Rails":layouts_and_rendering.html#using-partials guide. +<erb> +# app/views/posts/new.html.erb -The +form_for+ block is used to create an HTML form. Within this block, you have -access to methods to build various controls on the form. For example, -+f.text_field :name+ tells Rails to create a text input on the form and to hook -it up to the +name+ attribute of the instance being displayed. You can only use -these methods with attributes of the model that the form is based on (in this -case +name+, +title+, and +content+). Rails uses +form_for+ in preference to -having you write raw HTML because the code is more succinct, and because it -explicitly ties the form to a particular model instance. +<h1>New post</h1> -The +form_for+ block is also smart enough to work out if you are doing a _New -Post_ or an _Edit Post_ action, and will set the form +action+ tags and submit -button names appropriately in the HTML output. +<%= render 'form' %> -TIP: If you need to create an HTML form that displays arbitrary fields, not tied -to a model, you should use the +form_tag+ method, which provides shortcuts for -building forms that are not necessarily tied to a model instance. +<%= link_to 'Back', :action => :index %> -When the user clicks the +Create Post+ button on this form, the browser will -send information back to the +create+ action of the controller (Rails knows to -call the +create+ action because the form is sent with an HTTP POST request; -that's one of the conventions that were mentioned earlier): -<ruby> -def create - @post = Post.new(params[:post]) +# app/views/posts/edit.html.erb - respond_to do |format| - if @post.save - format.html { redirect_to(@post, - :notice => 'Post was successfully created.') } - format.json { render :json => @post, - :status => :created, :location => @post } - else - format.html { render :action => "new" } - format.json { render :json => @post.errors, - :status => :unprocessable_entity } - end - end -end -</ruby> +<h1>Edit post</h1> -The +create+ action instantiates a new Post object from the data supplied by the -user on the form, which Rails makes available in the +params+ hash. After -successfully saving the new post, +create+ returns the appropriate format that -the user has requested (HTML in our case). It then redirects the user to the -resulting post +show+ action and sets a notice to the user that the Post was -successfully created. - -If the post was not successfully saved, due to a validation error, then the -controller returns the user back to the +new+ action with any error messages so -that the user has the chance to fix the error and try again. - -The "Post was successfully created." message is stored in the Rails -+flash+ hash (usually just called _the flash_), so that messages can be carried -over to another action, providing the user with useful information on the status -of their request. In the case of +create+, the user never actually sees any page -rendered during the post creation process, because it immediately redirects to -the new +Post+ as soon as Rails saves the record. The Flash carries over a message to -the next action, so that when the user is redirected back to the +show+ action, -they are presented with a message saying "Post was successfully created." - -h4. Showing an Individual Post - -When you click the +show+ link for a post on the index page, it will bring you -to a URL like +http://localhost:3000/posts/1+. Rails interprets this as a call -to the +show+ action for the resource, and passes in +1+ as the +:id+ parameter. -Here's the +show+ action: +<%= render 'form' %> -<ruby> -def show - @post = Post.find(params[:id]) +<%= link_to 'Back', :action => :index %> +</erb> - respond_to do |format| - format.html # show.html.erb - format.json { render :json => @post } - end -end -</ruby> +Point your browser to +"http://localhost:3000/posts/new":http://localhost:3000/posts/new and +try creating a new post. Everything still works. Now try editing the +post and you'll receive the following error: + +!images/getting_started/undefined_method_post_path.png(Undefined method +post_path)! + +To understand this error, you need to understand how +form_for+ works. +When you pass an object to +form_for+ and you don't specify a +:url+ +option, Rails will try to guess the +action+ and +method+ options by +checking if the passed object is a new record or not. Rails follows the +REST convention, so to create a new +Post+ object it will look for a +route named +posts_path+, and to update a +Post+ object it will look for +a route named +post_path+ and pass the current object. Similarly, rails +knows that it should create new objects via POST and update them via +PUT. + +If you run +rake routes+ from the console you'll see that we already +have a +posts_path+ route, which was created automatically by Rails. +However, we don't have a +post_path+ yet, which is the reason why we +received an error before. -The +show+ action uses +Post.find+ to search for a single record in the database -by its id value. After finding the record, Rails displays it by using -+app/views/posts/show.html.erb+: +<shell> +# rake routes + + posts GET /posts(.:format) posts#index + posts_new GET /posts/new(.:format) posts#new +posts_create POST /posts/create(.:format) posts#create + GET /posts/:id(.:format) posts#show + GET /posts/:id/edit(.:format) posts#edit + PUT /posts/:id(.:format) posts#update + root / welcome#index +</shell> -<erb> -<p id="notice"><%= notice %></p> +To fix this, open +config/routes.rb+ and modify the +get "posts/:id"+ +line like this: -<p> - <b>Name:</b> - <%= @post.name %> -</p> +<ruby> +get "posts/:id" => "posts#show", :as => :post +</ruby> -<p> - <b>Title:</b> - <%= @post.title %> -</p> +Now you'll be able to update posts again. -<p> - <b>Content:</b> - <%= @post.content %> -</p> +h4. Deleting Posts +We're now ready to cover the "D" part of CRUD, deleting posts from the +database. Following the REST convention, we're going to add a route for +deleting posts: -<%= link_to 'Edit', edit_post_path(@post) %> | -<%= link_to 'Back', posts_path %> -</erb> +<ruby> +# config/routes.rb -h4. Editing Posts +delete "posts/:id" => "posts#destroy" +</ruby> -Like creating a new post, editing a post is a two-part process. The first step -is a request to +edit_post_path(@post)+ with a particular post. This calls the -+edit+ action in the controller: +We use the +delete+ method for destroying resources, which is mapped to +the +destroy+ action, which is provided below: <ruby> -def edit +# app/controllers/posts_controller.rb + +def destroy @post = Post.find(params[:id]) + @post.destroy + + redirect_to :action => :index end </ruby> -After finding the requested post, Rails uses the +edit.html.erb+ view to display -it: +You can call +destroy+ on Active Record objects when you want to delete +them from the dabase. Note that we don't need to add a view for this +action since we're redirecting to the +index+ action. -<erb> -<h1>Editing post</h1> +Finally, add a 'destroy' link to your +index+ action to wrap everything +together. -<%= render 'form' %> +<erb> +<table> + <tr> + <th>Title</th> + <th>Text</th> + <th></th> + <th></th> + <th></th> + </tr> -<%= link_to 'Show', @post %> | -<%= link_to 'Back', posts_path %> +<% @posts.each do |post| %> + <tr> + <td><%= post.title %></td> + <td><%= post.text %></td> + <td><%= link_to 'Show', :action => :show, :id => post.id %></td> + <td><%= link_to 'Edit', :action => :edit, :id => post.id %></td> + <td><%= link_to 'Destroy', { :action => :destroy, :id => post.id }, :method => :delete, :confirm => 'Are you sure?' %></td> + </tr> +<% end %> +</table> </erb> -Again, as with the +new+ action, the +edit+ action is using the +form+ partial. -This time, however, the form will do a PUT action to the +PostsController+ and the -submit button will display "Update Post". +Here we're using +link_to+ in a different way. We wrap the ++:action+ and +:id+ attributes in a hash so that we can pass other +arguments to +link_to+. The +:method+ and +:confirm+ +options are used as html5 attributes so that when the click is linked, +Rails will first show a confirm dialog to the user, and then submit the +link with method +delete+. This is done via javascript automatically. -Submitting the form created by this view will invoke the +update+ action within -the controller: +!images/getting_started/confirm_dialog.png(Confirm Dialog)! -<ruby> -def update - @post = Post.find(params[:id]) +Congratulations, you can now create, show, list, update and destroy +posts. In the next section will see how Rails can aid us when creating +REST applications, and how we can refactor our Blog app to take +advantage of it. - respond_to do |format| - if @post.update_attributes(params[:post]) - format.html { redirect_to(@post, - :notice => 'Post was successfully updated.') } - format.json { head :no_content } - else - format.html { render :action => "edit" } - format.json { render :json => @post.errors, - :status => :unprocessable_entity } - end - end -end -</ruby> +h4. Going Deeper into REST -In the +update+ action, Rails first uses the +:id+ parameter passed back from -the edit view to locate the database record that's being edited. The -+update_attributes+ call then takes the +post+ parameter (a hash) from the request -and applies it to this record. If all goes well, the user is redirected to the -post's +show+ action. If there are any problems, it redirects back to the +edit+ action to -correct them. +We've now covered all the CRUD actions of a REST app. We did so by +declaring separate routes with the appropriate verbs into ++config/routes.rb+. Here's how that file looks so far: -h4. Destroying a Post +<ruby> +get "posts" => "posts#index" +get "posts/new" +post "posts/create" +get "posts/:id" => "posts#show", :as => :post +get "posts/:id/edit" => "posts#edit" +put "posts/:id" => "posts#update" +delete "posts/:id" => "posts#destroy" +</ruby> -Finally, clicking one of the +destroy+ links sends the associated id to the -+destroy+ action: +That's a lot to type for covering a single *resource*. Fortunately, +Rails provides a +resources+ method which can be used to declare a +standard REST resource. Here's how +config/routes/rb+ looks after the +cleanup: <ruby> -def destroy - @post = Post.find(params[:id]) - @post.destroy +Blog::Application.routes.draw do - respond_to do |format| - format.html { redirect_to posts_url } - format.json { head :no_content } - end + resources :posts + + root :to => "welcome#index" end </ruby> -The +destroy+ method of an Active Record model instance removes the -corresponding record from the database. After that's done, there isn't any -record to display, so Rails redirects the user's browser to the index action of -the controller. +If you run +rake routes+, you'll see that all the routes that we +declared before are still available, and the app still works as before. + +<shell> +# rake routes + posts GET /posts(.:format) posts#index + POST /posts(.:format) posts#create + new_post GET /posts/new(.:format) posts#new +edit_post GET /posts/:id/edit(.:format) posts#edit + post GET /posts/:id(.:format) posts#show + PUT /posts/:id(.:format) posts#update + DELETE /posts/:id(.:format) posts#destroy + root / welcome#index +</shell> + +TIP: In general, Rails encourages the use of resources objects in place +of declaring routes manually. For more information about routing, see +"Rails Routing from the Outside In":routing.html. h3. Adding a Second Model -Now that you've seen what a model built with scaffolding looks like, it's time to -add a second model to the application. The second model will handle comments on +It's time to add a second model to the application. The second model will handle comments on blog posts. h4. Generating a Model -Models in Rails use a singular name, and their corresponding database tables use -a plural name. For the model to hold comments, the convention is to use the name -+Comment+. Even if you don't want to use the entire apparatus set up by -scaffolding, most Rails developers still use generators to make things like -models and controllers. To create the new model, run this command in your -terminal: +We're going to se the same generator that we used before when creating +the +Post+ model. This time we'll create a +Comment+ model to hold +reference of post comments. Run this command in your terminal: <shell> $ rails generate model Comment commenter:string body:text post:references @@ -1435,7 +1251,6 @@ You'll need to edit the +post.rb+ file to add the other side of the association: <ruby> class Post < ActiveRecord::Base - validates :name, :presence => true validates :title, :presence => true, :length => { :minimum => 5 } @@ -1454,9 +1269,7 @@ h4. Adding a Route for Comments As with the +welcome+ controller, we will need to add a route so that Rails knows where we would like to navigate to see +comments+. Open up the -+config/routes.rb+ file again. Near the top, you will see the entry for +posts+ -that was added automatically by the scaffold generator: <tt>resources -:posts</tt>. Edit it as follows: ++config/routes.rb+ file again, and edit it as follows: <ruby> resources :posts do @@ -1474,7 +1287,7 @@ In":routing.html guide. h4. Generating a Controller With the model in hand, you can turn your attention to creating a matching -controller. Again, there's a generator for this: +controller. Again, we'll use the same generator we used before: <shell> $ rails generate controller Comments @@ -1501,40 +1314,33 @@ So first, we'll wire up the Post show template (+/app/views/posts/show.html.erb+) to let us make a new comment: <erb> -<p id="notice"><%= notice %></p> - -<p> - <b>Name:</b> - <%= @post.name %> -</p> - <p> - <b>Title:</b> + <strong>Title:</strong> <%= @post.title %> </p> <p> - <b>Content:</b> - <%= @post.content %> + <strong>Text:</strong> + <%= @post.texthttp://beginningruby.org/ %> </p> <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> - <div class="field"> + <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> - </div> - <div class="field"> + </p> + <p> <%= f.label :body %><br /> <%= f.text_area :body %> - </div> - <div class="actions"> + </p> + <p> <%= f.submit %> - </div> + </p> <% end %> <%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> | +<%= link_to 'Back to Posts', posts_path %> </erb> This adds a form on the +Post+ show page that creates a new comment by @@ -1567,60 +1373,53 @@ template. This is where we want the comment to show, so let's add that to the +app/views/posts/show.html.erb+. <erb> -<p id="notice"><%= notice %></p> - <p> - <b>Name:</b> - <%= @post.name %> -</p> - -<p> - <b>Title:</b> + <strong>Title:</strong> <%= @post.title %> </p> <p> - <b>Content:</b> - <%= @post.content %> + <strong>Text:</strong> + <%= @post.texthttp://beginningruby.org/ %> </p> <h2>Comments</h2> <% @post.comments.each do |comment| %> <p> - <b>Commenter:</b> + <strong>Commenter:</strong> <%= comment.commenter %> </p> <p> - <b>Comment:</b> + <strong>Comment:</strong> <%= comment.body %> </p> <% end %> <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> - <div class="field"> + <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> - </div> - <div class="field"> + </p> + <p> <%= f.label :body %><br /> <%= f.text_area :body %> - </div> - <div class="actions"> + </p> + <p> <%= f.submit %> - </div> + </p> <% end %> -<br /> - <%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> | +<%= link_to 'Back to Posts', posts_path %> </erb> Now you can add posts and comments to your blog and have them show up in the right places. +!images/getting_started/post_with_comments.png(Post with Comments)! + h3. Refactoring Now that we have posts and comments working, take a look at the @@ -1635,12 +1434,12 @@ following into it: <erb> <p> - <b>Commenter:</b> + <strong>Commenter:</strong> <%= comment.commenter %> </p> <p> - <b>Comment:</b> + <strong>Comment:</strong> <%= comment.body %> </p> </erb> @@ -1649,21 +1448,14 @@ Then you can change +app/views/posts/show.html.erb+ to look like the following: <erb> -<p id="notice"><%= notice %></p> - <p> - <b>Name:</b> - <%= @post.name %> -</p> - -<p> - <b>Title:</b> + <strong>Title:</strong> <%= @post.title %> </p> <p> - <b>Content:</b> - <%= @post.content %> + <strong>Text:</strong> + <%= @post.texthttp://beginningruby.org/ %> </p> <h2>Comments</h2> @@ -1671,23 +1463,21 @@ following: <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> - <div class="field"> + <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> - </div> - <div class="field"> + </p> + <p> <%= f.label :body %><br /> <%= f.text_area :body %> - </div> - <div class="actions"> + </p> + <p> <%= f.submit %> - </div> + </p> <% end %> -<br /> - <%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> | +<%= link_to 'Back to Posts', posts_path %> </erb> This will now render the partial in +app/views/comments/_comment.html.erb+ once @@ -1703,50 +1493,38 @@ create a file +app/views/comments/_form.html.erb+ containing: <erb> <%= form_for([@post, @post.comments.build]) do |f| %> - <div class="field"> + <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> - </div> - <div class="field"> + </p> + <p> <%= f.label :body %><br /> <%= f.text_area :body %> - </div> - <div class="actions"> + </p> + <p> <%= f.submit %> - </div> + </p> <% end %> </erb> Then you make the +app/views/posts/show.html.erb+ look like the following: <erb> -<p id="notice"><%= notice %></p> - -<p> - <b>Name:</b> - <%= @post.name %> -</p> - <p> - <b>Title:</b> + <strong>Title:</strong> <%= @post.title %> </p> <p> - <b>Content:</b> - <%= @post.content %> + <strong>Text:</strong> + <%= @post.texthttp://beginningruby.org/ %> </p> -<h2>Comments</h2> -<%= render @post.comments %> - <h2>Add a comment:</h2> <%= render "comments/form" %> -<br /> - <%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> | +<%= link_to 'Back to Posts', posts_path %> </erb> The second render just defines the partial template we want to render, @@ -1768,12 +1546,12 @@ So first, let's add the delete link in the <erb> <p> - <b>Commenter:</b> + <strong>Commenter:</strong> <%= comment.commenter %> </p> <p> - <b>Comment:</b> + <strong>Comment:</strong> <%= comment.body %> </p> @@ -1822,7 +1600,6 @@ model, +app/models/post.rb+, as follows: <ruby> class Post < ActiveRecord::Base - validates :name, :presence => true validates :title, :presence => true, :length => { :minimum => 5 } has_many :comments, :dependent => :destroy @@ -1851,11 +1628,8 @@ class PostsController < ApplicationController http_basic_authenticate_with :name => "dhh", :password => "secret", :except => [:index, :show] - # GET /posts - # GET /posts.json def index @posts = Post.all - respond_to do |format| # snipped for brevity </ruby> @@ -1877,213 +1651,6 @@ Authentication challenge !images/challenge.png(Basic HTTP Authentication Challenge)! -h3. Building a Multi-Model Form - -Another feature of your average blog is the ability to tag posts. To implement -this feature your application needs to interact with more than one model on a -single form. Rails offers support for nested forms. - -To demonstrate this, we will add support for giving each post multiple tags, -right in the form where you create the post. First, create a new model to hold -the tags: - -<shell> -$ rails generate model Tag name:string post:references -</shell> - -Again, run the migration to create the database table: - -<shell> -$ rake db:migrate -</shell> - -Next, edit the +post.rb+ file to create the other side of the association, and -to tell Rails (via the +accepts_nested_attributes_for+ macro) that you intend to -edit tags via posts: - -<ruby> -class Post < ActiveRecord::Base - validates :name, :presence => true - validates :title, :presence => true, - :length => { :minimum => 5 } - - has_many :comments, :dependent => :destroy - has_many :tags - - accepts_nested_attributes_for :tags, :allow_destroy => :true, - :reject_if => proc { |attrs| attrs.all? { |k, v| v.blank? } } -end -</ruby> - -The +:allow_destroy+ option tells Rails to enable destroying tags through the -nested attributes (you'll handle that by displaying a "remove" checkbox on the -view that you'll build shortly). The +:reject_if+ option prevents saving new -tags that do not have any attributes filled in. - -We will modify +views/posts/_form.html.erb+ to render a partial to make a tag: - -<erb> -<% @post.tags.build %> -<%= form_for(@post) do |post_form| %> - <% if @post.errors.any? %> - <div id="errorExplanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2> - <ul> - <% @post.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> - <% end %> - - <div class="field"> - <%= post_form.label :name %><br /> - <%= post_form.text_field :name %> - </div> - <div class="field"> - <%= post_form.label :title %><br /> - <%= post_form.text_field :title %> - </div> - <div class="field"> - <%= post_form.label :content %><br /> - <%= post_form.text_area :content %> - </div> - <h2>Tags</h2> - <%= render :partial => 'tags/form', - :locals => {:form => post_form} %> - <div class="actions"> - <%= post_form.submit %> - </div> -<% end %> -</erb> - -Note that we have changed the +f+ in +form_for(@post) do |f|+ to +post_form+ to -make it easier to understand what is going on. - -This example shows another option of the render helper, being able to pass in -local variables, in this case, we want the local variable +form+ in the partial -to refer to the +post_form+ object. - -We also add a <tt>@post.tags.build</tt> at the top of this form. This is to make -sure there is a new tag ready to have its name filled in by the user. If you do -not build the new tag, then the form will not appear as there is no new Tag -object ready to create. - -Now create the folder <tt>app/views/tags</tt> and make a file in there called -<tt>_form.html.erb</tt> which contains the form for the tag: - -<erb> -<%= form.fields_for :tags do |tag_form| %> - <div class="field"> - <%= tag_form.label :name, 'Tag:' %> - <%= tag_form.text_field :name %> - </div> - <% unless tag_form.object.nil? || tag_form.object.new_record? %> - <div class="field"> - <%= tag_form.label :_destroy, 'Remove:' %> - <%= tag_form.check_box :_destroy %> - </div> - <% end %> -<% end %> -</erb> - -Finally, we will edit the <tt>app/views/posts/show.html.erb</tt> template to -show our tags. - -<erb> -<p id="notice"><%= notice %></p> - -<p> - <b>Name:</b> - <%= @post.name %> -</p> - -<p> - <b>Title:</b> - <%= @post.title %> -</p> - -<p> - <b>Content:</b> - <%= @post.content %> -</p> - -<p> - <b>Tags:</b> - <%= @post.tags.map { |t| t.name }.join(", ") %> -</p> - -<h2>Comments</h2> -<%= render @post.comments %> - -<h2>Add a comment:</h2> -<%= render "comments/form" %> - - -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> | -</erb> - -With these changes in place, you'll find that you can edit a post and its tags -directly on the same view. - -However, that method call <tt>@post.tags.map { |t| t.name }.join(", ")</tt> is -awkward, we could handle this by making a helper method. - -h3. View Helpers - -View Helpers live in <tt>app/helpers</tt> and provide small snippets of reusable -code for views. In our case, we want a method that strings a bunch of objects -together using their name attribute and joining them with a comma. As this is -for the Post show template, we put it in the PostsHelper. - -Open up <tt>app/helpers/posts_helper.rb</tt> and add the following: - -<erb> -module PostsHelper - def join_tags(post) - post.tags.map { |t| t.name }.join(", ") - end -end -</erb> - -Now you can edit the view in <tt>app/views/posts/show.html.erb</tt> to look like -this: - -<erb> -<p id="notice"><%= notice %></p> - -<p> - <b>Name:</b> - <%= @post.name %> -</p> - -<p> - <b>Title:</b> - <%= @post.title %> -</p> - -<p> - <b>Content:</b> - <%= @post.content %> -</p> - -<p> - <b>Tags:</b> - <%= join_tags(@post) %> -</p> - -<h2>Comments</h2> -<%= render @post.comments %> - -<h2>Add a comment:</h2> -<%= render "comments/form" %> - - -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> | -</erb> - h3. What's Next? Now that you've seen your first Rails application, you should feel free to diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index 5439459b42..74805b2754 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -13,7 +13,7 @@ Ruby on Rails Guides and <%= link_to 'Free Kindle Reading Apps', 'http://www.amazon.com/gp/kindle/kcp' %> for the iPad, iPhone, Mac, Android, etc. Download them from <%= link_to 'here', @mobi %>. </dd> - <dd class="work-in-progress">Guides marked with this icon are currently being worked on. While they might still be useful to you, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections to the author.</dd> + <dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd> </dl> </div> <% end %> diff --git a/guides/source/layouts_and_rendering.textile b/guides/source/layouts_and_rendering.textile index c2bba56581..e4a1fd6951 100644 --- a/guides/source/layouts_and_rendering.textile +++ b/guides/source/layouts_and_rendering.textile @@ -78,16 +78,16 @@ If we want to display the properties of all the books in our view, we can do so <tr> <td><%= book.title %></td> <td><%= book.content %></td> - <td><%= link_to 'Show', book %></td> - <td><%= link_to 'Edit', edit_book_path(book) %></td> - <td><%= link_to 'Remove', book, :confirm => 'Are you sure?', :method => :delete %></td> + <td><%= link_to "Show", book %></td> + <td><%= link_to "Edit", edit_book_path(book) %></td> + <td><%= link_to "Remove", book, :confirm => "Are you sure?", :method => :delete %></td> </tr> <% end %> </table> <br /> -<%= link_to 'New book', new_book_path %> +<%= link_to "New book", new_book_path %> </ruby> NOTE: The actual rendering is done by subclasses of +ActionView::TemplateHandlers+. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. Beginning with Rails 2, the standard extensions are +.erb+ for ERB (HTML with embedded Ruby), and +.builder+ for Builder (XML generator). @@ -177,13 +177,13 @@ h5. Rendering an Action's Template from Another Controller What if you want to render a template from an entirely different controller from the one that contains the action code? You can also do that with +render+, which accepts the full path (relative to +app/views+) of the template to render. For example, if you're running code in an +AdminProductsController+ that lives in +app/controllers/admin+, you can render the results of an action to a template in +app/views/products+ this way: <ruby> -render 'products/show' +render "products/show" </ruby> Rails knows that this view belongs to a different controller because of the embedded slash character in the string. If you want to be explicit, you can use the +:template+ option (which was required on Rails 2.2 and earlier): <ruby> -render :template => 'products/show' +render :template => "products/show" </ruby> h5. Rendering an Arbitrary File @@ -216,18 +216,18 @@ In fact, in the BooksController class, inside of the update action where we want <ruby> render :edit render :action => :edit -render 'edit' -render 'edit.html.erb' -render :action => 'edit' -render :action => 'edit.html.erb' -render 'books/edit' -render 'books/edit.html.erb' -render :template => 'books/edit' -render :template => 'books/edit.html.erb' -render '/path/to/rails/app/views/books/edit' -render '/path/to/rails/app/views/books/edit.html.erb' -render :file => '/path/to/rails/app/views/books/edit' -render :file => '/path/to/rails/app/views/books/edit.html.erb' +render "edit" +render "edit.html.erb" +render :action => "edit" +render :action => "edit.html.erb" +render "books/edit" +render "books/edit.html.erb" +render :template => "books/edit" +render :template => "books/edit.html.erb" +render "/path/to/rails/app/views/books/edit" +render "/path/to/rails/app/views/books/edit.html.erb" +render :file => "/path/to/rails/app/views/books/edit" +render :file => "/path/to/rails/app/views/books/edit.html.erb" </ruby> Which one you use is really a matter of style and convention, but the rule of thumb is to use the simplest one that makes sense for the code you are writing. @@ -306,7 +306,7 @@ h6. The +:content_type+ Option By default, Rails will serve the results of a rendering operation with the MIME content-type of +text/html+ (or +application/json+ if you use the +:json+ option, or +application/xml+ for the +:xml+ option.). There are times when you might like to change this, and you can do so by setting the +:content_type+ option: <ruby> -render :file => filename, :content_type => 'application/rss' +render :file => filename, :content_type => "application/rss" </ruby> h6. The +:layout+ Option @@ -316,7 +316,7 @@ With most of the options to +render+, the rendered content is displayed as part You can use the +:layout+ option to tell Rails to use a specific file as the layout for the current action: <ruby> -render :layout => 'special_layout' +render :layout => "special_layout" </ruby> You can also tell Rails to render with no layout at all: @@ -378,7 +378,7 @@ You can use a symbol to defer the choice of layout until a request is processed: <ruby> class ProductsController < ApplicationController - layout :products_layout + layout "products_layout" def show @product = Product.find(params[:id]) @@ -398,7 +398,7 @@ You can even use an inline method, such as a Proc, to determine the layout. For <ruby> class ProductsController < ApplicationController - layout Proc.new { |controller| controller.request.xhr? ? 'popup' : 'application' } + layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" } end </ruby> @@ -445,7 +445,7 @@ end <ruby> class OldPostsController < SpecialPostsController - layout nil + layout false def show @post = Post.find(params[:id]) @@ -583,7 +583,7 @@ def show @book = Book.find_by_id(params[:id]) if @book.nil? @books = Book.all - render "index", :alert => 'Your book was not found!' + render "index", :alert => "Your book was not found!" end end </ruby> @@ -770,7 +770,7 @@ By default, the combined file will be delivered as +javascripts/all.js+. You can <erb> <%= javascript_include_tag "main", "columns", - :cache => 'cache/main/display' %> + :cache => "cache/main/display" %> </erb> You can even use dynamic paths such as +cache/#{current_site}/main/display+. @@ -833,7 +833,7 @@ By default, the combined file will be delivered as +stylesheets/all.css+. You ca <erb> <%= stylesheet_link_tag "main", "columns", - :cache => 'cache/main/display' %> + :cache => "cache/main/display" %> </erb> You can even use dynamic paths such as +cache/#{current_site}/main/display+. @@ -884,7 +884,7 @@ In addition to the above special tags, you can supply a final hash of standard H <erb> <%= image_tag "home.gif", :alt => "Go Home", :id => "HomeImage", - :class => 'nav_bar' %> + :class => "nav_bar" %> </erb> h5. Linking to Videos with the +video_tag+ @@ -905,7 +905,7 @@ Like an +image_tag+ you can supply a path, either absolute, or relative to the + The video tag also supports all of the +<video>+ HTML options through the HTML options hash, including: -* +:poster => 'image_name.png'+, provides an image to put in place of the video before it starts playing. +* +:poster => "image_name.png"+, provides an image to put in place of the video before it starts playing. * +:autoplay => true+, starts playing the video on page load. * +:loop => true+, loops the video once it gets to the end. * +:controls => true+, provides browser supplied controls for the user to interact with the video. @@ -1159,7 +1159,7 @@ In the event that the collection is empty, +render+ will return nil, so it shoul <erb> <h1>Products</h1> -<%= render(@products) || 'There are no products available.' %> +<%= render(@products) || "There are no products available." %> </erb> h5. Local Variables @@ -1175,7 +1175,7 @@ With this change, you can access an instance of the +@products+ collection as th You can also pass in arbitrary local variables to any partial you are rendering with the +:locals => {}+ option: <erb> -<%= render :partial => 'products', :collection => @products, +<%= render :partial => "products", :collection => @products, :as => :item, :locals => {:title => "Products Page"} %> </erb> @@ -1193,6 +1193,16 @@ h5. Spacer Templates Rails will render the +_product_ruler+ partial (with no data passed in to it) between each pair of +_product+ partials. +h5. Partial Layouts + +When rendering collections it is also possible to use the +:layout+ option: + +<erb> +<%= render :partial => "product", :collection => @products, :layout => "special_layout" %> +</erb> + +The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they do within the partial. + h4. Using Nested Layouts You may find that your application requires a layout that differs slightly from your regular application layout to support one particular controller. Rather than repeating the main layout and editing it, you can accomplish this by using nested layouts (sometimes called sub-templates). Here's an example: @@ -1204,8 +1214,8 @@ Suppose you have the following +ApplicationController+ layout: <erb> <html> <head> - <title><%= @page_title or 'Page Title' %></title> - <%= stylesheet_link_tag 'layout' %> + <title><%= @page_title or "Page Title" %></title> + <%= stylesheet_link_tag "layout" %> <style><%= yield :stylesheets %></style> </head> <body> @@ -1229,7 +1239,7 @@ On pages generated by +NewsController+, you want to hide the top menu and add a <div id="right_menu">Right menu items here</div> <%= content_for?(:news_content) ? yield(:news_content) : yield %> <% end %> -<%= render :template => 'layouts/application' %> +<%= render :template => "layouts/application" %> </erb> That's it. The News views will use the new layout, hiding the top menu and adding a new right menu inside the "content" div. diff --git a/guides/source/routing.textile b/guides/source/routing.textile index 836e0cdd70..4a50edbb15 100644 --- a/guides/source/routing.textile +++ b/guides/source/routing.textile @@ -837,7 +837,7 @@ If you have a large route file that you would like to break up into multiple fil draw :admin </ruby> -Then, create a file called +config/routes/admin.rb+. Name the file the same as the symbol passed to the +draw+ method). You can then use the normal routing DSL inside that file: +Then, create a file called +config/routes/admin.rb+. Name the file the same as the symbol passed to the +draw+ method. You can then use the normal routing DSL inside that file: <ruby> # in config/routes/admin.rb diff --git a/guides/source/security.textile b/guides/source/security.textile index c065529cac..ac55d60368 100644 --- a/guides/source/security.textile +++ b/guides/source/security.textile @@ -1,7 +1,6 @@ h2. Ruby On Rails Security Guide -This manual describes common security problems in web applications and how to avoid them with Rails. If you have any questions or suggestions, please -mail me, Heiko Webers, at 42 {_et_} rorsecurity.info. After reading it, you should be familiar with: +This manual describes common security problems in web applications and how to avoid them with Rails. After reading it, you should be familiar with: * All countermeasures _(highlight)that are highlighted_ * The concept of sessions in Rails, what to put in there and popular attack methods @@ -628,7 +627,7 @@ h4. Whitelists versus Blacklists -- _When sanitizing, protecting or verifying something, whitelists over blacklists._ -A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although, sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _(highlight)prefer to use whitelist approaches_: +A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _(highlight)prefer to use whitelist approaches_: * Use before_filter :only => [...] instead of :except => [...]. This way you don't forget to turn it off for newly added actions. * Use attr_accessible instead of attr_protected. See the mass-assignment section for details diff --git a/guides/source/upgrading_ruby_on_rails.textile b/guides/source/upgrading_ruby_on_rails.textile index e63548abc9..2b2e65c813 100644 --- a/guides/source/upgrading_ruby_on_rails.textile +++ b/guides/source/upgrading_ruby_on_rails.textile @@ -38,6 +38,10 @@ h4(#identity_map4_0). IdentityMap Rails 4.0 has removed <tt>IdentityMap</tt> from <tt>ActiveRecord</tt>, due to "some inconsistencies with associations":https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6. If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: <tt>config.active_record.identity_map</tt>. +h4(#active_model4_0). ActiveModel + +Rails 4.0 has changed how errors attach with the ConfirmationValidator. Now when confirmation validations fail the error will be attached to <tt>:#{attribute}_confirmation</tt> instead of <tt>attribute</tt>. + h3. Upgrading from Rails 3.1 to Rails 3.2 If your application is currently on any version of Rails older than 3.1.x, you should upgrade to Rails 3.1 before attempting an update to Rails 3.2. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index bc34ced283..01df2c5b64 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,11 @@ ## Rails 4.0.0 (unreleased) ## +* The application generator generates `public/humans.txt` with some basic data. *Paul Campbell* + +* Add `config.queue_consumer` to allow the default consumer to be configurable. *Carlos Antonio da Silva* + +* Add Rails.queue as an interface with a default implementation that consumes jobs in a separate thread. *Yehuda Katz* + * Remove Rack::SSL in favour of ActionDispatch::SSL. *Rafael Mendonça França* * Remove Active Resource from Rails framework. *Prem Sichangrist* diff --git a/railties/Rakefile b/railties/Rakefile index c4a91a1d36..993ba840ff 100755..100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rubygems/package_task' @@ -21,7 +20,8 @@ namespace :test do 'test', 'lib', "#{File.dirname(__FILE__)}/../activesupport/lib", - "#{File.dirname(__FILE__)}/../actionpack/lib" + "#{File.dirname(__FILE__)}/../actionpack/lib", + "#{File.dirname(__FILE__)}/../activemodel/lib" ] ruby "-I#{dash_i.join ':'}", file end @@ -60,12 +60,3 @@ task :release => :package do Rake::Gemcutter::Tasks.new(spec).define Rake::Task['gem:push'].invoke end - -desc "Publish the guides" -task :pguides => :generate_guides do - require 'rake/contrib/sshpublisher' - mkdir_p 'pkg' - `tar -czf pkg/guides.gz guides/output` - Rake::SshFilePublisher.new("web.rubyonrails.org", "/u/sites/guides.rubyonrails.org/public", "pkg", "guides.gz").upload - `ssh web.rubyonrails.org 'cd /u/sites/guides.rubyonrails.org/public/ && tar -xvzf guides.gz && mv guides/output/* . && rm -rf guides*'` -end diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index 945063e55c..59c3c56e59 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -22,6 +22,7 @@ end module Rails autoload :Info, 'rails/info' autoload :InfoController, 'rails/info_controller' + autoload :Queueing, 'rails/queueing' class << self def application @@ -37,6 +38,25 @@ module Rails application.config end + # Rails.queue is the application's queue. You can push a job onto + # the queue by: + # + # Rails.queue.push job + # + # A job is an object that responds to +run+. Queue consumers will + # pop jobs off of the queue and invoke the queue's +run+ method. + # + # Note that depending on your queue implementation, jobs may not + # be executed in the same process as they were created in, and + # are never executed in the same thread as they were created in. + # + # If necessary, a queue implementation may need to serialize your + # job for distribution to another process. The documentation of + # your queue will specify the requirements for that serialization. + def queue + application.queue + end + def initialize! application.initialize! end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index b063e7681d..c4edbae55b 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -66,9 +66,10 @@ module Rails end end - attr_accessor :assets, :sandbox + attr_accessor :assets, :sandbox, :queue_consumer alias_method :sandbox?, :sandbox attr_reader :reloaders + attr_writer :queue delegate :default_url_options, :default_url_options=, :to => :routes @@ -199,6 +200,14 @@ module Rails @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd)) end + def queue #:nodoc: + @queue ||= build_queue + end + + def build_queue # :nodoc: + config.queue.new + end + def to_app self end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 37297f0496..a2e5dece16 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -8,10 +8,11 @@ module Rails attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, :autoflush_log, :cache_classes, :cache_store, :consider_all_requests_local, :console, :dependency_loading, :exceptions_app, :file_watcher, :filter_parameters, - :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :preload_frameworks, - :railties_order, :relative_url_root, :secret_token, + :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, + :preload_frameworks, :railties_order, :relative_url_root, :secret_token, :serve_static_assets, :ssl_options, :static_cache_control, :session_options, - :time_zone, :reload_classes_only_on_change, :use_schema_cache_dump + :time_zone, :reload_classes_only_on_change, :use_schema_cache_dump, + :queue, :queue_consumer attr_writer :log_level attr_reader :encoding @@ -43,6 +44,8 @@ module Rails @autoflush_log = true @log_formatter = ActiveSupport::Logger::SimpleFormatter.new @use_schema_cache_dump = true + @queue = Rails::Queueing::Queue + @queue_consumer = Rails::Queueing::ThreadedConsumer @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 002c6026e4..84f2601f28 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -93,6 +93,13 @@ module Rails ActiveSupport::Dependencies.unhook! end end + + initializer :activate_queue_consumer do |app| + if config.queue == Rails::Queueing::Queue + app.queue_consumer = config.queue_consumer.start(app.queue) + at_exit { app.queue_consumer.shutdown } + end + end end end end diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index 6f9a200aa9..19f616ec50 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -3,12 +3,13 @@ require "active_support/core_ext/module/delegation" module Rails class Application class RoutesReloader - attr_reader :route_sets, :paths + attr_reader :route_sets, :paths, :external_routes delegate :execute_if_updated, :execute, :updated?, :to => :updater def initialize - @paths = [] - @route_sets = [] + @paths = [] + @route_sets = [] + @external_routes = [] end def reload! @@ -23,7 +24,11 @@ module Rails def updater @updater ||= begin - updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! } + dirs = @external_routes.each_with_object({}) do |dir, hash| + hash[dir.to_s] = %w(rb) + end + + updater = ActiveSupport::FileUpdateChecker.new(paths, dirs) { reload! } updater.execute updater end diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb index 539f68a3be..8cc8eb1103 100644 --- a/railties/lib/rails/backtrace_cleaner.rb +++ b/railties/lib/rails/backtrace_cleaner.rb @@ -17,8 +17,6 @@ module Rails private def add_gem_filters - return unless defined?(Gem) - gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) } return if gems_paths.empty? diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 82cdd6053b..7f473c237c 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -57,8 +57,7 @@ when 'server' when 'dbconsole' require 'rails/commands/dbconsole' - require APP_PATH - Rails::DBConsole.start(Rails.application) + Rails::DBConsole.start when 'application', 'runner' require "rails/commands/#{command}" diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 6fc127efae..aaba47117f 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -5,12 +5,37 @@ require 'rbconfig' module Rails class DBConsole - def self.start(app) - new(app).start + attr_reader :arguments, :config + + def self.start + new(config).start + end + + def self.config + config = begin + YAML.load(ERB.new(IO.read("config/database.yml")).result) + rescue SyntaxError, StandardError + require APP_PATH + Rails.application.config.database_configuration + end + + unless config[env] + abort "No database is configured for the environment '#{env}'" + end + + config[env] end - def initialize(app) - @app = app + def self.env + if Rails.respond_to?(:env) + Rails.env + else + ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + end + end + + def initialize(config, arguments = ARGV) + @config, @arguments = config, arguments end def start @@ -31,28 +56,10 @@ module Rails options['header'] = h end - opt.parse!(ARGV) - abort opt.to_s unless (0..1).include?(ARGV.size) + opt.parse!(arguments) + abort opt.to_s unless (0..1).include?(arguments.size) end - unless config = @app.config.database_configuration[Rails.env] - abort "No database is configured for the environment '#{Rails.env}'" - end - - - def find_cmd(*commands) - dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - commands += commands.map{|cmd| "#{cmd}.exe"} if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ - - full_path_command = nil - found = commands.detect do |cmd| - dir = dirs_on_path.detect do |path| - full_path_command = File.join(path, cmd) - File.executable? full_path_command - end - end - found ? full_path_command : abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end case config["adapter"] when /^mysql/ @@ -72,17 +79,17 @@ module Rails args << config['database'] - exec(find_cmd('mysql', 'mysql5'), *args) + find_cmd_and_exec(['mysql', 'mysql5'], *args) when "postgresql", "postgres" ENV['PGUSER'] = config["username"] if config["username"] ENV['PGHOST'] = config["host"] if config["host"] ENV['PGPORT'] = config["port"].to_s if config["port"] ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && include_password - exec(find_cmd('psql'), config["database"]) + find_cmd_and_exec('psql', config["database"]) when "sqlite" - exec(find_cmd('sqlite'), config["database"]) + find_cmd_and_exec('sqlite', config["database"]) when "sqlite3" args = [] @@ -91,7 +98,7 @@ module Rails args << "-header" if options['header'] args << config['database'] - exec(find_cmd('sqlite3'), *args) + find_cmd_and_exec('sqlite3', *args) when "oracle", "oracle_enhanced" logon = "" @@ -102,12 +109,35 @@ module Rails logon << "@#{config['database']}" if config['database'] end - exec(find_cmd('sqlplus'), logon) + find_cmd_and_exec('sqlplus', logon) else abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" end end + + protected + + def find_cmd_and_exec(commands, *args) + commands = Array(commands) + + dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) + commands += commands.map{|cmd| "#{cmd}.exe"} if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ + + full_path_command = nil + found = commands.detect do |cmd| + dir = dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.executable? full_path_command + end + end + + if found + exec full_path_command, *args + else + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") + end + end end end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index e8cc5d9e3b..2802981e7a 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -9,7 +9,6 @@ if ARGV.first.nil? end ARGV.clone.options do |opts| - script_name = File.basename($0) opts.banner = "Usage: runner [options] ('Some.ruby(code)' or a filename)" opts.separator "" diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index d8ca6cbd21..3d66019e5e 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -1,6 +1,6 @@ require 'active_support/deprecation' require 'active_support/ordered_options' -require 'active_support/core_ext/hash/deep_dup' +require 'active_support/core_ext/object' require 'rails/paths' require 'rails/rack' @@ -16,7 +16,7 @@ module Rails # # config.middleware.use Magical::Unicorns # - # This will put the +Magical::Unicorns+ middleware on the end of the stack. + # This will put the <tt>Magical::Unicorns</tt> middleware on the end of the stack. # You can use +insert_before+ if you wish to add a middleware before another: # # config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index de0b9a3761..9bf9cbe022 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -332,7 +332,7 @@ module Rails # # == Loading priority # - # In order to change engine's priority you can use config.railties_order in main application. + # In order to change engine's priority you can use +config.railties_order+ in main application. # It will affect the priority of loading views, helpers, assets and all the other files # related to engine or application. # @@ -486,8 +486,10 @@ module Rails end def routes - @routes ||= ActionDispatch::Routing::RouteSet.new - @routes.draw_paths.concat paths["config/routes"].paths + @routes ||= ActionDispatch::Routing::RouteSet.new.tap do |routes| + routes.draw_paths.concat paths["config/routes"].paths + end + @routes.append(&Proc.new) if block_given? @routes end @@ -546,10 +548,12 @@ module Rails initializer :add_routing_paths do |app| paths = self.paths["config/routes.rb"].existent + external_paths = self.paths["config/routes"].paths if routes? || paths.any? app.routes_reloader.paths.unshift(*paths) app.routes_reloader.route_sets << routes + app.routes_reloader.external_routes.unshift(*external_paths) end end @@ -604,7 +608,12 @@ module Rails desc "Copy migrations from #{railtie_name} to application" task :migrations do ENV["FROM"] = railtie_name - Rake::Task["railties:install:migrations"].invoke + if Rake::Task.task_defined?("railties:install:migrations") + Rake::Task["railties:install:migrations"].invoke + else + Rake::Task["app:railties:install:migrations"].invoke + end + end end end @@ -617,7 +626,7 @@ module Rails end def routes? - defined?(@routes) + defined?(@routes) && @routes end def has_migrations? diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index b71119af77..ffbc0b4bd6 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -34,6 +34,10 @@ The common rails commands available for engines are: destroy Undo code generated with "generate" (short-cut alias: "d") All commands can be run with -h for more information. + +If you want to run any commands that need to be run in context +of the application, like `rails server` or `rails console`, +you should do it from application's directory (typically test/dummy). EOT exit(1) end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 8c8ed5010f..6df33d65e9 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -49,6 +49,9 @@ module Rails class_option :skip_javascript, :type => :boolean, :aliases => "-J", :default => false, :desc => "Skip JavaScript files" + class_option :skip_index_html, :type => :boolean, :aliases => "-I", :default => false, + :desc => "Skip public/index.html and app/assets/images/rails.png files" + class_option :dev, :type => :boolean, :default => false, :desc => "Setup the #{name} with Gemfile pointing to your Rails checkout" @@ -134,24 +137,24 @@ module Rails def rails_gemfile_entry if options.dev? <<-GEMFILE.strip_heredoc - gem 'rails', :path => '#{Rails::Generators::RAILS_DEV_PATH}' - gem 'journey', :git => 'https://github.com/rails/journey.git' - gem 'arel', :git => 'https://github.com/rails/arel.git' - gem 'active_record_deprecated_finders', :git => 'git://github.com/rails/active_record_deprecated_finders.git' + gem 'rails', path: '#{Rails::Generators::RAILS_DEV_PATH}' + gem 'journey', github: 'rails/journey' + gem 'arel', github: 'rails/arel' + gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders' GEMFILE elsif options.edge? <<-GEMFILE.strip_heredoc - gem 'rails', :git => 'https://github.com/rails/rails.git' - gem 'journey', :git => 'https://github.com/rails/journey.git' - gem 'arel', :git => 'https://github.com/rails/arel.git' - gem 'active_record_deprecated_finders', :git => 'git://github.com/rails/active_record_deprecated_finders.git' + gem 'rails', github: 'rails/rails' + gem 'journey', github: 'rails/journey' + gem 'arel', github: 'rails/arel' + gem 'active_record_deprecated_finders', github: 'rails/active_record_deprecated_finders' GEMFILE else <<-GEMFILE.strip_heredoc gem 'rails', '#{Rails::VERSION::STRING}' # Bundle edge Rails instead: - # gem 'rails', :git => 'https://github.com/rails/rails.git' + # gem 'rails', github: 'rails/rails' GEMFILE end end @@ -191,9 +194,9 @@ module Rails # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sprockets-rails', :git => 'https://github.com/rails/sprockets-rails.git' - gem 'sass-rails', :git => 'https://github.com/rails/sass-rails.git' - gem 'coffee-rails', :git => 'https://github.com/rails/coffee-rails.git' + gem 'sprockets-rails', github: 'rails/sprockets-rails' + gem 'sass-rails', github: 'rails/sass-rails' + gem 'coffee-rails', github: 'rails/coffee-rails' # See https://github.com/sstephenson/execjs#readme for more supported runtimes #{javascript_runtime_gemfile_entry} @@ -205,7 +208,7 @@ module Rails # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sprockets-rails', :git => 'https://github.com/rails/sprockets-rails.git' + gem 'sprockets-rails', github: 'rails/sprockets-rails' gem 'sass-rails', '~> 4.0.0.beta' gem 'coffee-rails', '~> 4.0.0.beta' @@ -227,7 +230,7 @@ module Rails if defined?(JRUBY_VERSION) "gem 'therubyrhino'\n" else - "# gem 'therubyracer', :platform => :ruby\n" + "# gem 'therubyracer', platform: :ruby\n" end end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 303331a4f0..d78d97b2b4 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -1,23 +1,27 @@ <h1>Listing <%= plural_table_name %></h1> <table> - <tr> -<% attributes.each do |attribute| -%> - <th><%= attribute.human_name %></th> -<% end -%> - <th></th> - <th></th> - <th></th> - </tr> + <thead> + <tr> + <% attributes.each do |attribute| -%> + <th><%= attribute.human_name %></th> + <% end -%> + <th></th> + <th></th> + <th></th> + </tr> + </thead> - <%%= content_tag_for(:tr, @<%= plural_table_name %>) do |<%= singular_table_name %>| %> -<% attributes.each do |attribute| -%> - <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> -<% end -%> - <td><%%= link_to 'Show', <%= singular_table_name %> %></td> - <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td> - <td><%%= link_to 'Destroy', <%= singular_table_name %>, confirm: 'Are you sure?', method: :delete %></td> - <%% end %> + <tbody> + <%%= content_tag_for(:tr, @<%= plural_table_name %>) do |<%= singular_table_name %>| %> + <% attributes.each do |attribute| -%> + <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> + <% end -%> + <td><%%= link_to 'Show', <%= singular_table_name %> %></td> + <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td> + <td><%%= link_to 'Destroy', <%= singular_table_name %>, confirm: 'Are you sure?', method: :delete %></td> + <%% end %> + </tbody> </table> <br /> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index f0745df667..c06b0f8994 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -97,6 +97,11 @@ module Rails def public_directory directory "public", "public", :recursive => false + if options[:skip_index_html] + remove_file "public/index.html" + remove_file 'app/assets/images/rails.png' + git_keep 'app/assets/images' + end end def script diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index 4dc1023f1f..6eb23f68a3 100755..100644 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 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 c8a3c13b95..6991b5a5f8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -43,6 +43,9 @@ module <%= app_const_base %> # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] + + # Enable escaping HTML in JSON. The default is false. + # config.active_support.escape_html_entities_in_json = true # Use SQL instead of Active Record's schema dumper when creating the database. # This is necessary if your schema can't be completely dumped by the schema dumper, @@ -55,7 +58,7 @@ module <%= app_const_base %> # parameters by using an attr_accessible or attr_protected declaration. <%= comment_if :skip_active_record %>config.active_record.whitelist_attributes = true - # Specifies wether or not has_many or has_one association option :dependent => :restrict raises + # Specifies whether or not has_many or has_one association option :dependent => :restrict raises # an exception. If set to true, then an ActiveRecord::DeleteRestrictionError exception would be # raised. If set to false, then an error will be added on the model instead. <%= comment_if :skip_active_record %>config.active_record.dependent_restrict_raises = false 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 eb4dfa7c89..24bcec854c 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 @@ -35,4 +35,7 @@ # Expands the lines which load the assets. config.assets.debug = true <%- end -%> + + # In development, use an in-memory queue for queueing + config.queue = Rails::Queueing::Queue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 1c980e5ce6..072aa8355d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -76,4 +76,8 @@ # Use default logging formatter so that PID and timestamp are not suppressed config.log_formatter = ::Logger::Formatter.new + + # Default the production mode queue to an in-memory queue. You will probably + # want to replace this with an out-of-process queueing solution + config.queue = Rails::Queueing::Queue end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index b725dd19f6..b27b88a3c6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -33,4 +33,7 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + + # Use the testing queue + config.queue = Rails::Queueing::TestQueue end diff --git a/railties/lib/rails/generators/rails/app/templates/public/humans.txt.tt b/railties/lib/rails/generators/rails/app/templates/public/humans.txt.tt new file mode 100644 index 0000000000..1c49e5c70a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/public/humans.txt.tt @@ -0,0 +1,9 @@ +# See more about this file at: http://humanstxt.org/ +# For format suggestions, see: http://humanstxt.org/Standard.html +/* TEAM */ + <%= ENV['USER'].titlecase %> + +/* APP */ + Name: <%= app_const_base %> + Date Created: <%= Date.today.strftime("%B %d, %Y") %> + Software: Ruby on Rails diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt index 085187fa58..1a3a5e4dd2 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/robots.txt +++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt @@ -1,5 +1,5 @@ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: -# User-Agent: * +# User-agent: * # Disallow: / diff --git a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb index 3fea27b916..2a849b7f2b 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb @@ -3,7 +3,7 @@ require 'rails/performance_test_help' class BrowsingTest < ActionDispatch::PerformanceTest # Refer to the documentation for all available options - # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] + # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory], # :output => 'tmp/performance', :formats => [:flat] } def test_homepage diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index f4263d1b98..722e37e20b 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -302,7 +302,7 @@ task :default => :test dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root) unless options[:pretend] || !File.exists?(dummy_application_path) contents = File.read(dummy_application_path) - contents[(contents.index("module Dummy"))..-1] + contents[(contents.index(/module ([\w]+)\n(.*)class Application/m))..-1] end end end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec index 8588e88077..82ffeebb86 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.summary = "TODO: Summary of <%= camelized %>." s.description = "TODO: Description of <%= camelized %>." - s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"] + s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] <% unless options.skip_test_unit? -%> s.test_files = Dir["test/**/*"] <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile index b7bc69d2e5..743036362e 100755..100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake begin require 'bundler/setup' rescue LoadError diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb index 2cb34fab7a..508e221c60 100644 --- a/railties/lib/rails/generators/test_case.rb +++ b/railties/lib/rails/generators/test_case.rb @@ -79,8 +79,8 @@ module Rails # # Finally, when a block is given, it yields the file content: # - # assert_file "app/controller/products_controller.rb" do |controller| - # assert_instance_method :index, content do |index| + # assert_file "app/controllers/products_controller.rb" do |controller| + # assert_instance_method :index, controller do |index| # assert_match(/Product\.all/, index) # end # end @@ -159,8 +159,8 @@ module Rails # Asserts the given method exists in the given content. When a block is given, # it yields the content of the method. # - # assert_file "app/controller/products_controller.rb" do |controller| - # assert_instance_method :index, content do |index| + # assert_file "app/controllers/products_controller.rb" do |controller| + # assert_instance_method :index, controller do |index| # assert_match(/Product\.all/, index) # end # end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index fced6f8896..b787d91821 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -10,7 +10,7 @@ module Rails # root.add "app/controllers", :eager_load => true # # The command above creates a new root object and add "app/controllers" as a path. - # This means we can get a +Rails::Paths::Path+ object back like below: + # This means we can get a <tt>Rails::Paths::Path</tt> object back like below: # # path = root["app/controllers"] # path.eager_load? # => true diff --git a/railties/lib/rails/queueing.rb b/railties/lib/rails/queueing.rb new file mode 100644 index 0000000000..b4bc7fcd18 --- /dev/null +++ b/railties/lib/rails/queueing.rb @@ -0,0 +1,73 @@ +require "thread" + +module Rails + module Queueing + # A Queue that simply inherits from STDLIB's Queue. Everytime this + # queue is used, Rails automatically sets up a ThreadedConsumer + # to consume it. + class Queue < ::Queue + end + + # In test mode, the Rails queue is backed by an Array so that assertions + # can be made about its contents. The test queue provides a +jobs+ + # method to make assertions about the queue's contents and a +drain+ + # method to drain the queue and run the jobs. + # + # Jobs are run in a separate thread to catch mistakes where code + # assumes that the job is run in the same thread. + class TestQueue < ::Queue + # Get a list of the jobs off this queue. This method may not be + # available on production queues. + def jobs + @que.dup + end + + # Drain the queue, running all jobs in a different thread. This method + # may not be available on production queues. + def drain + # run the jobs in a separate thread so assumptions of synchronous + # jobs are caught in test mode. + Thread.new { pop.run until empty? }.join + end + end + + # The threaded consumer will run jobs in a background thread in + # development mode or in a VM where running jobs on a thread in + # production mode makes sense. + # + # When the process exits, the consumer pushes a nil onto the + # queue and joins the thread, which will ensure that all jobs + # are executed before the process finally dies. + class ThreadedConsumer + def self.start(queue) + new(queue).start + end + + def initialize(queue) + @queue = queue + end + + def start + @thread = Thread.new do + while job = @queue.pop + begin + job.run + rescue Exception => e + handle_exception e + end + end + end + self + end + + def shutdown + @queue.push nil + @thread.join + end + + def handle_exception(e) + Rails.logger.error "Job Error: #{e.message}\n#{e.backtrace.join("\n")}" + end + end + end +end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index dc5de4084e..2102f8a03c 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -22,7 +22,7 @@ module Rails # # * creating initializers # * configuring a Rails framework for the application, like setting a generator - # * adding config.* keys to the environment + # * +adding config.*+ keys to the environment # * setting up a subscriber with ActiveSupport::Notifications # * adding rake tasks # diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index cf9e4ad500..1c6b3769a5 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -43,7 +43,7 @@ module Rails ActiveSupport.on_load(:before_configuration, :yield => true, &block) end - # Third configurable block to run. Does not run if config.cache_classes + # Third configurable block to run. Does not run if +config.cache_classes+ # set to false. def before_eager_load(&block) ActiveSupport.on_load(:before_eager_load, :yield => true, &block) diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index a443c73962..31e34023c0 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -14,6 +14,9 @@ # of the line (or closing ERB comment tag) is considered to be their text. class SourceAnnotationExtractor class Annotation < Struct.new(:line, :tag, :text) + def self.directories + @@directories ||= %w(app config lib script test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',') + end # Returns a representation of the annotation that looks like this: # @@ -48,7 +51,7 @@ class SourceAnnotationExtractor # Returns a hash that maps filenames under +dirs+ (recursively) to arrays # with their annotations. - def find(dirs=%w(app config lib script test)) + def find(dirs = Annotation.directories) dirs.inject({}) { |h, dir| h.update(find_in(dir)) } end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index e84b1d0644..7067253279 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -21,7 +21,11 @@ Gem::Specification.new do |s| s.rdoc_options << '--exclude' << '.' s.add_dependency('rake', '>= 0.8.7') - s.add_dependency('thor', '~> 0.14.6') + + # The current API of the Thor gem (0.14) will remain stable at least until Thor 2.0. Because + # Thor is so heavily used by other gems, we will accept Thor's semver guarantee to reduce + # the possibility of conflicts. + s.add_dependency('thor', '>= 0.14.6', '< 2.0') s.add_dependency('rdoc', '~> 3.4') s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 29ebdc6511..dfcf5aa27d 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -1,3 +1,5 @@ +ENV["RAILS_ENV"] ||= "test" + require File.expand_path("../../../load_paths", __FILE__) require 'stringio' diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index ac5ac2b93e..252dd0e31a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -41,6 +41,14 @@ module ApplicationTests FileUtils.rm_rf(new_app) if File.directory?(new_app) end + test "multiple queue construction is possible" do + require 'rails' + require "#{app_path}/config/environment" + mail_queue = Rails.application.build_queue + image_processing_queue = Rails.application.build_queue + assert_not_equal mail_queue, image_processing_queue + end + test "Rails.groups returns available groups" do require "rails" diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index f4e77ee244..07134cc935 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -26,5 +26,25 @@ module ApplicationTests require "#{app_path}/config/environment" assert app.config.session_options[:secure], "Expected session to be marked as secure" end + + test "session is not loaded if it's not used" do + make_basic_app + + class ::OmgController < ActionController::Base + def index + if params[:flash] + flash[:notice] = "notice" + end + + render :nothing => true + end + end + + get "/?flash=true" + get "/" + + assert last_request.env["HTTP_COOKIE"] + assert !last_response.headers["Set-Cookie"] + end end end diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb new file mode 100644 index 0000000000..da8bdeed52 --- /dev/null +++ b/railties/test/application/queue_test.rb @@ -0,0 +1,145 @@ +require 'isolation/abstract_unit' +require 'rack/test' + +module ApplicationTests + class GeneratorsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + def app_const + @app_const ||= Class.new(Rails::Application) + end + + test "the queue is a TestQueue in test mode" do + app("test") + assert_kind_of Rails::Queueing::TestQueue, Rails.application.queue + assert_kind_of Rails::Queueing::TestQueue, Rails.queue + end + + test "the queue is a Queue in development mode" do + app("development") + assert_kind_of Rails::Queueing::Queue, Rails.application.queue + assert_kind_of Rails::Queueing::Queue, Rails.queue + end + + test "in development mode, an enqueued job will be processed in a separate thread" do + app("development") + + job = Struct.new(:origin, :target).new(Thread.current) + def job.run + self.target = Thread.current + end + + Rails.queue.push job + sleep 0.1 + + assert job.target, "The job was run" + assert_not_equal job.origin, job.target + end + + test "in test mode, explicitly draining the queue will process it in a separate thread" do + app("test") + + job = Struct.new(:origin, :target).new(Thread.current) + def job.run + self.target = Thread.current + end + + Rails.queue.push job + Rails.queue.drain + + assert job.target, "The job was run" + assert_not_equal job.origin, job.target + end + + test "in test mode, the queue can be observed" do + app("test") + + job = Struct.new(:id) do + def run + end + end + + jobs = (1..10).map do |id| + job.new(id) + end + + jobs.each do |job| + Rails.queue.push job + end + + assert_equal jobs, Rails.queue.jobs + end + + def setup_custom_queue + add_to_env_config "production", <<-RUBY + require "my_queue" + config.queue = MyQueue + RUBY + + app_file "lib/my_queue.rb", <<-RUBY + class MyQueue + def push(job) + job.run + end + end + RUBY + + app("production") + end + + test "a custom queue implementation can be provided" do + setup_custom_queue + + assert_kind_of MyQueue, Rails.queue + + job = Struct.new(:id, :ran) do + def run + self.ran = true + end + end + + job1 = job.new(1) + Rails.queue.push job1 + + assert_equal true, job1.ran + end + + test "a custom consumer implementation can be provided" do + add_to_env_config "production", <<-RUBY + require "my_queue_consumer" + config.queue_consumer = MyQueueConsumer + RUBY + + app_file "lib/my_queue_consumer.rb", <<-RUBY + class MyQueueConsumer < Rails::Queueing::ThreadedConsumer + attr_reader :started + + def start + @started = true + self + end + end + RUBY + + app("production") + + assert_kind_of MyQueueConsumer, Rails.application.queue_consumer + assert Rails.application.queue_consumer.started + end + + test "default consumer is not used with custom queue implementation" do + setup_custom_queue + + assert_nil Rails.application.queue_consumer + end + end +end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index d069703d30..05d73dfc5c 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -12,7 +12,7 @@ module ApplicationTests teardown_app end - test 'notes' do + test 'notes finds notes for certain file_types' do app_file "app/views/home/index.html.erb", "<% # TODO: note in erb %>" app_file "app/views/home/index.html.haml", "-# TODO: note in haml" app_file "app/views/home/index.html.slim", "/ TODO: note in slim" @@ -49,7 +49,79 @@ module ApplicationTests assert_equal ' ', line[1] end end + end + + test 'notes finds notes in default directories' do + app_file "app/controllers/some_controller.rb", "# TODO: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + app_file "script/run_something.rb", "# TODO: note in script directory" + app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory" + + app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" + + boot_rails + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + + Rails.application.load_tasks + + Dir.chdir(app_path) do + output = `bundle exec rake notes` + lines = output.scan(/\[([0-9\s]+)\]/).flatten + + assert_match /note in app directory/, output + assert_match /note in config directory/, output + assert_match /note in lib directory/, output + assert_match /note in script directory/, output + assert_match /note in test directory/, output + assert_no_match /note in some_other directory/, output + + assert_equal 5, lines.size + + lines.each do |line_number| + assert_equal 4, line_number.size + end + end + end + + test 'notes finds notes in custom directories' do + app_file "app/controllers/some_controller.rb", "# TODO: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + app_file "script/run_something.rb", "# TODO: note in script directory" + app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory" + + app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" + + boot_rails + + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + + Rails.application.load_tasks + + Dir.chdir(app_path) do + output = `SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes` + lines = output.scan(/\[([0-9\s]+)\]/).flatten + + assert_match /note in app directory/, output + assert_match /note in config directory/, output + assert_match /note in lib directory/, output + assert_match /note in script directory/, output + assert_match /note in test directory/, output + + assert_match /note in some_other directory/, output + + assert_equal 6, lines.size + + lines.each do |line_number| + assert_equal 4, line_number.size + end + end end private diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index 1dac086119..977a5fc7e8 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -191,6 +191,65 @@ module ApplicationTests end {"development" => "baz", "production" => "bar"}.each do |mode, expected| + test "reloads routes when external 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 + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + draw :external + end + RUBY + + app_file 'config/routes/external.rb', <<-RUBY + get 'foo', :to => 'foo#bar' + RUBY + + app(mode) + + get '/foo' + assert_equal 'bar', last_response.body + + app_file 'config/routes/external.rb', <<-RUBY + get 'foo', :to => 'foo#baz' + RUBY + + sleep 0.1 + + get '/foo' + assert_equal expected, last_response.body + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + draw :external + draw :other_external + end + RUBY + + app_file 'config/routes/other_external.rb', <<-RUBY + get 'win', :to => 'foo#baz' + RUBY + + sleep 0.1 + + get '/win' + + if mode == "development" + assert_equal expected, last_response.body + else + assert_equal 404, last_response.status + end + end + test "reloads routes when configuration is changed in #{mode}" do controller :foo, <<-RUBY class FooController < ApplicationController diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb index cbe7d35f6d..2dd74f8fd1 100644 --- a/railties/test/backtrace_cleaner_test.rb +++ b/railties/test/backtrace_cleaner_test.rb @@ -1,26 +1,24 @@ require 'abstract_unit' require 'rails/backtrace_cleaner' -if defined? Gem - class BacktraceCleanerVendorGemTest < ActiveSupport::TestCase - def setup - @cleaner = Rails::BacktraceCleaner.new - end +class BacktraceCleanerVendorGemTest < ActiveSupport::TestCase + def setup + @cleaner = Rails::BacktraceCleaner.new + end + + test "should format installed gems correctly" do + @backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + @result = @cleaner.clean(@backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0] + end - test "should format installed gems correctly" do - @backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + test "should format installed gems not in Gem.default_dir correctly" do + @target_dir = Gem.path.detect { |p| p != Gem.default_dir } + # skip this test if default_dir is the only directory on Gem.path + if @target_dir + @backtrace = [ "#{@target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] @result = @cleaner.clean(@backtrace, :all) assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0] end - - test "should format installed gems not in Gem.default_dir correctly" do - @target_dir = Gem.path.detect { |p| p != Gem.default_dir } - # skip this test if default_dir is the only directory on Gem.path - if @target_dir - @backtrace = [ "#{@target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] - @result = @cleaner.clean(@backtrace, :all) - assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0] - end - end end end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb new file mode 100644 index 0000000000..85a7edfacd --- /dev/null +++ b/railties/test/commands/dbconsole_test.rb @@ -0,0 +1,160 @@ +require 'abstract_unit' +require 'rails/commands/dbconsole' + +class Rails::DBConsoleTest < ActiveSupport::TestCase + def teardown + %w[PGUSER PGHOST PGPORT PGPASSWORD].each{|key| ENV.delete(key)} + end + + def test_config + Rails::DBConsole.const_set(:APP_PATH, "erb") + + app_config({}) + capture_abort { Rails::DBConsole.config } + assert aborted + assert_match /No database is configured for the environment '\w+'/, output + + app_config(test: "with_init") + assert_equal Rails::DBConsole.config, "with_init" + + app_db_file("test:\n without_init") + assert_equal Rails::DBConsole.config, "without_init" + + app_db_file("test:\n <%= Rails.something_app_specific %>") + assert_equal Rails::DBConsole.config, "with_init" + + app_db_file("test:\n\ninvalid") + assert_equal Rails::DBConsole.config, "with_init" + end + + def test_env + assert_equal Rails::DBConsole.env, "test" + + Rails.stubs(:respond_to?).with(:env).returns(false) + assert_equal Rails::DBConsole.env, "test" + + ENV['RAILS_ENV'] = nil + ENV['RACK_ENV'] = "rack_env" + assert_equal Rails::DBConsole.env, "rack_env" + + ENV['RAILS_ENV'] = "rails_env" + assert_equal Rails::DBConsole.env, "rails_env" + ensure + ENV['RAILS_ENV'] = "test" + end + + def test_mysql + dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db') + start(adapter: 'mysql', database: 'db') + assert !aborted + end + + def test_mysql_full + dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db') + start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') + assert !aborted + end + + def test_mysql_include_password + dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--user=user', '--password=qwerty', 'db') + start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) + assert !aborted + end + + def test_postgresql + dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') + start(adapter: 'postgresql', database: 'db') + assert !aborted + end + + def test_postgresql_full + dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') + start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432) + assert !aborted + assert_equal 'user', ENV['PGUSER'] + assert_equal 'host', ENV['PGHOST'] + assert_equal '5432', ENV['PGPORT'] + assert_not_equal 'q1w2e3', ENV['PGPASSWORD'] + end + + def test_postgresql_include_password + dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') + start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p']) + assert !aborted + assert_equal 'user', ENV['PGUSER'] + assert_equal 'q1w2e3', ENV['PGPASSWORD'] + end + + def test_sqlite + dbconsole.expects(:find_cmd_and_exec).with('sqlite', 'db') + start(adapter: 'sqlite', database: 'db') + assert !aborted + end + + def test_sqlite3 + dbconsole.expects(:find_cmd_and_exec).with('sqlite3', 'db') + start(adapter: 'sqlite3', database: 'db') + assert !aborted + end + + def test_sqlite3_mode + dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-html', 'db') + start({adapter: 'sqlite3', database: 'db'}, ['--mode', 'html']) + assert !aborted + end + + def test_sqlite3_header + dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-header', 'db') + start({adapter: 'sqlite3', database: 'db'}, ['--header']) + assert !aborted + end + + def test_oracle + dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db') + start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret') + assert !aborted + end + + def test_oracle_include_password + dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user/secret@db') + start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p']) + assert !aborted + end + + def test_unknown_command_line_client + start(adapter: 'unknown', database: 'db') + assert aborted + assert_match /Unknown command-line client for db/, output + end + + private + attr_reader :aborted, :output + + def dbconsole + @dbconsole ||= Rails::DBConsole.new(nil) + end + + def start(config = {}, argv = []) + dbconsole.stubs(config: config.stringify_keys, arguments: argv) + capture_abort { dbconsole.start } + end + + def capture_abort + @aborted = false + @output = capture(:stderr) do + begin + yield + rescue SystemExit + @aborted = true + end + end + end + + def app_db_file(result) + IO.stubs(:read).with("config/database.yml").returns(result) + end + + def app_config(result) + Rails.application.config.stubs(:database_configuration).returns(result.stringify_keys) + end +end diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb new file mode 100644 index 0000000000..68406dce4c --- /dev/null +++ b/railties/test/engine_test.rb @@ -0,0 +1,24 @@ +require 'abstract_unit' + +class EngineTest < ActiveSupport::TestCase + it "reports routes as available only if they're actually present" do + engine = Class.new(Rails::Engine) do + def initialize(*args) + @routes = nil + super + end + end + + assert !engine.routes? + end + + it "does not add more paths to routes on each call" do + engine = Class.new(Rails::Engine) + + engine.routes + length = engine.routes.draw_paths.length + + engine.routes + assert_equal length, engine.routes.draw_paths.length + end +end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index ef6877ed30..a0c308a3b8 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -246,10 +246,17 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_file "Gemfile", /gem\s+["']therubyrhino["']$/ else - assert_file "Gemfile", /# gem\s+["']therubyracer["']+, :platform => :ruby$/ + assert_file "Gemfile", /# gem\s+["']therubyracer["']+, platform: :ruby$/ end end + def test_generator_if_skip_index_html_is_given + run_generator [destination_root, "--skip-index-html"] + assert_no_file "public/index.html" + assert_no_file "app/assets/images/rails.png" + assert_file "app/assets/images/.gitkeep" + end + def test_creation_of_a_test_directory run_generator assert_file 'test' @@ -376,6 +383,12 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/run bundle install/, output) end + def test_humans_txt_file + date = Date.today.strftime("%B %d, %Y") + run_generator [File.join(destination_root, 'things-43')] + assert_file "things-43/public/humans.txt", /Name: Things43/, /Software: Ruby on Rails/, /Date Created: #{date}/ + end + protected def action(*args, &block) diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index abafcefde5..6f00fc5d26 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -64,9 +64,10 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase def test_namespaced_controller_dont_indent_blank_lines run_generator - assert_file "app/controllers/test_app/account_controller.rb" - File.readlines(File.expand_path("app/controllers/test_app/account_controller.rb", destination_root)).each do |line| - assert_no_match line.chomp, /^\s+$/, "Don't indent blank lines" + assert_file "app/controllers/test_app/account_controller.rb" do |content| + content.split("\n").each do |line| + assert_no_match line, /^\s+$/, "Don't indent blank lines" + end end end end diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb index 4bb5f04a79..51374e5f3f 100644 --- a/railties/test/generators/plugin_new_generator_test.rb +++ b/railties/test/generators/plugin_new_generator_test.rb @@ -32,7 +32,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase content = capture(:stderr){ run_generator [File.join(destination_root, "things4.3")] } assert_equal "Invalid plugin name things4.3. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content - + content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] } assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content end @@ -211,7 +211,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase def test_creating_gemspec run_generator assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ - assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*"\]/ + assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/ assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/ assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ end @@ -236,6 +236,13 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase assert_no_file "test/dummy" end + def test_creating_dummy_application_with_different_name + run_generator [destination_root, "--dummy_path", "spec/fake"] + assert_file "spec/fake" + assert_file "spec/fake/config/application.rb" + assert_no_file "test/dummy" + end + def test_creating_dummy_without_tests_but_with_dummy_path run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"] assert_file "spec/dummy" diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 92117855b7..e78e67725d 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -104,13 +104,13 @@ module SharedGeneratorTests generator([destination_root], :dev => true).expects(:bundle_command).with('install').once quietly { generator.invoke_all } rails_path = File.expand_path('../../..', Rails.root) - assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:path\s+=>\s+["']#{Regexp.escape(rails_path)}["']$/ + assert_file 'Gemfile', /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/ end def test_edge_option generator([destination_root], :edge => true).expects(:bundle_command).with('install').once quietly { generator.invoke_all } - assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+:git\s+=>\s+["']#{Regexp.escape("https://github.com/rails/rails.git")}["']$} + assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$} end def test_skip_gemfile diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index b28cc6e04d..03be81e59f 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -8,7 +8,7 @@ # Rails booted up. require 'fileutils' -require 'rubygems' +require 'bundler/setup' require 'minitest/autorun' require 'active_support/test_case' @@ -17,9 +17,8 @@ RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") # These files do not require any others and are needed # to run the tests -require "#{RAILS_FRAMEWORK_ROOT}/activesupport/lib/active_support/testing/isolation" -require "#{RAILS_FRAMEWORK_ROOT}/activesupport/lib/active_support/testing/declarative" -require "#{RAILS_FRAMEWORK_ROOT}/activesupport/lib/active_support/core_ext/kernel/reporting" +require "active_support/testing/isolation" +require "active_support/core_ext/kernel/reporting" module TestHelpers module Paths @@ -249,7 +248,6 @@ module TestHelpers def use_frameworks(arr) to_remove = [:actionmailer, - :activemodel, :activerecord] - arr if to_remove.include? :activerecord remove_from_config "config.active_record.whitelist_attributes = true" @@ -268,7 +266,6 @@ class ActiveSupport::TestCase include TestHelpers::Paths include TestHelpers::Rack include TestHelpers::Generation - extend ActiveSupport::Testing::Declarative end # Create a scope and build a fixture rails app diff --git a/railties/test/queueing/test_queue_test.rb b/railties/test/queueing/test_queue_test.rb new file mode 100644 index 0000000000..78c6c617fe --- /dev/null +++ b/railties/test/queueing/test_queue_test.rb @@ -0,0 +1,67 @@ +require 'abstract_unit' +require 'rails/queueing' + +class TestQueueTest < ActiveSupport::TestCase + class Job + def initialize(&block) + @block = block + end + + def run + @block.call if @block + end + end + + def setup + @queue = Rails::Queueing::TestQueue.new + end + + def test_drain_raises + @queue.push Job.new { raise } + assert_raises(RuntimeError) { @queue.drain } + end + + def test_jobs + @queue.push 1 + @queue.push 2 + assert_equal [1,2], @queue.jobs + end + + def test_contents + assert @queue.empty? + job = Job.new + @queue.push job + refute @queue.empty? + assert_equal job, @queue.pop + end + + def test_order + processed = [] + + job1 = Job.new { processed << 1 } + job2 = Job.new { processed << 2 } + + @queue.push job1 + @queue.push job2 + @queue.drain + + assert_equal [1,2], processed + end + + def test_drain + t = nil + ran = false + + job = Job.new do + ran = true + t = Thread.current + end + + @queue.push job + @queue.drain + + assert @queue.empty? + assert ran, "The job runs synchronously when the queue is drained" + assert_not_equal t, Thread.current + end +end diff --git a/railties/test/queueing/threaded_consumer_test.rb b/railties/test/queueing/threaded_consumer_test.rb new file mode 100644 index 0000000000..c34a966d6e --- /dev/null +++ b/railties/test/queueing/threaded_consumer_test.rb @@ -0,0 +1,100 @@ +require 'abstract_unit' +require 'rails/queueing' + +class TestThreadConsumer < ActiveSupport::TestCase + class Job + attr_reader :id + def initialize(id, &block) + @id = id + @block = block + end + + def run + @block.call if @block + end + end + + def setup + @queue = Rails::Queueing::Queue.new + @consumer = Rails::Queueing::ThreadedConsumer.start(@queue) + end + + def teardown + @queue.push nil + end + + test "the jobs are executed" do + ran = false + + job = Job.new(1) do + ran = true + end + + @queue.push job + sleep 0.1 + assert_equal true, ran + end + + test "the jobs are not executed synchronously" do + ran = false + + job = Job.new(1) do + sleep 0.1 + ran = true + end + + @queue.push job + assert_equal false, ran + end + + test "shutting down the queue synchronously drains the jobs" do + ran = false + + job = Job.new(1) do + sleep 0.1 + ran = true + end + + @queue.push job + assert_equal false, ran + + @consumer.shutdown + + assert_equal true, ran + end + + test "log job that raises an exception" do + require "active_support/log_subscriber/test_helper" + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + Rails.logger = logger + + job = Job.new(1) do + raise "RuntimeError: Error!" + end + + @queue.push job + sleep 0.1 + + assert_equal 1, logger.logged(:error).size + assert_match(/Job Error: RuntimeError: Error!/, logger.logged(:error).last) + end + + test "test overriding exception handling" do + @consumer.shutdown + @consumer = Class.new(Rails::Queueing::ThreadedConsumer) do + attr_reader :last_error + def handle_exception(e) + @last_error = e.message + end + end.start(@queue) + + job = Job.new(1) do + raise "RuntimeError: Error!" + end + + @queue.push job + sleep 0.1 + + assert_equal "RuntimeError: Error!", @consumer.last_error + end +end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index beae2c5f2c..9a6b2b66ca 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -86,7 +86,6 @@ module RailtiesTest add_to_config "ActiveRecord::Base.timestamped_migrations = false" boot_rails - railties = Rails.application.railties.all.map(&:railtie_name) Dir.chdir(app_path) do output = `bundle exec rake bukkits:install:migrations` @@ -112,6 +111,37 @@ module RailtiesTest end end + test "mountable engine should copy migrations within engine_path" do + @plugin.write "lib/bukkits.rb", <<-RUBY + module Bukkits + class Engine < ::Rails::Engine + isolate_namespace Bukkits + end + end + RUBY + + @plugin.write "db/migrate/0_add_first_name_to_users.rb", <<-RUBY + class AddFirstNameToUsers < ActiveRecord::Migration + end + RUBY + + @plugin.write "Rakefile", <<-RUBY + APP_RAKEFILE = '#{app_path}/Rakefile' + load 'rails/tasks/engine.rake' + RUBY + + add_to_config "ActiveRecord::Base.timestamped_migrations = false" + + boot_rails + + Dir.chdir(@plugin.path) do + output = `bundle exec rake app:bukkits:install:migrations` + assert File.exists?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb") + assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output) + assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length + end + end + test "no rake task without migrations" do boot_rails require 'rake' |