diff options
57 files changed, 563 insertions, 357 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index dc3e6d5c6a..9fb914ac40 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,13 +1,40 @@ +* Respect `SCRIPT_NAME` when using `redirect` with a relative path + + Example: + # application routes.rb + mount BlogEngine => '/blog' + + # engine routes.rb + get '/admin' => redirect('admin/dashboard') + + This now redirects to the path `/blog/admin/dashboard`, whereas before it would've + generated an invalid url because there would be no slash between the host name and + the path. It also allows redirects to work where the application is deployed to a + subdirectory of a website. + + Fixes #7977 + + *Andrew White* + +* Fixing repond_with working directly on the options hash + This fixes an issue where the respond_with worked directly with the given + options hash, so that if a user relied on it after calling respond_with, + the hash wouldn't be the same. + + Fixes #12029. + + *bluehotdog* + * Fix `ActionDispatch::RemoteIp::GetIp#calculate_ip` to only check for spoofing attacks if both `HTTP_CLIENT_IP` and `HTTP_X_FORWARDED_FOR` are set. - Fixes #10844 + Fixes #10844. *Tamir Duberstein* * Strong parameters should permit nested number as key. - Fixes #12293 + Fixes #12293. *kennyj* @@ -23,14 +50,14 @@ * Fix an issue where router can't recognize downcased url encoding path. - Fixes #12269 + Fixes #12269. *kennyj* * Fix custom flash type definition. Misusage of the `_flash_types` class variable caused an error when reloading controllers with custom flash types. - Fixes #12057 + Fixes #12057. *Ricardo de Cillo* @@ -51,21 +78,21 @@ * Fix an issue where :if and :unless controller action procs were being run before checking for the correct action in the :only and :unless options. - Fixes #11799 + Fixes #11799. *Nicholas Jakobsen* * Fix an issue where `assert_dom_equal` and `assert_dom_not_equal` were ignoring the passed failure message argument. - Fixes #11751 + Fixes #11751. *Ryan McGeary* * Allow REMOTE_ADDR, HTTP_HOST and HTTP_USER_AGENT to be overridden from the environment passed into `ActionDispatch::TestRequest.new`. - Fixes #11590 + Fixes #11590. *Andrew White* @@ -80,7 +107,7 @@ * Skip routes pointing to a redirect or mounted application when generating urls using an options hash as they aren't relevant and generate incorrect urls. - Fixes #8018 + Fixes #8018. *Andrew White* @@ -98,7 +125,7 @@ * Fix `ActionDispatch::ParamsParser#parse_formatted_parameters` to rewind body input stream on parsing json params. - Fixes #11345 + Fixes #11345. *Yuri Bol*, *Paul Nikitochkin* @@ -131,7 +158,7 @@ was setting `request.formats` with an array containing a `nil` value, which raised an error when setting the controller formats. - Fixes #10965 + Fixes #10965. *Becker* @@ -140,7 +167,7 @@ no `:to` present in the options hash so should only affect routes using the shorthand syntax (i.e. endpoint is inferred from the path). - Fixes #9856 + Fixes #9856. *Yves Senn*, *Andrew White* diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index db7b56f47e..3b0d094f4f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -73,7 +73,7 @@ module ActionController # <input type="text" name="post[address]" value="hyacintvej"> # # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. - # If the address input had been named \"post[address][street]", the params would have included + # If the address input had been named <tt>post[address][street]</tt>, the params would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # # == Sessions diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 66dabd821f..a072fce1a1 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -326,6 +326,7 @@ module ActionController #:nodoc: if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! + options = options.clone options[:default_response] = collector.response (options.delete(:responder) || self.class.responder).call(self, resources, options) end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 68094f129f..3e54c7e71c 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -30,6 +30,10 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + end + body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) headers = { @@ -48,6 +52,11 @@ module ActionDispatch def inspect "redirect(#{status})" end + + private + def relative_path?(path) + path && !path.empty? && path[0] != '/' + end end class PathRedirect < Redirect @@ -81,6 +90,11 @@ module ActionDispatch url_options[:path] = (url_options[:path] % escape_path(params)) end + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + end + ActionDispatch::Http::URL.url_for url_options end @@ -104,6 +118,10 @@ module ActionDispatch # # get 'docs/:article', to: redirect('/wiki/%{article}') # + # Note that if you return a path without a leading slash then the url is prefixed with the + # current SCRIPT_NAME environment variable. This is typically '/' but may be different in + # a mounted engine or where the application is deployed to a subdirectory of a website. + # # Alternatively you can use one of the other syntaxes: # # The block version of redirect allows for the easy encapsulation of any logic associated with diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb index 76af9e3414..a70592fa1b 100644 --- a/actionpack/test/controller/mime/respond_with_test.rb +++ b/actionpack/test/controller/mime/respond_with_test.rb @@ -65,7 +65,17 @@ class RespondWithController < ActionController::Base respond_with(resource, :responder => responder) end + def respond_with_additional_params + @params = RespondWithController.params + respond_with({:result => resource}, @params) + end + protected + def self.params + { + :foo => 'bar' + } + end def resource Customer.new("david", request.delete? ? nil : 13) @@ -145,6 +155,11 @@ class RespondWithControllerTest < ActionController::TestCase Mime::Type.unregister(:mobile) end + def test_respond_with_shouldnt_modify_original_hash + get :respond_with_additional_params + assert_equal RespondWithController.params, assigns(:params) + end + def test_using_resource @request.accept = "application/xml" get :using_resource diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 113608ecf4..e519fff51e 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -31,6 +31,14 @@ module TestGenerationPrefix get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine" get "/conflicting_url", :to => "inside_engine_generating#conflicting" get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test + + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } + + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end routes @@ -182,6 +190,48 @@ module TestGenerationPrefix assert_equal "engine", last_response.body end + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + # Inside Application test "[APP] generating engine's route includes prefix" do get "/generate" @@ -281,6 +331,14 @@ module TestGenerationPrefix routes = ActionDispatch::Routing::RouteSet.new routes.draw do get "/posts/:id", :to => "posts#show", :as => :post + + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } + + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end routes @@ -331,5 +389,47 @@ module TestGenerationPrefix get "/posts/1" assert_equal "/posts/1", last_response.body end + + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do + get "/relative_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do + get "/relative_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do + get "/relative_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end end end diff --git a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 8e1aea50a9..d7e8e99200 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -531,7 +531,7 @@ module ActionView # my_date = Time.now + 2.days # # # Generates a select field for days that defaults to the day for the date in my_date. - # select_day(my_time) + # select_day(my_date) # # # Generates a select field for days that defaults to the number given. # select_day(5) @@ -541,7 +541,7 @@ module ActionView # # # Generates a select field for days that defaults to the day for the date in my_date # # that is named 'due' rather than 'day'. - # select_day(my_time, field_name: 'due') + # select_day(my_date, field_name: 'due') # # # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a # # generic prompt. diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 8a4918a8c0..2f5246f42a 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -455,7 +455,7 @@ module ActionView html_options, name = name, nil if block_given? html_options = (html_options || {}).stringify_keys - extras = %w{ cc bcc body subject }.map { |item| + extras = %w{ cc bcc body subject }.map! { |item| option = html_options.delete(item) || next "#{item}=#{Rack::Utils.escape_path(option)}" }.compact diff --git a/actionview/test/lib/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index c6e7a523b9..c6e7a523b9 100644 --- a/actionview/test/lib/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb diff --git a/actionview/test/lib/controller/fake_controllers.rb b/actionview/test/lib/controller/fake_controllers.rb deleted file mode 100644 index 1a2863b689..0000000000 --- a/actionview/test/lib/controller/fake_controllers.rb +++ /dev/null @@ -1,35 +0,0 @@ -class ContentController < ActionController::Base; end - -module Admin - class AccountsController < ActionController::Base; end - class PostsController < ActionController::Base; end - class StuffController < ActionController::Base; end - class UserController < ActionController::Base; end - class UsersController < ActionController::Base; end -end - -module Api - class UsersController < ActionController::Base; end - class ProductsController < ActionController::Base; end -end - -class AccountController < ActionController::Base; end -class ArchiveController < ActionController::Base; end -class ArticlesController < ActionController::Base; end -class BarController < ActionController::Base; end -class BlogController < ActionController::Base; end -class BooksController < ActionController::Base; end -class CarsController < ActionController::Base; end -class CcController < ActionController::Base; end -class CController < ActionController::Base; end -class FooController < ActionController::Base; end -class GeocodeController < ActionController::Base; end -class NewsController < ActionController::Base; end -class NotesController < ActionController::Base; end -class PagesController < ActionController::Base; end -class PeopleController < ActionController::Base; end -class PostsController < ActionController::Base; end -class SubpathBooksController < ActionController::Base; end -class SymbolsController < ActionController::Base; end -class UserController < ActionController::Base; end -class UsersController < ActionController::Base; end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index eb21b69163..e8602ecbcf 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,7 @@ +* Fix `has_secure_password` to honor bcrypt-ruby's cost attribute. + + *T.J. Schuck* + * Updated the `ActiveModel::Dirty#changed_attributes` method to be indifferent between using symbols and strings as keys. diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 51655fe3da..11e755649c 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = 'activemodel' s.version = version s.summary = 'A toolkit for building modeling frameworks (part of Rails).' - s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' + s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.' s.required_ruby_version = '>= 1.9.3' diff --git a/activemodel/examples/validations.rb b/activemodel/examples/validations.rb index c94cd17e18..b8e74acd5e 100644 --- a/activemodel/examples/validations.rb +++ b/activemodel/examples/validations.rb @@ -1,3 +1,4 @@ +require File.expand_path('../../../load_paths', __FILE__) require 'active_model' class Person diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 17fafe4be9..f87c36e39e 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -103,7 +103,7 @@ module ActiveModel def password=(unencrypted_password) unless unencrypted_password.blank? @password = unencrypted_password - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 98e5c747d5..41d0b2263e 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -82,6 +82,14 @@ class SecurePasswordTest < ActiveModel::TestCase assert_equal BCrypt::Engine::DEFAULT_COST, @user.password_digest.cost end + test "Password digest cost honors bcrypt cost attribute when min_cost is false" do + ActiveModel::SecurePassword.min_cost = false + BCrypt::Engine.cost = 5 + + @user.password = "secret" + assert_equal BCrypt::Engine.cost, @user.password_digest.cost + end + test "Password digest cost can be set to bcrypt min cost to speed up tests" do ActiveModel::SecurePassword.min_cost = true diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 83c94caa53..658645275d 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,9 @@ +* For missed association exception message + which is raised in `ActiveRecord::Associations::Preloader` class + added owner record class name in order to simplify to find problem code. + + *Paul Nikitochkin* + * `has_and_belongs_to_many` is now transparently implemented in terms of `has_many :through`. Behavior should remain the same, if not, it is a bug. @@ -199,6 +205,12 @@ *Yves Senn* +* Fixes bug when using includes combined with select, the select statement was overwritten. + + Fixes #11773 + + *Edo Balvers* + * Load fixtures from linked folders. *Kassio Borges* diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 4066a1c9d3..d8d68eb908 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -18,18 +18,15 @@ module ActiveRecord::Associations::Builder VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] - attr_reader :name, :scope, :options - def self.build(model, name, scope, options, &block) - builder = create_builder model, name, scope, options, &block - reflection = builder.build(model) - builder.define_accessors model, reflection + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension + define_accessors model, reflection define_callbacks model, reflection - builder.define_extensions model reflection end - def self.create_builder(model, name, scope, options, &block) + def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) if scope.is_a?(Hash) @@ -37,38 +34,44 @@ module ActiveRecord::Associations::Builder scope = nil end - new(name, scope, options, &block) - end + validate_options(options) + + scope = build_scope(scope, extension) - def initialize(name, scope, options) - @name = name - @scope = scope - @options = options + ActiveRecord::Reflection.create(macro, name, scope, options, model) + end - validate_options + def self.build_scope(scope, extension) + new_scope = scope if scope && scope.arity == 0 - @scope = proc { instance_exec(&scope) } + new_scope = proc { instance_exec(&scope) } + end + + if extension + new_scope = wrap_scope new_scope, extension end + + new_scope end - def build(model) - ActiveRecord::Reflection.create(macro, name, scope, options, model) + def self.wrap_scope(scope, extension) + scope end - def macro + def self.macro raise NotImplementedError end - def valid_options + def self.valid_options(options) VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end - def validate_options - options.assert_valid_keys(valid_options) + def self.validate_options(options) + options.assert_valid_keys(valid_options(options)) end - def define_extensions(model) + def self.define_extensions(model, name) end def self.define_callbacks(model, reflection) @@ -84,14 +87,14 @@ module ActiveRecord::Associations::Builder # end # # Post.first.comments and Post.first.comments= methods are defined by this method... - - def define_accessors(model, reflection) + def self.define_accessors(model, reflection) mixin = model.generated_feature_methods - define_readers(mixin) - define_writers(mixin) + name = reflection.name + define_readers(mixin, name) + define_writers(mixin, name) end - def define_readers(mixin) + def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}(*args) association(:#{name}).reader(*args) @@ -99,7 +102,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers(mixin) + def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) @@ -111,8 +114,6 @@ module ActiveRecord::Associations::Builder raise NotImplementedError end - private - def self.add_before_destroy_callbacks(model, reflection) unless valid_dependent_options.include? reflection.options[:dependent] raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 9d55ec850e..aa43c34d86 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def macro + def self.macro :belongs_to end - def valid_options + def self.valid_options(options) super + [:foreign_type, :polymorphic, :touch] end @@ -18,14 +18,12 @@ module ActiveRecord::Associations::Builder add_touch_callbacks(model, reflection) if reflection.options[:touch] end - def define_accessors(mixin, reflection) + def self.define_accessors(mixin, reflection) super add_counter_cache_methods mixin end - private - - def add_counter_cache_methods(mixin) + def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_create mixin.class_eval do diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 15f9f9a65f..2ff67f904d 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,4 +1,4 @@ -# This class is inherited by the has_many and has_many_and_belongs_to_many association classes +# This class is inherited by the has_many and has_many_and_belongs_to_many association classes require 'active_record/associations' @@ -7,22 +7,11 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def valid_options + def self.valid_options(options) super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - attr_reader :block_extension - - def initialize(name, scope, options) - super - @mod = nil - if block_given? - @mod = Module.new(&Proc.new) - @scope = wrap_scope @scope, @mod - end - end - def self.define_callbacks(model, reflection) super name = reflection.name @@ -32,10 +21,11 @@ module ActiveRecord::Associations::Builder } end - def define_extensions(model) - if @mod + def self.define_extensions(model, name) + if block_given? extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - model.parent.const_set(extension_module_name, @mod) + extension = Module.new(&Proc.new) + model.parent.const_set(extension_module_name, extension) end end @@ -58,8 +48,7 @@ module ActiveRecord::Associations::Builder end # Defines the setter and getter methods for the collection_singular_ids. - - def define_readers(mixin) + def self.define_readers(mixin, name) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -69,7 +58,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers(mixin) + def self.define_writers(mixin, name) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -79,9 +68,7 @@ module ActiveRecord::Associations::Builder CODE end - private - - def wrap_scope(scope, mod) + def self.wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else 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 af596a3a64..1c9c04b044 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 @@ -84,11 +84,11 @@ module ActiveRecord::Associations::Builder middle_name = [lhs_model.name.downcase.pluralize, association_name].join('_').gsub(/::/, '_').to_sym middle_options = middle_options join_model - hm_builder = HasMany.create_builder(lhs_model, - middle_name, - nil, - middle_options) - hm_builder.build lhs_model + + HasMany.create_reflection(lhs_model, + middle_name, + nil, + middle_options) end private diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 7909b93622..227184cd19 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def macro + def self.macro :has_many end - def valid_options + def self.valid_options(options) super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index f359efd496..064a3c8b51 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def macro + def self.macro :has_one end - def valid_options + def self.valid_options(options) valid = super + [:order, :as] valid += [:through, :source, :source_type] if options[:through] valid @@ -14,8 +14,6 @@ module ActiveRecord::Associations::Builder [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - private - def self.add_before_destroy_callbacks(model, reflection) super unless reflection.options[:through] end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 10d568ebc0..2a4b1c441f 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,19 +1,18 @@ -# This class is inherited by the has_one and belongs_to association classes +# This class is inherited by the has_one and belongs_to association classes module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def valid_options + def self.valid_options(options) super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] end - def define_accessors(model, reflection) + def self.define_accessors(model, reflection) super - define_constructors(model.generated_feature_methods) if reflection.constructable? + define_constructors(model.generated_feature_methods, reflection.name) if reflection.constructable? end # Defines the (build|create)_association methods for belongs_to or has_one association - - def define_constructors(mixin) + def self.define_constructors(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def build_#{name}(*args, &block) association(:#{name}).build(*args, &block) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 3acb50d73f..6e08f67286 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -4,7 +4,31 @@ module ActiveRecord autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' - attr_reader :join_parts, :reflections, :alias_tracker, :base_klass + attr_reader :alias_tracker, :base_klass, :join_root + + def self.make_tree(associations) + hash = {} + walk_tree associations, hash + hash + end + + def self.walk_tree(associations, hash) + case associations + when Symbol, String + hash[associations.to_sym] ||= {} + when Array + associations.each do |assoc| + walk_tree assoc, hash + end + when Hash + associations.each do |k,v| + cache = hash[k] ||= {} + walk_tree v, cache + end + else + raise ConfigurationError, associations.inspect + end + end # base is the base class on which operation is taking place. # associations is the list of associations which are joined using hash, symbol or array. @@ -30,38 +54,37 @@ module ActiveRecord def initialize(base, associations, joins) @base_klass = base @table_joins = joins - @join_parts = [JoinBase.new(base)] - @associations = {} - @reflections = [] + @join_root = JoinBase.new(base) @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 - build(associations, join_parts.last, Arel::InnerJoin) + tree = self.class.make_tree associations + build tree, @join_root, Arel::InnerJoin end - def graft(*associations) - associations.each do |association| - join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) - end - self + def reflections + join_root.drop(1).map!(&:reflection) end - def join_associations - join_parts.drop 1 - end + def merge_outer_joins!(other) + left = join_root + right = other.join_root - def join_base - join_parts.first + if left.match? right + merge_node left, right + else + # If the roots aren't the same, then deep copy the RHS to the LHS + left.children.concat right.children.map { |node| + deep_copy left, node + } + end end - def join_relation(relation) - join_associations.inject(relation) do |rel,association| - association.join_relation(rel) - end + def join_constraints + join_root.children.flat_map { |c| c.flat_map(&:join_constraints) } end def columns - join_parts.collect { |join_part| + join_root.collect { |join_part| table = join_part.aliased_table join_part.column_names_with_alias.collect{ |column_name, aliased_name| table[column_name].as Arel.sql(aliased_name) @@ -69,100 +92,80 @@ module ActiveRecord }.flatten end - def instantiate(rows) - primary_key = join_base.aliased_primary_key + def instantiate(result_set) + primary_key = join_root.aliased_primary_key parents = {} - records = rows.map { |model| - primary_id = model[primary_key] - parent = parents[primary_id] ||= join_base.instantiate(model) - construct(parent, @associations, join_associations, model) + type_caster = result_set.column_type primary_key + + records = result_set.map { |row_hash| + primary_id = type_caster.type_cast row_hash[primary_key] + parent = parents[primary_id] ||= join_root.instantiate(row_hash) + construct(parent, join_root, row_hash, result_set) parent }.uniq - remove_duplicate_results!(base_klass, records, @associations) + remove_duplicate_results!(base_klass, records, join_root.children) records end - protected + private + + def merge_node(left, right) + intersection, missing = right.children.map { |node1| + [left.children.find { |node2| node1.match? node2 }, node1] + }.partition(&:first) + + intersection.each { |l,r| merge_node l, r } + + left.children.concat missing.map { |_,node| deep_copy left, node } + end + + def deep_copy(parent, node) + dup = build_join_association(node.reflection, parent, Arel::OuterJoin) + dup.children.concat node.children.map { |n| deep_copy dup, n } + dup + end def remove_duplicate_results!(base, records, associations) - case associations - when Symbol, String - reflection = base.reflections[associations] + associations.each do |node| + reflection = base.reflect_on_association(node.name) remove_uniq_by_reflection(reflection, records) - when Array - associations.each do |association| - remove_duplicate_results!(base, records, association) - end - when Hash - associations.each_key do |name| - reflection = base.reflections[name] - remove_uniq_by_reflection(reflection, records) - - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end + + parent_records = [] + records.each do |record| + if descendant = record.send(reflection.name) + if reflection.collection? + parent_records.concat descendant.target.uniq + else + parent_records << descendant end end + end - remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? + unless parent_records.empty? + remove_duplicate_results!(reflection.klass, parent_records, node.children) end end end - def cache_joined_association(association) - associations = [] - parent = association.parent - while parent != join_base - associations.unshift(parent.reflection.name) - parent = parent.parent - end - ref = associations.inject(@associations) do |cache,key| - cache[key] - end - ref[association.reflection.name] ||= {} + def find_reflection(klass, name) + klass.reflect_on_association(name) or + raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" end def build(associations, parent, join_type) - case associations - when Symbol, String - reflection = parent.reflections[associations.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?" - unless join_association = find_join_association(reflection, parent) - @reflections << reflection - join_association = build_join_association(reflection, parent, join_type) - @join_parts << join_association - cache_joined_association(join_association) - end - join_association - when Array - associations.each do |association| - build(association, parent, join_type) - end - when Hash - associations.each do |left, right| - join_association = build(left, parent, join_type) - build(right, join_association, join_type) - end - else - raise ConfigurationError, associations.inspect + associations.each do |name, right| + reflection = find_reflection parent.base_klass, name + join_association = build_join_association reflection, parent, join_type + parent.children << join_association + build right, join_association, join_type end end - def find_join_association(name_or_reflection, parent) - if String === name_or_reflection - name_or_reflection = name_or_reflection.to_sym - end - - join_associations.detect { |j| - j.reflection == name_or_reflection && j.parent == parent - } + def build_scalar(reflection, parent, join_type) + join_association = build_join_association(reflection, parent, join_type) + parent.children << join_association end def remove_uniq_by_reflection(reflection, records) @@ -178,38 +181,21 @@ module ActiveRecord raise EagerLoadPolymorphicError.new(reflection) end - JoinAssociation.new(reflection, self, parent, join_type) + JoinAssociation.new(reflection, join_root.to_a.length, parent, join_type, alias_tracker) end - def construct(parent, associations, join_parts, row) - case associations - when Symbol, String - name = associations.to_s - - join_part = join_parts.detect { |j| - j.reflection.name.to_s == name && - j.parent_table_name == parent.class.table_name } - - raise(ConfigurationError, "No such association") unless join_part - - join_parts.delete(join_part) - construct_association(parent, join_part, row) - when Array - associations.each do |association| - construct(parent, association, join_parts, row) - end - when Hash - associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc| - association = construct(parent, association_name, join_parts, row) - construct(association, assoc, join_parts, row) if association - end - else - raise ConfigurationError, associations.inspect + def construct(ar_parent, parent, row, rs) + parent.children.each do |node| + association = construct_association(ar_parent, parent, node, row, rs) + construct(association, node, row, rs) if association end end - def construct_association(record, join_part, row) - return if record.id.to_s != join_part.parent.record_id(row).to_s + def construct_association(record, parent, join_part, row, rs) + caster = rs.column_type(parent.aliased_primary_key) + row_id = caster.type_cast row[parent.aliased_primary_key] + + return if record.id != row_id macro = join_part.reflection.macro if macro == :has_one diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 2307564ce7..3af613d2d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -9,15 +9,6 @@ module ActiveRecord # The reflection of the association represented attr_reader :reflection - # The JoinDependency object which this JoinAssociation exists within. This is mainly - # relevant for generating aliases which do not conflict with other joins which are - # part of the query. - attr_reader :join_dependency - - # A JoinBase instance representing the active record we are joining onto. - # (So in Author.has_many :posts, the Author would be that base record.) - attr_reader :parent - # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin attr_accessor :join_type @@ -25,39 +16,26 @@ module ActiveRecord attr_reader :aliased_prefix attr_reader :tables + attr_reader :alias_tracker delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection - delegate :alias_tracker, :to => :join_dependency - def initialize(reflection, join_dependency, parent, join_type) - super(reflection.klass) + def initialize(reflection, index, parent, join_type, alias_tracker) + super(reflection.klass, parent) @reflection = reflection - @join_dependency = join_dependency - @parent = parent + @alias_tracker = alias_tracker @join_type = join_type - @aliased_prefix = "t#{ join_dependency.join_parts.size }" + @aliased_prefix = "t#{ index }" @tables = construct_tables.reverse end def parent_table_name; parent.table_name; end alias :alias_suffix :parent_table_name - def ==(other) - other.class == self.class && - other.reflection == reflection && - other.parent == parent - end - - def find_parent_in(other_join_dependency) - other_join_dependency.join_parts.detect do |join_part| - case parent - when JoinBase - parent.base_klass == join_part.base_klass - else - parent == join_part - end - end + def match?(other) + return true if self == other + super && reflection == other.reflection end def join_constraints @@ -147,11 +125,6 @@ module ActiveRecord constraint end - def join_relation(joining_relation) - self.join_type = Arel::OuterJoin - joining_relation.joins(self) - end - def table tables.last end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index c689d06594..48de12bcd5 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -4,9 +4,13 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinBase < JoinPart # :nodoc: - def ==(other) - other.class == self.class && - other.base_klass == base_klass + def initialize(klass) + super(klass, nil) + end + + def match?(other) + return true if self == other + super && base_klass == other.base_klass end def aliased_prefix diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 8024105472..d39ce94c99 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -8,25 +8,42 @@ module ActiveRecord # operations (for example a has_and_belongs_to_many JoinAssociation would result in # two; one for the join table and one for the target table). class JoinPart # :nodoc: + include Enumerable + + # A JoinBase instance representing the active record we are joining onto. + # (So in Author.has_many :posts, the Author would be that base record.) + attr_reader :parent + # The Active Record class which this join part is associated 'about'; for a JoinBase # this is the actual base model, for a JoinAssociation this is the target model of the # association. - attr_reader :base_klass + attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass + delegate :table_name, :column_names, :primary_key, :arel_engine, :to => :base_klass - def initialize(base_klass) + def initialize(base_klass, parent) @base_klass = base_klass + @parent = parent @cached_record = {} @column_names_with_alias = nil + @children = [] end - def aliased_table - Arel::Nodes::TableAlias.new table, aliased_table_name + def name + reflection.name end - def ==(other) - raise NotImplementedError + def match?(other) + self.class == other.class + end + + def each(&block) + yield self + children.each { |child| child.each(&block) } + end + + def aliased_table + Arel::Nodes::TableAlias.new table, aliased_table_name end # An Arel::Table for the active_record @@ -54,7 +71,7 @@ module ActiveRecord unless @column_names_with_alias @column_names_with_alias = [] - ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i| + column_names.each_with_index do |column_name, i| @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 713ff80d47..2393667ac8 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -153,13 +153,13 @@ module ActiveRecord records.group_by do |record| reflection = record.class.reflect_on_association(association) - reflection || raise_config_error(association) + reflection || raise_config_error(record, association) end end - def raise_config_error(association) + def raise_config_error(record, association) raise ActiveRecord::ConfigurationError, - "Association named '#{association}' was not found; " \ + "Association named '#{association}' was not found on #{record.class.name}; " \ "perhaps you misspelled it?" end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index ea21836c65..3166df57eb 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord through_reflection.name, through_scope) - through_records = owners.map do |owner, h| + through_records = owners.map do |owner| association = owner.association through_reflection.name [owner, Array(association.reader)] diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index e4c484d64b..128a9377c1 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -23,11 +23,14 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # + # Additionally, an <tt>after_touch</tt> callback is triggered whenever an + # object is touched. + # # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects # are instantiated as well. # - # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the + # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index e826762def..7e1e120288 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -160,7 +160,7 @@ module ActiveRecord end def type_condition(table = arel_table) - sti_column = table[inheritance_column.to_sym] + sti_column = table[inheritance_column] sti_names = ([self] + descendants).map { |model| model.sti_name } sti_column.in(sti_names) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 267aa28058..a73a140ef1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -401,7 +401,8 @@ module ActiveRecord end # Saves the record with the updated_at/on attributes set to the current time. - # Please note that no validation is performed and no callbacks are executed. + # Please note that no validation is performed and only the +after_touch+ + # callback is executed. # If an attribute name is passed, that attribute is updated along with # updated_at/on attributes. # diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8ddf5cfa66..e88c5d17cb 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -367,21 +367,22 @@ module ActiveRecord protected - def actual_source_reflection # FIXME: this is a horrible name - self - end + def actual_source_reflection # FIXME: this is a horrible name + self + end private - def calculate_constructable(macro, options) - case macro - when :belongs_to - !options[:polymorphic] - when :has_one - !options[:through] - else - true + + def calculate_constructable(macro, options) + case macro + when :belongs_to + !options[:polymorphic] + when :has_one + !options[:through] + else + true + end end - end # Attempts to find the inverse association name automatically. # If it cannot find a suitable inverse association name, it returns diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 0132a02f83..8583286de5 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -261,13 +261,13 @@ module ActiveRecord end def construct_relation_for_association_find(join_dependency) - relation = except(:select).select(join_dependency.columns) + relation = except(:select).select(join_dependency.columns + select_values) apply_join_dependency(relation, join_dependency) end def apply_join_dependency(relation, join_dependency) relation = relation.except(:includes, :eager_load, :preload) - relation = join_dependency.join_relation(relation) + relation = relation.joins join_dependency if using_limitable_reflections?(join_dependency.reflections) relation diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index c05632e688..182b9ed89c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -94,7 +94,7 @@ module ActiveRecord []) relation.joins! rest - @relation = join_dependency.join_relation(relation) + @relation = relation.joins join_dependency end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9fcb6db726..9c9690215a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -928,7 +928,7 @@ module ActiveRecord :string_join when Hash, Symbol, Array :association_join - when ActiveRecord::Associations::JoinDependency::JoinAssociation + when ActiveRecord::Associations::JoinDependency :stashed_join when Arel::Nodes::Join :join_node @@ -950,10 +950,11 @@ module ActiveRecord join_list ) - join_dependency.graft(*stashed_association_joins) + stashed_association_joins.each do |dep| + join_dependency.merge_outer_joins! dep + end - joins = join_dependency.join_associations.map!(&:join_constraints) - joins.flatten! + joins = join_dependency.join_constraints joins.each { |join| manager.from(join) } diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index d0f1cb5b75..1dc3bf3f12 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -46,6 +46,10 @@ module ActiveRecord IDENTITY_TYPE end + def column_type(name) + @column_types[name] || identity_type + end + def each if block_given? hash_rows.each { |row| yield row } diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 47dff7d0ea..f8f2832ab1 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -75,7 +75,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - builder = ActiveRecord::Associations::Builder::HasMany.new(:association_name, nil, {}) { } - builder.define_extensions(model) + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 5299e4e17e..3b61b91d62 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -785,7 +785,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1) john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1) assert_equal sarah.agents, [john] - assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents + assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents end def test_associate_existing_with_nonstandard_primary_key_on_belongs_to diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index f814947ab2..396d4ef1e9 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -486,6 +486,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal Developer.where(name: 'David').map(&:id).sort, developers end + def test_includes_with_select + query = Post.select('comments_count AS ranking').order('ranking').includes(:comments) + .where(comments: { id: 1 }) + + assert_equal ['comments_count AS ranking'], query.select_values + assert_equal 1, query.to_a.size + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 93a2b89ede..61fd762304 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -105,7 +105,7 @@ will be the template used for the email, formatted in HTML: <h1>Welcome to example.com, <%= @user.name %></h1> <p> You have successfully signed up to example.com, - your username is: <%= @user.login %>.<br/> + your username is: <%= @user.login %>.<br> </p> <p> To login to the site, just follow this link: <%= @url %>. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 5cda104138..d19dd11181 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -68,7 +68,7 @@ Consider the following loop for names: ```html+erb <h1>Names of all the people</h1> <% @people.each do |person| %> - Name: <%= person.name %><br/> + Name: <%= person.name %><br> <% end %> ``` @@ -269,7 +269,7 @@ Rails will render the `_product_ruler` partial (with no data passed to it) betwe ### Layouts -Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. +Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails application has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. Partial Layouts --------------- diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 34baae509b..a184f0753d 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -116,7 +116,7 @@ to Active Record instances: locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to a model. * `type` - Specifies that the model uses [Single Table - Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html). + Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance). * `(association_name)_type` - Stores the type for [polymorphic associations](association_basics.html#polymorphic-associations). * `(table_name)_count` - Used to cache the number of belonging objects on diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 0b2f0a47fa..797b996357 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -528,7 +528,7 @@ If you validate the presence of an object associated via a `has_one` or Since `false.blank?` is true, if you want to validate the presence of a boolean field you should use `validates :field_name, inclusion: { in: [true, false] }`. -The default error message is _"can't be empty"_. +The default error message is _"can't be blank"_. ### `absence` @@ -783,7 +783,7 @@ end Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank ``` -There is also an ability to pass custom exception to `:strict` option +There is also an ability to pass custom exception to `:strict` option. ```ruby class Person < ActiveRecord::Base diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 51ff921d7b..1b0b93c3bc 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -379,12 +379,13 @@ About your application's environment Ruby version 1.9.3 (x86_64-linux) RubyGems version 1.3.6 Rack version 1.3 -Rails version 4.0.0 +Rails version 4.1.0 JavaScript Runtime Node.js (V8) -Active Record version 4.0.0 -Action Pack version 4.0.0 -Action Mailer version 4.0.0 -Active Support version 4.0.0 +Active Record version 4.1.0 +Action Pack version 4.1.0 +Action View version 4.1.0 +Action Mailer version 4.1.0 +Active Support version 4.1.0 Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index bb2e8e906f..2f322d15da 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1490,8 +1490,8 @@ So first, we'll wire up the Post show template </p> <% end %> -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> +<%= link_to 'Back', posts_path %> +| <%= link_to 'Edit', edit_post_path(@post) %> ``` This adds a form on the `Post` show page that creates a new comment by diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 91d12b4432..fe6b1ad906 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -468,14 +468,24 @@ def initialize!(group=:default) #:nodoc: end ``` -As you can see, you can only initialize an app once. This is also where the initializers are run. +As you can see, you can only initialize an app once. The initializers are run through +the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb` -TODO: review this +```ruby +def run_initializers(group=:default, *args) + return if instance_variable_defined?(:@ran) + initializers.tsort_each do |initializer| + initializer.run(*args) if initializer.belongs_to?(group) + end + @ran = true +end +``` -The initializers code itself is tricky. What Rails is doing here is it -traverses all the class ancestors looking for an `initializers` method, -sorting them and running them. For example, the `Engine` class will make -all the engines available by providing the `initializers` method. +The run_initializers code itself is tricky. What Rails is doing here is +traversing all the class ancestors looking for those that respond to an +`initializers` method. It then sorts the ancestors by name, and runs them. +For example, the `Engine` class will make all the engines available by +providing an `initializers` method on them. The `Rails::Application` class, as defined in `railties/lib/rails/application.rb` defines `bootstrap`, `railtie`, and `finisher` initializers. The `bootstrap` initializers diff --git a/guides/source/plugins.md b/guides/source/plugins.md index ca55ee0df2..d0aa2e55a2 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -34,9 +34,15 @@ different rails applications using RubyGems and Bundler if desired. Rails ships with a `rails plugin new` command which creates a - skeleton for developing any kind of Rails extension with the ability - to run integration tests using a dummy Rails application. See usage - and options by asking for help: +skeleton for developing any kind of Rails extension with the ability +to run integration tests using a dummy Rails application. Create your +plugin with the command: + +```bash +$ rails plugin new yaffle +``` + +See usage and options by asking for help: ```bash $ rails plugin --help diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 642c70fd9d..d53e0cd2bd 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -225,9 +225,13 @@ config.middleware.delete "Rack::MethodOverride" Much of Action Controller's functionality is implemented as Middlewares. The following list explains the purpose of each of them: + **`Rack::Sendfile`** + +* Sets server specific X-Sendfile header. Configure this via `config.action_dispatch.x_sendfile_header` option. + **`ActionDispatch::Static`** -* Used to serve static assets. Disabled if `config.serve_static_assets` is true. +* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`. **`Rack::Lock`** diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index a4babbe8c3..4e6bf899af 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,7 @@ +* Expose MiddlewareStack#unshift to environment configuration. + + *Ben Pickles* + * Include `web-console` into newly generated applications' Gemfile. *Genadi Samokovarov* diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index 20313a2608..dd70c272c6 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -1,4 +1,3 @@ -require 'rbconfig' require 'rails/app_rails_loader' # If we are inside a Rails application this method performs an exec and thus diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 485bd1eb09..4201dac42f 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -1,6 +1,7 @@ require 'fileutils' require 'optparse' require 'action_dispatch' +require 'rails' module Rails class Server < ::Rack::Server @@ -32,7 +33,7 @@ module Rails opt_parser.parse! args - options[:log_stdout] = options[:daemonize].blank? && options[:environment] == "development" + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" options[:server] = args.shift options end diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index c694513960..f5d7dede66 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -59,6 +59,10 @@ module Rails @operations << [__method__, args, block] end + def unshift(*args, &block) + @operations << [__method__, args, block] + end + def merge_into(other) #:nodoc: @operations.each do |operation, args, block| other.send(operation, *args, &block) diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 31a35a09bb..20d1d76d78 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -144,6 +144,12 @@ module ApplicationTests assert_equal "Rack::Config", middleware.second end + test 'unshift middleware' do + add_to_config 'config.middleware.unshift Rack::Config' + boot! + assert_equal 'Rack::Config', middleware.first + end + test "Rails.cache does not respond to middleware" do add_to_config "config.cache_store = :memory_store" boot! diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 20afca2618..ba688f1e9e 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -27,26 +27,62 @@ class Rails::ServerTest < ActiveSupport::TestCase end def test_environment_with_rails_env - with_rails_env 'production' do - server = Rails::Server.new - assert_equal 'production', server.options[:environment] + with_rack_env nil do + with_rails_env 'production' do + server = Rails::Server.new + assert_equal 'production', server.options[:environment] + end end end def test_environment_with_rack_env - with_rack_env 'production' do - server = Rails::Server.new - assert_equal 'production', server.options[:environment] + with_rails_env nil do + with_rack_env 'production' do + server = Rails::Server.new + assert_equal 'production', server.options[:environment] + end end end def test_log_stdout - args = ["-e", "development"] - options = Rails::Server::Options.new.parse!(args) - assert_equal true, options[:log_stdout] + with_rack_env nil do + with_rails_env nil do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] - args = ["-e", "production"] - options = Rails::Server::Options.new.parse!(args) - assert_equal false, options[:log_stdout] + args = ["-e", "development"] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] + + args = ["-e", "production"] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:log_stdout] + + with_rack_env 'development' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] + end + + with_rack_env 'production' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:log_stdout] + end + + with_rails_env 'development' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] + end + + with_rails_env 'production' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:log_stdout] + end + end + end end end diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb index 6223c85bbf..330fe150ca 100644 --- a/railties/test/env_helpers.rb +++ b/railties/test/env_helpers.rb @@ -1,7 +1,10 @@ +require 'rails' + module EnvHelpers private def with_rails_env(env) + Rails.instance_variable_set :@_env, nil switch_env 'RAILS_ENV', env do switch_env 'RACK_ENV', nil do yield @@ -10,6 +13,7 @@ module EnvHelpers end def with_rack_env(env) + Rails.instance_variable_set :@_env, nil switch_env 'RACK_ENV', env do switch_env 'RAILS_ENV', nil do yield |