diff options
221 files changed, 2718 insertions, 1554 deletions
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index e45a1cd5ff..32e6183ff6 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -26,10 +26,9 @@ require 'action_view' require 'action_mailer/version' # Common Active Support usage in Action Mailer +require 'active_support/rails' require 'active_support/core_ext/class' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/string/inflections' require 'active_support/lazy_load_hooks' diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f0b1fcbe63..73ac8ea12b 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -1,6 +1,5 @@ require 'mail' require 'action_mailer/collector' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 529140dfad..108969ed4c 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -1,5 +1,4 @@ require 'active_support/test_case' -require 'active_support/core_ext/class/attribute' module ActionMailer class NonInferrableMailerError < ::StandardError diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index d5326e3d0b..861e086372 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,9 @@ ## Rails 4.0.0 (unreleased) ## +* Add the ability to pass options as part of the array notation for `polymorphic_url` *Romain Tribes* + +* Fixed issue with where Digest authentication would not work behind a proxy. *Arthur Smith* + * Added ActionController::Live. Mix it in to your controller and you can stream data to the client live. For example: diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index b95ea5f0b2..867a7954e0 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -1,9 +1,6 @@ require 'action_pack' -require 'active_support/concern' -require 'active_support/dependencies/autoload' -require 'active_support/core_ext/class/attribute' +require 'active_support/rails' require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/anonymous' require 'active_support/i18n' diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 1d06c83338..ceb90f8cee 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -1,3 +1,4 @@ +require 'active_support/rails' require 'abstract_controller' require 'action_dispatch' require 'action_controller/metal/live' @@ -54,11 +55,9 @@ require 'action_view' require 'action_controller/vendor/html-scanner' # Common Active Support usage in Action Controller -require 'active_support/concern' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/load_error' require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/name_error' require 'active_support/core_ext/uri' require 'active_support/inflector' diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 0fb419f941..a7c0e971e7 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActionController class LogSubscriber < ActiveSupport::LogSubscriber diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 0a0f5393ce..b38f990efa 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' require 'action_dispatch/middleware/stack' module ActionController diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 2fcd933d32..747e1273be 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -29,19 +29,19 @@ module ActionController self.status = status self.location = url_for(location) if location - if include_content_headers?(self.status) + if include_content?(self.status) self.content_type = content_type || (Mime[formats.first] if formats) + self.response_body = " " else headers.delete('Content-Type') headers.delete('Content-Length') + self.response_body = "" end - - self.response_body = " " end private # :nodoc: - def include_content_headers?(status) + def include_content?(status) case status when 100..199 false diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 66cdfd40ff..d2cbbd3330 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActionController # The \Rails framework provides a large number of helpers for working with assets, dates, forms, diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb index b55c4643be..420b22cf56 100644 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ b/actionpack/lib/action_controller/metal/hide_actions.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActionController # Adds the ability to prevent public methods on a controller to be called as actions. diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index d84588d3df..03b8d8db1a 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,5 +1,4 @@ require 'base64' -require 'active_support/core_ext/object/blank' module ActionController # Makes it dead easy to do HTTP Basic, Digest and Token authentication. @@ -194,7 +193,7 @@ module ActionController return false unless password method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] - uri = credentials[:uri][0,1] == '/' ? request.original_fullpath : request.original_url + uri = credentials[:uri] [true, false].any? do |trailing_question_mark| [true, false].any? do |password_is_ha1| @@ -372,7 +371,7 @@ module ActionController # def test_access_granted_from_xml # get( # "/notes/1.xml", nil, - # :authorization => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) + # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) # ) # # assert_equal 200, status diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 4674f0348d..43a9e3aa9d 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -1,9 +1,9 @@ require 'action_dispatch/http/response' require 'delegate' -module ActionController # :nodoc: +module ActionController # Mix this module in to your controller, and all actions in that controller - # will be able to stream data to the client as it's written. For example: + # will be able to stream data to the client as it's written. # # class MyController < ActionController::Base # include ActionController::Live @@ -18,20 +18,20 @@ module ActionController # :nodoc: # end # end # - # There are a few caveats with this use. You *cannot* write headers after the + # There are a few caveats with this use. You *cannot* write headers after the # response has been committed (Response#committed? will return truthy). # Calling +write+ or +close+ on the response stream will cause the response - # object to be committed. Make sure all headers are set before calling write + # object to be committed. Make sure all headers are set before calling write # or close on your stream. # # You *must* call close on your stream when you're finished, otherwise the # socket may be left open forever. # - # The final caveat is that you actions are executed in a separate thread than - # the main thread. Make sure your actions are thread safe, and this shouldn't + # The final caveat is that your actions are executed in a separate thread than + # the main thread. Make sure your actions are thread safe, and this shouldn't # be a problem (don't share state across threads, etc). module Live - class Buffer < ActionDispatch::Response::Buffer # :nodoc: + class Buffer < ActionDispatch::Response::Buffer #:nodoc: def initialize(response) super(response, Queue.new) end @@ -47,18 +47,18 @@ module ActionController # :nodoc: def each while str = @buf.pop - yield str + yield(str) end end def close super - @buf.push nil + @buf.push(nil) end end - class Response < ActionDispatch::Response # :nodoc: - class Header < DelegateClass(Hash) # :nodoc: + class Response < ActionDispatch::Response #:nodoc: all + class Header < DelegateClass(Hash) def initialize(response, header) @response = response super(header) @@ -78,7 +78,7 @@ module ActionController # :nodoc: end def initialize(status = 200, header = {}, body = []) - header = Header.new self, header + header = Header.new(self, header) super(status, header, body) end @@ -89,18 +89,18 @@ module ActionController # :nodoc: private - def build_buffer(response, body) - buf = Live::Buffer.new response - body.each { |part| buf.write part } - buf - end + def build_buffer(response, body) + buf = Live::Buffer.new(response) + body.each { |part| buf.write(part) } + buf + end end def process(name) t1 = Thread.current locals = t1.keys.map { |key| [key, t1[key]] } - # This processes the action in a child thread. It lets us return the + # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client Thread.new { diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 4665fea91a..64461c73e0 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,6 +1,4 @@ require 'abstract_controller/collector' -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActionController #:nodoc: module MimeResponds diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index aa67fa7f23..2736948ce0 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 1927c8bdc7..78aeeef2bf 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' require 'set' module ActionController diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 53534c0307..d5f1cbc1a8 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' require 'action_controller/metal/exceptions' module ActionController #:nodoc: diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index ca1ecc43a1..54a1b13c7c 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,7 +1,5 @@ require 'rack/session/abstract/id' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/module/anonymous' module ActionController 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 6b269e7a31..6b4ececda2 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,5 @@ require 'set' require 'cgi' -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' module HTML diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index c259b865cc..cc81970ddc 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -22,7 +22,7 @@ #++ require 'active_support' -require 'active_support/dependencies/autoload' +require 'active_support/rails' require 'active_support/core_ext/module/attribute_accessors' require 'action_pack' diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 647a9e3c19..a7f93b780e 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActionDispatch module Http diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 6413929be3..47cf41cfa3 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/duplicable' diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index fe39c220a5..fd86966c50 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,6 +1,5 @@ require 'set' require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/object/blank' module Mime class Mimes < Array diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 17e74656af..d336808e7c 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,6 +1,4 @@ require 'digest/md5' -require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/class/attribute_accessors' require 'monitor' diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 338b116940..852f1cf6f5 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/module/delegation' module ActionDispatch # Provide callbacks to be executed before and after the request dispatch. diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 919882223c..ba5d332d49 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/module/attribute_accessors' diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 6fff94707c..44290445d4 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -1,6 +1,5 @@ require 'securerandom' require 'active_support/core_ext/string/access' -require 'active_support/core_ext/object/blank' module ActionDispatch # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 64159fa8e7..7c12590c49 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -3,7 +3,6 @@ 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 diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 7efc094f98..9b159b2caf 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/object/blank' require 'action_dispatch/middleware/session/abstract_store' require 'rack/session/cookie' diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 0a65b4dbcc..5e2f1ff1e0 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,7 +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' require 'action_dispatch/routing/redirection' @@ -404,6 +403,10 @@ module ActionDispatch # # # Matches any request starting with 'path' # match 'path' => 'c#a', :anchor => false + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. def match(path, options=nil) end @@ -1186,6 +1189,10 @@ module ActionDispatch # sekret_comment PATCH/PUT /comments/:id(.:format) # sekret_comment DELETE /comments/:id(.:format) # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. + # # === Examples # # # routes call <tt>Admin::PostsController</tt> diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 3d7b8878b8..93e8418d9c 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -62,6 +62,7 @@ module ActionDispatch # # # calls post_url(post) # polymorphic_url(post) # => "http://example.com/posts/1" + # polymorphic_url([post, :foo => 'bar']) # => "http://example.com/posts/1?foo=bar" # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" @@ -92,6 +93,7 @@ module ActionDispatch def polymorphic_url(record_or_hash_or_array, options = {}) if record_or_hash_or_array.kind_of?(Array) record_or_hash_or_array = record_or_hash_or_array.compact + options.reverse_merge!(record_or_hash_or_array.extract_options!) if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) proxy = record_or_hash_or_array.shift end @@ -196,7 +198,8 @@ module ActionDispatch def extract_record(record_or_hash_or_array) case record_or_hash_or_array - when Array; record_or_hash_or_array.last + when Array + record_or_hash_or_array.last when Hash; record_or_hash_or_array[:id] else record_or_hash_or_array end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 0bbed6cbea..6bb15ba3b6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,6 +1,5 @@ require 'journey' require 'forwardable' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index b4c8f839ac..b15e0446de 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' module ActionDispatch module Assertions diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 5f9c3bbf48..d19d116a1f 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -1,5 +1,4 @@ require 'action_controller/vendor/html-scanner' -require 'active_support/core_ext/object/inclusion' #-- # Copyright (c) 2006 Assaf Arkin (http://labnotes.org) diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 50ca28395b..2b3095a234 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -1,7 +1,6 @@ require 'stringio' require 'uri' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/try' require 'rack/test' diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 639ae6f398..c63778f870 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' require 'rack/utils' diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 03dfa110e3..4bd72c5520 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -22,6 +22,7 @@ #++ require 'active_support' +require 'active_support/rails' require 'action_pack' module ActionView diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 7bfbc1f0aa..749332eca7 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -1,6 +1,4 @@ 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/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index e27111012d..901f433c70 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/object/blank' module ActionView # = Active Model Helpers diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb index 1a54ca2399..e42e49fb04 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/file' require 'action_view/helpers/tag_helper' 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 e0dbfe62c6..14d62af67b 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 @@ -1,4 +1,3 @@ -require 'active_support/concern' require 'active_support/core_ext/file' require 'action_view/helpers/asset_tag_helpers/asset_include_tag' 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 91318b2812..e3a86a8889 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 @@ -1,4 +1,3 @@ -require 'active_support/concern' require 'active_support/core_ext/file' require 'action_view/helpers/asset_tag_helpers/asset_include_tag' diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 651d6e4d16..c98101a195 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' module ActionView diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index 878a8734a4..d8b92c5cab 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -4,6 +4,9 @@ module ActionView # Provides a set of methods for making it easier to debug Rails objects. module Helpers module DebugHelper + + include TagHelper + # Returns a YAML representation of +object+ wrapped with <pre> and </pre>. # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead. # Useful for inspecting an object at the time of rendering. @@ -26,10 +29,11 @@ module ActionView def debug(object) begin Marshal::dump(object) - "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>".html_safe + object = ERB::Util.html_escape(object.to_yaml).gsub(" ", " ").html_safe + content_tag(:pre, object, :class => "debug_dump") rescue Exception # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback - "<code class='debug_dump'>#{h(object.inspect)}</code>".html_safe + content_tag(:code, object.to_yaml, :class => "debug_dump") end end end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index b34f6c8650..13a5671a17 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -4,13 +4,10 @@ require 'action_view/helpers/tag_helper' 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' require 'action_controller/model_naming' diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index c88af0355f..f3237f82d5 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -1,7 +1,6 @@ require 'cgi' require 'erb' require 'action_view/helpers/form_helper' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' module ActionView diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index d7d9c45120..46ee196a2a 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -1,6 +1,5 @@ require 'cgi' require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/module/attribute_accessors' diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 9572f1c192..3327c69d61 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' require 'set' diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 0cc0d069ea..0f599d5f41 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/filters' require 'active_support/core_ext/array/extract_options' diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 47dd932c71..f0ea92b018 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/module/remove_method' module ActionView diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index a08a566b35..edefeac184 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActionView # = Action View Partials @@ -337,17 +336,16 @@ module ActionView end end + if as = options[:as] + raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/ + as = as.to_sym + end + if @path - @variable, @variable_counter = retrieve_variable(@path) + @variable, @variable_counter = retrieve_variable(@path, as) @template_keys = retrieve_template_keys else - paths.map! { |path| retrieve_variable(path).unshift(path) } - end - - 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 lowercase letter or underscore, " + - "and is followed by any combination of letters, numbers and underscores.") + paths.map! { |path| retrieve_variable(path, as).unshift(path) } end self @@ -456,10 +454,22 @@ module ActionView keys end - def retrieve_variable(path) - variable = @options.fetch(:as) { path[%r'_?(\w+)(\.\w+)*$', 1] }.try(:to_sym) + def retrieve_variable(path, as) + variable = as || begin + base = path[-1] == "/" ? "" : File.basename(path) + raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ + $1.to_sym + end variable_counter = :"#{variable}_counter" if @collection [variable, variable_counter] end + + IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores." + + def raise_invalid_identifier(path) + raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path)) + end end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index cd79468502..a04eac1d3f 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' require 'active_support/core_ext/kernel/singleton_class' require 'thread' diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 19b9112afd..aa8eac7846 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,5 +1,4 @@ require 'action_dispatch/http/mime_type' -require 'active_support/core_ext/class/attribute' require 'erubis' module ActionView diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 53bde48e4d..55f79bf761 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/remove_method' require 'action_controller' require 'action_controller/test_case' diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index afb714484b..9454a6d726 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -309,6 +309,27 @@ class PolymorphicRoutesTest < ActionController::TestCase end end + def test_with_array_containing_simple_hash + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}?foo=bar", polymorphic_url([@project, :foo => 'bar' ]) + end + end + + def test_with_array_containing_complex_hash + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}?foo=bar&nested%5Bfoo%5D=bar", polymorphic_url([@project, :nested => { :foo => 'bar' }, :foo => 'bar']) + end + end + + def test_with_array_containing_simple_hash_options_take_precedence + with_test_routes do + @project.save + assert_equal "http://example.com/projects/#{@project.id}?foo=baz", polymorphic_url([@project, :foo => 'bar' ], :foo => 'baz') + end + end + def test_with_array_containing_single_name with_test_routes do @project.save diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 828ea5b0fb..b11ad633bd 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -139,11 +139,12 @@ class HttpDigestAuthenticationTest < ActionController::TestCase test "authentication request with request-uri that doesn't match credentials digest-uri" do @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please') - @request.env['ORIGINAL_FULLPATH'] = "/http_digest_authentication_test/dummy_digest/altered/uri" + @request.env['PATH_INFO'] = "/proxied/uri" get :display - assert_response :unauthorized - assert_equal "Authentication Failed", @response.body + assert_response :success + assert assigns(:logged_in) + assert_equal 'Definitely Maybe', @response.body end test "authentication request with absolute request uri (as in webrick)" do diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 5ce1acccf1..20e433d1ec 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -114,7 +114,8 @@ module ActionController def test_render_text get :render_text - assert @response.stream.closed?, 'stream should be closed' + assert_equal 'zomg', response.body + assert response.stream.closed?, 'stream should be closed' end end end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index c8e036b116..6f9dd44e21 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' require 'controller/fake_models' require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/object/inclusion' class StarStarMimeController < ActionController::Base layout nil @@ -851,7 +850,7 @@ class RespondWithControllerTest < ActionController::TestCase put :using_resource assert_equal "application/xml", @response.content_type assert_equal 204, @response.status - assert_equal " ", @response.body + assert_equal "", @response.body end def test_using_resource_for_put_with_json_yields_no_content_on_success @@ -860,7 +859,7 @@ class RespondWithControllerTest < ActionController::TestCase put :using_resource assert_equal "application/json", @response.content_type assert_equal 204, @response.status - assert_equal " ", @response.body + assert_equal "", @response.body end def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure @@ -902,7 +901,7 @@ class RespondWithControllerTest < ActionController::TestCase delete :using_resource assert_equal "application/xml", @response.content_type assert_equal 204, @response.status - assert_equal " ", @response.body + assert_equal "", @response.body end def test_using_resource_for_delete_with_json_yields_no_content_on_success @@ -912,7 +911,7 @@ class RespondWithControllerTest < ActionController::TestCase delete :using_resource assert_equal "application/json", @response.content_type assert_equal 204, @response.status - assert_equal " ", @response.body + assert_equal "", @response.body end def test_using_resource_for_delete_with_html_redirects_on_failure diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 5bcd79ebec..7396c850ad 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -110,6 +110,36 @@ module BareMetalTest assert_nil headers['Content-Type'] assert_nil headers['Content-Length'] end + + test "head :no_content (204) does not return any content" do + content = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).third.first + assert_empty content + end + + test "head :reset_content (205) does not return any content" do + content = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).third.first + assert_empty content + end + + test "head :not_modified (304) does not return any content" do + content = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).third.first + assert_empty content + end + + test "head :continue (100) does not return any content" do + content = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).third.first + assert_empty content + end + + test "head :switching_protocols (101) does not return any content" do + content = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).third.first + assert_empty content + end + + test "head :processing (102) does not return any content" do + content = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).third.first + assert_empty content + end end class BareControllerTest < ActionController::TestCase diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index 156d87c321..d0be4f66d1 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -126,7 +126,7 @@ module RenderTemplate test "rendering a template with error properly excerts the code" do get :with_error assert_status 500 - assert_match "undefined local variable or method `idontexist'", response.body + assert_match "undefined local variable or method `idontexist", response.body end end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 6bebe7e1ed..3f047fc9b5 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -186,7 +186,7 @@ class TestController < ActionController::Base # :ported: def render_text_hello_world_with_layout - @variable_for_layout = ", I'm here!" + @variable_for_layout = ", I am here!" render :text => "hello world", :layout => true end @@ -844,7 +844,7 @@ class RenderTest < ActionController::TestCase # :ported: def test_do_with_render_text_and_layout get :render_text_hello_world_with_layout - assert_equal "<html>hello world, I'm here!</html>", @response.body + assert_equal "<html>hello world, I am here!</html>", @response.body end # :ported: diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index de1bff17eb..c7c367f18a 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/with_options' -require 'active_support/core_ext/object/inclusion' class ResourcesController < ActionController::Base def index() render :nothing => true end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 205238990e..d0308b0e74 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -2,7 +2,6 @@ require 'erb' require 'abstract_unit' require 'controller/fake_controllers' -require 'active_support/core_ext/object/inclusion' class TestRoutingMapper < ActionDispatch::IntegrationTest SprocketsApp = lambda { |env| diff --git a/actionpack/test/template/erb_util_test.rb b/actionpack/test/template/erb_util_test.rb index ca2710e9b3..4132c8ca04 100644 --- a/actionpack/test/template/erb_util_test.rb +++ b/actionpack/test/template/erb_util_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'active_support/core_ext/object/inclusion' class ErbUtilTest < ActiveSupport::TestCase include ERB::Util @@ -8,11 +7,11 @@ class ErbUtilTest < ActiveSupport::TestCase define_method "test_html_escape_#{expected.gsub(/\W/, '')}" do assert_equal expected, html_escape(given) end + end - unless given == '"' - define_method "test_json_escape_#{expected.gsub(/\W/, '')}" do - assert_equal ERB::Util::JSON_ESCAPE[given], json_escape(given) - end + ERB::Util::JSON_ESCAPE.each do |given, expected| + define_method "test_json_escape_#{expected.gsub(/\W/, '')}" do + assert_equal ERB::Util::JSON_ESCAPE[given], json_escape(given) end end @@ -40,13 +39,13 @@ class ErbUtilTest < ActiveSupport::TestCase def test_rest_in_ascii (0..127).to_a.map {|int| int.chr }.each do |chr| - next if chr.in?('&"<>') + next if chr.in?('&"<>\'') assert_equal chr, html_escape(chr) end end def test_html_escape_once - assert_equal '1 < 2 & 3', html_escape_once('1 < 2 & 3') + assert_equal '1 <>&"' 2 & 3', html_escape_once('1 <>&"\' 2 & 3') end def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 7da293ce23..ab22dbfa44 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1,6 +1,5 @@ require 'abstract_unit' require 'controller/fake_models' -require 'active_support/core_ext/object/inclusion' class FormHelperTest < ActionView::TestCase include RenderERBUtils diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index bfc73172eb..66a572e719 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -1,6 +1,5 @@ require 'abstract_unit' require 'tzinfo' -require 'active_support/core_ext/object/inclusion' class Map < Hash def category @@ -1125,7 +1124,7 @@ class FormOptionsHelperTest < ActionView::TestCase def test_options_for_select_with_element_attributes assert_dom_equal( - "<option value=\"<Denmark>\" class=\"bold\"><Denmark></option>\n<option value=\"USA\" onclick=\"alert('Hello World');\">USA</option>\n<option value=\"Sweden\">Sweden</option>\n<option value=\"Germany\">Germany</option>", + "<option value=\"<Denmark>\" class=\"bold\"><Denmark></option>\n<option value=\"USA\" onclick=\"" + ERB::Util.html_escape("alert('Hello World');") + "\">USA</option>\n<option value=\"Sweden\">Sweden</option>\n<option value=\"Germany\">Germany</option>", options_for_select([ [ "<Denmark>", { :class => 'bold' } ], [ "USA", { :onclick => "alert('Hello World');" } ], [ "Sweden" ], "Germany" ]) ) end diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 9afa4a2927..dcae5b2352 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'active_support/core_ext/object/inclusion' class FormTagHelperTest < ActionView::TestCase include RenderERBUtils @@ -214,6 +213,12 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_select_tag_escapes_prompt + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "<script>alert(1337)</script>" + expected = %(<select id="places" name="places"><option value=""><script>alert(1337)</script></option><option>Home</option><option>Work</option><option>Pub</option></select>) + assert_dom_equal expected, actual + end + def test_select_tag_with_prompt_and_include_blank actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string", :include_blank => true expected = %(<select name="places" id="places"><option value="">string</option><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>) @@ -374,7 +379,7 @@ class FormTagHelperTest < ActionView::TestCase def test_submit_tag assert_dom_equal( - %(<input name='commit' data-disable-with="Saving..." onclick="alert('hello!')" type="submit" value="Save" />), + %(<input name='commit' data-disable-with="Saving..." onclick=") + ERB::Util.html_escape("alert('hello!')") + %(" type="submit" value="Save" />), submit_tag("Save", :onclick => "alert('hello!')", :data => { :disable_with => "Saving..." }) ) end diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 3ce1d20bd9..164b8b9fa1 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -187,6 +187,13 @@ module RenderTestCases assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message end + def test_render_partial_with_hyphen + e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in") } + assert_equal "The partial name (test/a-in) is not a valid Ruby identifier; " + + "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_errors e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise") } assert_match %r!method.*doesnt_exist!, e.message diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb index 322bea3fb0..061f5bb53f 100644 --- a/actionpack/test/template/template_test.rb +++ b/actionpack/test/template/template_test.rb @@ -84,7 +84,7 @@ class TestERBTemplate < ActiveSupport::TestCase def test_locals @template = new_template("<%= my_local %>") @template.locals = [:my_local] - assert_equal "I'm a local", render(:my_local => "I'm a local") + assert_equal "I am a local", render(:my_local => "I am a local") end def test_restores_buffer diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index a3ab091c6c..75ec1d8f16 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -107,8 +107,8 @@ class TextHelperTest < ActionView::TestCase end def test_truncate_with_link_options - assert_equal "Here's a long test and I...<a href=\"#\">Continue</a>", - truncate("Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' } + assert_equal "Here is a long test and ...<a href=\"#\">Continue</a>", + truncate("Here is a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' } end def test_truncate_should_be_html_safe @@ -149,8 +149,8 @@ class TextHelperTest < ActionView::TestCase end def test_truncate_with_block_should_escape_the_block - assert_equal "Here's a long test and I...<script>alert('foo');</script>", - truncate("Here's a long test and I need a continue to read link", :length => 27) { "<script>alert('foo');</script>" } + assert_equal "Here is a long test and ...<script>" + ERB::Util.html_escape("alert('foo');") + "</script>", + truncate("Here is a long test and I need a continue to read link", :length => 27) { "<script>alert('foo');</script>" } end def test_highlight_should_be_html_safe diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index cb6f378ecb..2c67b2210b 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -244,7 +244,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_link_tag_with_custom_onclick link = link_to("Hello", "http://www.example.com", :onclick => "alert('yay!')") - expected = %{<a href="http://www.example.com" onclick="alert('yay!')">Hello</a>} + expected = %{<a href="http://www.example.com" onclick="} + ERB::Util.html_escape("alert('yay!')") + %{">Hello</a>} assert_dom_equal expected, link end @@ -254,12 +254,12 @@ class UrlHelperTest < ActiveSupport::TestCase link_to("Hello", "http://www.example.com", :data => { :confirm => "Are you sure?" }) ) assert_dom_equal( - "<a href=\"http://www.example.com\" data-confirm=\"You can't possibly be sure, can you?\">Hello</a>", - link_to("Hello", "http://www.example.com", :data => { :confirm => "You can't possibly be sure, can you?" }) + "<a href=\"http://www.example.com\" data-confirm=\"You cant possibly be sure, can you?\">Hello</a>", + link_to("Hello", "http://www.example.com", :data => { :confirm => "You cant possibly be sure, can you?" }) ) assert_dom_equal( - "<a href=\"http://www.example.com\" data-confirm=\"You can't possibly be sure,\n can you?\">Hello</a>", - link_to("Hello", "http://www.example.com", :data => { :confirm => "You can't possibly be sure,\n can you?" }) + "<a href=\"http://www.example.com\" data-confirm=\"You cant possibly be sure,\n can you?\">Hello</a>", + link_to("Hello", "http://www.example.com", :data => { :confirm => "You cant possibly be sure,\n can you?" }) ) end @@ -272,14 +272,14 @@ class UrlHelperTest < ActiveSupport::TestCase end assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do assert_dom_equal( - "<a href=\"http://www.example.com\" data-confirm=\"You can't possibly be sure, can you?\">Hello</a>", - link_to("Hello", "http://www.example.com", :confirm => "You can't possibly be sure, can you?") + "<a href=\"http://www.example.com\" data-confirm=\"You cant possibly be sure, can you?\">Hello</a>", + link_to("Hello", "http://www.example.com", :confirm => "You cant possibly be sure, can you?") ) end assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do assert_dom_equal( - "<a href=\"http://www.example.com\" data-confirm=\"You can't possibly be sure,\n can you?\">Hello</a>", - link_to("Hello", "http://www.example.com", :confirm => "You can't possibly be sure,\n can you?") + "<a href=\"http://www.example.com\" data-confirm=\"You cant possibly be sure,\n can you?\">Hello</a>", + link_to("Hello", "http://www.example.com", :confirm => "You cant possibly be sure,\n can you?") ) end end diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index f6bacf5ec0..ec2d734647 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -22,6 +22,7 @@ #++ require 'active_support' +require 'active_support/rails' require 'active_model/version' module ActiveModel diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index eb06250060..ef04f1fa49 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,8 +1,14 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/deprecation' module ActiveModel # Raised when an attribute is not defined. + # + # class User < ActiveRecord::Base + # has_many :pets + # end + # + # user = User.first + # user.pets.select(:id).first.user_id + # # => ActiveModel::MissingAttributeError: missing attribute: user_id class MissingAttributeError < NoMethodError end # == Active Model Attribute Methods diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 57b1bc2ada..48c53f0789 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' require 'active_support/inflector' module ActiveModel diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 9bd5f903cd..c0b268fa4d 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,7 +1,6 @@ require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/object/blank' module ActiveModel # == Active Model Dirty diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 4ed3462e7e..1026b0f4d3 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -2,7 +2,6 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/object/blank' module ActiveModel # == Active Model Errors @@ -437,6 +436,19 @@ module ActiveModel # Raised when a validation cannot be corrected by end users and are considered # exceptional. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # + # validates_presence_of :name, strict: true + # end + # + # person = Person.new + # person.name = nil + # person.valid? + # # => ActiveModel::StrictValidationFailed: Name can't be blank class StrictValidationFailed < StandardError end end diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index e57fcf1610..f9841abcb0 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -1,10 +1,50 @@ -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' require 'active_model/mass_assignment_security/permission_set' require 'active_model/mass_assignment_security/sanitizer' module ActiveModel - # = Active Model Mass-Assignment Security + # == Active Model Mass-Assignment Security + # + # Mass assignment security provides an interface for protecting attributes + # from end-user assignment. For more complex permissions, mass assignment + # security may be handled outside the model by extending a non-ActiveRecord + # class, such as a controller, with this behavior. + # + # For example, a logged in user may need to assign additional attributes + # depending on their role: + # + # class AccountsController < ApplicationController + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessible :first_name, :last_name + # attr_accessible :first_name, :last_name, :plan_id, as: :admin + # + # def update + # ... + # @account.update_attributes(account_params) + # ... + # end + # + # protected + # + # def account_params + # role = admin ? :admin : :default + # sanitize_for_mass_assignment(params[:account], role) + # end + # + # end + # + # === Configuration options + # + # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible + # values are: + # * <tt>:logger</tt> (default) - writes filtered attributes to logger + # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> + # on any protected attribute update. + # + # You can specify your own sanitizer object eg. <tt>MySanitizer.new</tt>. + # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for + # example implementation. module MassAssignmentSecurity extend ActiveSupport::Concern @@ -17,55 +57,17 @@ module ActiveModel self.mass_assignment_sanitizer = :logger end - # Mass assignment security provides an interface for protecting attributes - # from end-user assignment. For more complex permissions, mass assignment security - # may be handled outside the model by extending a non-ActiveRecord class, - # such as a controller, with this behavior. - # - # For example, a logged in user may need to assign additional attributes depending - # on their role: - # - # class AccountsController < ApplicationController - # include ActiveModel::MassAssignmentSecurity - # - # attr_accessible :first_name, :last_name - # attr_accessible :first_name, :last_name, :plan_id, :as => :admin - # - # def update - # ... - # @account.update_attributes(account_params) - # ... - # end - # - # protected - # - # def account_params - # role = admin ? :admin : :default - # sanitize_for_mass_assignment(params[:account], role) - # end - # - # end - # - # = Configuration options - # - # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible values are: - # * <tt>:logger</tt> (default) - writes filtered attributes to logger - # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> on any protected attribute update - # - # You can specify your own sanitizer object eg. MySanitizer.new. - # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation. - # - # module ClassMethods # Attributes named in this macro are protected from mass-assignment # whenever attributes are sanitized before assignment. A role for the - # attributes is optional, if no role is provided then :default is used. - # A role can be defined by using the :as option with a symbol or an array of symbols as the value. + # attributes is optional, if no role is provided then <tt>:default</tt> + # is used. A role can be defined by using the <tt>:as</tt> option with a + # symbol or an array of symbols as the value. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. Example: + # tampering with URLs or forms. # # class Customer # include ActiveModel::MassAssignmentSecurity @@ -74,7 +76,7 @@ module ActiveModel # # attr_protected :logins_count # # Suppose that admin can not change email for customer - # attr_protected :logins_count, :email, :as => :admin + # attr_protected :logins_count, :email, as: :admin # # def assign_attributes(values, options = {}) # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| @@ -83,23 +85,23 @@ module ActiveModel # end # end # - # When using the :default role: + # When using the <tt>:default</tt> role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5 }, :as => :default) - # customer.name # => "David" - # customer.email # => "a@b.com" - # customer.logins_count # => nil + # customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5 }, as: :default) + # customer.name # => "David" + # customer.email # => "a@b.com" + # customer.logins_count # => nil # - # And using the :admin role: + # And using the <tt>:admin</tt> role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin) - # customer.name # => "David" - # customer.email # => nil - # customer.logins_count # => nil + # customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5}, as: :admin) + # customer.name # => "David" + # customer.email # => nil + # customer.logins_count # => nil # - # customer.email = "c@d.com" + # customer.email = 'c@d.com' # customer.email # => "c@d.com" # # To start from an all-closed default and enable attributes as needed, @@ -125,8 +127,9 @@ module ActiveModel # mass-assignment. # # Like +attr_protected+, a role for the attributes is optional, - # if no role is provided then :default is used. A role can be defined by - # using the :as option with a symbol or an array of symbols as the value. + # if no role is provided then <tt>:default</tt> is used. A role can be + # defined by using the <tt>:as</tt> option with a symbol or an array of + # symbols as the value. # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of @@ -142,9 +145,9 @@ module ActiveModel # attr_accessor :name, :credit_rating # # # Both admin and default user can change name of a customer - # attr_accessible :name, :as => [:admin, :default] + # attr_accessible :name, as: [:admin, :default] # # Only admin can change credit rating of a customer - # attr_accessible :credit_rating, :as => :admin + # attr_accessible :credit_rating, as: :admin # # def assign_attributes(values, options = {}) # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| @@ -153,20 +156,20 @@ module ActiveModel # end # end # - # When using the :default role: + # When using the <tt>:default</tt> role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) + # customer.assign_attributes({ name: 'David', credit_rating: 'Excellent', last_login: 1.day.ago }, as: :default) # customer.name # => "David" # customer.credit_rating # => nil # - # customer.credit_rating = "Average" + # customer.credit_rating = 'Average' # customer.credit_rating # => "Average" # - # And using the :admin role: + # And using the <tt>:admin</tt> role: # # customer = Customer.new - # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.assign_attributes({ name: 'David', credit_rating: 'Excellent', last_login: 1.day.ago }, as: :admin) # customer.name # => "David" # customer.credit_rating # => "Excellent" # @@ -186,23 +189,131 @@ module ActiveModel self._active_authorizer = self._accessible_attributes end + # Returns an instance of <tt>ActiveModel::MassAssignmentSecurity::BlackList</tt> + # with the attributes protected by #attr_protected method. If no +role+ + # is provided, then <tt>:default</tt> is used. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :email, :logins_count + # + # attr_protected :logins_count + # attr_protected :logins_count, :email, as: :admin + # end + # + # Customer.protected_attributes + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count"}> + # + # Customer.protected_attributes(:default) + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count"}> + # + # Customer.protected_attributes(:admin) + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {"logins_count", "email"}> def protected_attributes(role = :default) protected_attributes_configs[role] end + # Returns an instance of <tt>ActiveModel::MassAssignmentSecurity::WhiteList</tt> + # with the attributes protected by #attr_accessible method. If no +role+ + # is provided, then <tt>:default</tt> is used. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :credit_rating + # + # attr_accessible :name, as: [:admin, :default] + # attr_accessible :credit_rating, as: :admin + # end + # + # Customer.accessible_attributes + # # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}> + # + # Customer.accessible_attributes(:default) + # # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}> + # + # Customer.accessible_attributes(:admin) + # # => #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name", "credit_rating"}> def accessible_attributes(role = :default) accessible_attributes_configs[role] end + # Returns a hash with the protected attributes (by #attr_accessible or + # #attr_protected) per role. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :credit_rating + # + # attr_accessible :name, as: [:admin, :default] + # attr_accessible :credit_rating, as: :admin + # end + # + # Customer.active_authorizers + # # => { + # # :admin=> #<ActiveModel::MassAssignmentSecurity::WhiteList: {"name", "credit_rating"}>, + # # :default=>#<ActiveModel::MassAssignmentSecurity::WhiteList: {"name"}> + # # } def active_authorizers self._active_authorizer ||= protected_attributes_configs end alias active_authorizer active_authorizers + # Returns an empty array by default. You can still override this to define + # the default attributes protected by #attr_protected method. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # def self.attributes_protected_by_default + # [:name] + # end + # end + # + # Customer.protected_attributes + # # => #<ActiveModel::MassAssignmentSecurity::BlackList: {:name}> def attributes_protected_by_default [] end + # Defines sanitize method. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name + # + # attr_protected :name + # + # def assign_attributes(values) + # sanitize_for_mass_assignment(values).each do |k, v| + # send("#{k}=", v) + # end + # end + # end + # + # # See ActiveModel::MassAssignmentSecurity::StrictSanitizer for more information. + # Customer.mass_assignment_sanitizer = :strict + # + # customer = Customer.new + # customer.assign_attributes(name: 'David') + # # => ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes for Customer: name + # + # Also, you can specify your own sanitizer object. + # + # class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer + # def process_removed_attributes(klass, attrs) + # raise StandardError + # end + # end + # + # Customer.mass_assignment_sanitizer = CustomSanitizer.new + # + # customer = Customer.new + # customer.assign_attributes(name: 'David') + # # => StandardError: StandardError def mass_assignment_sanitizer=(value) self._mass_assignment_sanitizer = if value.is_a?(Symbol) const_get(:"#{value.to_s.camelize}Sanitizer").new(self) @@ -228,11 +339,11 @@ module ActiveModel protected - def sanitize_for_mass_assignment(attributes, role = nil) + def sanitize_for_mass_assignment(attributes, role = nil) #:nodoc: _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role)) end - def mass_assignment_authorizer(role) + def mass_assignment_authorizer(role) #:nodoc: self.class.active_authorizer[role || :default] end end diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb index 44ce5a489d..dafb7cdff3 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -1,6 +1,6 @@ module ActiveModel module MassAssignmentSecurity - class Sanitizer + class Sanitizer #:nodoc: # Returns all attributes not denied by the authorizer. def sanitize(klass, attributes, authorizer) rejected = [] @@ -18,7 +18,7 @@ module ActiveModel end end - class LoggerSanitizer < Sanitizer + class LoggerSanitizer < Sanitizer #:nodoc: def initialize(target) @target = target super() @@ -50,7 +50,7 @@ module ActiveModel end end - class StrictSanitizer < Sanitizer + class StrictSanitizer < Sanitizer #:nodoc: def initialize(target = nil) super() end @@ -65,7 +65,7 @@ module ActiveModel end end - class Error < StandardError + class Error < StandardError #:nodoc: def initialize(klass, attrs) super("Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}") end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index ea4f9341c6..7ba439fb3e 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,8 +1,6 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/object/blank' module ActiveModel class Name @@ -220,6 +218,14 @@ module ActiveModel # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information # (See ActiveModel::Name for more information). + # + # class Person < ActiveModel::Model + # end + # + # Person.model_name # => Person + # Person.model_name.class # => ActiveModel::Name + # Person.model_name.singular # => "person" + # Person.model_name.plural # => "people" def model_name @_model_name ||= begin namespace = self.parents.detect do |n| @@ -256,11 +262,11 @@ module ActiveModel # Returns string to use while generating route names. It differs for # namespaced models regarding whether it's inside isolated engine. # - # For isolated engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> post + # # For isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> post # - # For shared engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> blog_post + # # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_post def self.singular_route_key(record_or_class) model_name_from_record_or_class(record_or_class).singular_route_key end @@ -268,11 +274,11 @@ module ActiveModel # Returns string to use while generating route names. It differs for # namespaced models regarding whether it's inside isolated engine. # - # For isolated engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> posts + # # For isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> posts # - # For shared engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + # # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts # # The route key also considers if the noun is uncountable and, in # such cases, automatically appends _index. @@ -283,11 +289,11 @@ module ActiveModel # Returns string to use for params names. It differs for # namespaced models regarding whether it's inside isolated engine. # - # For isolated engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> post + # # For isolated engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> post # - # For shared engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post + # # For shared engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post def self.param_key(record_or_class) model_name_from_record_or_class(record_or_class).param_key end diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb index 8de6918d18..77bc0f71e3 100644 --- a/activemodel/lib/active_model/observer_array.rb +++ b/activemodel/lib/active_model/observer_array.rb @@ -5,20 +5,21 @@ module ActiveModel # a particular model class. class ObserverArray < Array attr_reader :model_class - def initialize(model_class, *args) + def initialize(model_class, *args) #:nodoc: @model_class = model_class super(*args) end - # Returns true if the given observer is disabled for the model class. - def disabled_for?(observer) + # Returns +true+ if the given observer is disabled for the model class, + # +false+ otherwise. + def disabled_for?(observer) #:nodoc: disabled_observers.include?(observer.class) end # Disables one or more observers. This supports multiple forms: # # ORM.observers.disable :all - # # => disables all observers for all models subclassed from + # # => disables all observers for all models subclassed from # # an ORM base class that includes ActiveModel::Observing # # e.g. ActiveRecord::Base # @@ -43,7 +44,7 @@ module ActiveModel # Enables one or more observers. This supports multiple forms: # # ORM.observers.enable :all - # # => enables all observers for all models subclassed from + # # => enables all observers for all models subclassed from # # an ORM base class that includes ActiveModel::Observing # # e.g. ActiveRecord::Base # @@ -71,11 +72,11 @@ module ActiveModel protected - def disabled_observers + def disabled_observers #:nodoc: @disabled_observers ||= Set.new end - def observer_class_for(observer) + def observer_class_for(observer) #:nodoc: return observer if observer.is_a?(Class) if observer.respond_to?(:to_sym) # string/symbol @@ -86,25 +87,25 @@ module ActiveModel end end - def start_transaction + def start_transaction #:nodoc: disabled_observer_stack.push(disabled_observers.dup) each_subclass_array do |array| array.start_transaction end end - def disabled_observer_stack + def disabled_observer_stack #:nodoc: @disabled_observer_stack ||= [] end - def end_transaction + def end_transaction #:nodoc: @disabled_observers = disabled_observer_stack.pop each_subclass_array do |array| array.end_transaction end end - def transaction + def transaction #:nodoc: start_transaction begin @@ -114,13 +115,13 @@ module ActiveModel end end - def each_subclass_array + def each_subclass_array #:nodoc: model_class.descendants.each do |subclass| yield subclass.observers end end - def set_enablement(enabled, observers) + def set_enablement(enabled, observers) #:nodoc: if block_given? transaction do set_enablement(enabled, observers) diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 976dcf23d8..9db7639ea3 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -4,11 +4,11 @@ require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/enumerable' -require 'active_support/deprecation' require 'active_support/core_ext/object/try' require 'active_support/descendants_tracker' module ActiveModel + # == Active Model Observers Activation module Observing extend ActiveSupport::Concern @@ -17,9 +17,7 @@ module ActiveModel end module ClassMethods - # == Active Model Observers Activation - # - # Activates the observers assigned. Examples: + # Activates the observers assigned. # # class ORM # include ActiveModel::Observing @@ -35,34 +33,95 @@ module ActiveModel # ORM.observers = Cacher, GarbageCollector # # Note: Setting this does not instantiate the observers yet. - # +instantiate_observers+ is called during startup, and before + # <tt>instantiate_observers</tt> is called during startup, and before # each development request. def observers=(*values) observers.replace(values.flatten) end - # Gets an array of observers observing this model. - # The array also provides +enable+ and +disable+ methods - # that allow you to selectively enable and disable observers. - # (see <tt>ActiveModel::ObserverArray.enable</tt> and - # <tt>ActiveModel::ObserverArray.disable</tt> for more on this) + # Gets an array of observers observing this model. The array also provides + # +enable+ and +disable+ methods that allow you to selectively enable and + # disable observers (see ActiveModel::ObserverArray.enable and + # ActiveModel::ObserverArray.disable for more on this). + # + # class ORM + # include ActiveModel::Observing + # end + # + # ORM.observers = :cacher, :garbage_collector + # ORM.observers # => [:cacher, :garbage_collector] + # ORM.observers.class # => ActiveModel::ObserverArray def observers @observers ||= ObserverArray.new(self) end - # Gets the current observer instances. + # Returns the current observer instances. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # Foo.instantiate_observers + # + # Foo.observer_instances # => [#<FooObserver:0x007fc212c40820>] def observer_instances @observer_instances ||= [] end # Instantiate the global observers. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # + # foo = Foo.new + # foo.status = false + # foo.notify_observers(:on_spec) + # foo.status # => false + # + # Foo.instantiate_observers # => [FooObserver] + # + # foo = Foo.new + # foo.status = false + # foo.notify_observers(:on_spec) + # foo.status # => true def instantiate_observers observers.each { |o| instantiate_observer(o) } end - # Add a new observer to the pool. - # The new observer needs to respond to 'update', otherwise it - # raises an +ArgumentError+ exception. + # Add a new observer to the pool. The new observer needs to respond to + # <tt>update</tt>, otherwise it raises an +ArgumentError+ exception. + # + # class Foo + # include ActiveModel::Observing + # end + # + # class FooObserver < ActiveModel::Observer + # end + # + # Foo.add_observer(FooObserver.instance) + # + # Foo.observers_instance + # # => [#<FooObserver:0x007fccf55d9390>] def add_observer(observer) unless observer.respond_to? :update raise ArgumentError, "observer needs to respond to 'update'" @@ -70,16 +129,47 @@ module ActiveModel observer_instances << observer end - # Notify list of observers of a change. + # 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 <tt>custom_notification</tt>, passing as arguments + # the current object and <tt>:foo</tt>. def notify_observers(*args) observer_instances.each { |observer| observer.update(*args) } end - # Total number of observers. + # Returns the total number of instantiated observers. + # + # class Foo + # include ActiveModel::Observing + # + # attr_accessor :status + # end + # + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end + # + # Foo.observers = FooObserver + # Foo.observers_count # => 0 + # Foo.instantiate_observers + # Foo.observers_count # => 1 def observers_count observer_instances.size end + # <tt>count_observers</tt> is deprecated. Use #observers_count. def count_observers msg = "count_observers is deprecated in favor of observers_count" ActiveSupport::Deprecation.warn(msg) @@ -104,27 +194,36 @@ module ActiveModel end # Notify observers when the observed class is subclassed. - def inherited(subclass) + def inherited(subclass) #:nodoc: super notify_observers :observed_class_inherited, subclass end end - # Fires notifications to model's observers + # Notify a change to the list of observers. + # + # class Foo + # include ActiveModel::Observing # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) + # attr_accessor :status # end # - # Custom notifications can be sent in a similar fashion: + # class FooObserver < ActiveModel::Observer + # def on_spec(record, *args) + # record.status = true + # end + # end # - # notify_observers(:custom_notification, :foo) + # Foo.observers = FooObserver + # Foo.instantiate_observers # => [FooObserver] # - # This will call +custom_notification+, passing as arguments - # the current object and :foo. + # foo = Foo.new + # foo.status = false + # foo.notify_observers(:on_spec) + # foo.status # => true # + # See ActiveModel::Observing::ClassMethods.notify_observers for more + # information. def notify_observers(method, *extra_args) self.class.notify_observers(method, self, *extra_args) end @@ -136,15 +235,15 @@ module ActiveModel # behavior outside the original class. This is a great way to reduce the # clutter that normally comes when the model class is burdened with # functionality that doesn't pertain to the core responsibility of the - # class. Example: + # class. # # class CommentObserver < ActiveModel::Observer # def after_save(comment) - # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver + # Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver # end # end # - # This Observer sends an email when a Comment#save is finished. + # This Observer sends an email when a <tt>Comment#save</tt> is finished. # # class ContactObserver < ActiveModel::Observer # def after_create(contact) @@ -161,44 +260,50 @@ module ActiveModel # == Observing a class that can't be inferred # # Observers will by default be mapped to the class with which they share a - # name. So CommentObserver will be tied to observing Comment, ProductManagerObserver - # to ProductManager, and so on. If you want to name your observer differently than - # the class you're interested in observing, you can use the <tt>Observer.observe</tt> - # class method which takes either the concrete class (Product) or a symbol for that - # class (:product): + # name. So <tt>CommentObserver</tt> will be tied to observing <tt>Comment</tt>, + # <tt>ProductManagerObserver</tt> to <tt>ProductManager</tt>, and so on. If + # you want to name your observer differently than the class you're interested + # in observing, you can use the <tt>Observer.observe</tt> class method which + # takes either the concrete class (<tt>Product</tt>) or a symbol for that + # class (<tt>:product</tt>): # # class AuditObserver < ActiveModel::Observer # observe :account # # def after_update(account) - # AuditTrail.new(account, "UPDATED") + # AuditTrail.new(account, 'UPDATED') # end # end # - # If the audit observer needs to watch more than one kind of object, this can be - # specified with multiple arguments: + # If the audit observer needs to watch more than one kind of object, this can + # be specified with multiple arguments: # # class AuditObserver < ActiveModel::Observer # observe :account, :balance # # def after_update(record) - # AuditTrail.new(record, "UPDATED") + # AuditTrail.new(record, 'UPDATED') # end # end # - # The AuditObserver will now act on both updates to Account and Balance by treating - # them both as records. + # The <tt>AuditObserver</tt> will now act on both updates to <tt>Account</tt> + # and <tt>Balance</tt> by treating them both as records. # - # If you're using an Observer in a Rails application with Active Record, be sure to - # read about the necessary configuration in the documentation for + # If you're using an Observer in a Rails application with Active Record, be + # sure to read about the necessary configuration in the documentation for # ActiveRecord::Observer. - # class Observer include Singleton extend ActiveSupport::DescendantsTracker class << self # Attaches the observer to the supplied model classes. + # + # class AuditObserver < ActiveModel::Observer + # observe :account, :balance + # end + # + # AuditObserver.observed_classes # => [Account, Balance] def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } @@ -207,6 +312,8 @@ module ActiveModel # Returns an array of Classes to observe. # + # AccountObserver.observed_classes # => [Account] + # # You can override this instead of using the +observe+ helper. # # class AuditObserver < ActiveModel::Observer @@ -215,11 +322,14 @@ module ActiveModel # end # end def observed_classes - [observed_class].compact.flatten + Array(observed_class) end - # The class observed by default is inferred from the observer's class name: - # assert_equal Person, PersonObserver.observed_class + # Returns the class observed by default. It's inferred from the observer's + # class name. + # + # PersonObserver.observed_class # => Person + # AccountObserver.observed_class # => Account def observed_class name[/(.*)Observer/, 1].try :constantize end @@ -227,7 +337,7 @@ module ActiveModel # Start observing the declared classes and their subclasses. # Called automatically by the instance method. - def initialize + def initialize #:nodoc: observed_classes.each { |klass| add_observer!(klass) } end @@ -238,7 +348,7 @@ 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, *extra_args, &block) #:nodoc: - return if !respond_to?(observed_method) || disabled_for?(object) + return if !respond_to?(observed_method) || disabled_for?(object) send(observed_method, object, *extra_args, &block) end @@ -255,7 +365,7 @@ module ActiveModel end # Returns true if notifications are disabled for this object. - def disabled_for?(object) + def disabled_for?(object) #:nodoc: klass = object.class return false unless klass.respond_to?(:observers) klass.observers.disabled_for?(self) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 3eab745c89..d011402081 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -6,12 +6,12 @@ module ActiveModel # Adds methods to set and authenticate against a BCrypt password. # This mechanism requires you to have a password_digest attribute. # - # Validations for presence of password on create, confirmation of password (using - # a "password_confirmation" attribute) are automatically added. - # If you wish to turn off validations, pass 'validations: false' as an argument. - # You can add more validations by hand if need be. + # Validations for presence of password on create, confirmation of password + # (using a +password_confirmation+ attribute) are automatically added. If + # you wish to turn off validations, pass <tt>validations: false</tt> as an + # argument. You can add more validations by hand if need be. # - # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password: + # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password: # # gem 'bcrypt-ruby', '~> 3.0.0' # @@ -22,35 +22,36 @@ module ActiveModel # has_secure_password # end # - # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") + # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') # user.save # => false, password required - # user.password = "mUc3m00RsqyRe" + # user.password = 'mUc3m00RsqyRe' # user.save # => false, confirmation doesn't match - # user.password_confirmation = "mUc3m00RsqyRe" + # user.password_confirmation = 'mUc3m00RsqyRe' # user.save # => true - # user.authenticate("notright") # => false - # user.authenticate("mUc3m00RsqyRe") # => user - # User.find_by_name("david").try(:authenticate, "notright") # => false - # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user + # user.authenticate('notright') # => false + # user.authenticate('mUc3m00RsqyRe') # => user + # User.find_by_name('david').try(:authenticate, 'notright') # => false + # User.find_by_name('david').try(:authenticate, 'mUc3m00RsqyRe') # => user def has_secure_password(options = {}) # Load bcrypt-ruby only when has_secure_password is used. - # This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library. + # This is to avoid ActiveModel (and by extension the entire framework) + # being dependent on a binary library. gem 'bcrypt-ruby', '~> 3.0.0' require 'bcrypt' attr_reader :password - + if options.fetch(:validations, true) validates_confirmation_of :password validates_presence_of :password, :on => :create + + before_create { raise "Password digest missing on new record" if password_digest.blank? } end - - before_create { raise "Password digest missing on new record" if password_digest.blank? } include InstanceMethodsOnActivation if respond_to?(:attributes_protected_by_default) - def self.attributes_protected_by_default + def self.attributes_protected_by_default #:nodoc: super + ['password_digest'] end end @@ -58,13 +59,32 @@ module ActiveModel end module InstanceMethodsOnActivation - # Returns self if the password is correct, otherwise false. + # Returns +self+ if the password is correct, otherwise +false+. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') + # user.save + # user.authenticate('notright') # => false + # user.authenticate('mUc3m00RsqyRe') # => user def authenticate(unencrypted_password) BCrypt::Password.new(password_digest) == unencrypted_password && self end - # Encrypts the password into the password_digest attribute, only if the + # Encrypts the password into the +password_digest+ attribute, only if the # new password is not blank. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new + # user.password = nil + # user.password_digest # => nil + # user.password = 'mUc3m00RsqyRe' + # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." def password=(unencrypted_password) unless unencrypted_password.blank? @password = unencrypted_password diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index e4c7553cb8..a4252b995d 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,5 +1,4 @@ require 'active_support/json' -require 'active_support/core_ext/class/attribute' module ActiveModel # == Active Model JSON Serializer @@ -19,8 +18,8 @@ module ActiveModel # passed through +options+. # # The option <tt>include_root_in_json</tt> controls the top-level behavior - # of +as_json+. If true +as_json+ will emit a single root node named after - # the object's type. The default value for <tt>include_root_in_json</tt> + # of +as_json+. If +true+, +as_json+ will emit a single root node named + # after the object's type. The default value for <tt>include_root_in_json</tt> # option is +false+. # # user = User.find(1) @@ -101,6 +100,40 @@ module ActiveModel end end + # Sets the model +attributes+ from a JSON string. Returns +self+. + # + # class Person + # include ActiveModel::Serializers::JSON + # + # attr_accessor :name, :age, :awesome + # + # def attributes=(hash) + # hash.each do |key, value| + # instance_variable_set("@#{key}", value) + # end + # end + # + # def attributes + # instance_values + # end + # end + # + # json = { name: 'bob', age: 22, awesome:true }.to_json + # person = Person.new + # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true + # + # The default value for +include_root+ is +false+. You can change it to + # +true+ if the given JSON string includes a single root node. + # + # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json + # person = Person.new + # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true def from_json(json, include_root=include_root_in_json) hash = ActiveSupport::JSON.decode(json) hash = hash.values.first if include_root diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 2b3e9ce134..016d821fdf 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -110,7 +110,7 @@ module ActiveModel end end - # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. + # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. def add_associations(association, records, opts) merged_options = opts.merge(options.slice(:builder, :indent)) merged_options[:skip_instruct] = true @@ -161,8 +161,8 @@ module ActiveModel # Returns XML representing the model. Configuration can be # passed through +options+. # - # Without any +options+, the returned XML string will include all the model's - # attributes. For example: + # Without any +options+, the returned XML string will include all the + # model's attributes. # # user = User.find(1) # user.to_xml @@ -175,18 +175,42 @@ module ActiveModel # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> # </user> # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes - # included, and work similar to the +attributes+ method. + # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the + # attributes included, and work similar to the +attributes+ method. # # To include the result of some method calls on the model use <tt>:methods</tt>. # # To include associations use <tt>:include</tt>. # - # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. + # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> def to_xml(options = {}, &block) Serializer.new(self, options).serialize(&block) end + # Sets the model +attributes+ from a JSON string. Returns +self+. + # + # class Person + # include ActiveModel::Serializers::Xml + # + # attr_accessor :name, :age, :awesome + # + # def attributes=(hash) + # hash.each do |key, value| + # instance_variable_set("@#{key}", value) + # end + # end + # + # def attributes + # instance_values + # end + # end + # + # xml = { name: 'bob', age: 22, awesome:true }.to_xml + # person = Person.new + # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> + # person.name # => "bob" + # person.age # => 22 + # person.awesome # => true def from_xml(xml) self.attributes = Hash.from_xml(xml).values.first self diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 55ea6be796..4762f39044 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/except' require 'active_model/errors' @@ -33,11 +32,11 @@ module ActiveModel # person.first_name = 'zoolander' # person.valid? # => false # person.invalid? # => true - # person.errors # => #<Hash {:first_name=>["starts with z."]}> + # person.errors.messages # => {:first_name=>["starts with z."]} # - # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ method - # to your instances initialized with a new <tt>ActiveModel::Errors</tt> object, so - # there is no need for you to do this manually. + # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ + # method to your instances initialized with a new <tt>ActiveModel::Errors</tt> + # object, so there is no need for you to do this manually. module Validations extend ActiveSupport::Concern @@ -63,24 +62,25 @@ module ActiveModel # # attr_accessor :first_name, :last_name # - # validates_each :first_name, :last_name, :allow_blank => true 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 # # 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>) + # (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. + # 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 validates_each(*attr_names, &block) validates_with BlockValidator, _merge_attributes(attr_names), &block end @@ -97,7 +97,7 @@ module ActiveModel # validate :must_be_friends # # def must_be_friends - # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee) # end # end # @@ -111,7 +111,7 @@ module ActiveModel # end # # def must_be_friends - # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee) # end # end # @@ -121,23 +121,24 @@ module ActiveModel # include ActiveModel::Validations # # validate do - # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee) # 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>) + # (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. + # 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) @@ -171,39 +172,101 @@ module ActiveModel end # List all validators that are being used to validate a specific attribute. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name , :age + # + # validates_presence_of :name + # validates_inclusion_of :age, in: 0..99 + # end + # + # Person.validators_on(:name) + # # => [ + # # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>, + # # #<ActiveModel::Validations::InclusionValidator:0x007fe603bb8780 @attributes=[:age], @options={:in=>0..99}> + # # ] def validators_on(*attributes) attributes.map do |attribute| _validators[attribute.to_sym] end.flatten end - # Check if method is an attribute method or not. + # Returns +true+ if +attribute+ is an attribute method, +false+ otherwise. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # end + # + # User.attribute_method?(:name) # => true + # User.attribute_method?(:age) # => false def attribute_method?(attribute) method_defined?(attribute) end # Copy validators on inheritance. - def inherited(base) + def inherited(base) #:nodoc: dup = _validators.dup base._validators = dup.each { |k, v| dup[k] = v.dup } super end end - # Clean the +Errors+ object if instance is duped - def initialize_dup(other) # :nodoc: + # Clean the +Errors+ object if instance is duped. + def initialize_dup(other) #:nodoc: @errors = nil super end - # Returns the +Errors+ object that holds all information about attribute error messages. + # Returns the +Errors+ object that holds all information about attribute + # error messages. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name + # end + # + # person = Person.new + # person.valid? # => false + # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={:name=>["can't be blank"]}> def errors @errors ||= Errors.new(self) end - # Runs all the specified validations and returns true if no errors were added - # otherwise false. Context can optionally be supplied to define which callbacks - # to test against (the context is defined on the validations using :on). + # Runs all the specified validations and returns +true+ if no errors were + # added otherwise +false+. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name + # end + # + # person = Person.new + # person.name = '' + # person.valid? # => false + # person.name = 'david' + # person.valid? # => true + # + # Context can optionally be supplied to define which callbacks to test + # against (the context is defined on the validations using <tt>:on</tt>). + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name, on: :new + # end + # + # person = Person.new + # person.valid? # => true + # person.valid?(:new) # => false def valid?(context = nil) current_context, self.validation_context = validation_context, context errors.clear @@ -212,8 +275,35 @@ module ActiveModel self.validation_context = current_context end - # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, - # false otherwise. + # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were + # added, +false+ otherwise. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name + # end + # + # person = Person.new + # person.name = '' + # person.invalid? # => true + # person.name = 'david' + # person.invalid? # => false + # + # Context can optionally be supplied to define which callbacks to test + # against (the context is defined on the validations using <tt>:on</tt>). + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates_presence_of :name, on: :new + # end + # + # person = Person.new + # person.invalid? # => false + # person.invalid?(:new) # => true def invalid?(context = nil) !valid?(context) end @@ -238,7 +328,7 @@ module ActiveModel protected - def run_validations! + def run_validations! #:nodoc: run_callbacks :validate errors.empty? end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index dbafd0bd1a..bf3fe7ff04 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -2,24 +2,24 @@ require 'active_support/callbacks' module ActiveModel module Validations + # == Active Model Validation callbacks + # + # Provides an interface for any class to have +before_validation+ and + # +after_validation+ callbacks. + # + # First, include ActiveModel::Validations::Callbacks from the class you are + # creating: + # + # class MyModel + # include ActiveModel::Validations::Callbacks + # + # before_validation :do_stuff_before_validation + # after_validation :do_stuff_after_validation + # end + # + # Like other <tt>before_*</tt> callbacks if +before_validation+ returns + # +false+ then <tt>valid?</tt> will not be called. module Callbacks - # == Active Model Validation callbacks - # - # Provides an interface for any class to have <tt>before_validation</tt> and - # <tt>after_validation</tt> callbacks. - # - # First, include ActiveModel::Validations::Callbacks from the class you are - # creating: - # - # class MyModel - # include ActiveModel::Validations::Callbacks - # - # before_validation :do_stuff_before_validation - # after_validation :do_stuff_after_validation - # end - # - # Like other before_* callbacks if <tt>before_validation</tt> returns false - # then <tt>valid?</tt> will not be called. extend ActiveSupport::Concern included do @@ -28,6 +28,30 @@ module ActiveModel end module ClassMethods + # Defines a callback that will get called right before validation + # happens. + # + # class Person + # include ActiveModel::Validations + # include ActiveModel::Validations::Callbacks + # + # attr_accessor :name + # + # validates_length_of :name, maximum: 6 + # + # before_validation :remove_whitespaces + # + # private + # + # def remove_whitespaces + # name.strip! + # end + # end + # + # person = Person.new + # person.name = ' bob ' + # person.valid? # => true + # person.name # => "bob" def before_validation(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] @@ -37,6 +61,33 @@ module ActiveModel set_callback(:validation, :before, *args, &block) end + # Defines a callback that will get called right after validation + # happens. + # + # class Person + # include ActiveModel::Validations + # include ActiveModel::Validations::Callbacks + # + # attr_accessor :name, :status + # + # validates_presence_of :name + # + # after_validation :set_status + # + # private + # + # def set_status + # self.status = errors.empty? + # end + # end + # + # person = Person.new + # person.name = '' + # person.valid? # => false + # person.status # => false + # person.name = 'bob' + # person.valid? # => true + # person.status # => true def after_validation(*args, &block) options = args.extract_options! options[:prepend] = true @@ -49,7 +100,7 @@ module ActiveModel protected # Overwrite run validations to include callbacks. - def run_validations! + def run_validations! #:nodoc: run_callbacks(:validation) { super } end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index aa72ea41c7..e4a1f9e80a 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -36,12 +36,12 @@ module ActiveModel def validate_each(record, attribute, value) value = tokenize(value) value_length = value.respond_to?(:length) ? value.length : value.to_s.length - + errors_options = options.except(*RESERVED_OPTIONS) + CHECKS.each do |key, validity_check| next unless check_value = options[key] next if value_length.send(validity_check, check_value) - errors_options = options.except(*RESERVED_OPTIONS) errors_options[:count] = check_value default_message = options[MESSAGES[key]] diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 4592a1deb0..f159e40858 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveModel diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index ecda9dffcf..5892ad29d1 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -11,18 +11,18 @@ module ActiveModel # # Examples of using the default rails validators: # - # validates :terms, :acceptance => true - # validates :password, :confirmation => true - # validates :username, :exclusion => { :in => %w(admin superuser) } - # validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create } - # validates :age, :inclusion => { :in => 0..9 } - # validates :first_name, :length => { :maximum => 30 } - # validates :age, :numericality => true - # validates :username, :presence => true - # validates :username, :uniqueness => true + # validates :terms, acceptance: true + # validates :password, confirmation: true + # validates :username, exclusion: { in: %w(admin superuser) } + # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, on: :create } + # validates :age, inclusion: { in: 0..9 } + # validates :first_name, length: { maximum: 30 } + # validates :age, numericality: true + # validates :username, presence: true + # validates :username, uniqueness: true # # The power of the +validates+ method comes when using custom validators - # and default validators in one call for a given attribute e.g. + # and default validators in one call for a given attribute. # # class EmailValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) @@ -35,12 +35,12 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :name, :email # - # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } - # validates :email, :presence => true, :email => true + # validates :name, presence: true, uniqueness: true, length: { maximum: 100 } + # validates :email, presence: true, email: true # end # # Validator classes may also exist within the class being validated - # allowing custom modules of validators to be included as needed e.g. + # allowing custom modules of validators to be included as needed. # # class Film # include ActiveModel::Validations @@ -51,25 +51,27 @@ module ActiveModel # end # end # - # validates :name, :title => true + # validates :name, title: true # end # - # Additionally validator classes may be in another namespace and still used within any class. + # Additionally validator classes may be in another namespace and still + # used within any class. # # validates :name, :'film/title' => true # - # The validators hash can also handle regular expressions, ranges, - # arrays and strings in shortcut form, e.g. + # The validators hash can also handle regular expressions, ranges, arrays + # and strings in shortcut form. # - # validates :email, :format => /@/ - # validates :gender, :inclusion => %w(male female) - # validates :password, :length => 6..20 + # validates :email, format: /@/ + # validates :gender, inclusion: %w(male female) + # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your - # validator's initializer as +options[:in]+ while other types including - # regular expressions and strings are passed as +options[:with]+ + # validator's initializer as <tt>options[:in]</tt> while other types + # including regular expressions and strings are passed as <tt>options[:with]</tt>. # # There is also a list of options that could be used along with validators: + # # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. @@ -87,14 +89,12 @@ module ActiveModel # # Example: # - # validates :password, :presence => true, :confirmation => true, :if => :password_required? - # - # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ - # can be given to one specific validator, as a hash: - # - # validates :password, :presence => { :if => :password_required? }, :confirmation => true + # validates :password, presence: true, confirmation: true, if: :password_required? # + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ + # and +:strict+ can be given to one specific validator, as a hash: # + # validates :password, presence: { if: :password_required? }, confirmation: true def validates(*attributes) defaults = attributes.extract_options!.dup validations = defaults.slice!(*_validates_default_keys) @@ -122,8 +122,20 @@ module ActiveModel # users and are considered exceptional. So each validator defined with bang # or <tt>:strict</tt> option set to <tt>true</tt> will always raise # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error - # when validation fails. - # See <tt>validates</tt> for more information about the validation itself. + # when validation fails. See <tt>validates</tt> for more information about + # the validation itself. + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :name + # validates! :name, presence: true + # end + # + # person = Person.new + # person.name = '' + # person.valid? + # # => ActiveModel::StrictValidationFailed: Name can't be blank def validates!(*attributes) options = attributes.extract_options! options[:strict] = true @@ -134,7 +146,7 @@ module ActiveModel # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. - def _validates_default_keys + def _validates_default_keys #:nodoc: [:if, :unless, :on, :allow_blank, :allow_nil , :strict] end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 3c516f8b22..869591cd9e 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -34,7 +34,7 @@ module ActiveModel # class MyValidator < ActiveModel::Validator # def validate(record) # if some_complex_logic - # record.errors.add :base, "This record is invalid" + # record.errors.add :base, 'This record is invalid' # end # end # @@ -48,30 +48,32 @@ module ActiveModel # # class Person # include ActiveModel::Validations - # validates_with MyValidator, MyOtherValidator, :on => :create + # validates_with MyValidator, MyOtherValidator, on: :create # end # # Configuration options: # * <tt>:on</tt> - Specifies when this validation is active - # (<tt>:create</tt> or <tt>:update</tt> + # (<tt>:create</tt> or <tt>:update</tt>. # * <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. + # 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. + # (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. # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information. # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>: + # to the class and available as +options+: # # class Person # include ActiveModel::Validations - # validates_with MyValidator, :my_custom_key => "my custom value" + # validates_with MyValidator, my_custom_key: 'my custom value' # end # # class MyValidator < ActiveModel::Validator @@ -119,17 +121,17 @@ module ActiveModel # class Person # include ActiveModel::Validations # - # validate :instance_validations, :on => :create + # validate :instance_validations, on: :create # # def instance_validations # validates_with MyValidator, MyOtherValidator # end # end # - # Standard configuration options (:on, :if and :unless), which are - # 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. + # Standard configuration options (<tt>:on</tt>, <tt>:if</tt> and + # <tt>:unless</tt>), which are 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 +options+, please refer to the diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index d5d0798704..87e4319421 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,8 +1,6 @@ require "active_support/core_ext/module/anonymous" -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/inclusion' -module ActiveModel #:nodoc: +module ActiveModel # == Active Model Validator # @@ -28,7 +26,7 @@ module ActiveModel #:nodoc: # end # # Any class that inherits from ActiveModel::Validator must implement a method - # called <tt>validate</tt> which accepts a <tt>record</tt>. + # called +validate+ which accepts a +record+. # # class Person # include ActiveModel::Validations @@ -42,8 +40,8 @@ module ActiveModel #:nodoc: # end # end # - # To cause a validation error, you must add to the <tt>record</tt>'s errors directly - # from within the validators message + # To cause a validation error, you must add to the +record+'s errors directly + # from within the validators message. # # class MyValidator < ActiveModel::Validator # def validate(record) @@ -63,7 +61,7 @@ module ActiveModel #:nodoc: # end # # The easiest way to add custom validators for validating individual attributes - # is with the convenient <tt>ActiveModel::EachValidator</tt>. For example: + # is with the convenient <tt>ActiveModel::EachValidator</tt>. # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) @@ -72,7 +70,7 @@ module ActiveModel #:nodoc: # end # # This can now be used in combination with the +validates+ method - # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this) + # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this). # # class Person # include ActiveModel::Validations @@ -83,8 +81,7 @@ module ActiveModel #:nodoc: # # Validator may also define a +setup+ instance method which will get called # with the class that using that validator as its argument. This can be - # useful when there are prerequisites such as an +attr_accessor+ being present - # for example: + # useful when there are prerequisites such as an +attr_accessor+ being present. # # class MyValidator < ActiveModel::Validator # def setup(klass) @@ -94,15 +91,13 @@ module ActiveModel #:nodoc: # # This setup method is only called when used with validation macros or the # class level <tt>validates_with</tt> method. - # class Validator attr_reader :options - # Returns the kind of the validator. Examples: + # Returns the kind of the validator. # # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness - # def self.kind @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? end @@ -113,6 +108,9 @@ module ActiveModel #:nodoc: end # Return the kind for this validator. + # + # PresenceValidator.new.kind # => :presence + # UniquenessValidator.new.kind # => :uniqueness def kind self.class.kind end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 418a24294a..b141cec059 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -1,6 +1,5 @@ require "cases/helper" require 'active_support/logger' -require 'active_support/core_ext/object/inclusion' class SanitizerTest < ActiveModel::TestCase attr_accessor :logger diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 5f18909301..8650b0e495 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/user' +require 'models/oauthed_user' require 'models/visitor' require 'models/administrator' @@ -8,6 +9,7 @@ class SecurePasswordTest < ActiveModel::TestCase setup do @user = User.new @visitor = Visitor.new + @oauthed_user = OauthedUser.new end test "blank password" do @@ -73,4 +75,10 @@ class SecurePasswordTest < ActiveModel::TestCase @user.run_callbacks :create end end + + test "Oauthed user can be created with blank digest" do + assert_nothing_raised do + @oauthed_user.run_callbacks :create + end + end end diff --git a/activemodel/test/models/oauthed_user.rb b/activemodel/test/models/oauthed_user.rb new file mode 100644 index 0000000000..9750bc19d4 --- /dev/null +++ b/activemodel/test/models/oauthed_user.rb @@ -0,0 +1,11 @@ +class OauthedUser + extend ActiveModel::Callbacks + include ActiveModel::Validations + include ActiveModel::SecurePassword + + define_model_callbacks :create + + has_secure_password(validations: false) + + attr_accessor :password_digest, :password_salt +end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 5f3b2eaf7c..8275577467 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,8 +1,60 @@ ## Rails 4.0.0 (unreleased) ## +* Allow Relation#merge to take a proc. + + This was requested by DHH to allow creating of one's own custom + association macros. + + For example: + + module Commentable + def has_many_comments(extra) + has_many :comments, -> { where(:foo).merge(extra) } + end + end + + class Post < ActiveRecord::Base + extend Commentable + has_many_comments -> { where(:bar) } + end + + *Jon Leighton* + +* Add CollectionProxy#scope + + This can be used to get a Relation from an association. + + Previously we had a #scoped method, but we're deprecating that for + AR::Base, so it doesn't make sense to have it here. + + This was requested by DHH, to facilitate code like this: + + Project.scope.order('created_at DESC').page(current_page).tagged_with(@tag).limit(5).scoping do + @topics = @project.topics.scope + @todolists = @project.todolists.scope + @attachments = @project.attachments.scope + @documents = @project.documents.scope + end + + *Jon Leighton* + +* Add `Relation#load` + + This method explicitly loads the records and then returns `self`. + + Rather than deciding between "do I want an array or a relation?", + most people are actually asking themselves "do I want to eager load + or lazy load?" Therefore, this method provides a way to explicitly + eager-load without having to switch from a `Relation` to an array. + + Example: + + @posts = Post.where(published: true).load + + *Jon Leighton* + * `Model.all` now returns an `ActiveRecord::Relation`, rather than an - array of records. Use `Model.to_a` or `Relation#to_a` if you really - want an array. + array of records. Use ``Relation#to_a` if you really want an array. In some specific cases, this may cause breakage when upgrading. However in most cases the `ActiveRecord::Relation` will just act as a @@ -19,21 +71,14 @@ *Jon Leighton* -* Added an `update_columns` method. This new method updates the given attributes on an object, - without calling save, hence skipping validations and callbacks. - Example: - - User.first.update_columns(name: "sebastian", age: 25) # => true - - *Sebastian Martinez + Rafael Mendonça França* - -* Removed `:finder_sql` and `:counter_sql` collection association options. Please - use scopes instead. +* `:finder_sql` and `:counter_sql` options on collection associations + are deprecated. Please transition to using scopes. *Jon Leighton* -* Removed `:insert_sql` and `:delete_sql` `has_and_belongs_to_many` - association options. Please use `has_many :through` instead. +* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` + associations are deprecated. Please transition to using `has_many + :through` *Jon Leighton* @@ -398,7 +443,7 @@ RAILS_ENV=production bundle exec rake db:schema:cache:dump => generate db/schema_cache.dump - 2) add config.use_schema_cache_dump = true in config/production.rb. BTW, true is default. + 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default. 3) boot rails. RAILS_ENV=production bundle exec rails server diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index bedf37e1df..5a51aaaced 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,6 +22,7 @@ #++ require 'active_support' +require 'active_support/rails' require 'active_model' require 'arel' require 'active_record_deprecated_finders' @@ -31,6 +32,28 @@ require 'active_record/version' module ActiveRecord extend ActiveSupport::Autoload + autoload :Base + autoload :Callbacks + autoload :Core + autoload :CounterCache + autoload :ConnectionHandling + autoload :DynamicMatchers + autoload :Explain + autoload :Inheritance + autoload :Integration + autoload :Migration + autoload :Migrator, 'active_record/migration' + autoload :Model + autoload :ModelSchema + autoload :NestedAttributes + autoload :Observer + autoload :Persistence + autoload :QueryCache + autoload :Querying + autoload :ReadonlyAttributes + autoload :Reflection + autoload :Sanitization + # ActiveRecord::SessionStore depends on the abstract store in Action Pack. # Eager loading this class would break client code that eager loads Active # Record standalone. @@ -41,6 +64,17 @@ module ActiveRecord # session store its autoload happens at boot time. autoload :SessionStore + autoload :Schema + autoload :SchemaDumper + autoload :SchemaMigration + autoload :Scoping + autoload :Serialization + autoload :Store + autoload :Timestamp + autoload :Transactions + autoload :Translation + autoload :Validations + eager_autoload do autoload :ActiveRecordError, 'active_record/errors' autoload :ConnectionNotEstablished, 'active_record/errors' @@ -62,42 +96,10 @@ module ActiveRecord autoload :PredicateBuilder autoload :SpawnMethods autoload :Batches - autoload :Explain autoload :Delegation end - autoload :Base - autoload :Callbacks - autoload :Core - autoload :CounterCache - autoload :ConnectionHandling - autoload :DynamicMatchers - autoload :Explain - autoload :Inheritance - autoload :Integration - autoload :Migration - autoload :Migrator, 'active_record/migration' - autoload :Model - autoload :ModelSchema - autoload :NestedAttributes - autoload :Observer - autoload :Persistence - autoload :QueryCache - autoload :Querying - autoload :ReadonlyAttributes - autoload :Reflection autoload :Result - autoload :Sanitization - autoload :Schema - autoload :SchemaDumper - autoload :SchemaMigration - autoload :Scoping - autoload :Serialization - autoload :Store - autoload :Timestamp - autoload :Transactions - autoload :Translation - autoload :Validations end module Coders diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index edf82eb170..f5ee4f3ebe 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,9 +1,6 @@ require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/class/attribute' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -198,26 +195,6 @@ module ActiveRecord # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1)</tt> # - # === Overriding generated methods - # - # Association methods are generated in a module that is included into the model class, - # which allows you to easily override with your own methods and call the original - # generated method with +super+. For example: - # - # class Car < ActiveRecord::Base - # belongs_to :owner - # belongs_to :old_owner - # def owner=(new_owner) - # self.old_owner = self.owner - # super - # end - # end - # - # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is - # included in the model class immediately after the (anonymous) generated attributes methods - # module, meaning an association will override the methods for an attribute with the same name. - # # === A word of warning # # Don't create associations that have the same name as instance methods of @@ -265,6 +242,26 @@ module ActiveRecord # others.uniq | X | X | X # others.reset | X | X | X # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # If your model class is <tt>Project</tt>, the module is + # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # # == Cardinality and associations # # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many @@ -400,7 +397,28 @@ module ActiveRecord # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically # saved when the parent is saved. # - # === Association callbacks + # == Customizing the query + # + # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax + # to customize them. For example, to add a condition: + # + # class Blog < ActiveRecord::Base + # has_many :published_posts, -> { where published: true }, class_name: 'Post' + # end + # + # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods. + # + # === Accessing the owner object + # + # Sometimes it is useful to have access to the owner object when building the query. The owner + # is passed as a parameter to the block. For example, the following association would find all + # events that occur on the user's birthday: + # + # class User < ActiveRecord::Base + # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' + # end + # + # == Association callbacks # # Similar to the normal callbacks that hook into the life cycle of an Active Record object, # you can also define callbacks that get triggered when you add an object to or remove an @@ -427,7 +445,7 @@ module ActiveRecord # added to the collection. Same with the +before_remove+ callbacks; if an exception is # thrown the object doesn't get removed. # - # === Association extensions + # == Association extensions # # The proxy objects that control the access to associations can be extended through anonymous # modules. This is especially beneficial for adding new finders, creators, and other @@ -457,20 +475,11 @@ module ActiveRecord # end # # class Account < ActiveRecord::Base - # has_many :people, :extend => FindOrCreateByNameExtension + # has_many :people, -> { extending FindOrCreateByNameExtension } # end # # class Company < ActiveRecord::Base - # has_many :people, :extend => FindOrCreateByNameExtension - # end - # - # If you need to use multiple named extension modules, you can specify an array of modules - # with the <tt>:extend</tt> option. - # In the case of name conflicts between methods in the modules, methods in modules later - # in the array supercede those earlier in the array. - # - # class Account < ActiveRecord::Base - # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] + # has_many :people, -> { extending FindOrCreateByNameExtension } # end # # Some extensions can only be made to work with knowledge of the association's internals. @@ -488,7 +497,7 @@ module ActiveRecord # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside # association extensions. # - # === Association Join Models + # == Association Join Models # # Has Many associations can be configured with the <tt>:through</tt> option to use an # explicit join model to retrieve the data. This operates similarly to a @@ -572,7 +581,7 @@ module ActiveRecord # belongs_to :tag, :inverse_of => :taggings # end # - # === Nested Associations + # == Nested Associations # # You can actually specify *any* association with the <tt>:through</tt> option, including an # association which has a <tt>:through</tt> option itself. For example: @@ -615,7 +624,7 @@ module ActiveRecord # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. # - # === Polymorphic Associations + # == Polymorphic Associations # # Polymorphic associations on models are not restricted on what types of models they # can be associated with. Rather, they specify an interface that a +has_many+ association @@ -745,7 +754,7 @@ module ActiveRecord # to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base - # has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true] + # has_many :approved_comments, -> { where approved: true }, :class_name => 'Comment' # end # # Post.includes(:approved_comments) @@ -757,14 +766,11 @@ module ActiveRecord # returning all the associated objects: # # class Picture < ActiveRecord::Base - # has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10 + # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment' # end # # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments. # - # When eager loaded, conditions are interpolated in the context of the model class, not - # the model instance. Conditions are lazily interpolated before the actual model exists. - # # Eager loading is supported with polymorphic associations. # # class Address < ActiveRecord::Base @@ -842,8 +848,8 @@ module ActiveRecord # module MyApplication # module Business # class Firm < ActiveRecord::Base - # has_many :clients - # end + # has_many :clients + # end # # class Client < ActiveRecord::Base; end # end @@ -1081,15 +1087,6 @@ module ActiveRecord # from the association name. So <tt>has_many :products</tt> will by default be linked # to the Product class, but if the real class name is SpecialProduct, you'll have to # specify it with this option. - # [:conditions] - # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from - # the association are scoped if a hash is used. - # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published - # posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>. - # [:order] - # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, - # such as <tt>last_name, first_name DESC</tt>. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ @@ -1107,25 +1104,6 @@ module ActiveRecord # If using with the <tt>:through</tt> option, the association on the join model must be # a +belongs_to+, and the records which get deleted are the join records, rather than # the associated records. - # - # [:extend] - # Specify a named module for extending the proxy. See "Association extensions". - # [:include] - # Specify second-order associations that should be eager loaded when the collection is loaded. - # [:group] - # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> - # returns. Uses the <tt>HAVING</tt> SQL-clause. - # [:limit] - # An integer determining the limit on the number of rows that should be returned. - # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, - # it would skip the first 4 rows. - # [:select] - # 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. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] @@ -1152,10 +1130,6 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:uniq] - # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>. - # [:readonly] - # If true, all the associated objects are readonly through the association. # [:validate] # If +false+, don't validate the associated objects when saving the parent object. true by default. # [:autosave] @@ -1171,14 +1145,14 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # has_many :comments, :order => "posted_on" - # has_many :comments, :include => :author - # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name" - # has_many :tracks, :order => "position", :dependent => :destroy - # has_many :comments, :dependent => :nullify - # has_many :tags, :as => :taggable - # has_many :reports, :readonly => true - # has_many :subscribers, :through => :subscriptions, :source => :user + # has_many :comments, -> { order "posted_on" } + # has_many :comments, -> { includes :author } + # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :tracks, -> { order "position" }, dependent: :destroy + # has_many :comments, dependent: :nullify + # has_many :tags, as: :taggable + # has_many :reports, -> { readonly } + # has_many :subscribers, through: :subscriptions, source: :user def has_many(name, scope = nil, options = {}, &extension) Builder::HasMany.build(self, name, scope, options, &extension) end @@ -1228,14 +1202,6 @@ module ActiveRecord # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but # if the real class name is Person, you'll have to specify it with this option. - # [:conditions] - # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash - # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create - # an enabled account with <tt>@company.create_account</tt> or <tt>@company.build_account</tt>. - # [:order] - # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, - # such as <tt>last_name, first_name DESC</tt>. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. @@ -1248,14 +1214,8 @@ module ActiveRecord # will use "person_id" as the default <tt>:foreign_key</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. - # [:include] - # Specify second-order associations that should be eager loaded when this object is loaded. # [: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 - # 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>, # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the @@ -1269,8 +1229,6 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:readonly] - # If true, the associated object is readonly through the association. # [:validate] # If +false+, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] @@ -1289,12 +1247,12 @@ module ActiveRecord # has_one :credit_card, :dependent => :destroy # destroys the associated credit card # has_one :credit_card, :dependent => :nullify # updates the associated records foreign # # key value to NULL rather than destroying it - # has_one :last_comment, :class_name => "Comment", :order => "posted_on" - # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" - # has_one :attachment, :as => :attachable - # has_one :boss, :readonly => :true - # has_one :club, :through => :membership - # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable + # has_one :last_comment, -> { order 'posted_on' }, :class_name => "Comment" + # has_one :project_manager, -> { where role: 'project_manager' }, :class_name => "Person" + # has_one :attachment, as: :attachable + # has_one :boss, readonly: :true + # has_one :club, through: :membership + # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable def has_one(name, scope = nil, options = {}) Builder::HasOne.build(self, name, scope, options) end @@ -1341,13 +1299,6 @@ module ActiveRecord # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. - # [:conditions] - # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. - # [:select] - # 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. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> @@ -1380,14 +1331,10 @@ module ActiveRecord # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) # Note: Specifying a counter cache will add it to that model's list of readonly attributes # using +attr_readonly+. - # [:include] - # Specify second-order associations that should be eager loaded when this object is loaded. # [:polymorphic] # Specify this association is a polymorphic association by passing +true+. # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). - # [:readonly] - # If true, the associated object is readonly through the association. # [:validate] # If +false+, don't validate the associated objects when saving the parent object. +false+ by default. # [:autosave] @@ -1408,16 +1355,16 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # belongs_to :firm, :foreign_key => "client_of" - # belongs_to :person, :primary_key => "name", :foreign_key => "person_name" - # belongs_to :author, :class_name => "Person", :foreign_key => "author_id" - # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", - # :conditions => 'discounts > #{payments_count}' - # belongs_to :attachable, :polymorphic => true - # belongs_to :project, :readonly => true - # belongs_to :post, :counter_cache => true - # belongs_to :company, :touch => true - # belongs_to :company, :touch => :employees_last_updated_at + # belongs_to :firm, foreign_key: "client_of" + # belongs_to :person, primary_key: "name", foreign_key: "person_name" + # belongs_to :author, class_name: "Person", foreign_key: "author_id" + # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # class_name: "Coupon", foreign_key: "coupon_id" + # belongs_to :attachable, polymorphic: true + # belongs_to :project, readonly: true + # belongs_to :post, counter_cache: true + # belongs_to :company, touch: true + # belongs_to :company, touch: :employees_last_updated_at def belongs_to(name, scope = nil, options = {}) Builder::BelongsTo.build(self, name, scope, options) end @@ -1531,35 +1478,6 @@ module ActiveRecord # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. # So if a Person class makes a +has_and_belongs_to_many+ association to Project, # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. - # [:conditions] - # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are - # scoped if a hash is used. - # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> - # or <tt>@blog.posts.build</tt>. - # [:order] - # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, - # such as <tt>last_name, first_name DESC</tt> - # [:uniq] - # If true, duplicate associated objects will be ignored by accessors and query methods. - # [:extend] - # Anonymous module for extending the proxy, see "Association extensions". - # [:include] - # Specify second-order associations that should be eager loaded when the collection is loaded. - # [:group] - # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. - # Uses the <tt>HAVING</tt> SQL-clause. - # [:limit] - # An integer determining the limit on the number of rows that should be returned. - # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, - # it would skip the first 4 rows. - # [:select] - # 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. # [:validate] @@ -1574,10 +1492,10 @@ module ActiveRecord # # Option examples: # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ] - # has_and_belongs_to_many :nations, :class_name => "Country" - # has_and_belongs_to_many :categories, :join_table => "prods_cats" - # has_and_belongs_to_many :categories, :readonly => true + # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :nations, class_name: "Country" + # has_and_belongs_to_many :categories, join_table: "prods_cats" + # has_and_belongs_to_many :categories, -> { readonly } def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 9e464ff681..4db7038d2e 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/inclusion' module ActiveRecord module Associations @@ -81,10 +80,15 @@ module ActiveRecord loaded! end - def scoped + def scope target_scope.merge(association_scope) end + def scoped + ActiveSupport::Deprecation.warn("#scoped is deprecated. use #scope instead.") + scope + end + # The scope for this association. # # Note that the association_scope is merged into the target_scope only when the @@ -140,6 +144,14 @@ module ActiveRecord reset end + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + owner.send(:instance_exec, record, &sql) + else + sql + end + end + # We can't dump @reflection since it contains the scope proc def marshal_dump reflection = @reflection diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index cb97490ef1..1303822868 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -5,7 +5,7 @@ module ActiveRecord attr_reader :association, :alias_tracker - delegate :klass, :owner, :reflection, :to => :association + delegate :klass, :owner, :reflection, :interpolate, :to => :association delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection def initialize(association) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f45ab1aff4..c3f32b5ed9 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -77,6 +77,16 @@ module ActiveRecord::Associations::Builder end end + def check_valid_dependent!(dependent, valid_options) + unless valid_options.include?(dependent) + valid_options_message = valid_options.map(&:inspect).to_sentence( + words_connector: ', ', two_words_connector: ' or ', last_word_connector: ' or ') + + raise ArgumentError, "The :dependent option expects either " \ + "#{valid_options_message} (#{dependent.inspect})" + end + end + def dependent_restrict_raises? ActiveRecord::Base.dependent_restrict_raises == true end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 4bef996297..f205a456f7 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: @@ -72,16 +71,14 @@ module ActiveRecord::Associations::Builder end def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete]) - raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" - end + if dependent = options[:dependent] + check_valid_dependent! dependent, [:destroy, :delete] - method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}" + method_name = "belongs_to_dependent_#{dependent}_for_#{name}" model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) def #{method_name} association = #{name} - association.#{options[:dependent]} if association + association.#{dependent} if association end eoruby model.after_destroy method_name diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index b28d6a746c..3fb0a57450 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,9 +1,10 @@ + module ActiveRecord::Associations::Builder class CollectionAssociation < Association #:nodoc: CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def valid_options - super + [:table_name, :before_add, :after_add, :before_remove, :after_remove] + super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove] end attr_reader :block_extension, :extension_module @@ -14,6 +15,7 @@ module ActiveRecord::Associations::Builder end def build + show_deprecation_warnings wrap_block_extension reflection = super CALLBACKS.each { |callback_name| define_callback(callback_name) } @@ -24,6 +26,14 @@ module ActiveRecord::Associations::Builder true end + def show_deprecation_warnings + [:finder_sql, :counter_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).") + end + end + end + private def wrap_block_extension diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index a30e2dab26..8df28ad876 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:join_table, :association_foreign_key] + super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] end def build @@ -14,6 +14,16 @@ module ActiveRecord::Associations::Builder reflection end + def show_deprecation_warnings + super + + [:delete_sql, :insert_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).") + end + end + end + private def define_destroy_hook diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 81df1fb135..9e60dbc30b 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: @@ -19,14 +18,11 @@ module ActiveRecord::Associations::Builder private def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete_all, :nullify, :restrict]) - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ - ":nullify or :restrict (#{options[:dependent].inspect})" - end + if dependent = options[:dependent] + check_valid_dependent! dependent, [:destroy, :delete_all, :nullify, :restrict] + dependent_restrict_deprecation_warning if dependent == :restrict - dependent_restrict_deprecation_warning if options[:dependent] == :restrict - send("define_#{options[:dependent]}_dependency_method") + send("define_#{dependent}_dependency_method") model.before_destroy dependency_method_name end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index cdb45e8e58..9c84f1913a 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: @@ -25,14 +24,11 @@ module ActiveRecord::Associations::Builder private def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete, :nullify, :restrict]) - raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ - ":nullify or :restrict (#{options[:dependent].inspect})" - end - - dependent_restrict_deprecation_warning if options[:dependent] == :restrict - send("define_#{options[:dependent]}_dependency_method") + if dependent = options[:dependent] + check_valid_dependent! dependent, [:destroy, :delete, :nullify, :restrict] + dependent_restrict_deprecation_warning if dependent == :restrict + + send("define_#{dependent}_dependency_method") model.before_destroy dependency_method_name end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 30522e3a5d..f8c1103ea9 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -44,13 +44,13 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader - if loaded? + if loaded? || options[:finder_sql] load_target.map do |record| record.send(reflection.association_primary_key) end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scoped.pluck(column) + scope.pluck(column) end end @@ -71,7 +71,7 @@ module ActiveRecord if block_given? load_target.select.each { |e| yield e } else - scoped.select(select) + scope.select(select) end end @@ -79,7 +79,11 @@ module ActiveRecord if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else - scoped.find(*args) + if options[:finder_sql] + find_by_scan(*args) + else + scope.find(*args) + end end end @@ -160,32 +164,41 @@ module ActiveRecord # Calculate sum using SQL, not Enumerable. def sum(*args) if block_given? - scoped.sum(*args) { |*block_args| yield(*block_args) } + scope.sum(*args) { |*block_args| yield(*block_args) } else - scoped.sum(*args) + scope.sum(*args) end end - # Count all records using SQL. Construct options and pass them with + # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the + # association, it will be used for the query. Otherwise, construct options and pass them with # scope to the target class's +count+. def count(column_name = nil, count_options = {}) column_name, count_options = nil, column_name if column_name.is_a?(Hash) - if association_scope.uniq_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - count_options[:distinct] = true - end + if options[:counter_sql] || options[:finder_sql] + unless count_options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + end - value = scoped.count(column_name, count_options) + reflection.klass.count_by_sql(custom_counter_sql) + else + if association_scope.uniq_value + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name ||= reflection.klass.primary_key + count_options[:distinct] = true + end - limit = options[:limit] - offset = options[:offset] + value = scope.count(column_name, count_options) - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value + limit = options[:limit] + offset = options[:offset] + + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value + end end end @@ -310,7 +323,8 @@ module ActiveRecord if record.new_record? include_in_memory?(record) else - loaded? ? target.include?(record) : scoped.exists?(record) + load_target if options[:finder_sql] + loaded? ? target.include?(record) : scope.exists?(record) end else false @@ -344,8 +358,31 @@ module ActiveRecord private + def custom_counter_sql + if options[:counter_sql] + interpolate(options[:counter_sql]) + else + # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ + interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do + count_with = $2.to_s + count_with = '*' if count_with.blank? || count_with =~ /,/ + "SELECT #{$1}COUNT(#{count_with}) FROM" + end + end + end + + def custom_finder_sql + interpolate(options[:finder_sql]) + end + def find_target - records = scoped.to_a + records = + if options[:finder_sql] + reflection.klass.find_by_sql(custom_finder_sql) + else + scope.to_a + end + records.each { |record| set_inverse_instance(record) } records end @@ -403,7 +440,7 @@ module ActiveRecord end def create_scope - scoped.scope_for_create.stringify_keys + scope.scope_for_create.stringify_keys end def delete_or_destroy(records, method) @@ -484,6 +521,7 @@ module ActiveRecord # Otherwise, go to the database only if none of the following are true: # * target already loaded # * owner is new record + # * custom :finder_sql exists # * target contains new or changed record(s) # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) @@ -492,6 +530,7 @@ module ActiveRecord else !(loaded? || owner.new_record? || + options[:finder_sql] || target.any? { |record| record.new_record? || record.changed? } || args.first.kind_of?(Integer)) end @@ -508,11 +547,25 @@ module ActiveRecord end end + # If using a custom finder_sql, #find scans the entire collection. + def find_by_scan(*args) + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + end + # Fetches the first/last using SQL if possible, otherwise from the target array. def first_or_last(type, *args) args.shift if args.first.is_a?(Hash) && args.first.empty? - collection = fetch_first_or_last_using_find?(args) ? scoped : load_target + collection = fetch_first_or_last_using_find?(args) ? scope : load_target collection.send(type, *args) end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 49891f7675..ee8b816ef4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -34,15 +34,25 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate :target, :load_target, :loaded?, :to => :@association + def initialize(association) #:nodoc: + @association = association + super association.klass, association.klass.arel_table + merge! association.scope + end + + def target + @association.target + end + + def load_target + @association.load_target + end + + def loaded? + @association.loaded? + end ## - # :method: select - # - # :call-seq: - # select(select = nil) - # select(&block) - # # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. @@ -96,13 +106,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook">, # # #<Pet id: 3, name: "Choo-Choo"> # # ] + def select(select = nil, &block) + @association.select(select, &block) + end ## - # :method: find - # - # :call-seq: - # find(*args, &block) - # # Finds an object in the collection responding to the +id+. Uses the same # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ # error if the object can not be found. @@ -129,13 +137,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def find(*args, &block) + @association.find(*args, &block) + end ## - # :method: first - # - # :call-seq: - # first(limit = nil) - # # Returns the first record, or the first +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -162,13 +168,11 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.first # => nil # another_person_without.pets.first(3) # => [] + def first(*args) + @association.first(*args) + end ## - # :method: last - # - # :call-seq: - # last(limit = nil) - # # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -195,13 +199,11 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.last # => nil # another_person_without.pets.last(3) # => [] + def last(*args) + @association.last(*args) + end ## - # :method: build - # - # :call-seq: - # build(attributes = {}, options = {}, &block) - # # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object, but have not yet been saved. # You can pass an array of attributes hashes, this will return an array @@ -226,13 +228,11 @@ module ActiveRecord # # person.pets.size # => 5 # size of the collection # person.pets.count # => 0 # count from database + def build(attributes = {}, options = {}, &block) + @association.build(attributes, options, &block) + end ## - # :method: create - # - # :call-seq: - # create(attributes = {}, options = {}, &block) - # # Returns a new object of the collection type that has been instantiated with # attributes, linked to this object and that has already been saved (if it # passes the validations). @@ -259,13 +259,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def create(attributes = {}, options = {}, &block) + @association.create(attributes, options, &block) + end ## - # :method: create! - # - # :call-seq: - # create!(attributes = {}, options = {}, &block) - # # Like +create+, except that if the record is invalid, raises an exception. # # class Person @@ -279,13 +277,11 @@ module ActiveRecord # # person.pets.create!(name: nil) # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank + def create!(attributes = {}, options = {}, &block) + @association.create!(attributes, options, &block) + end ## - # :method: concat - # - # :call-seq: - # concat(*records) - # # Add one or more records to the collection by setting their foreign keys # to the association's primary key. Since << flattens its argument list and # inserts each record, +push+ and +concat+ behave identically. Returns +self+ @@ -310,13 +306,11 @@ module ActiveRecord # # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) # person.pets.size # => 5 + def concat(*records) + @association.concat(*records) + end ## - # :method: replace - # - # :call-seq: - # replace(other_array) - # # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # @@ -339,14 +333,12 @@ module ActiveRecord # # person.pets.replace(["doo", "ggie", "gaga"]) # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String + def replace(other_array) + @association.replace(other_array) + end ## - # :method: delete_all - # - # :call-seq: - # delete_all() - # - # Deletes all the records from the collection. For +has_many+ asssociations, + # Deletes all the records from the collection. For +has_many+ associations, # the deletion is done according to the strategy specified by the <tt>:dependent</tt> # option. Returns an array with the deleted records. # @@ -434,13 +426,11 @@ module ActiveRecord # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound + def delete_all + @association.delete_all + end ## - # :method: destroy_all - # - # :call-seq: - # destroy_all() - # # Deletes the records of the collection directly from the database. # This will _always_ remove the records ignoring the +:dependent+ # option. @@ -463,15 +453,11 @@ module ActiveRecord # person.pets # => [] # # Pet.find(1) # => Couldn't find Pet with id=1 + def destroy_all + @association.destroy_all + end ## - # :method: delete - # - # :call-seq: - # delete(*records) - # delete(*fixnum_ids) - # delete(*string_ids) - # # Deletes the +records+ supplied and removes them from the collection. For # +has_many+ associations, the deletion is done according to the strategy # specified by the <tt>:dependent</tt> option. Returns an array with the @@ -586,13 +572,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def delete(*records) + @association.delete(*records) + end ## - # :method: destroy - # - # :call-seq: - # destroy(*records) - # # Destroys the +records+ supplied and removes them from the collection. # This method will _always_ remove record from the database ignoring # the +:dependent+ option. Returns an array with the removed records. @@ -661,13 +645,11 @@ module ActiveRecord # person.pets # => [] # # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + def destroy(*records) + @association.destroy(*records) + end ## - # :method: uniq - # - # :call-seq: - # uniq() - # # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -682,13 +664,11 @@ module ActiveRecord # # person.pets.select(:name).uniq # # => [#<Pet name: "Fancy-Fancy">] + def uniq + @association.uniq + end ## - # :method: count - # - # :call-seq: - # count() - # # Count all records using SQL. # # class Person < ActiveRecord::Base @@ -702,13 +682,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def count(column_name = nil, options = {}) + @association.count(column_name, options) + end ## - # :method: size - # - # :call-seq: - # size() - # # Returns the size of the collection. If the collection hasn't been loaded, # it executes a <tt>SELECT COUNT(*)</tt> query. # @@ -729,13 +707,11 @@ module ActiveRecord # person.pets.size # => 3 # # Because the collection is already loaded, this will behave like # # collection.size and no SQL count query is executed. + def size + @association.size + end ## - # :method: length - # - # :call-seq: - # length() - # # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. @@ -755,10 +731,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def length + @association.length + end ## - # :method: empty? - # # Returns +true+ if the collection is empty. # # class Person < ActiveRecord::Base @@ -772,14 +749,11 @@ module ActiveRecord # # person.pets.count # => 0 # person.pets.empty? # => true + def empty? + @association.empty? + end ## - # :method: any? - # - # :call-seq: - # any? - # any?{|item| block} - # # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base @@ -809,14 +783,11 @@ module ActiveRecord # pet.group == 'dogs' # end # # => true + def any?(&block) + @association.any?(&block) + end ## - # :method: many? - # - # :call-seq: - # many? - # many?{|item| block} - # # Returns true if the collection has more than one record. # Equivalent to <tt>collection.size > 1</tt>. # @@ -851,13 +822,11 @@ module ActiveRecord # pet.group == 'cats' # end # # => true + def many?(&block) + @association.many?(&block) + end ## - # :method: include? - # - # :call-seq: - # include?(record) - # # Returns +true+ if the given object is present in the collection. # # class Person < ActiveRecord::Base @@ -868,17 +837,8 @@ module ActiveRecord # # person.pets.include?(Pet.find(20)) # => true # person.pets.include?(Pet.find(21)) # => false - delegate :select, :find, :first, :last, - :build, :create, :create!, - :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, - :sum, :count, :size, :length, :empty?, - :any?, :many?, :include?, - :to => :@association - - def initialize(association) #:nodoc: - @association = association - super association.klass, association.klass.arel_table - merge! association.scoped + def include?(record) + @association.include?(record) end alias_method :new, :build @@ -892,17 +852,21 @@ module ActiveRecord # method, which gets the current scope, which is this object, which # delegates to @association, and so on. def scoping - @association.scoped.scoping { yield } + @association.scope.scoping { yield } end - def spawn + # Returns a <tt>Relation</tt> object for the records in this association + def scope association = @association - @association.scoped.extending! do + @association.scope.extending! do define_method(:proxy_association) { association } end end + # :nodoc: + alias spawn scope + # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal # to the corresponding element in the other array, otherwise returns diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index e5b40f3911..93618721bb 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -18,12 +18,16 @@ module ActiveRecord end end - stmt = join_table.compile_insert( - join_table[reflection.foreign_key] => owner.id, - join_table[reflection.association_foreign_key] => record.id - ) + if options[:insert_sql] + owner.connection.insert(interpolate(options[:insert_sql], record)) + else + stmt = join_table.compile_insert( + join_table[reflection.foreign_key] => owner.id, + join_table[reflection.association_foreign_key] => record.id + ) - owner.connection.insert stmt + owner.connection.insert stmt + end record end @@ -35,17 +39,22 @@ module ActiveRecord end def delete_records(records, method) - relation = join_table - condition = relation[reflection.foreign_key].eq(owner.id) - - unless records == :all - condition = condition.and( - relation[reflection.association_foreign_key] - .in(records.map { |x| x.id }.compact) - ) - end + if sql = options[:delete_sql] + records = load_target if records == :all + records.each { |record| owner.connection.delete(interpolate(sql, record)) } + else + relation = join_table + condition = relation[reflection.foreign_key].eq(owner.id) - owner.connection.delete(relation.where(condition).compile_delete) + unless records == :all + condition = condition.and( + relation[reflection.association_foreign_key] + .in(records.map { |x| x.id }.compact) + ) + end + + owner.connection.delete(relation.where(condition).compile_delete) + end end def invertible_for?(record) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 41c6ca92cc..7a363c896e 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -33,7 +33,13 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = has_cached_counter? ? owner[cached_counter_attribute_name] : scoped.count + count = if has_cached_counter? + owner.send(:read_attribute, cached_counter_attribute_name) + elsif options[:counter_sql] || options[:finder_sql] + reflection.klass.count_by_sql(custom_counter_sql) + else + scope.count + end # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a @@ -84,10 +90,10 @@ module ActiveRecord update_counter(-records.length) unless inverse_updates_counter_cache? else if records == :all - scope = scoped + scope = self.scope else keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) + scope = self.scope.where(reflection.association_primary_key => keys) end if method == :delete_all diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 6c5a9d73a9..88ff11f953 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association @@ -126,7 +125,7 @@ module ActiveRecord # even when we just want to delete everything. records = load_target if records == :all - scope = through_association.scoped + scope = through_association.scope scope.where! construct_join_attributes(*records) case method @@ -171,7 +170,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scoped.to_a + scope.to_a end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7086dfa34c..c5aed6e26a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Belongs To Has One Association diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index fa77a8733b..cbf5e734ea 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -10,7 +10,7 @@ module ActiveRecord @reflection = reflection @preload_scope = preload_scope @model = owners.first && owners.first.class - @scoped = nil + @scope = nil @owners_by_key = nil end @@ -24,12 +24,12 @@ module ActiveRecord raise NotImplementedError end - def scoped - @scoped ||= build_scope + def scope + @scope ||= build_scope end def records_for(ids) - scoped.where(association_key.in(ids)) + scope.where(association_key.in(ids)) end def table diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index a1a921bcb4..b84cb4922d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -35,11 +35,11 @@ module ActiveRecord private def create_scope - scoped.scope_for_create.stringify_keys.except(klass.primary_key) + scope.scope_for_create.stringify_keys.except(klass.primary_key) end def find_target - scoped.first.tap { |record| set_inverse_instance(record) } + scope.first.tap { |record| set_inverse_instance(record) } end # Implemented by subclasses diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 6cbacb79ea..6992840040 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index f36df4a444..ced15bc330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/enumerable' -require 'active_support/deprecation' module ActiveRecord # = Active Record Attribute Methods diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index a24b4b7839..60e5b0e2bb 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord @@ -98,7 +96,7 @@ module ActiveRecord def changes_from_zero_to_string?(old, value) # For columns with old 0 and value non-empty string - old == 0 && value.present? && value != '0' + old == 0 && value.is_a?(String) && value.present? && value != '0' end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 1e841dc8e0..a8b23abb7c 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord module AttributeMethods diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index e300c9721f..fa5b2ef336 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 189985b671..a4705b24ca 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -4,7 +4,6 @@ require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/descendants_tracker' require 'active_support/time' -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' @@ -13,11 +12,8 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' @@ -329,4 +325,4 @@ module ActiveRecord #:nodoc: end end -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new) 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 f9ae1ed475..dca355aa93 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'date' require 'set' require 'bigdecimal' 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 09f103100e..86d6266af9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation/reporting' require 'active_record/migration/join_table' module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 28a9821913..b3f9187429 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -2,7 +2,6 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' -require 'active_support/deprecation' require 'active_record/connection_adapters/schema_cache' require 'monitor' diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 2b0bc3f497..1126fe7fce 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'arel/visitors/bind_visitor' module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 01bd3ae26c..b9045cf1e7 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,5 +1,4 @@ require 'set' -require 'active_support/deprecation' module ActiveRecord # :stopdoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 71a84011bc..8e9ce80697 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,5 +1,4 @@ require 'active_record/connection_adapters/abstract_adapter' -require 'active_support/core_ext/object/blank' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' require 'arel/visitors/bind_visitor' diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index bda41df80f..7863c795ed 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord module ConnectionHandling diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index df8d805c8c..1145d2138c 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,7 +1,5 @@ -require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/deep_dup' -require 'active_support/core_ext/module/delegation' require 'thread' module ActiveRecord diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 7ade385c70..9e0390bed1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,5 +1,4 @@ require 'active_support/lazy_load_hooks' -require 'active_support/core_ext/class/attribute' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 96d24b72b3..e19ff5edd2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,7 +2,6 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/object/blank' require 'active_record/fixtures/file' require 'active_record/errors' diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 4a24024105..7d759c1048 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d58176bc62..703265c334 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,4 @@ -require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" -require 'active_support/deprecation' require 'set' module ActiveRecord @@ -52,7 +50,7 @@ module ActiveRecord # # class AddSsl < ActiveRecord::Migration # def up - # add_column :accounts, :ssl_enabled, :boolean, :default => 1 + # add_column :accounts, :ssl_enabled, :boolean, :default => true # end # # def down diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 03f6d36ddb..a326dabcd3 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -1,6 +1,3 @@ -require 'active_support/deprecation' -require 'active_support/concern' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord @@ -112,26 +109,39 @@ module ActiveRecord end end - module DeprecationProxy #:nodoc: - class << self - instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } - - def method_missing(name, *args, &block) - if Model.respond_to?(name) - Model.send(name, *args, &block) - else - ActiveSupport::Deprecation.warn( - "The object passed to the active_record load hook was previously ActiveRecord::Base " \ - "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ - "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ - "a module rather than a class. (Model is included in Base, so anything added to Model " \ - "will be available on Base as well.)" - ) - Base.send(name, *args, &block) - end + class DeprecationProxy < BasicObject #:nodoc: + def initialize(model = Model, base = Base) + @model = model + @base = base + end + + def method_missing(name, *args, &block) + if @model.respond_to?(name, true) + @model.send(name, *args, &block) + else + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + @base.send(name, *args, &block) end + end - alias send method_missing + alias send method_missing + + def extend(*mods) + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \ + "would add singleton methods to Model. This is presumably not what you want, since the " \ + "methods would not be inherited down to Base. Rather than using extend, please use " \ + "ActiveSupport::Concern + include, which will ensure that your class methods are " \ + "inherited." + ) + @base.extend *mods end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 9981a9a804..def48c03bf 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 7febb5539f..be013a068c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,8 +1,6 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/class/attribute' module ActiveRecord ActiveSupport.on_load(:active_record_config) do @@ -409,7 +407,7 @@ module ActiveRecord association.target else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids) + attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) end attributes_collection.each do |attributes| diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index e940d357da..6b2f6f98a5 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Observer diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 32b62d12e1..593fed5a85 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord # = Active Record Persistence diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index d64dee10fe..2bd8ecda20 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Query Cache diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index b8b54efe9f..13e09eda53 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,9 +1,7 @@ -require 'active_support/core_ext/module/delegation' -require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :take, :take!, :first, :first!, :last, :last!, :to_a, :exists?, :any?, :many?, :to => :all + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all delegate :find_by, :find_by!, :to => :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 9432a70c41..672d9a4246 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,6 +29,8 @@ module ActiveRecord 'ActiveRecord::RecordNotSaved' => :unprocessable_entity ) + config.active_record.use_schema_cache_dump = true + rake_tasks do require "active_record/base" load "active_record/railties/databases.rake" @@ -66,6 +68,25 @@ module ActiveRecord end end + initializer "active_record.check_schema_cache_dump" do |app| + if config.active_record.delete(:use_schema_cache_dump) + config.after_initialize do |app| + ActiveSupport.on_load(:active_record) do + filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + + if File.file?(filename) + cache = Marshal.load File.binread filename + if cache.version == ActiveRecord::Migrator.current_version + ActiveRecord::Model.connection.schema_cache = cache + else + warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." + end + end + end + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| @@ -117,21 +138,6 @@ module ActiveRecord end end - ActiveSupport.on_load(:active_record) do - if app.config.use_schema_cache_dump - filename = File.join(app.config.paths["db"].first, "schema_cache.dump") - - if File.file?(filename) - cache = Marshal.load File.binread filename - if cache.version == ActiveRecord::Migrator.current_version - ActiveRecord::Model.connection.schema_cache = cache - else - warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." - end - end - end - end - end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index f5991e893c..6bb0c39b79 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -20,7 +20,7 @@ db_namespace = namespace :db do end desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' - task :create => [:load_config, :rails_env] do + task :create => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.create_current end @@ -31,7 +31,7 @@ db_namespace = namespace :db do end desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' - task :drop => [:load_config, :rails_env] do + task :drop => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end @@ -88,7 +88,7 @@ db_namespace = namespace :db do end desc 'Display status of migrations' - task :status => [:environment, :load_config, :rails_env] do + task :status => [:environment, :load_config] do config = ActiveRecord::Base.configurations[Rails.env] ActiveRecord::Base.establish_connection(config) unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) @@ -142,12 +142,12 @@ db_namespace = namespace :db do end # desc "Retrieves the charset for the current environment's database" - task :charset => [:environment, :load_config, :rails_env] do + task :charset => [:environment, :load_config] do puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task :collation => [:environment, :load_config, :rails_env] do + task :collation => [:environment, :load_config] do begin puts ActiveRecord::Tasks::DatabaseTasks.collation_current rescue NoMethodError @@ -184,7 +184,7 @@ db_namespace = namespace :db do namespace :fixtures do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :load => [:environment, :load_config, :rails_env] do + task :load => [:environment, :load_config] do require 'active_record/fixtures' ActiveRecord::Base.establish_connection(Rails.env) @@ -222,7 +222,7 @@ db_namespace = namespace :db do namespace :schema do desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR' - task :dump => [:environment, :load_config, :rails_env] do + task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" File.open(filename, "w:utf-8") do |file| @@ -248,7 +248,7 @@ db_namespace = namespace :db do namespace :cache do desc 'Create a db/schema_cache.dump file.' - task :dump => [:environment, :load_config, :rails_env] do + task :dump => [:environment, :load_config] do con = ActiveRecord::Base.connection filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") @@ -277,7 +277,7 @@ db_namespace = namespace :db do end desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' - task :dump => [:environment, :load_config, :rails_env] do + task :dump => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[Rails.env]['adapter'] diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 960b78dc38..1d8c566e40 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,5 +1,3 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' module ActiveRecord module ReadonlyAttributes diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b5da659874..cf949a893f 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Reflection diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 3821c6122a..9ed3256ae9 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' module ActiveRecord # = Active Record Relation @@ -186,45 +184,9 @@ module ActiveRecord # Converts relation objects to Array. def to_a - # We monitor here the entire execution rather than individual SELECTs - # because from the point of view of the user fetching the records of a - # relation is a single unit of work. You want to know if this call takes - # too long, not if the individual queries take too long. - # - # It could be the case that none of the queries involved surpass the - # threshold, and at the same time the sum of them all does. The user - # should get a query plan logged in that case. - logging_query_plan do - exec_queries - end - end - - def exec_queries - return @records if loaded? - - default_scoped = with_default_scope - - if default_scoped.equal?(self) - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) - - preload = preload_values - preload += includes_values unless eager_loading? - preload.each do |associations| - ActiveRecord::Associations::Preloader.new(@records, associations).run - end - - # @readonly_value is true only if set explicitly. @implicit_readonly is true if there - # are JOINS and no explicit SELECT. - readonly = readonly_value.nil? ? @implicit_readonly : readonly_value - @records.each { |record| record.readonly! } if readonly - else - @records = default_scoped.to_a - end - - @loaded = true + load @records end - private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -466,11 +428,32 @@ module ActiveRecord where(primary_key => id_or_array).delete_all end + # Causes the records to be loaded from the database if they have not + # been loaded already. You can use this if for some reason you need + # to explicitly load some records before actually using them. The + # return value is the relation itself, not the records. + # + # Post.where(published: true).load # => #<ActiveRecord::Relation> + def load + unless loaded? + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan { exec_queries } + end + + self + end + # Forces reloading of relation. def reload reset - to_a # force reload - self + load end def reset @@ -566,6 +549,30 @@ module ActiveRecord private + def exec_queries + default_scoped = with_default_scope + + if default_scoped.equal?(self) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + + preload = preload_values + preload += includes_values unless eager_loading? + preload.each do |associations| + ActiveRecord::Associations::Preloader.new(@records, associations).run + end + + # @readonly_value is true only if set explicitly. @implicit_readonly is true if there + # are JOINS and no explicit SELECT. + readonly = readonly_value.nil? ? @implicit_readonly : readonly_value + @records.each { |record| record.readonly! } if readonly + else + @records = default_scoped.to_a + end + + @loaded = true + @records + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index fddfb5c11e..4d14506965 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord module Batches diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index e40b958b54..d93e7c8997 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' module ActiveRecord diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index a1c7e5b549..ab8b36c8ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord module Delegation # :nodoc: diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index c01aed2d8e..84aaa39fed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord @@ -311,7 +310,7 @@ module ActiveRecord @records.first else @first ||= - if order_values.empty? && primary_key + if with_default_scope.order_values.empty? && primary_key order(arel_table[primary_key].asc).limit(1).to_a.first else limit(1).to_a.first diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index b04dd7c6a7..71aaedee1e 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActiveRecord diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 94db2846f3..8e6254f918 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/blank' module ActiveRecord module QueryMethods @@ -214,7 +213,7 @@ module ActiveRecord references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? - self.order_values += args + self.order_values = args + self.order_values self end @@ -226,7 +225,7 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY id ASC, name ASC'. + # generates a query with 'ORDER BY name ASC, id ASC'. def reorder(*args) args.blank? ? self : spawn.reorder!(*args) end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index d21f02cd5f..5394c1b28b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' require 'active_record/relation/merger' @@ -24,6 +23,13 @@ module ActiveRecord # # 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!) # + # Procs will be evaluated by merge: + # + # Post.where(published: true).merge(-> { joins(:comments) }) + # # => Post.where(published: true).joins(:comments) + # + # This is mainly intended for sharing common conditions between multiple associations. + # def merge(other) if other.is_a?(Array) to_a & other @@ -36,8 +42,12 @@ module ActiveRecord # Like #merge, but applies changes in place. def merge!(other) - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + if !other.is_a?(Relation) && other.respond_to?(:to_proc) + instance_exec(&other) + else + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge + end end # Removes from the query the condition(s) specified in +skips+. diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index b502907c21..690409d62c 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Sanitization diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 599e68379a..a540bc0a3b 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Schema diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 66a486ae0a..0c3fd1bd29 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Scoping diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index b35fec7920..a2a85d4b96 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,5 +1,3 @@ -require 'active_support/concern' -require 'active_support/deprecation' module ActiveRecord module Scoping diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 4cd86cefe1..75f31229b5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,9 +1,6 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/class/attribute' -require 'active_support/deprecation' module ActiveRecord # = Active Record Named \Scopes diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 81576e7cd3..5151f349b7 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,6 +1,4 @@ -require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/class/attribute' module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index c7a6c37d50..c035ad43a2 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' require 'active_support/test_case' ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e5b7a6bfba..c32e0d6bf8 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index afce149da9..1fa6629980 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,6 +1,6 @@ module ActiveRecord module Validations - class AssociatedValidator < ActiveModel::EachValidator + class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any? record.errors.add(attribute, :invalid, options.merge(:value => value)) @@ -9,7 +9,8 @@ module ActiveRecord end module ClassMethods - # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. + # Validates whether the associated object or objects are all valid + # themselves. Works with any kind of association. # # class Book < ActiveRecord::Base # has_many :pages @@ -18,23 +19,28 @@ module ActiveRecord # validates_associated :pages, :library # end # - # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion. + # WARNING: This validation must not be used on both ends of an association. + # Doing so will lead to a circular dependency and cause infinite recursion. # - # NOTE: This validation will not fail if the association hasn't been assigned. If you want to - # ensure that the association is both present and guaranteed to be valid, you also need to - # use +validates_presence_of+. + # NOTE: This validation will not fail if the association hasn't been + # assigned. If you want to ensure that the association is both present and + # guaranteed to be valid, you also need to use +validates_presence_of+. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is invalid") + # + # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <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. + # * <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 validates_associated(*attr_names) validates_with AssociatedValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5a24135f8e..c117872ac8 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/array/prepend_and_append' module ActiveRecord module Validations - class UniquenessValidator < ActiveModel::EachValidator + class UniquenessValidator < ActiveModel::EachValidator #:nodoc: def initialize(options) super(options.reverse_merge(:case_sensitive => true)) end @@ -87,54 +87,67 @@ module ActiveRecord end module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. - # Useful for making sure that only one user + # Validates whether the value of the specified attributes are unique + # across the system. Useful for making sure that only one user # can be named "davidhh". # # class Person < ActiveRecord::Base # validates_uniqueness_of :user_name # end # - # It can also validate whether the value of the specified attributes are unique based on a scope parameter: + # It can also validate whether the value of the specified attributes are + # unique based on a <tt>:scope</tt> parameter: # # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id + # validates_uniqueness_of :user_name, scope: :account_id # end # - # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once - # per semester for a particular class. + # Or even multiple scope parameters. For example, making sure that a + # teacher can only be on the schedule once per semester for a particular + # class. # # class TeacherSchedule < ActiveRecord::Base - # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] + # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id] # end # - # It is also possible to limit the uniqueness constraint to a set of records matching certain conditions. - # In this example archived articles are not being taken into consideration when validating uniqueness + # It is also possible to limit the uniqueness constraint to a set of + # records matching certain conditions. In this example archived articles + # are not being taken into consideration when validating uniqueness # of the title attribute: # # class Article < ActiveRecord::Base - # validates_uniqueness_of :title, :conditions => where('status != ?', 'archived') + # validates_uniqueness_of :title, conditions: where('status != ?', 'archived') # end # - # When the record is created, a check is performed to make sure that no record exists in the database - # with the given value for the specified attribute (that maps to a column). When the record is updated, + # When the record is created, a check is performed to make sure that no + # record exists in the database with the given value for the specified + # attribute (that maps to a column). When the record is updated, # the same check is made but disregarding the record itself. # # Configuration options: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:conditions</tt> - Specify the conditions to be included as a <tt>WHERE</tt> SQL fragment to limit - # the uniqueness constraint lookup. (e.g. <tt>:conditions => where('status = ?', 'active')</tt>) - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <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. + # + # * <tt>:message</tt> - Specifies a custom error message (default is: + # "has already been taken"). + # * <tt>:scope</tt> - One or more columns by which to limit the scope of + # the uniqueness constraint. + # * <tt>:conditions</tt> - Specify the conditions to be included as a + # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup + # (e.g. <tt>conditions: where('status = ?', 'active')</tt>). + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by + # non-text columns (+true+ by default). + # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the + # attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the + # attribute is blank (default is +false+). + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, + # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a +true+ or +false+ value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>, + # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a +true+ or +false+ + # value. # # === Concurrency and integrity # @@ -190,15 +203,16 @@ module ActiveRecord # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an - # ActiveRecord::RecordNotUnique exception. - # For other adapters you will have to parse the (database-specific) exception - # message to detect such a case. + # ActiveRecord::RecordNotUnique exception. For other adapters you will + # have to parse the (database-specific) exception message to detect such + # a case. + # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: + # # * ActiveRecord::ConnectionAdapters::MysqlAdapter # * ActiveRecord::ConnectionAdapters::Mysql2Adapter # * ActiveRecord::ConnectionAdapters::SQLite3Adapter # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter - # def validates_uniqueness_of(*attr_names) validates_with UniquenessValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb index 90923f6e74..a976571dee 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb @@ -1,5 +1,4 @@ require 'rails/generators/active_record' -require 'active_support/core_ext/object/inclusion' module ActiveRecord module Generators 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 a9e18dd8fe..f3520d43e0 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 @@ -65,6 +65,19 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end +class DeveloperWithCounterSQL < ActiveRecord::Base + self.table_name = 'developers' + + ActiveSupport::Deprecation.silence do + has_and_belongs_to_many :projects, + :class_name => "DeveloperWithCounterSQL", + :join_table => "developers_projects", + :association_foreign_key => "project_id", + :foreign_key => "developer_id", + :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" } + end +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -346,11 +359,36 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_deleting_array david = Developer.find(1) david.projects.reload - david.projects.delete(Project.to_a) + david.projects.delete(Project.all.to_a) assert_equal 0, david.projects.size assert_equal 0, david.projects(true).size end + def test_deleting_with_sql + david = Developer.find(1) + active_record = Project.find(1) + active_record.developers.reload + assert_equal 3, active_record.developers_by_sql.size + + active_record.developers_by_sql.delete(david) + assert_equal 2, active_record.developers_by_sql(true).size + end + + def test_deleting_array_with_sql + active_record = Project.find(1) + active_record.developers.reload + assert_equal 3, active_record.developers_by_sql.size + + active_record.developers_by_sql.delete(Developer.all) + assert_equal 0, active_record.developers_by_sql(true).size + end + + def test_deleting_all_with_sql + project = Project.find(1) + project.developers_by_sql.delete_all + assert_equal 0, project.developers_by_sql.size + end + def test_deleting_all david = Developer.find(1) david.projects.reload @@ -388,7 +426,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_destroying_many david = Developer.find(1) david.projects.reload - projects = Project.to_a + projects = Project.all.to_a assert_no_difference "Project.count" do david.projects.destroy(*projects) @@ -499,6 +537,25 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert ! project.developers.include?(developer) end + def test_find_in_association_with_custom_finder_sql + assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find" + + active_record = projects(:active_record) + active_record.developers_with_finder_sql.reload + assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find" + end + + def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations + # interpolate once: + assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation" + # interpolate again, for a different project id + assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation" + end + + def test_find_in_association_with_custom_finder_sql_and_string_id + assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find" + end + def test_find_with_merged_options assert_equal 1, projects(:active_record).limited_developers.size assert_equal 1, projects(:active_record).limited_developers.to_a.size @@ -513,9 +570,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis') end - def test_find_should_append_to_association_order + def test_find_should_prepend_to_association_order ordered_developers = projects(:active_record).developers.order('projects.id') - assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values + assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values end def test_dynamic_find_all_should_respect_readonly_access @@ -734,6 +791,21 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, david.projects.count end + def test_count_with_counter_sql + developer = DeveloperWithCounterSQL.create(:name => 'tekin') + developer.project_ids = [projects(:active_record).id] + developer.save + developer.reload + assert_equal 1, developer.projects.count + end + + unless current_adapter?(:PostgreSQLAdapter) + def test_count_with_finder_sql + assert_equal 3, projects(:active_record).developers_with_finder_sql.count + assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count + end + end + def test_association_proxy_transaction_method_starts_transaction_in_association_class Post.expects(:transaction) Category.first.posts.transaction do @@ -772,4 +844,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.build assert project.developers.include?(developer) end + + test ":insert_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + def klass.name; 'Foo'; end + assert_deprecated { klass.has_and_belongs_to_many :posts, :insert_sql => 'lol' } + end + + test ":delete_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + def klass.name; 'Foo'; end + assert_deprecated { klass.has_and_belongs_to_many :posts, :delete_sql => 'lol' } + 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 6122104335..43440c1146 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -20,6 +20,49 @@ require 'models/car' require 'models/bulb' require 'models/engine' +class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + ActiveSupport::Deprecation.silence do + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" + end + end + def test_should_fail + assert_raise(ArgumentError) do + Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) + end + end +end + +class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + ActiveSupport::Deprecation.silence do + has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items" + end + end + def test_should_fail + assert_raise(ArgumentError) do + Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) + end + end +end + +class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestCase + class Invoice < ActiveRecord::Base + ActiveSupport::Deprecation.silence do + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" + end + end + + def test_should_count_distinct_results + invoice = Invoice.new + invoice.custom_line_items << LineItem.new(:amount => 0) + invoice.custom_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 1, invoice.custom_line_items.count + end +end + class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -231,9 +274,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size end - def test_find_should_append_to_association_order + def test_find_should_prepend_to_association_order ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id') - assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values + assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values end def test_dynamic_find_should_respect_association_order @@ -270,6 +313,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name end + def test_finding_using_sql + firm = Firm.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.order("id").first.clients_using_sql.size + end + + def test_finding_using_sql_take_into_account_only_uniq_ids + firm = Firm.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.order("id").first.clients_using_counter_sql.size + assert Firm.order("id").first.clients_using_counter_sql.any? + assert_equal 0, Firm.order("id").first.clients_using_zero_counter_sql.size + assert !Firm.order("id").first.clients_using_zero_counter_sql.any? + end + + def test_counting_non_existant_items_using_sql + assert_equal 0, Firm.order("id").first.no_clients_using_counter_sql.size + end + + def test_counting_using_finder_sql + assert_equal 2, Firm.find(4).clients_using_sql.count + end + def test_belongs_to_sanity c = Client.new assert_nil c.firm @@ -297,6 +371,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) } end + def test_find_string_ids_when_using_finder_sql + firm = Firm.order("id").first + + client = firm.clients_using_finder_sql.find("2") + assert_kind_of Client, client + + client_ary = firm.clients_using_finder_sql.find(["2"]) + assert_kind_of Array, client_ary + assert_equal client, client_ary.first + + client_ary = firm.clients_using_finder_sql.find("2", "3") + assert_kind_of Array, client_ary + assert_equal 2, client_ary.size + assert client_ary.include?(client) + end + def test_find_all firm = Firm.all.merge!(:order => "id").first assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length @@ -1040,7 +1130,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_adding_array_and_collection - assert_nothing_raised { Firm.first.clients + Firm.to_a.last.clients } + assert_nothing_raised { Firm.first.clients + Firm.all.last.clients } end def test_replace_with_less @@ -1124,6 +1214,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids end + def test_get_ids_for_unloaded_finder_sql_associations_loads_them + company = companies(:first_firm) + assert !company.clients_using_sql.loaded? + assert_equal [companies(:second_client).id], company.clients_using_sql_ids + assert company.clients_using_sql.loaded? + end + def test_get_ids_for_ordered_association assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids end @@ -1184,6 +1281,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert ! firm.clients.loaded? end + def test_include_loads_collection_if_target_uses_finder_sql + firm = companies(:first_firm) + client = firm.clients_using_sql.first + + firm.reload + assert ! firm.clients_using_sql.loaded? + assert firm.clients_using_sql.include?(client) + assert firm.clients_using_sql.loaded? + end + + def test_include_returns_false_for_non_matching_record_to_verify_scoping firm = companies(:first_firm) client = Client.create!(:name => 'Not Associated') @@ -1536,4 +1644,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase post.taggings_with_delete_all.delete_all end end + + test ":finder_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + assert_deprecated { klass.has_many :foo, :finder_sql => 'lol' } + end + + test ":counter_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' } + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 3ba2987f14..112735839f 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -226,7 +226,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_and_create_should_not_happen_within_scope pirate = pirates(:blackbeard) - scoped_count = pirate.association(:foo_bulb).scoped.where_values.count + scoped_count = pirate.association(:foo_bulb).scope.where_values.count bulb = pirate.build_foo_bulb assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 1c8090de8a..d4b7960047 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'models/tag' require 'models/tagging' require 'models/post' diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index b7c2bcad00..c0f1945cec 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -218,6 +218,13 @@ class AssociationProxyTest < ActiveRecord::TestCase def test_scoped_allows_conditions assert developers(:david).projects.merge!(where: 'foo').where_values.include?('foo') end + + test "getting a scope from an association" do + david = developers(:david) + + assert david.projects.scope.is_a?(ActiveRecord::Relation) + assert_equal david.projects, david.projects.scope + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index f4c40b8b97..55d45d9d93 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'thread' module ActiveRecord diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index c2619e51a9..807971d678 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'models/minimalistic' require 'models/developer' require 'models/auto_id' diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 195597ace6..062f196a12 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1635,9 +1635,9 @@ class BasicsTest < ActiveRecord::TestCase end def test_all - developers = Developer.to_a - assert_kind_of Array, developers - assert_equal Developer.to_a, developers + developers = Developer.all + assert_kind_of ActiveRecord::Relation, developers + assert_equal Developer.all, developers end def test_all_with_conditions @@ -2032,12 +2032,4 @@ class BasicsTest < ActiveRecord::TestCase klass = Class.new(ActiveRecord::Base) assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values end - - test "Model.to_a returns an array" do - assert_equal Post.all.to_a, Post.to_a - end - - test "Model.all returns a relation" do - assert Post.all.is_a?(ActiveRecord::Relation) - end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 7583539d04..cdd4b49042 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -116,7 +116,7 @@ class EachTest < ActiveRecord::TestCase end def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order - special_posts_ids = SpecialPostWithDefaultScope.to_a.map(&:id).sort + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort posts = [] SpecialPostWithDefaultScope.find_in_batches do |batch| posts.concat(batch) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 55f6ec06c0..40e712072f 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -542,7 +542,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_plucks_with_ids - assert_equal Company.to_a.map(&:id).sort, Company.ids.sort + assert_equal Company.all.map(&:id).sort, Company.ids.sort end def test_pluck_not_auto_table_name_prefix_if_column_included diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index b3a281d960..81c5850a47 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'models/default' require 'models/entrant' diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 3f26e10dda..248f4efe3e 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -201,7 +201,7 @@ class DirtyTest < ActiveRecord::TestCase end end - def test_nullable_integer_zero_to_string_zero_not_marked_as_changed + def test_integer_zero_to_string_zero_not_marked_as_changed pirate = Pirate.new pirate.parrot_id = 0 pirate.catchphrase = 'arrr' @@ -213,6 +213,19 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end + def test_integer_zero_to_integer_zero_not_marked_as_changed + pirate = Pirate.new + pirate.parrot_id = 0 + pirate.catchphrase = 'arrr' + assert pirate.save! + + assert !pirate.changed? + + pirate.parrot_id = 0 + assert !pirate.changed? + end + + def test_zero_to_blank_marked_as_changed pirate = Pirate.new pirate.catchphrase = "Yarrrr, me hearties" diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 52e747db35..6dce8ccdd1 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -112,7 +112,7 @@ if ActiveRecord::Base.connection.supports_explain? base.expects(:collecting_sqls_for_explain).never base.logger.expects(:warn).never base.silence_auto_explain do - with_threshold(0) { Car.to_a } + with_threshold(0) { Car.all } end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 342f36f626..20c8e8894d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -120,7 +120,7 @@ class FinderTest < ActiveRecord::TestCase # 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.to_a + devs = Developer.all last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id) assert_equal 2, last_devs.size end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 44d08a8ee4..018064233a 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -6,8 +6,8 @@ gem 'minitest' require 'minitest/autorun' require 'stringio' -require 'cases/test_case' require 'active_record' +require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb index 9b9c09d2d8..8f095e4953 100644 --- a/activerecord/test/cases/inclusion_test.rb +++ b/activerecord/test/cases/inclusion_test.rb @@ -84,8 +84,10 @@ class InclusionUnitTest < ActiveRecord::TestCase end def test_deprecation_proxy - assert_equal ActiveRecord::Model.name, ActiveRecord::Model::DeprecationProxy.name - assert_equal ActiveRecord::Base.superclass, assert_deprecated { ActiveRecord::Model::DeprecationProxy.superclass } + proxy = ActiveRecord::Model::DeprecationProxy.new + + assert_equal ActiveRecord::Model.name, proxy.name + assert_equal ActiveRecord::Base.superclass, assert_deprecated { proxy.superclass } sup, sup2 = nil, nil ActiveSupport.on_load(:__test_active_record_model_deprecation) do @@ -93,11 +95,29 @@ class InclusionUnitTest < ActiveRecord::TestCase sup2 = send(:superclass) end assert_deprecated do - ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, ActiveRecord::Model::DeprecationProxy) + ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, proxy) end assert_equal ActiveRecord::Base.superclass, sup assert_equal ActiveRecord::Base.superclass, sup2 end + + test "including in deprecation proxy" do + model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup + proxy = ActiveRecord::Model::DeprecationProxy.new(model, base) + + mod = Module.new + proxy.include mod + assert model < mod + end + + test "extending in deprecation proxy" do + model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup + proxy = ActiveRecord::Model::DeprecationProxy.new(model, base) + + mod = Module.new + assert_deprecated { proxy.extend mod } + assert base.singleton_class < mod + end end class InclusionFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index b14f580c77..e80259a7f1 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -27,7 +27,7 @@ class InheritanceTest < ActiveRecord::TestCase end }) company.save! - company = Company.to_a.find { |x| x.id == company.id } + company = Company.all.to_a.find { |x| x.id == company.id } assert_equal ' ', company.type end @@ -179,7 +179,7 @@ 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.to_a.first.name + assert_equal "I am a client", Client.first.name # Order by added as otherwise Oracle tests were failing because of different order of results assert_equal "37signals", Firm.all.merge!(:order => "id").to_a.first.name end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 049a57d8b9..70d00aecf9 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -54,7 +54,7 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_basic_query_logging - Developer.to_a + Developer.all.load wait assert_equal 1, @logger.logged(:debug).size assert_match(/Developer Load/, @logger.logged(:debug).last) @@ -71,8 +71,8 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_cached_queries ActiveRecord::Base.cache do - Developer.to_a - Developer.to_a + Developer.all.load + Developer.all.load end wait assert_equal 2, @logger.logged(:debug).size @@ -82,7 +82,7 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_basic_query_doesnt_log_when_level_is_not_debug @logger.level = INFO - Developer.to_a + Developer.all.load wait assert_equal 0, @logger.logged(:debug).size end @@ -90,8 +90,8 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_cached_queries_doesnt_log_when_level_is_not_debug @logger.level = INFO ActiveRecord::Base.cache do - Developer.to_a - Developer.to_a + Developer.all.load + Developer.all.load end wait assert_equal 0, @logger.logged(:debug).size diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb index df45445ef2..d1a85ee5e4 100644 --- a/activerecord/test/cases/migration/rename_column_test.rb +++ b/activerecord/test/cases/migration/rename_column_test.rb @@ -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.to_a.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.to_a.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/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 9f0e2d5fc8..bd121126e7 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.to_a.empty? + assert !Topic.all.empty? - assert_equal Topic.to_a, Topic.base - assert_equal Topic.to_a, Topic.base.to_a - assert_equal Topic.first, Topic.base.first - assert_equal Topic.to_a, Topic.base.map { |i| i } + assert_equal Topic.all.to_a, Topic.base + assert_equal Topic.all.to_a, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.all.to_a, Topic.base.map { |i| i } end def test_found_items_are_cached @@ -37,11 +37,11 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_delegates_finds_and_calculations_to_the_base_class - assert !Topic.to_a.empty? + assert !Topic.all.empty? - assert_equal Topic.to_a, Topic.base.to_a - assert_equal Topic.first, Topic.base.first - assert_equal Topic.count, Topic.base.count + assert_equal Topic.all.to_a, Topic.base.to_a + 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 @@ -97,7 +97,7 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes_returning_nil - all_topics = Topic.to_a + all_topics = Topic.all assert_equal all_topics, Topic.written_before(nil) end @@ -138,9 +138,9 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_active_records_have_scope_named__all__ - assert !Topic.to_a.empty? + assert !Topic.all.empty? - assert_equal Topic.to_a, Topic.base + assert_equal Topic.all.to_a, Topic.base end def test_active_records_have_scope_named__scoped__ @@ -444,6 +444,6 @@ class NamedScopeTest < ActiveRecord::TestCase assert_deprecated do klass.send(:default_scope, klass.where(:id => posts(:welcome).id)) end - assert_equal [posts(:welcome).title], klass.to_a.map(&:title) + assert_equal [posts(:welcome).title], klass.all.map(&:title) end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 9b9f11c7de..df076c97b4 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -28,7 +28,7 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_find_with_readonly_option - Developer.to_a.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? } diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c803b93746..588da68ec1 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -189,8 +189,8 @@ class ReflectionTest < ActiveRecord::TestCase def test_reflection_of_all_associations # FIXME these assertions bust a lot - assert_equal 34, Firm.reflect_on_all_associations.size - assert_equal 24, Firm.reflect_on_all_associations(:has_many).size + assert_equal 39, Firm.reflect_on_all_associations.size + assert_equal 29, Firm.reflect_on_all_associations(:has_many).size assert_equal 10, Firm.reflect_on_all_associations(:has_one).size assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index ba77bdcc8b..d318dab1e1 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -70,7 +70,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scoped_find_all Developer.where("name = 'David'").scoping do - assert_equal [developers(:david)], Developer.to_a + assert_equal [developers(:david)], Developer.all end end @@ -179,7 +179,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase def test_merge_inner_scope_has_priority Developer.limit(5).scoping do Developer.limit(10).scoping do - assert_equal 10, Developer.to_a.size + assert_equal 10, Developer.all.size end end end @@ -313,32 +313,32 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scope expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.to_a.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary } assert_equal expected, received end def test_default_scope_as_class_method - assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.to_a + assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all end def test_default_scope_as_class_method_referencing_scope - assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.to_a + assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all end def test_default_scope_as_block_referencing_scope - assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.to_a + assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all end def test_default_scope_with_lambda - assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.to_a + assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all end def test_default_scope_with_block - assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.to_a + assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all end def test_default_scope_with_callable - assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.to_a + assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all end def test_default_scope_is_unscoped_on_find @@ -351,12 +351,12 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.to_a.map(&:id).sort + assert_equal Developer.where(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.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.to_a.map(&:id).sort + assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end @@ -385,7 +385,7 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_scope_overwrites_default - expected = Developer.all.merge!(:order => 'salary DESC, name DESC').to_a.collect { |dev| dev.name } + expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name } received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name } assert_equal expected, received end @@ -397,13 +397,13 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_order_after_reorder_combines_orders - expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] } + expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] } assert_equal expected, received end - def test_order_in_default_scope_should_prevail - expected = Developer.all.merge!(:order => 'salary desc').to_a.collect { |dev| dev.salary } + def test_order_in_default_scope_should_not_prevail + expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } assert_equal expected, received end @@ -472,16 +472,16 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_select_ignored_by_aggregations - assert_equal DeveloperWithSelect.to_a.count, DeveloperWithSelect.count + assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end def test_default_scope_select_ignored_by_grouped_aggregations - assert_equal Hash[Developer.to_a.group_by(&:salary).map { |s, d| [s, d.count] }], + assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }], DeveloperWithSelect.group(:salary).count end def test_default_scope_order_ignored_by_aggregations - assert_equal DeveloperOrderedBySalary.to_a.count, DeveloperOrderedBySalary.count + assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count end def test_default_scope_find_last @@ -508,10 +508,10 @@ class DefaultScopingTest < ActiveRecord::TestCase threads << Thread.new do Thread.current[:long_default_scope] = true - assert_equal 1, ThreadsafeDeveloper.to_a.count + assert_equal 1, ThreadsafeDeveloper.all.to_a.count end threads << Thread.new do - assert_equal 1, ThreadsafeDeveloper.to_a.count + assert_equal 1, ThreadsafeDeveloper.all.to_a.count end threads.each(&:join) end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 034339e413..5fb54b1ca1 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -247,5 +247,9 @@ module ActiveRecord assert relation.merge!(where: :foo).equal?(relation) assert_equal [:foo], relation.where_values end + + test 'merge with a proc' do + assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 9c64cb35e4..0bd48913e1 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -165,7 +165,7 @@ class RelationTest < ActiveRecord::TestCase end def test_finding_with_order_concatenated - topics = Topic.order('author_name').order('title') + topics = Topic.order('title').order('author_name') assert_equal 4, topics.to_a.size assert_equal topics(:fourth).title, topics.first.title end @@ -1032,7 +1032,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.where(:author_id => 1).to_a, author_posts.to_a all_posts = relation.except(:where, :order, :limit) - assert_equal Post.to_a, all_posts.to_a + assert_equal Post.all, all_posts end def test_only @@ -1068,20 +1068,20 @@ class RelationTest < ActiveRecord::TestCase end def test_default_scope_order_with_scope_order - assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name + assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name + assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name end def test_order_using_scoping car1 = CoolCar.order('id DESC').scoping do CoolCar.all.merge!(:order => 'id asc').first end - assert_equal 'zyke', car1.name + assert_equal 'honda', car1.name car2 = FastCar.order('id DESC').scoping do FastCar.all.merge!(:order => 'id asc').first end - assert_equal 'zyke', car2.name + assert_equal 'honda', car2.name end def test_unscoped_block_style @@ -1342,4 +1342,12 @@ class RelationTest < ActiveRecord::TestCase node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first assert_equal table_alias, node.relation end + + test '#load' do + relation = Post.all + assert_queries(1) do + assert_equal relation, relation.load + end + assert_no_queries { relation.to_a } + end end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 94a13d386c..f3f7054794 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' ActiveSupport::Deprecation.silence do require 'active_record/test_case' end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index ff4f1412d2..7249ae9e4d 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -286,7 +286,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase # getting appended to. def test_modules - projects = MyApplication::Business::Project.to_a + projects = MyApplication::Business::Project.all xml = projects.to_xml root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize assert_match "<#{root} type=\"array\">", xml diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index ff6b7d085f..5bfbb5e855 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -36,9 +36,13 @@ module Namespaced end class Firm < Company - has_many :clients, -> { order "id" }, :dependent => :destroy, - :before_remove => :log_before_remove, - :after_remove => :log_after_remove + ActiveSupport::Deprecation.silence do + has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql => + "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + + "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )", + :before_remove => :log_before_remove, + :after_remove => :log_after_remove + end has_many :unsorted_clients, :class_name => "Client" has_many :unsorted_clients_with_symbol, :class_name => :Client has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client" @@ -51,6 +55,19 @@ class Firm < Company has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client" has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client" + ActiveSupport::Deprecation.silence do + has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" } + has_many :clients_using_counter_sql, :class_name => "Client", + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " }, + :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" } + has_many :clients_using_zero_counter_sql, :class_name => "Client", + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }, + :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" } + has_many :no_clients_using_counter_sql, :class_name => "Client", + :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', + :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' + has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1' + end has_many :plain_clients, :class_name => 'Client' has_many :readonly_clients, -> { readonly }, :class_name => 'Client' has_many :clients_using_primary_key, :class_name => 'Client', diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 0d14fa1be1..eb2aedc425 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -11,6 +11,9 @@ module MyApplication has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client" has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client" has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" + ActiveSupport::Deprecation.silence do + has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + end has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 6b53ead34d..af3ec4be83 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -7,6 +7,19 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer" has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer" has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer" + + ActiveSupport::Deprecation.silence do + has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" } + has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc { + "SELECT + t.*, j.* + FROM + developers_projects j, + developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" + } + has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" } + end + has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"}, :after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"}, :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index cf32e2d7a0..5fcea299f8 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,7 @@ ## Rails 4.0.0 (unreleased) ## +* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument. *David Celis* + * `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!` *DHH* * `Time#change` now works with time values with offsets other than UTC or the local time zone. *Andrew White* diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index e5f81078ee..0d5f3501e5 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -6,18 +6,21 @@ class Object end class NilClass + # Returns +self+. def to_param self end end class TrueClass + # Returns +self+. def to_param self end end class FalseClass + # Returns +self+. def to_param self end @@ -35,12 +38,12 @@ class Hash # Returns a string representation of the receiver suitable for use as a URL # query string: # - # {:name => 'David', :nationality => 'Danish'}.to_param + # {name: 'David', nationality: 'Danish'}.to_param # # => "name=David&nationality=Danish" # # An optional namespace can be passed to enclose the param names: # - # {:name => 'David', :nationality => 'Danish'}.to_param('user') + # {name: 'David', nationality: 'Danish'}.to_param('user') # # => "user[name]=David&user[nationality]=Danish" # # The string pairs "key=value" that conform the query string diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index efa2d43f20..6edfcd7493 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -13,6 +13,11 @@ class String # the singular form will be returned if <tt>count == 1</tt>. # For any other value of +count+ the plural will be returned. # + # If the optional parameter +locale+ is specified, + # the word will be pluralized as a word of that language. + # By default, this parameter is set to <tt>:en</tt>. + # You must define your own inflection rules for languages other than English. + # # 'post'.pluralize # => "posts" # 'octopus'.pluralize # => "octopi" # 'sheep'.pluralize # => "sheep" @@ -21,15 +26,23 @@ class String # 'CamelOctopus'.pluralize # => "CamelOctopi" # 'apple'.pluralize(1) # => "apple" # 'apple'.pluralize(2) # => "apples" - def pluralize(count = nil) + # 'ley'.pluralize(:es) # => "leyes" + # 'ley'.pluralize(1, :es) # => "ley" + def pluralize(count = nil, locale = :en) + locale = count if count.is_a?(Symbol) if count == 1 self else - ActiveSupport::Inflector.pluralize(self) + ActiveSupport::Inflector.pluralize(self, locale) end end # The reverse of +pluralize+, returns the singular form of a word in a string. + # + # If the optional parameter +locale+ is specified, + # the word will be singularized as a word of that language. + # By default, this paramter is set to <tt>:en</tt>. + # You must define your own inflection rules for languages other than English. # # 'posts'.singularize # => "post" # 'octopi'.singularize # => "octopus" @@ -37,8 +50,9 @@ class String # 'word'.singularize # => "word" # 'the blue mailmen'.singularize # => "the blue mailman" # 'CamelOctopi'.singularize # => "CamelOctopus" - def singularize - ActiveSupport::Inflector.singularize(self) + # 'leyes'.singularize(:es) # => "ley" + def singularize(locale = :en) + ActiveSupport::Inflector.singularize(self, locale) end # +constantize+ tries to find a declared constant with the name specified 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 5226ff0cbe..c17d695967 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -3,9 +3,9 @@ require 'active_support/core_ext/kernel/singleton_class' class ERB module Util - HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' } + HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' } - HTML_ESCAPE_ONCE_REGEXP = /[\"><]|&(?!([a-zA-Z]+|(#\d+));)/ + HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ JSON_ESCAPE_REGEXP = /[&"><]/ # A utility method for escaping HTML tag characters. @@ -21,7 +21,7 @@ class ERB if s.html_safe? s else - s.encode(s.encoding, :xml => :attr)[1...-1].html_safe + s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe end end diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index ca2d8cb270..ef882ebd09 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -1,7 +1,7 @@ require 'active_support/inflector/inflections' module ActiveSupport - Inflector.inflections do |inflect| + Inflector.inflections(:en) do |inflect| inflect.plural(/$/, 's') inflect.plural(/s$/i, 's') inflect.plural(/^(ax|test)is$/i, '\1es') diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index c9e50a9462..091692e5a4 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,13 +1,15 @@ require 'active_support/core_ext/array/prepend_and_append' +require 'active_support/i18n' module ActiveSupport module Inflector extend self # A singleton instance of this class is yielded by Inflector.inflections, which can then be used to specify additional - # inflection rules. + # inflection rules. If passed an optional locale, rules for other languages can be specified. The default locale is + # <tt>:en</tt>. Only rules for English are provided. # - # ActiveSupport::Inflector.inflections do |inflect| + # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1\2en' # inflect.singular /^(ox)en/i, '\1' # @@ -20,8 +22,9 @@ module ActiveSupport # pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may # already have been loaded. class Inflections - def self.instance - @__instance__ ||= new + def self.instance(locale = :en) + @__instance__ ||= Hash.new { |h, k| h[k] = new } + @__instance__[locale] end attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex @@ -160,16 +163,18 @@ module ActiveSupport end # Yields a singleton instance of Inflector::Inflections so you can specify additional - # inflector rules. + # inflector rules. If passed an optional locale, rules for other languages can be specified. + # If not specified, defaults to <tt>:en</tt>. Only rules for English are provided. + # # - # ActiveSupport::Inflector.inflections do |inflect| + # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.uncountable "rails" # end - def inflections + def inflections(locale = :en) if block_given? - yield Inflections.instance + yield Inflections.instance(locale) else - Inflections.instance + Inflections.instance(locale) end end end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index c14a43de0d..44214d16fa 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -10,31 +10,41 @@ module ActiveSupport # # The Rails core team has stated patches for the inflections library will not be accepted # in order to avoid breaking legacy applications which may be relying on errant inflections. - # If you discover an incorrect inflection and require it for your application, you'll need - # to correct it yourself (explained below). + # If you discover an incorrect inflection and require it for your application or wish to + # define rules for languages other than English, please correct or add them yourself (explained below). module Inflector extend self # Returns the plural form of the word in the string. # + # If passed an optional +locale+ parameter, the word will be + # pluralized using rules defined for that language. By default, + # this parameter is set to <tt>:en</tt>. + # # "post".pluralize # => "posts" # "octopus".pluralize # => "octopi" # "sheep".pluralize # => "sheep" # "words".pluralize # => "words" # "CamelOctopus".pluralize # => "CamelOctopi" - def pluralize(word) - apply_inflections(word, inflections.plurals) + # "ley".pluralize(:es) # => "leyes" + def pluralize(word, locale = :en) + apply_inflections(word, inflections(locale).plurals) end # The reverse of +pluralize+, returns the singular form of a word in a string. # + # If passed an optional +locale+ parameter, the word will be + # pluralized using rules defined for that language. By default, + # this parameter is set to <tt>:en</tt>. + # # "posts".singularize # => "post" # "octopi".singularize # => "octopus" # "sheep".singularize # => "sheep" # "word".singularize # => "word" # "CamelOctopi".singularize # => "CamelOctopus" - def singularize(word) - apply_inflections(word, inflections.singulars) + # "leyes".singularize(:es) # => "ley" + def singularize(word, locale = :en) + apply_inflections(word, inflections(locale).singulars) end # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+ diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb new file mode 100644 index 0000000000..933c6e6136 --- /dev/null +++ b/activesupport/lib/active_support/rails.rb @@ -0,0 +1,30 @@ +# This is private interface. +# +# Rails components cherry pick from Active Support as needed, but there are a +# few features that are used for sure some way or another and it is not worth +# to put individual requires absolutely everywhere. Think blank? for example. +# +# This file is loaded by every Rails component except Active Support itself, +# but it does not belong to the Rails public interface. It is internal to +# Rails and can change anytime. + +# Defines Object#blank? and Object#present?. +require 'active_support/core_ext/object/blank' + +# Defines Object#in?. +require 'active_support/core_ext/object/inclusion' + +# Rails own autoload, eager_load, etc. +require 'active_support/dependencies/autoload' + +# Support for ClassMethods and the included macro. +require 'active_support/concern' + +# Defines Class#class_attribute. +require 'active_support/core_ext/class/attribute' + +# Defines Module#delegate. +require 'active_support/core_ext/module/delegation' + +# Defines ActiveSupport::Deprecation. +require 'active_support/deprecation' diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index e5b774425e..3b08ebe35f 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -498,8 +498,8 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "ERB::Util.html_escape should escape unsafe characters" do - string = '<>&"' - expected = '<>&"' + string = '<>&"\'' + expected = '<>&"'' assert_equal expected, ERB::Util.html_escape(string) end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 1f32e4ff92..cd91002147 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -354,6 +354,35 @@ class InflectorTest < ActiveSupport::TestCase RUBY end + def test_inflector_locality + ActiveSupport::Inflector.inflections(:es) do |inflect| + inflect.plural(/$/, 's') + inflect.plural(/z$/i, 'ces') + + inflect.singular(/s$/, '') + inflect.singular(/es$/, '') + + inflect.irregular('el', 'los') + end + + assert_equal('hijos', 'hijo'.pluralize(:es)) + assert_equal('luces', 'luz'.pluralize(:es)) + assert_equal('luzs', 'luz'.pluralize) + + assert_equal('sociedad', 'sociedades'.singularize(:es)) + assert_equal('sociedade', 'sociedades'.singularize) + + assert_equal('los', 'el'.pluralize(:es)) + assert_equal('els', 'el'.pluralize) + + ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear } + + assert ActiveSupport::Inflector.inflections(:es).plurals.empty? + assert ActiveSupport::Inflector.inflections(:es).singulars.empty? + assert !ActiveSupport::Inflector.inflections.plurals.empty? + assert !ActiveSupport::Inflector.inflections.singulars.empty? + end + def test_clear_all with_dup do ActiveSupport::Inflector.inflections do |inflect| @@ -467,7 +496,7 @@ class InflectorTest < ActiveSupport::TestCase # there are module functions that access ActiveSupport::Inflector.inflections, # so we need to replace the singleton itself. def with_dup - original = ActiveSupport::Inflector.inflections + original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__) ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original.dup) ensure ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original) diff --git a/guides/source/action_view_overview.textile b/guides/source/action_view_overview.textile index fdfa97effa..1fd98a5bbe 100644 --- a/guides/source/action_view_overview.textile +++ b/guides/source/action_view_overview.textile @@ -785,7 +785,7 @@ h5. content_for Calling +content_for+ stores a block of markup in an identifier for later use. You can make subsequent calls to the stored content in other templates or the layout by passing the identifier as an argument to +yield+. -For example, let's say we have a standard application layout, but also a special page that requires certain Javascript that the rest of the site doesn't need. We can use +content_for+ to include this Javascript on our special page without fattening up the rest of the site. +For example, let's say we have a standard application layout, but also a special page that requires certain JavaScript that the rest of the site doesn't need. We can use +content_for+ to include this JavaScript on our special page without fattening up the rest of the site. *app/views/layouts/application.html.erb* diff --git a/guides/source/active_record_querying.textile b/guides/source/active_record_querying.textile index b13932e8cb..dff829a4c1 100644 --- a/guides/source/active_record_querying.textile +++ b/guides/source/active_record_querying.textile @@ -492,7 +492,7 @@ This code will generate SQL like this: SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) </sql> -h3. Ordering +h3(#ordering). Ordering To retrieve records from the database in a specific order, you can use the +order+ method. @@ -518,6 +518,13 @@ Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") </ruby> +If you want to call +order+ multiple times e.g. in different context, new order will prepend previous one + +<ruby> +Client.order("orders_count ASC").order("created_at DESC") +# SELECT * FROM clients ORDER BY created_at DESC, orders_count ASC +</ruby> + h3. Selecting Specific Fields By default, <tt>Model.find</tt> selects all the fields from the result set using +select *+. diff --git a/guides/source/ajax_on_rails.textile b/guides/source/ajax_on_rails.textile index 26e0270a31..67b0c9f0d3 100644 --- a/guides/source/ajax_on_rails.textile +++ b/guides/source/ajax_on_rails.textile @@ -28,7 +28,7 @@ the +ul+ node. h4. Asynchronous JavaScript + XML AJAX means Asynchronous JavaScript + XML. Asynchronous means that the page is not -reloaded, the request made is separate from the regular page request. Javascript +reloaded, the request made is separate from the regular page request. JavaScript is used to evaluate the response and the XML part is a bit misleading as XML is not required, you respond to the HTTP request with JSON or regular HTML as well. diff --git a/guides/source/asset_pipeline.textile b/guides/source/asset_pipeline.textile index d0952fcf29..42b5d102e7 100644 --- a/guides/source/asset_pipeline.textile +++ b/guides/source/asset_pipeline.textile @@ -121,7 +121,7 @@ h5. Search paths When a file is referenced from a manifest or a helper, Sprockets searches the three default asset locations for it. -The default locations are: +app/assets/images+ and the subdirectories +javascripts+ and +stylesheets+ in all three asset locations. +The default locations are: +app/assets/images+ and the subdirectories +javascripts+ and +stylesheets+ in all three asset locations, but these subdirectories are not special. Any path under +assets/*+ will be searched. For example, these files: @@ -129,6 +129,7 @@ For example, these files: app/assets/javascripts/home.js lib/assets/javascripts/moovinator.js vendor/assets/javascripts/slider.js +vendor/assets/somepackage/phonebox.js </plain> would be referenced in a manifest like this: @@ -137,6 +138,7 @@ would be referenced in a manifest like this: //= require home //= require moovinator //= require slider +//= require phonebox </plain> Assets inside subdirectories can also be accessed. @@ -153,13 +155,13 @@ is referenced as: You can view the search path by inspecting +Rails.application.config.assets.paths+ in the Rails console. -Additional (fully qualified) paths can be added to the pipeline in +config/application.rb+. For example: +Besides the standard +assets/*+ paths, additional (fully qualified) paths can be added to the pipeline in +config/application.rb+. For example: <ruby> -config.assets.paths << Rails.root.join("app", "assets", "flash") +config.assets.paths << Rails.root.join("lib", "videoplayer", "flash") </ruby> -Paths are traversed in the order that they occur in the search path. +Paths are traversed in the order that they occur in the search path. By default, this means the files in +app/assets+ take precedence, and will mask corresponding paths in +lib+ and +vendor+. It is important to note that files you want to reference outside a manifest must be added to the precompile array or they will not be available in the production environment. @@ -651,7 +653,7 @@ WARNING: If you are upgrading an existing application and intend to use this opt h3. Assets Cache Store -Sprockets uses the default Rails cache store will be used to cache assets in development and production. This can be changed by setting +config.assets.cache_store+. +The default Rails cache store will be used by Sprockets to cache assets in development and production. This can be changed by setting +config.assets.cache_store+. <ruby> config.assets.cache_store = :memory_store diff --git a/guides/source/association_basics.textile b/guides/source/association_basics.textile index 4dca7a508c..2b8b1bd937 100644 --- a/guides/source/association_basics.textile +++ b/guides/source/association_basics.textile @@ -630,12 +630,12 @@ The <tt>create_<em>association</em></tt> method returns a new object of the asso h5. Options for +belongs_to+ -While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the +belongs_to+ association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options: +While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the +belongs_to+ association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this assocation uses two such options: <ruby> class Order < ActiveRecord::Base - belongs_to :customer, :counter_cache => true, - :conditions => "active = 1" + belongs_to :customer, :dependent => :destroy, + :counter_cache => true end </ruby> @@ -643,15 +643,11 @@ The +belongs_to+ association supports these options: * +:autosave+ * +:class_name+ -* +:conditions+ * +:counter_cache+ * +:dependent+ * +:foreign_key+ -* +:include+ * +:inverse_of+ * +:polymorphic+ -* +:readonly+ -* +:select+ * +:touch+ * +:validate+ @@ -669,16 +665,6 @@ class Order < ActiveRecord::Base end </ruby> -h6(#belongs_to-conditions). +:conditions+ - -The +:conditions+ option lets you specify the conditions that the associated object must meet (in the syntax used by an SQL +WHERE+ clause). - -<ruby> -class Order < ActiveRecord::Base - belongs_to :customer, :conditions => "active = 1" -end -</ruby> - h6(#belongs_to-counter_cache). +:counter_cache+ The +:counter_cache+ option can be used to make finding the number of belonging objects more efficient. Consider these models: @@ -737,35 +723,31 @@ end TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. -h6(#belongs_to-includes). +:include+ +h6(#belongs_to-inverse_of). +:inverse_of+ -You can use the +:include+ option to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: +The +:inverse_of+ option specifies the name of the +has_many+ or +has_one+ association that is the inverse of this association. Does not work in combination with the +:polymorphic+ options. <ruby> -class LineItem < ActiveRecord::Base - belongs_to :order +class Customer < ActiveRecord::Base + has_many :orders, :inverse_of => :customer end class Order < ActiveRecord::Base - belongs_to :customer - has_many :line_items -end - -class Customer < ActiveRecord::Base - has_many :orders + belongs_to :customer, :inverse_of => :orders end </ruby> -If you frequently retrieve customers directly from line items (+@line_item.order.customer+), then you can make your code somewhat more efficient by including customers in the association from line items to orders: +h6(#belongs_to-polymorphic). +:polymorphic+ -<ruby> -class LineItem < ActiveRecord::Base - belongs_to :order, :include => :customer -end +Passing +true+ to the +:polymorphic+ option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>. +h6(#belongs_to-touch). +:touch+ + +If you set the +:touch+ option to +:true+, then the +updated_at+ or +updated_on+ timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: + +<ruby> class Order < ActiveRecord::Base - belongs_to :customer - has_many :line_items + belongs_to :customer, :touch => true end class Customer < ActiveRecord::Base @@ -773,43 +755,58 @@ class Customer < ActiveRecord::Base end </ruby> -NOTE: There's no need to use +:include+ for immediate associations - that is, if you have +Order belongs_to :customer+, then the customer is eager-loaded automatically when it's needed. - -h6(#belongs_to-inverse_of). +:inverse_of+ - -The +:inverse_of+ option specifies the name of the +has_many+ or +has_one+ association that is the inverse of this association. Does not work in combination with the +:polymorphic+ options. +In this case, saving or destroying an order will update the timestamp on the associated customer. You can also specify a particular timestamp attribute to update: <ruby> -class Customer < ActiveRecord::Base - has_many :orders, :inverse_of => :customer -end - class Order < ActiveRecord::Base - belongs_to :customer, :inverse_of => :orders + belongs_to :customer, :touch => :orders_updated_at end </ruby> -h6(#belongs_to-polymorphic). +:polymorphic+ +h6(#belongs_to-validate). +:validate+ -Passing +true+ to the +:polymorphic+ option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>. +If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved. -h6(#belongs_to-readonly). +:readonly+ +h5(#belongs_to-scopes_for_belongs_to). Scopes for +belongs_to+ -If you set the +:readonly+ option to +true+, then the associated object will be read-only when retrieved via the association. +There may be times when you wish to customize the query used by +belongs_to+. Such customizations can be achieved via a scope block. For example: -h6(#belongs_to-select). +:select+ +<ruby> +class Order < ActiveRecord::Base + belongs_to :customer, -> { where :active => true }, + :dependent => :destroy +end +</ruby> -The +:select+ option lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated object. By default, Rails retrieves all columns. +You can use any of the standard "querying methods":active_record_querying.html inside the scope block. The following ones are discussed below: -TIP: If you set the +:select+ option on a +belongs_to+ association, you should also set the +foreign_key+ option to guarantee the correct results. +* +where+ +* +includes+ +* +readonly+ +* +select+ -h6(#belongs_to-touch). +:touch+ +h6(#belongs_to-where). +where+ -If you set the +:touch+ option to +:true+, then the +updated_at+ or +updated_on+ timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: +The +where+ method lets you specify the conditions that the associated object must meet. <ruby> class Order < ActiveRecord::Base - belongs_to :customer, :touch => true + belongs_to :customer, -> { where :active => true } +end +</ruby> + +h6(#belongs_to-includes). +includes+ + +You can use the +includes+ method let you specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: + +<ruby> +class LineItem < ActiveRecord::Base + belongs_to :order +end + +class Order < ActiveRecord::Base + belongs_to :customer + has_many :line_items end class Customer < ActiveRecord::Base @@ -817,17 +814,34 @@ class Customer < ActiveRecord::Base end </ruby> -In this case, saving or destroying an order will update the timestamp on the associated customer. You can also specify a particular timestamp attribute to update: +If you frequently retrieve customers directly from line items (+@line_item.order.customer+), then you can make your code somewhat more efficient by including customers in the association from line items to orders: <ruby> +class LineItem < ActiveRecord::Base + belongs_to :order, -> { includes :customer } +end + class Order < ActiveRecord::Base - belongs_to :customer, :touch => :orders_updated_at + belongs_to :customer + has_many :line_items +end + +class Customer < ActiveRecord::Base + has_many :orders end </ruby> -h6(#belongs_to-validate). +:validate+ +NOTE: There's no need to use +includes+ for immediate associations - that is, if you have +Order belongs_to :customer+, then the customer is eager-loaded automatically when it's needed. -If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved. +h6(#belongs_to-readonly). +readonly+ + +If you use +readonly+, then the associated object will be read-only when retrieved via the association. + +h6(#belongs_to-select). +select+ + +The +select+ method lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated object. By default, Rails retrieves all columns. + +TIP: If you use the +select+ method on a +belongs_to+ association, you should also set the +:foreign_key+ option to guarantee the correct results. h5(#belongs_to-do_any_associated_objects_exist). Do Any Associated Objects Exist? @@ -924,15 +938,10 @@ The +has_one+ association supports these options: * +:as+ * +:autosave+ * +:class_name+ -* +:conditions+ * +:dependent+ * +:foreign_key+ -* +:include+ * +:inverse_of+ -* +:order+ * +:primary_key+ -* +:readonly+ -* +:select+ * +:source+ * +:source_type+ * +:through+ @@ -956,16 +965,6 @@ class Supplier < ActiveRecord::Base end </ruby> -h6(#has_one-conditions). +:conditions+ - -The +:conditions+ option lets you specify the conditions that the associated object must meet (in the syntax used by an SQL +WHERE+ clause). - -<ruby> -class Supplier < ActiveRecord::Base - has_one :account, :conditions => "confirmed = 1" -end -</ruby> - h6(#has_one-dependent). +:dependent+ If you set the +:dependent+ option to +:destroy+, then deleting this object will call the +destroy+ method on the associated object to delete that object. If you set the +:dependent+ option to +:delete+, then deleting this object will delete the associated object _without_ calling its +destroy+ method. If you set the +:dependent+ option to +:nullify+, then deleting this object will set the foreign key in the association object to +NULL+. @@ -985,30 +984,74 @@ end TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. -h6(#has_one-include). +:include+ +h6(#has_one-inverse_of). +:inverse_of+ -You can use the +:include+ option to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: +The +:inverse_of+ option specifies the name of the +belongs_to+ association that is the inverse of this association. Does not work in combination with the +:through+ or +:as+ options. <ruby> class Supplier < ActiveRecord::Base - has_one :account + has_one :account, :inverse_of => :supplier end class Account < ActiveRecord::Base - belongs_to :supplier - belongs_to :representative + belongs_to :supplier, :inverse_of => :account end +</ruby> -class Representative < ActiveRecord::Base - has_many :accounts +h6(#has_one-primary_key). +:primary_key+ + +By convention, Rails assumes that the column used to hold the primary key of this model is +id+. You can override this and explicitly specify the primary key with the +:primary_key+ option. + +h6(#has_one-source). +:source+ + +The +:source+ option specifies the source association name for a +has_one :through+ association. + +h6(#has_one-source_type). +:source_type+ + +The +:source_type+ option specifies the source association type for a +has_one :through+ association that proceeds through a polymorphic association. + +h6(#has_one-through). +:through+ + +The +:through+ option specifies a join model through which to perform the query. +has_one :through+ associations were discussed in detail <a href="#the-has_one-through-association">earlier in this guide</a>. + +h6(#has_one-validate). +:validate+ + +If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved. + +h5(#belongs_to-scopes_for_has_one). Scopes for +has_one+ + +There may be times when you wish to customize the query used by +has_one+. Such customizations can be achieved via a scope block. For example: + +<ruby> +class Supplier < ActiveRecord::Base + has_one :account, -> { where :active => true } end </ruby> -If you frequently retrieve representatives directly from suppliers (+@supplier.account.representative+), then you can make your code somewhat more efficient by including representatives in the association from suppliers to accounts: +You can use any of the standard "querying methods":active_record_querying.html inside the scope block. The following ones are discussed below: + +* +where+ +* +includes+ +* +readonly+ +* +select+ + +h6(#has_one-where). +where+ + +The +where+ method lets you specify the conditions that the associated object must meet. + +<ruby> +class Supplier < ActiveRecord::Base + has_one :account, -> { where "confirmed = 1" } +end +</ruby> + +h6(#has_one-includes). +includes+ + +You can use the +includes+ method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: <ruby> class Supplier < ActiveRecord::Base - has_one :account, :include => :representative + has_one :account end class Account < ActiveRecord::Base @@ -1021,51 +1064,30 @@ class Representative < ActiveRecord::Base end </ruby> -h6(#has_one-inverse_of). +:inverse_of+ - -The +:inverse_of+ option specifies the name of the +belongs_to+ association that is the inverse of this association. Does not work in combination with the +:through+ or +:as+ options. +If you frequently retrieve representatives directly from suppliers (+@supplier.account.representative+), then you can make your code somewhat more efficient by including representatives in the association from suppliers to accounts: <ruby> class Supplier < ActiveRecord::Base - has_one :account, :inverse_of => :supplier + has_one :account, -> { includes :representative } end class Account < ActiveRecord::Base - belongs_to :supplier, :inverse_of => :account + belongs_to :supplier + belongs_to :representative end -</ruby> - -h6(#has_one-order). +:order+ - -The +:order+ option dictates the order in which associated objects will be received (in the syntax used by an SQL +ORDER BY+ clause). Because a +has_one+ association will only retrieve a single associated object, this option should not be needed. - -h6(#has_one-primary_key). +:primary_key+ - -By convention, Rails assumes that the column used to hold the primary key of this model is +id+. You can override this and explicitly specify the primary key with the +:primary_key+ option. - -h6(#has_one-readonly). +:readonly+ - -If you set the +:readonly+ option to +true+, then the associated object will be read-only when retrieved via the association. - -h6(#has_one-select). +:select+ - -The +:select+ option lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated object. By default, Rails retrieves all columns. - -h6(#has_one-source). +:source+ -The +:source+ option specifies the source association name for a +has_one :through+ association. - -h6(#has_one-source_type). +:source_type+ - -The +:source_type+ option specifies the source association type for a +has_one :through+ association that proceeds through a polymorphic association. +class Representative < ActiveRecord::Base + has_many :accounts +end +</ruby> -h6(#has_one-through). +:through+ +h6(#has_one-readonly). +readonly+ -The +:through+ option specifies a join model through which to perform the query. +has_one :through+ associations were discussed in detail <a href="#the-has_one-through-association">earlier in this guide</a>. +If you use the +readonly+ method, then the associated object will be read-only when retrieved via the association. -h6(#has_one-validate). +:validate+ +h6(#has_one-select). +select+ -If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved. +The +select+ method lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated object. By default, Rails retrieves all columns. h5(#has_one-do_any_associated_objects_exist). Do Any Associated Objects Exist? @@ -1256,23 +1278,13 @@ The +has_many+ association supports these options: * +:as+ * +:autosave+ * +:class_name+ -* +:conditions+ * +:dependent+ -* +:extend+ * +:foreign_key+ -* +:group+ -* +:include+ * +:inverse_of+ -* +:limit+ -* +:offset+ -* +:order+ * +:primary_key+ -* +:readonly+ -* +:select+ * +:source+ * +:source_type+ * +:through+ -* +:uniq+ * +:validate+ h6(#has_many-as). +:as+ @@ -1293,75 +1305,124 @@ class Customer < ActiveRecord::Base end </ruby> -h6(#has_many-conditions). +:conditions+ +h6(#has_many-dependent). +:dependent+ + +If you set the +:dependent+ option to +:destroy+, then deleting this object will call the +destroy+ method on the associated objects to delete those objects. If you set the +:dependent+ option to +:delete_all+, then deleting this object will delete the associated objects _without_ calling their +destroy+ method. If you set the +:dependent+ option to +:nullify+, then deleting this object will set the foreign key in the associated objects to +NULL+. +If you set the +:dependent+ option to +:restrict+, then the deletion of the object is restricted if a dependent associated object exist and a +DeleteRestrictionError+ exception is raised. + +NOTE: The default behavior for +:dependent => :restrict+ is to raise a +DeleteRestrictionError+ when associated objects exist. Since Rails 4.0 this behavior is being deprecated in favor of adding an error to the base model. To silence the warning in Rails 4.0, you should fix your code to not expect this Exception and add +config.active_record.dependent_restrict_raises = false+ to your application config. + +NOTE: This option is ignored when you use the +:through+ option on the association. -The +:conditions+ option lets you specify the conditions that the associated object must meet (in the syntax used by an SQL +WHERE+ clause). +h6(#has_many-foreign_key). +:foreign_key+ + +By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: <ruby> class Customer < ActiveRecord::Base - has_many :confirmed_orders, :class_name => "Order", - :conditions => "confirmed = 1" + has_many :orders, :foreign_key => "cust_id" end </ruby> -You can also set conditions via a hash: +TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. + +h6(#has_many-inverse_of). +:inverse_of+ + +The +:inverse_of+ option specifies the name of the +belongs_to+ association that is the inverse of this association. Does not work in combination with the +:through+ or +:as+ options. <ruby> class Customer < ActiveRecord::Base - has_many :confirmed_orders, :class_name => "Order", - :conditions => { :confirmed => true } + has_many :orders, :inverse_of => :customer +end + +class Order < ActiveRecord::Base + belongs_to :customer, :inverse_of => :orders end </ruby> -If you use a hash-style +:conditions+ option, then record creation via this association will be automatically scoped using the hash. In this case, using +@customer.confirmed_orders.create+ or +@customer.confirmed_orders.build+ will create orders where the confirmed column has the value +true+. +h6(#has_many-primary_key). +:primary_key+ + +By convention, Rails assumes that the column used to hold the primary key of the association is +id+. You can override this and explicitly specify the primary key with the +:primary_key+ option. + +h6(#has_many-source). +:source+ + +The +:source+ option specifies the source association name for a +has_many :through+ association. You only need to use this option if the name of the source association cannot be automatically inferred from the association name. + +h6(#has_many-source_type). +:source_type+ + +The +:source_type+ option specifies the source association type for a +has_many :through+ association that proceeds through a polymorphic association. + +h6(#has_many-through). +:through+ + +The +:through+ option specifies a join model through which to perform the query. +has_many :through+ associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has_many-through-association">earlier in this guide</a>. + +h6(#has_many-validate). +:validate+ + +If you set the +:validate+ option to +false+, then associated objects will not be validated whenever you save this object. By default, this is +true+: associated objects will be validated when this object is saved. -If you need to evaluate conditions dynamically at runtime, use a proc: +h5(#has_many-scopes_for_has_many). Scopes for +has_many+ + +There may be times when you wish to customize the query used by +has_many+. Such customizations can be achieved via a scope block. For example: <ruby> class Customer < ActiveRecord::Base - has_many :latest_orders, :class_name => "Order", - :conditions => proc { ["orders.created_at > ?", 10.hours.ago] } + has_many :orders, -> { where :processed => true } end </ruby> -h6(#has_many-dependent). +:dependent+ - -If you set the +:dependent+ option to +:destroy+, then deleting this object will call the +destroy+ method on the associated objects to delete those objects. If you set the +:dependent+ option to +:delete_all+, then deleting this object will delete the associated objects _without_ calling their +destroy+ method. If you set the +:dependent+ option to +:nullify+, then deleting this object will set the foreign key in the associated objects to +NULL+. -If you set the +:dependent+ option to +:restrict+, then the deletion of the object is restricted if a dependent associated object exist and a +DeleteRestrictionError+ exception is raised. - -NOTE: The default behavior for +:dependent => :restrict+ is to raise a +DeleteRestrictionError+ when associated objects exist. Since Rails 4.0 this behavior is being deprecated in favor of adding an error to the base model. To silence the warning in Rails 4.0, you should fix your code to not expect this Exception and add +config.active_record.dependent_restrict_raises = false+ to your application config. +You can use any of the standard "querying methods":active_record_querying.html inside the scope block. The following ones are discussed below: -NOTE: This option is ignored when you use the +:through+ option on the association. +* +where+ +* +extending+ +* +group+ +* +includes+ +* +limit+ +* +offset+ +* +order+ +* +readonly+ +* +select+ +* +uniq+ -h6(#has_many-extend). +:extend+ +h6(#has_many-where). +where+ -The +:extend+ option specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. +The +where+ method lets you specify the conditions that the associated object must meet. -h6(#has_many-foreign_key). +:foreign_key+ +<ruby> +class Customer < ActiveRecord::Base + has_many :confirmed_orders, -> { where "confirmed = 1" }, + :class_name => "Order" +end +</ruby> -By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: +You can also set conditions via a hash: <ruby> class Customer < ActiveRecord::Base - has_many :orders, :foreign_key => "cust_id" + has_many :confirmed_orders, -> { where :confirmed => true }, + :class_name => "Order" end </ruby> -TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. +If you use a hash-style +where+ option, then record creation via this association will be automatically scoped using the hash. In this case, using +@customer.confirmed_orders.create+ or +@customer.confirmed_orders.build+ will create orders where the confirmed column has the value +true+. + +h6(#has_many-extending). +extending+ -h6(#has_many-group). +:group+ +The +extending+ method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. -The +:group+ option supplies an attribute name to group the result set by, using a +GROUP BY+ clause in the finder SQL. +h6(#has_many-group). +group+ + +The +group+ method supplies an attribute name to group the result set by, using a +GROUP BY+ clause in the finder SQL. <ruby> class Customer < ActiveRecord::Base - has_many :line_items, :through => :orders, :group => "orders.id" + has_many :line_items, -> { group 'orders.id' }, + :through => :orders end </ruby> -h6(#has_many-include). +:include+ +h6(#has_many-includes). +includes+ -You can use the +:include+ option to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: +You can use the +includes+ method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: <ruby> class Customer < ActiveRecord::Base @@ -1382,7 +1443,7 @@ If you frequently retrieve line items directly from customers (+@customer.orders <ruby> class Customer < ActiveRecord::Base - has_many :orders, :include => :line_items + has_many :orders, -> { includes :line_items } end class Order < ActiveRecord::Base @@ -1395,74 +1456,45 @@ class LineItem < ActiveRecord::Base end </ruby> -h6(#has_many-inverse_of). +:inverse_of+ - -The +:inverse_of+ option specifies the name of the +belongs_to+ association that is the inverse of this association. Does not work in combination with the +:through+ or +:as+ options. - -<ruby> -class Customer < ActiveRecord::Base - has_many :orders, :inverse_of => :customer -end - -class Order < ActiveRecord::Base - belongs_to :customer, :inverse_of => :orders -end -</ruby> - -h6(#has_many-limit). +:limit+ +h6(#has_many-limit). +limit+ -The +:limit+ option lets you restrict the total number of objects that will be fetched through an association. +The +limit+ method lets you restrict the total number of objects that will be fetched through an association. <ruby> class Customer < ActiveRecord::Base - has_many :recent_orders, :class_name => "Order", - :order => "order_date DESC", :limit => 100 + has_many :recent_orders, + -> { order('order_date desc').limit(100) }, + :class_name => "Order", end </ruby> -h6(#has_many-offset). +:offset+ +h6(#has_many-offset). +offset+ -The +:offset+ option lets you specify the starting offset for fetching objects via an association. For example, if you set +:offset => 11+, it will skip the first 11 records. +The +offset+ method lets you specify the starting offset for fetching objects via an association. For example, +-> { offset(11) }+ will skip the first 11 records. -h6(#has_many-order). +:order+ +h6(#has_many-order). +order+ -The +:order+ option dictates the order in which associated objects will be received (in the syntax used by an SQL +ORDER BY+ clause). +The +order+ method dictates the order in which associated objects will be received (in the syntax used by an SQL +ORDER BY+ clause). <ruby> class Customer < ActiveRecord::Base - has_many :orders, :order => "date_confirmed DESC" + has_many :orders, -> { order "date_confirmed DESC" } end </ruby> -h6(#has_many-primary_key). +:primary_key+ - -By convention, Rails assumes that the column used to hold the primary key of the association is +id+. You can override this and explicitly specify the primary key with the +:primary_key+ option. - -h6(#has_many-readonly). +:readonly+ - -If you set the +:readonly+ option to +true+, then the associated objects will be read-only when retrieved via the association. - -h6(#has_many-select). +:select+ - -The +:select+ option lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns. - -WARNING: If you specify your own +:select+, be sure to include the primary key and foreign key columns of the associated model. If you do not, Rails will throw an error. - -h6(#has_many-source). +:source+ - -The +:source+ option specifies the source association name for a +has_many :through+ association. You only need to use this option if the name of the source association cannot be automatically inferred from the association name. +h6(#has_many-readonly). +readonly+ -h6(#has_many-source_type). +:source_type+ +If you use the +readonly+ method, then the associated objects will be read-only when retrieved via the association. -The +:source_type+ option specifies the source association type for a +has_many :through+ association that proceeds through a polymorphic association. +h6(#has_many-select). +select+ -h6(#has_many-through). +:through+ +The +select+ method lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns. -The +:through+ option specifies a join model through which to perform the query. +has_many :through+ associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has_many-through-association">earlier in this guide</a>. +WARNING: If you specify your own +select+, be sure to include the primary key and foreign key columns of the associated model. If you do not, Rails will throw an error. -h6(#has_many-uniq). +:uniq+ +h6(#has_many-uniq). +uniq+ -Set the +:uniq+ option to true to keep the collection free of duplicates. This is mostly useful together with the +:through+ option. +Use the +uniq+ method to keep the collection free of duplicates. This is mostly useful together with the +:through+ option. <ruby> class Person < ActiveRecord::Base @@ -1480,12 +1512,12 @@ Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Readin In the above case there are two readings and +person.posts+ brings out both of them even though these records are pointing to the same post. -Now let's set +:uniq+ to true: +Now let's set +uniq+: <ruby> class Person has_many :readings - has_many :posts, :through => :readings, :uniq => true + has_many :posts, -> { uniq }, :through => :readings end person = Person.create(:name => 'honda') @@ -1498,10 +1530,6 @@ Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Readin In the above case there are still two readings. However +person.posts+ shows only one post because the collection loads only unique records. -h6(#has_many-validate). +:validate+ - -If you set the +:validate+ option to +false+, then associated objects will not be validated whenever you save this object. By default, this is +true+: associated objects will be validated when this object is saved. - h5(#has_many-when_are_objects_saved). When are Objects Saved? When you assign an object to a +has_many+ association, that object is automatically saved (in order to update its foreign key). If you assign multiple objects in one statement, then they are all saved. @@ -1687,18 +1715,8 @@ The +has_and_belongs_to_many+ association supports these options: * +:association_foreign_key+ * +:autosave+ * +:class_name+ -* +:conditions+ -* +:extend+ * +:foreign_key+ -* +:group+ -* +:include+ * +:join_table+ -* +:limit+ -* +:offset+ -* +:order+ -* +:readonly+ -* +:select+ -* +:uniq+ * +:validate+ h6(#has_and_belongs_to_many-association_foreign_key). +:association_foreign_key+ @@ -1729,102 +1747,126 @@ class Parts < ActiveRecord::Base end </ruby> -h6(#has_and_belongs_to_many-conditions). +:conditions+ +h6(#has_and_belongs_to_many-foreign_key). +:foreign_key+ -The +:conditions+ option lets you specify the conditions that the associated object must meet (in the syntax used by an SQL +WHERE+ clause). +By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: <ruby> -class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, - :conditions => "factory = 'Seattle'" +class User < ActiveRecord::Base + has_and_belongs_to_many :friends, :class_name => "User", + :foreign_key => "this_user_id", + :association_foreign_key => "other_user_id" end </ruby> -You can also set conditions via a hash: +h6(#has_and_belongs_to_many-join_table). +:join_table+ + +If the default name of the join table, based on lexical ordering, is not what you want, you can use the +:join_table+ option to override the default. + +h6(#has_and_belongs_to_many-validate). +:validate+ + +If you set the +:validate+ option to +false+, then associated objects will not be validated whenever you save this object. By default, this is +true+: associated objects will be validated when this object is saved. + +h5(#has_and_belongs_to_many-scopes_for_has_and_belongs_to_many). Scopes for +has_and_belongs_to_many+ + +There may be times when you wish to customize the query used by +has_and_belongs_to_many+. Such customizations can be achieved via a scope block. For example: <ruby> class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, - :conditions => { :factory => 'Seattle' } + has_and_belongs_to_many :assemblies, -> { where :active => true } end </ruby> -If you use a hash-style +:conditions+ option, then record creation via this association will be automatically scoped using the hash. In this case, using +@parts.assemblies.create+ or +@parts.assemblies.build+ will create orders where the +factory+ column has the value "Seattle". +You can use any of the standard "querying methods":active_record_querying.html inside the scope block. The following ones are discussed below: -h6(#has_and_belongs_to_many-extend). +:extend+ +* +where+ +* +extending+ +* +group+ +* +includes+ +* +limit+ +* +offset+ +* +order+ +* +readonly+ +* +select+ +* +uniq+ -The +:extend+ option specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. +h6(#has_and_belongs_to_many-where). +where+ -h6(#has_and_belongs_to_many-foreign_key). +:foreign_key+ - -By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: +The +where+ method lets you specify the conditions that the associated object must meet. <ruby> -class User < ActiveRecord::Base - has_and_belongs_to_many :friends, :class_name => "User", - :foreign_key => "this_user_id", - :association_foreign_key => "other_user_id" +class Parts < ActiveRecord::Base + has_and_belongs_to_many :assemblies, + -> { where "factory = 'Seattle'" } end </ruby> -h6(#has_and_belongs_to_many-group). +:group+ - -The +:group+ option supplies an attribute name to group the result set by, using a +GROUP BY+ clause in the finder SQL. +You can also set conditions via a hash: <ruby> class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, :group => "factory" + has_and_belongs_to_many :assemblies, + -> { where :factory => 'Seattle' } end </ruby> -h6(#has_and_belongs_to_many-include). +:include+ - -You can use the +:include+ option to specify second-order associations that should be eager-loaded when this association is used. +If you use a hash-style +where+, then record creation via this association will be automatically scoped using the hash. In this case, using +@parts.assemblies.create+ or +@parts.assemblies.build+ will create orders where the +factory+ column has the value "Seattle". -h6(#has_and_belongs_to_many-join_table). +:join_table+ +h6(#has_and_belongs_to_many-extending). +extending+ -If the default name of the join table, based on lexical ordering, is not what you want, you can use the +:join_table+ option to override the default. +The +extending+ method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. -h6(#has_and_belongs_to_many-limit). +:limit+ +h6(#has_and_belongs_to_many-group). +group+ -The +:limit+ option lets you restrict the total number of objects that will be fetched through an association. +The +group+ method supplies an attribute name to group the result set by, using a +GROUP BY+ clause in the finder SQL. <ruby> class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, :order => "created_at DESC", - :limit => 50 + has_and_belongs_to_many :assemblies, -> { group "factory" } end </ruby> -h6(#has_and_belongs_to_many-offset). +:offset+ +h6(#has_and_belongs_to_many-includes). +includes+ -The +:offset+ option lets you specify the starting offset for fetching objects via an association. For example, if you set +:offset => 11+, it will skip the first 11 records. +You can use the +includes+ method to specify second-order associations that should be eager-loaded when this association is used. -h6(#has_and_belongs_to_many-order). +:order+ +h6(#has_and_belongs_to_many-limit). +limit+ -The +:order+ option dictates the order in which associated objects will be received (in the syntax used by an SQL +ORDER BY+ clause). +The +limit+ method lets you restrict the total number of objects that will be fetched through an association. <ruby> class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, :order => "assembly_name ASC" + has_and_belongs_to_many :assemblies, + -> { order("created_at DESC").limit(50) } end </ruby> -h6(#has_and_belongs_to_many-readonly). +:readonly+ +h6(#has_and_belongs_to_many-offset). +offset+ -If you set the +:readonly+ option to +true+, then the associated objects will be read-only when retrieved via the association. +The +offset+ method lets you specify the starting offset for fetching objects via an association. For example, if you set +offset(11)+, it will skip the first 11 records. -h6(#has_and_belongs_to_many-select). +:select+ +h6(#has_and_belongs_to_many-order). +order+ -The +:select+ option lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns. +The +order+ method dictates the order in which associated objects will be received (in the syntax used by an SQL +ORDER BY+ clause). -h6(#has_and_belongs_to_many-uniq). +:uniq+ +<ruby> +class Parts < ActiveRecord::Base + has_and_belongs_to_many :assemblies, + -> { order "assembly_name ASC" } +end +</ruby> -Specify the +:uniq => true+ option to remove duplicates from the collection. +h6(#has_and_belongs_to_many-readonly). +readonly+ -h6(#has_and_belongs_to_many-validate). +:validate+ +If you use the +readonly+ method, then the associated objects will be read-only when retrieved via the association. -If you set the +:validate+ option to +false+, then associated objects will not be validated whenever you save this object. By default, this is +true+: associated objects will be validated when this object is saved. +h6(#has_and_belongs_to_many-select). +select+ + +The +select+ method lets you override the SQL +SELECT+ clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns. + +h6(#has_and_belongs_to_many-uniq). +uniq+ + +Use the +uniq+ method to remove duplicates from the collection. h5(#has_and_belongs_to_many-when_are_objects_saved). When are Objects Saved? @@ -1904,20 +1946,11 @@ module FindRecentExtension end class Customer < ActiveRecord::Base - has_many :orders, :extend => FindRecentExtension + has_many :orders, -> { extending FindRecentExtension } end class Supplier < ActiveRecord::Base - has_many :deliveries, :extend => FindRecentExtension -end -</ruby> - -To include more than one extension module in a single association, specify an array of modules: - -<ruby> -class Customer < ActiveRecord::Base - has_many :orders, - :extend => [FindRecentExtension, FindActiveExtension] + has_many :deliveries, -> { extending FindRecentExtension } end </ruby> diff --git a/guides/source/command_line.textile b/guides/source/command_line.textile index 19e42cea93..39b75c2781 100644 --- a/guides/source/command_line.textile +++ b/guides/source/command_line.textile @@ -160,7 +160,7 @@ $ rails generate controller Greetings hello create app/assets/stylesheets/greetings.css.scss </shell> -What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a javascript file and a stylesheet file. +What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a JavaScript file and a stylesheet file. Check out the controller and modify it a little (in +app/controllers/greetings_controller.rb+): @@ -477,6 +477,53 @@ h4. Miscellaneous * +rake secret+ will give you a pseudo-random key to use for your session secret. * <tt>rake time:zones:all</tt> lists all the timezones Rails knows about. +h4. Writing Rake Tasks + +If you have (or want to write) any automation scripts outside your app (data import, checks, etc), you can make them as rake tasks. It's easy. + +INFO: "Complete guide about how to write tasks":http://rake.rubyforge.org/files/doc/rakefile_rdoc.html is available in the official documentation. + +Tasks should be placed in <tt>Rails.root/lib/tasks</tt> and should have a +.rake+ extension. + +Each task should be defined in next format (dependencies are optional): + +<ruby> +desc "I am short, but comprehensive description for my cool task" +task :task_name => [:prerequisite_task, :another_task_we_depend_on] do + # All your magick here + # Any valid Ruby code is allowed +end +</ruby> + +If you need to pass parameters, you can use next format (both arguments and dependencies are optional): + +<ruby> +task :task_name, [:arg_1] => [:pre_1, :pre_2] do |t, args| + # You can use args from here +end +</ruby> + +You can group tasks by placing them in namespaces: + +<ruby> +namespace :do + desc "This task does nothing" + task :nothing do + # Seriously, nothing + end +end +</ruby> + +You can see your tasks to be listed by <tt>rake -T</tt> command. And, according to the examples above, you can invoke them as follows: + +<shell> +rake task_name +rake "task_name[value 1]" # entire argument string should be quoted +rake do:nothing +</shell> + +NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the +environment+ task, which will load your application code. + h3. The Rails Advanced Command Line More advanced use of the command line is focused around finding useful (even surprising at times) options in the utilities, and fitting those to your needs and specific work flow. Listed here are some tricks up Rails' sleeve. diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index cd9aab4892..cc5c352df4 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -157,7 +157,7 @@ Rails 3.1, by default, is set up to use the +sprockets+ gem to manage assets wit * +config.assets.digest+ enables the use of MD5 fingerprints in asset names. Set to +true+ by default in +production.rb+. -* +config.assets.debug+ disables the concatenation and compression of assets. Set to +false+ by default in +development.rb+. +* +config.assets.debug+ disables the concatenation and compression of assets. Set to +true+ by default in +development.rb+. * +config.assets.manifest+ defines the full path to be used for the asset precompiler's manifest file. Defaults to using +config.assets.prefix+. @@ -186,7 +186,7 @@ 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 run. Defaults to +true+. +* +javascripts+ turns on the hook for JavaScript files 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+. diff --git a/guides/source/contributing_to_ruby_on_rails.textile b/guides/source/contributing_to_ruby_on_rails.textile index a8a097d156..dd43ef795f 100644 --- a/guides/source/contributing_to_ruby_on_rails.textile +++ b/guides/source/contributing_to_ruby_on_rails.textile @@ -215,7 +215,7 @@ NOTE: Using the rake task to create the test databases ensures they have the cor NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". -If you’re using another database, check the files under +activerecord/test/connections+ for default connection information. You can edit these files to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails. +If you’re using another database, check the file +activerecord/test/config.yml+ or +activerecord/test/config.example.yml+ for default connection information. You can edit +activerecord/test/config.yml+ to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails. You can now run the tests as you did for +sqlite3+. The tasks are respectively diff --git a/guides/source/engines.textile b/guides/source/engines.textile index 53c2845731..11c837be32 100644 --- a/guides/source/engines.textile +++ b/guides/source/engines.textile @@ -657,6 +657,84 @@ h3. Improving engine functionality This section looks at overriding or adding functionality to the views, controllers and models provided by an engine. +h4. Overriding Models + +Engine Models can be extended by (1) implementing decorators, or (2) including modules. + +h5. Decorators + +Decorators extends Engine's model classes in the main application by open classing Engine models at run time execution. + +<ruby> +# MyApp/app/decorators/models/blorgh/post_decorator.rb + +Blorgh::Post.class_eval do + def time_since_created + Time.current - created_at + end +end +</ruby> + +h5. Modules + +The other strategy is to create modules within the Engine holding all the models' code and include these in the respective Engine's model classes. Thus the Engine's model classes contain a mere include line referencing the respective module. + +Engine models can be overriden in the main application by creating a file with the Engine's same namespace and including the module originally referenced in the Engine's model class. ["**ActiveSupport::Concern**":http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html] helps manage the correct ordering of module dependencies at run time (it's worth it to reach the linked documentation). + +<ruby> +# MyApp/app/models/blorgh/post.rb +# overrides Blorgh's original Post model + +class Blorgh::Post < ActiveRecord::Base + include Blorgh::Concerns::Models::Post + + def time_since_created + Time.current - created_at + end +end + + +# Blorgh/app/models/post.rb +# this class is overriden by the MyApp Post model + +class Post < ActiveRecord::Base + include Blorgh::Concerns::Models::Post +end + + +# Blorgh/app/concerns/models/post + +module Blorg::Concerns::Models::Post + extend ActiveSupport::Concern + + # 'included do' causes the code within to be evaluated in the conext + # where it is included, rather be executed in the module's context. + included do + attr_accessor :author_name + belongs_to :author, :class_name => "User" + + before_save :set_author + + private + + def set_author + self.author = User.find_or_create_by_name(author_name) + end + end + + # methods defined here will be instance methods by default + def some_method + 'some method string' + end + + module ClassMethods + def some_class_method + 'some class method string' + end + end +end +</ruby> + h4. Overriding views When Rails looks for a view to render, it will first look in the +app/views+ directory of the application. If it cannot find the view there, then it will check in the +app/views+ directories of all engines which have this directory. diff --git a/guides/source/getting_started.textile b/guides/source/getting_started.textile index 8d7c0d4bea..22da369a2a 100644 --- a/guides/source/getting_started.textile +++ b/guides/source/getting_started.textile @@ -373,7 +373,7 @@ Edit the +form_for+ line inside +app/views/posts/new.html.erb+ to look like this In this example, a +Hash+ object is passed to the +:url+ option. What Rails will do with this is that it will point the form to the +create+ action of the current controller, the +PostsController+, and will send a +POST+ request to that route. For this to work, you will need to add a route to +config/routes.rb+, right underneath the one for "posts/new": <ruby> -post "posts/create" +post "posts" => "posts#create" </ruby> By using the +post+ method rather than the +get+ method, Rails will define a route that will only respond to POST methods. The POST method is the typical method used by forms all over the web. @@ -1064,13 +1064,13 @@ received an error before. <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 + posts GET /posts(.:format) posts#index +posts_new GET /posts/new(.:format) posts#new + POST /posts(.: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> To fix this, open +config/routes.rb+ and modify the +get "posts/:id"+ @@ -1175,7 +1175,7 @@ declaring separate routes with the appropriate verbs into <ruby> get "posts" => "posts#index" get "posts/new" -post "posts/create" +post "posts" => "posts#create" get "posts/:id" => "posts#show", :as => :post get "posts/:id/edit" => "posts#edit" put "posts/:id" => "posts#update" diff --git a/guides/source/i18n.textile b/guides/source/i18n.textile index 8ad6ee4b73..c782539399 100644 --- a/guides/source/i18n.textile +++ b/guides/source/i18n.textile @@ -405,6 +405,10 @@ So that would give you: TIP: Right now you might need to add some more date/time formats in order to make the I18n backend work as expected (at least for the 'pirate' locale). Of course, there's a great chance that somebody already did all the work by *translating Rails' defaults for your locale*. See the "rails-i18n repository at Github":https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale for an archive of various locale files. When you put such file(s) in +config/locales/+ directory, they will automatically be ready for use. +h4. Inflection Rules For Other Locales + +Rails 4.0 allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In +config/initializers/inflections.rb+, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit. + h4. Localized Views Rails 2.3 introduces another convenient localization feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in +app/views/books/index.html.erb+ template. When you put a _localized variant_ of this template: *+index.es.html.erb+* in the same directory, Rails will render content in this template, when the locale is set to +:es+. When the locale is set to the default locale, the generic +index.html.erb+ view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in +public+, etc.) diff --git a/guides/source/performance_testing.textile b/guides/source/performance_testing.textile index 982fd1b070..b7d4f1ef33 100644 --- a/guides/source/performance_testing.textile +++ b/guides/source/performance_testing.textile @@ -1,40 +1,55 @@ h2. Performance Testing Rails Applications -This guide covers the various ways of performance testing a Ruby on Rails application. By referring to this guide, you will be able to: - -* Understand the various types of benchmarking and profiling metrics -* Generate performance and benchmarking tests -* Install and use a GC-patched Ruby binary to measure memory usage and object allocation -* Understand the benchmarking information provided by Rails inside the log files -* Learn about various tools facilitating benchmarking and profiling - -Performance testing is an integral part of the development cycle. It is very important that you don't make your end users wait for too long before the page is completely loaded. Ensuring a pleasant browsing experience for end users and cutting the cost of unnecessary hardware is important for any non-trivial web application. +This guide covers the various ways of performance testing a Ruby on Rails +application. By referring to this guide, you will be able to: + +* Understand the various types of benchmarking and profiling metrics. +* Generate performance and benchmarking tests. +* Install and use a GC-patched Ruby binary to measure memory usage and object + allocation. +* Understand the benchmarking information provided by Rails inside the log files. +* Learn about various tools facilitating benchmarking and profiling. + +Performance testing is an integral part of the development cycle. It is very +important that you don't make your end users wait for too long before the page +is completely loaded. Ensuring a pleasant browsing experience for end users and +cutting the cost of unnecessary hardware is important for any non-trivial web +application. endprologue. h3. Performance Test Cases -Rails performance tests are a special type of integration tests, designed for benchmarking and profiling the test code. With performance tests, you can determine where your application's memory or speed problems are coming from, and get a more in-depth picture of those problems. +Rails performance tests are a special type of integration tests, designed for +benchmarking and profiling the test code. With performance tests, you can +determine where your application's memory or speed problems are coming from, +and get a more in-depth picture of those problems. -In a freshly generated Rails application, +test/performance/browsing_test.rb+ contains an example of a performance test: +In a freshly generated Rails application, +test/performance/browsing_test.rb+ +contains an example of a performance test: <ruby> require 'test_helper' require 'rails/performance_test_help' -# Profiling results for each test method are written to tmp/performance. class BrowsingTest < ActionDispatch::PerformanceTest - def test_homepage + # Refer to the documentation for all available options + # self.profile_options = { runs: 5, metrics: [:wall_time, :memory], + # output: 'tmp/performance', formats: [:flat] } + + test "homepage" do get '/' end end </ruby> -This example is a simple performance test case for profiling a GET request to the application's homepage. +This example is a simple performance test case for profiling a GET request to +the application's homepage. h4. Generating Performance Tests -Rails provides a generator called +performance_test+ for creating new performance tests: +Rails provides a generator called +performance_test+ for creating new +performance tests: <shell> $ rails generate performance_test homepage @@ -47,8 +62,11 @@ require 'test_helper' require 'rails/performance_test_help' class HomepageTest < ActionDispatch::PerformanceTest - # Replace this with your real tests. - def test_homepage + # Refer to the documentation for all available options + # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory], + # :output => 'tmp/performance', :formats => [:flat] } + + test "homepage" do get '/' end end @@ -60,7 +78,7 @@ Let's assume your application has the following controller and model: <ruby> # routes.rb -root :to => 'home#index' +root to: 'home#dashboard' resources :posts # home_controller.rb @@ -97,9 +115,11 @@ end h5. Controller Example -Because performance tests are a special kind of integration test, you can use the +get+ and +post+ methods in them. +Because performance tests are a special kind of integration test, you can use +the +get+ and +post+ methods in them. -Here's the performance test for +HomeController#dashboard+ and +PostsController#create+: +Here's the performance test for +HomeController#dashboard+ and ++PostsController#create+: <ruby> require 'test_helper' @@ -111,21 +131,24 @@ class PostPerformanceTest < ActionDispatch::PerformanceTest login_as(:lifo) end - def test_homepage + test "homepage" do get '/dashboard' end - def test_creating_new_post - post '/posts', :post => { :body => 'lifo is fooling you' } + test "creating new post" do + post '/posts', post: { body: 'lifo is fooling you' } end end </ruby> -You can find more details about the +get+ and +post+ methods in the "Testing Rails Applications":testing.html guide. +You can find more details about the +get+ and +post+ methods in the +"Testing Rails Applications":testing.html guide. h5. Model Example -Even though the performance tests are integration tests and hence closer to the request/response cycle by nature, you can still performance test pure model code. +Even though the performance tests are integration tests and hence closer to +the request/response cycle by nature, you can still performance test pure model +code. Performance test for +Post+ model: @@ -134,11 +157,11 @@ require 'test_helper' require 'rails/performance_test_help' class PostModelTest < ActionDispatch::PerformanceTest - def test_creation - Post.create :body => 'still fooling you', :cost => '100' + test "creation" do + Post.create body: 'still fooling you', cost: '100' end - def test_slow_method + test "slow method" do # Using posts(:awesome) fixture posts(:awesome).slow_method end @@ -151,7 +174,8 @@ Performance tests can be run in two modes: Benchmarking and Profiling. h5. Benchmarking -Benchmarking makes it easy to quickly gather a few metrics about each test run. By default, each test case is run *4 times* in benchmarking mode. +Benchmarking makes it easy to quickly gather a few metrics about each test run. +By default, each test case is run *4 times* in benchmarking mode. To run performance tests in benchmarking mode: @@ -161,7 +185,10 @@ $ rake test:benchmark h5. Profiling -Profiling allows you to make an in-depth analysis of each of your tests by using an external profiler. Depending on your Ruby interpreter, this profiler can be native (Rubinius, JRuby) or not (MRI, which uses RubyProf). By default, each test case is run *once* in profiling mode. +Profiling allows you to make an in-depth analysis of each of your tests by using +an external profiler. Depending on your Ruby interpreter, this profiler can be +native (Rubinius, JRuby) or not (MRI, which uses RubyProf). By default, each +test case is run *once* in profiling mode. To run performance tests in profiling mode: @@ -171,23 +198,33 @@ $ rake test:profile h4. Metrics -Benchmarking and profiling run performance tests and give you multiple metrics. The availability of each metric is determined by the interpreter being used—none of them support all metrics—and by the mode in use. A brief description of each metric and their availability across interpreters/modes is given below. +Benchmarking and profiling run performance tests and give you multiple metrics. +The availability of each metric is determined by the interpreter being used—none +of them support all metrics—and by the mode in use. A brief description of each +metric and their availability across interpreters/modes is given below. h5. Wall Time -Wall time measures the real world time elapsed during the test run. It is affected by any other processes concurrently running on the system. +Wall time measures the real world time elapsed during the test run. It is +affected by any other processes concurrently running on the system. h5. Process Time -Process time measures the time taken by the process. It is unaffected by any other processes running concurrently on the same system. Hence, process time is likely to be constant for any given performance test, irrespective of the machine load. +Process time measures the time taken by the process. It is unaffected by any +other processes running concurrently on the same system. Hence, process time +is likely to be constant for any given performance test, irrespective of the +machine load. h5. CPU Time -Similar to process time, but leverages the more accurate CPU clock counter available on the Pentium and PowerPC platforms. +Similar to process time, but leverages the more accurate CPU clock counter +available on the Pentium and PowerPC platforms. h5. User Time -User time measures the amount of time the CPU spent in user-mode, i.e. within the process. This is not affected by other processes and by the time it possibly spends blocked. +User time measures the amount of time the CPU spent in user-mode, i.e. within +the process. This is not affected by other processes and by the time it possibly +spends blocked. h5. Memory @@ -223,11 +260,13 @@ h6(#profiling_1). Profiling |_.Rubinius | yes | no | no | no | no | no | no | no | |_.JRuby | yes | no | no | no | no | no | no | no | -NOTE: To profile under JRuby you'll need to run +export JRUBY_OPTS="-Xlaunch.inproc=false --profile.api"+ *before* the performance tests. +NOTE: To profile under JRuby you'll need to run +export JRUBY_OPTS="-Xlaunch.inproc=false --profile.api"+ +*before* the performance tests. h4. Understanding the Output -Performance tests generate different outputs inside +tmp/performance+ directory depending on their mode and metric. +Performance tests generate different outputs inside +tmp/performance+ directory +depending on their mode and metric. h5(#output-benchmarking). Benchmarking @@ -248,7 +287,9 @@ BrowsingTest#test_homepage (31 ms warmup) h6. CSV Files -Performance test results are also appended to +.csv+ files inside +tmp/performance+. For example, running the default +BrowsingTest#test_homepage+ will generate following five files: +Performance test results are also appended to +.csv+ files inside +tmp/performance+. +For example, running the default +BrowsingTest#test_homepage+ will generate +following five files: * BrowsingTest#test_homepage_gc_runs.csv * BrowsingTest#test_homepage_gc_time.csv @@ -256,7 +297,9 @@ Performance test results are also appended to +.csv+ files inside +tmp/performan * BrowsingTest#test_homepage_objects.csv * BrowsingTest#test_homepage_wall_time.csv -As the results are appended to these files each time the performance tests are run in benchmarking mode, you can collect data over a period of time. This can be very helpful in analyzing the effects of code changes. +As the results are appended to these files each time the performance tests are +run in benchmarking mode, you can collect data over a period of time. This can +be very helpful in analyzing the effects of code changes. Sample output of +BrowsingTest#test_homepage_wall_time.csv+: @@ -276,7 +319,10 @@ measurement,created_at,app,rails,ruby,platform h5(#output-profiling). Profiling -In profiling mode, performance tests can generate multiple types of outputs. The command line output is always presented but support for the others is dependent on the interpreter in use. A brief description of each type and their availability across interpreters is given below. +In profiling mode, performance tests can generate multiple types of outputs. +The command line output is always presented but support for the others is +dependent on the interpreter in use. A brief description of each type and +their availability across interpreters is given below. h6. Command Line @@ -291,15 +337,18 @@ BrowsingTest#test_homepage (58 ms warmup) h6. Flat -Flat output shows the metric—time, memory, etc—measure in each method. "Check Ruby-Prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/flat_txt.html. +Flat output shows the metric—time, memory, etc—measure in each method. +"Check Ruby-Prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/flat_txt.html. h6. Graph -Graph output shows the metric measure in each method, which methods call it and which methods it calls. "Check Ruby-Prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/graph_txt.html. +Graph output shows the metric measure in each method, which methods call it and +which methods it calls. "Check Ruby-Prof documentation for a better explanation":http://ruby-prof.rubyforge.org/files/examples/graph_txt.html. h6. Tree -Tree output is profiling information in calltree format for use by "kcachegrind":http://kcachegrind.sourceforge.net/html/Home.html and similar tools. +Tree output is profiling information in calltree format for use by "kcachegrind":http://kcachegrind.sourceforge.net/html/Home.html +and similar tools. h6. Output Availability @@ -311,24 +360,24 @@ h6. Output Availability h4. Tuning Test Runs -Test runs can be tuned by setting the +profile_options+ class variable on your test class. +Test runs can be tuned by setting the +profile_options+ class variable on your +test class. <ruby> require 'test_helper' require 'rails/performance_test_help' -# Profiling results for each test method are written to tmp/performance. class BrowsingTest < ActionDispatch::PerformanceTest - self.profile_options = { :runs => 5, - :metrics => [:wall_time, :memory] } + self.profile_options = { runs: 5, metrics: [:wall_time, :memory] } - def test_homepage + test "homepage" get '/' end end </ruby> -In this example, the test would run 5 times and measure wall time and memory. There are a few configurable options: +In this example, the test would run 5 times and measure wall time and memory. +There are a few configurable options: |_.Option |_.Description|_.Default|_.Mode| |+:runs+ |Number of runs.|Benchmarking: 4, Profiling: 1|Both| @@ -346,11 +395,13 @@ Metrics and formats have different defaults depending on the interpreter in use. |/2.JRuby |Benchmarking|+[:wall_time, :user_time, :memory, :gc_runs, :gc_time]+|N/A| |Profiling |+[:wall_time]+|+[:flat, :graph]+| -As you've probably noticed by now, metrics and formats are specified using a symbol array with each name "underscored.":http://api.rubyonrails.org/classes/String.html#method-i-underscore +As you've probably noticed by now, metrics and formats are specified using a +symbol array with each name "underscored.":http://api.rubyonrails.org/classes/String.html#method-i-underscore h4. Performance Test Environment -Performance tests are run in the +test+ environment. But running performance tests will set the following configuration parameters: +Performance tests are run in the +test+ environment. But running performance +tests will set the following configuration parameters: <shell> ActionController::Base.perform_caching = true @@ -358,11 +409,13 @@ ActiveSupport::Dependencies.mechanism = :require Rails.logger.level = ActiveSupport::BufferedLogger::INFO </shell> -As +ActionController::Base.perform_caching+ is set to +true+, performance tests will behave much as they do in the +production+ environment. +As +ActionController::Base.perform_caching+ is set to +true+, performance tests +will behave much as they do in the +production+ environment. h4. Installing GC-Patched MRI -To get the best from Rails' performance tests under MRI, you'll need to build a special Ruby binary with some super powers. +To get the best from Rails' performance tests under MRI, you'll need to build +a special Ruby binary with some super powers. The recommended patches for each MRI version are: @@ -371,13 +424,18 @@ The recommended patches for each MRI version are: |1.8.7|ruby187gc| |1.9.2 and above|gcdata| -All of these can be found on "RVM's _patches_ directory":https://github.com/wayneeseguin/rvm/tree/master/patches/ruby under each specific interpreter version. +All of these can be found on "RVM's _patches_ directory":https://github.com/wayneeseguin/rvm/tree/master/patches/ruby +under each specific interpreter version. -Concerning the installation itself, you can either do this easily by using "RVM":http://rvm.beginrescueend.com or you can build everything from source, which is a little bit harder. +Concerning the installation itself, you can either do this easily by using +"RVM":http://rvm.beginrescueend.com or you can build everything from source, +which is a little bit harder. h5. Install Using RVM -The process of installing a patched Ruby interpreter is very easy if you let RVM do the hard work. All of the following RVM commands will provide you with a patched Ruby interpreter: +The process of installing a patched Ruby interpreter is very easy if you let RVM +do the hard work. All of the following RVM commands will provide you with a +patched Ruby interpreter: <shell> $ rvm install 1.9.2-p180 --patch gcdata @@ -385,7 +443,8 @@ $ rvm install 1.8.7 --patch ruby187gc $ rvm install 1.9.2-p180 --patch ~/Downloads/downloaded_gcdata_patch.patch </shell> -You can even keep your regular interpreter by assigning a name to the patched one: +You can even keep your regular interpreter by assigning a name to the patched +one: <shell> $ rvm install 1.9.2-p180 --patch gcdata --name gcdata @@ -397,7 +456,9 @@ And it's done! You have installed a patched Ruby interpreter. h5. Install From Source -This process is a bit more complicated, but straightforward nonetheless. If you've never compiled a Ruby binary before, follow these steps to build a Ruby binary inside your home directory. +This process is a bit more complicated, but straightforward nonetheless. If +you've never compiled a Ruby binary before, follow these steps to build a +Ruby binary inside your home directory. h6. Download and Extract @@ -417,7 +478,9 @@ $ curl http://github.com/wayneeseguin/rvm/raw/master/patches/ruby/1.8.7/ruby187g h6. Configure and Install -The following will install Ruby in your home directory's +/rubygc+ directory. Make sure to replace +<homedir>+ with a full patch to your actual home directory. +The following will install Ruby in your home directory's +/rubygc+ directory. +Make sure to replace +<homedir>+ with a full patch to your actual home +directory. <shell> $ ./configure --prefix=/<homedir>/rubygc @@ -438,23 +501,22 @@ alias gcrails='~/rubygc/bin/rails' Don't forget to use your aliases from now on. -h6. Install RubyGems (1.8 only!) - -Download "RubyGems":http://rubyforge.org/projects/rubygems and install it from source. Rubygem's README file should have necessary installation instructions. Please note that this step isn't necessary if you've installed Ruby 1.9 and above. - h4. Using Ruby-Prof on MRI and REE -Add Ruby-Prof to your applications' Gemfile if you want to benchmark/profile under MRI or REE: +Add Ruby-Prof to your applications' Gemfile if you want to benchmark/profile +under MRI or REE: <ruby> -gem 'ruby-prof', :git => 'git://github.com/wycats/ruby-prof.git' +gem 'ruby-prof', git: 'git://github.com/wycats/ruby-prof.git' </ruby> Now run +bundle install+ and you're ready to go. h3. Command Line Tools -Writing performance test cases could be an overkill when you are looking for one time tests. Rails ships with two command line tools that enable quick and dirty performance testing: +Writing performance test cases could be an overkill when you are looking for one +time tests. Rails ships with two command line tools that enable quick and dirty +performance testing: h4. +benchmarker+ @@ -498,11 +560,14 @@ Example: $ rails profiler 'Item.all' 'CouchItem.all' --runs 2 --metrics process_time --formats flat </shell> -NOTE: Metrics and formats vary from interpreter to interpreter. Pass +--help+ to each tool to see the defaults for your interpreter. +NOTE: Metrics and formats vary from interpreter to interpreter. Pass +--help+ to +each tool to see the defaults for your interpreter. h3. Helper Methods -Rails provides various helper methods inside Active Record, Action Controller and Action View to measure the time taken by a given piece of code. The method is called +benchmark()+ in all the three components. +Rails provides various helper methods inside Active Record, Action Controller +and Action View to measure the time taken by a given piece of code. The method +is called +benchmark()+ in all the three components. h4. Model @@ -514,17 +579,19 @@ Project.benchmark("Creating project") do end </ruby> -This benchmarks the code enclosed in the +Project.benchmark("Creating project") do...end+ block and prints the result to the log file: +This benchmarks the code enclosed in the +Project.benchmark("Creating project") do...end+ +block and prints the result to the log file: <ruby> Creating project (185.3ms) </ruby> -Please refer to the "API docs":http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M001336 for additional options to +benchmark()+ +Please refer to the "API docs":http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.html#method-i-benchmark +for additional options to +benchmark()+. h4. Controller -Similarly, you could use this helper method inside "controllers":http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.html +Similarly, you could use this helper method inside "controllers.":http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.html <ruby> def process_projects @@ -535,7 +602,7 @@ def process_projects end </ruby> -NOTE: +benchmark+ is a class method inside controllers +NOTE: +benchmark+ is a class method inside controllers. h4. View @@ -549,7 +616,8 @@ And in "views":http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.ht h3. Request Logging -Rails log files contain very useful information about the time taken to serve each request. Here's a typical log file entry: +Rails log files contain very useful information about the time taken to serve +each request. Here's a typical log file entry: <shell> Processing ItemsController#index (for 127.0.0.1 at 2009-01-08 03:06:39) [GET] @@ -564,9 +632,14 @@ For this section, we're only interested in the last line: Completed in 5ms (View: 2, DB: 0) | 200 OK [http://0.0.0.0/items] </shell> -This data is fairly straightforward to understand. Rails uses millisecond(ms) as the metric to measure the time taken. The complete request spent 5 ms inside Rails, out of which 2 ms were spent rendering views and none was spent communication with the database. It's safe to assume that the remaining 3 ms were spent inside the controller. +This data is fairly straightforward to understand. Rails uses millisecond(ms) as +the metric to measure the time taken. The complete request spent 5 ms inside +Rails, out of which 2 ms were spent rendering views and none was spent +communication with the database. It's safe to assume that the remaining 3 ms +were spent inside the controller. -Michael Koziarski has an "interesting blog post":http://www.therailsway.com/2009/1/6/requests-per-second explaining the importance of using milliseconds as the metric. +Michael Koziarski has an "interesting blog post":http://www.therailsway.com/2009/1/6/requests-per-second +explaining the importance of using milliseconds as the metric. h3. Useful Links @@ -587,11 +660,12 @@ h4. Generic Tools h4. Tutorials and Documentation * "ruby-prof API Documentation":http://ruby-prof.rubyforge.org -* "Request Profiling Railscast":http://railscasts.com/episodes/98-request-profiling - Outdated, but useful for understanding call graphs +* "Request Profiling Railscast":http://railscasts.com/episodes/98-request-profiling - Outdated, but useful for understanding call graphs. h3. Commercial Products -Rails has been lucky to have a few companies dedicated to Rails-specific performance tools. A couple of those are: +Rails has been lucky to have a few companies dedicated to Rails-specific +performance tools. A couple of those are: * "New Relic":http://www.newrelic.com -* "Scout":http://scoutapp.com +* "Scout":http://scoutapp.com
\ No newline at end of file diff --git a/guides/source/security.textile b/guides/source/security.textile index 8879122b66..49e5da6bb7 100644 --- a/guides/source/security.textile +++ b/guides/source/security.textile @@ -608,7 +608,7 @@ This URL passes the filter because the regular expression matches – the second link_to "Homepage", @user.homepage </ruby> -The link looks innocent to visitors, but when it's clicked, it will execute the javascript function "exploit_code" or any other javascript the attacker provides. +The link looks innocent to visitors, but when it's clicked, it will execute the JavaScript function "exploit_code" or any other JavaScript the attacker provides. To fix the regular expression, \A and \z should be used instead of ^ and $, like so: diff --git a/guides/source/upgrading_ruby_on_rails.textile b/guides/source/upgrading_ruby_on_rails.textile index 4bf4751127..5024bc4c37 100644 --- a/guides/source/upgrading_ruby_on_rails.textile +++ b/guides/source/upgrading_ruby_on_rails.textile @@ -42,6 +42,8 @@ h4(#active_record4_0). Active Record The <tt>delete</tt> method in collection associations can now receive <tt>Fixnum</tt> or <tt>String</tt> arguments as record ids, besides records, pretty much like the <tt>destroy</tt> method does. Previously it raised <tt>ActiveRecord::AssociationTypeMismatch</tt> for such arguments. From Rails 4.0 on <tt>delete</tt> automatically tries to find the records matching the given ids before deleting them. +Rails 4.0 has changed how orders get stacked in +ActiveRecord::Relation+. In previous versions of rails new order was applied after previous defined order. But this is no long true. Check "ActiveRecord Query guide":active_record_querying.html#ordering for more information. + h4(#active_model4_0). Active Model Rails 4.0 has changed how errors attach with the <tt>ActiveModel::Validations::ConfirmationValidator</tt>. Now when confirmation validations fail the error will be attached to <tt>:#{attribute}_confirmation</tt> instead of <tt>attribute</tt>. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 048219002d..3531728421 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -102,6 +102,17 @@ module Rails # Stores some of the Rails initial environment parameters which # will be used by middlewares and engines to configure themselves. + # Currently stores: + # + # * "action_dispatch.parameter_filter" => config.filter_parameters, + # * "action_dispatch.secret_token" => config.secret_token, + # * "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, + # * "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, + # * "action_dispatch.logger" => Rails.logger, + # * "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner + # + # These parameters will be used by middlewares and engines to configure themselves + # def env_config @env_config ||= super.merge({ "action_dispatch.parameter_filter" => config.filter_parameters, diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index a2e5dece16..9521805778 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -11,7 +11,7 @@ module Rails :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, :queue, :queue_consumer attr_writer :log_level @@ -43,7 +43,6 @@ module Rails @exceptions_app = nil @autoflush_log = true @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @use_schema_cache_dump = true @queue = Rails::Queueing::Queue @queue_consumer = Rails::Queueing::ThreadedConsumer diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb index 5d8d9be237..9262c3379f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb @@ -1,8 +1,9 @@ # Be sure to restart your server when you modify this file. -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' @@ -10,6 +11,6 @@ # end # # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections do |inflect| +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/USAGE b/railties/lib/rails/generators/rails/scaffold_controller/USAGE index 673f69bc81..5cd51b62d4 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/USAGE +++ b/railties/lib/rails/generators/rails/scaffold_controller/USAGE @@ -1,8 +1,7 @@ Description: - Stubs out a scaffolded controller and its views. Pass the model name, - either CamelCased or under_scored, and a list of views as arguments. - The controller name is retrieved as a pluralized version of the model - name. + Stubs out a scaffolded controller, its seven RESTful actions and related + views. Pass the model name, either CamelCased or under_scored. The + controller name is retrieved as a pluralized version of the model name. To create a controller within a module, specify the model name as a path like 'parent_module/controller_name'. diff --git a/railties/lib/rails/tasks/misc.rake b/railties/lib/rails/tasks/misc.rake index 0dcca36d8b..0a9b9ff4ff 100644 --- a/railties/lib/rails/tasks/misc.rake +++ b/railties/lib/rails/tasks/misc.rake @@ -1,10 +1,3 @@ -task :rails_env do - # TODO Do we really need this? - unless defined? RAILS_ENV - RAILS_ENV = ENV['RAILS_ENV'] ||= 'development' - end -end - desc 'Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions).' task :secret do require 'securerandom' diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 4243a79b58..dd3af86b99 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -233,7 +233,7 @@ module ApplicationTests get '/posts' assert_match(/AssetNotPrecompiledError/, last_response.body) - assert_match(/app.js isn't precompiled/, last_response.body) + assert_match(/app.js isn't precompiled/, last_response.body) end test "assets raise AssetNotPrecompiledError when manifest file is present and requested file isn't precompiled if digest is disabled" do @@ -257,7 +257,7 @@ module ApplicationTests get '/posts' assert_match(/AssetNotPrecompiledError/, last_response.body) - assert_match(/app.js isn't precompiled/, last_response.body) + assert_match(/app.js isn't precompiled/, last_response.body) end test "precompile properly refers files referenced with asset_path and and run in the provided RAILS_ENV" do |