diff options
54 files changed, 1118 insertions, 771 deletions
diff --git a/Rakefile b/Rakefile index 8c72312f28..1f3c770c77 100644..100755 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,7 @@ +#!/usr/bin/env rake gem 'rdoc', '>= 2.5.10' require 'rdoc' -require 'rake' require 'rdoc/task' require 'net/http' diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index cba6e93c8a..123ef9bbbf 100644..100755 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/actionpack/Rakefile b/actionpack/Rakefile index a6ce08113f..9030db9f7a 100644..100755 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 6fe1f4ece5..9ba37134b8 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -258,9 +258,8 @@ module ActionController #:nodoc: # nil if :not_acceptable was sent to the client. # def retrieve_response_from_mimes(mimes=nil, &block) - collector = Collector.new { default_render } mimes ||= collect_mimes_from_class_level - mimes.each { |mime| collector.send(mime) } + collector = Collector.new(mimes) { default_render } block.call(collector) if block_given? if format = request.negotiate_mime(collector.order) @@ -277,8 +276,9 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :order - def initialize(&block) + def initialize(mimes, &block) @order, @responses, @default_response = [], {}, block + mimes.each { |mime| send(mime) } end def any(*args, &block) diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 67cf08445d..d6f6ab1855 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -55,7 +55,7 @@ module ActionController end add :json do |json, options| - json = json.to_json(options) unless json.respond_to?(:to_str) + json = json.to_json(options) unless json.kind_of?(String) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? self.content_type ||= Mime::JSON self.response_body = json diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 2250cfa88a..08eab5634a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -81,7 +81,7 @@ module Mime class << self - TRAILING_STAR_REGEXP = /(\w+)\/\*/ + TRAILING_STAR_REGEXP = /(text|application)\/\*/ def lookup(string) LOOKUP[string] @@ -120,7 +120,11 @@ module Mime params, q = header.split(/;\s*q=/) if params params.strip! - list << AcceptItem.new(index, params, q) unless params.empty? + if params =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1).each { |m| list << AcceptItem.new(index, m.to_s, q) } + else + list << AcceptItem.new(index, params, q) unless params.empty? + end end end list.sort! @@ -177,6 +181,18 @@ module Mime keys = Mime::LOOKUP.keys.select{|k| k.include?(input)} Mime::LOOKUP.values_at(*keys).uniq end + + # This method is opposite of register method. + # + # Usage: + # + # Mime::Type.unregister("text/x-mobile", :mobile) + def unregister(string, symbol) + EXTENSION_LOOKUP.delete(symbol.to_s) + LOOKUP.delete(string) + symbol = symbol.to_s.upcase.intern + Mime.module_eval { remove_const(symbol) if const_defined?(symbol) } + end end def initialize(string, symbol = nil, synonyms = []) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 1f7633cbea..1e7054f381 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -52,8 +52,7 @@ module ActionDispatch # Returns a \host:\port string for this request, such as "example.com" or # "example.com:8080". def host_with_port - opt_port = optional_port ? ":#{optional_port}" : nil - "#{host}#{opt_port}" + "#{host}#{port_string}" end # Returns the port number of this request as an integer. @@ -80,12 +79,18 @@ module ActionDispatch port == standard_port end - # Returns a \port suffix like "8080" if the \port number of this request + # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. def optional_port standard_port? ? nil : port end + # Returns a string \port suffix, including colon, like ":8080" if the \port + # number of this request is not the default HTTP \port 80 or HTTPS \port 443. + def port_string + standard_port? ? '' : ":#{port}" + end + def server_port @env['SERVER_PORT'].to_i end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index fb1f0d3f0c..01826fcede 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -705,61 +705,61 @@ module ActionDispatch end private - def scope_options + def scope_options #:nodoc: @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym } end - def merge_path_scope(parent, child) + def merge_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_shallow_path_scope(parent, child) + def merge_shallow_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_as_scope(parent, child) + def merge_as_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_shallow_prefix_scope(parent, child) + def merge_shallow_prefix_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_module_scope(parent, child) + def merge_module_scope(parent, child) #:nodoc: parent ? "#{parent}/#{child}" : child end - def merge_controller_scope(parent, child) + def merge_controller_scope(parent, child) #:nodoc: child end - def merge_path_names_scope(parent, child) + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_constraints_scope(parent, child) + def merge_constraints_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_defaults_scope(parent, child) + def merge_defaults_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_blocks_scope(parent, child) + def merge_blocks_scope(parent, child) #:nodoc: merged = parent ? parent.dup : [] merged << child if child merged end - def merge_options_scope(parent, child) + def merge_options_scope(parent, child) #:nodoc: (parent || {}).except(*override_keys(child)).merge(child) end - def merge_shallow_scope(parent, child) + def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - def override_keys(child) + def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end end @@ -1196,7 +1196,7 @@ module ActionDispatch @scope[:scope_level_resource] end - def apply_common_behavior_for(method, resources, options, &block) + def apply_common_behavior_for(method, resources, options, &block) #:nodoc: if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true @@ -1226,23 +1226,23 @@ module ActionDispatch false end - def action_options?(options) + def action_options?(options) #:nodoc: options[:only] || options[:except] end - def scope_action_options? + def scope_action_options? #:nodoc: @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) end - def scope_action_options + def scope_action_options #:nodoc: @scope[:options].slice(:only, :except) end - def resource_scope? + def resource_scope? #:nodoc: [:resource, :resources].include?(@scope[:scope_level]) end - def resource_method_scope? + def resource_method_scope? #:nodoc: [:collection, :member, :new].include?(@scope[:scope_level]) end @@ -1268,7 +1268,7 @@ module ActionDispatch @scope[:scope_level_resource] = old_resource end - def resource_scope(resource) + def resource_scope(resource) #:nodoc: with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do scope(parent_resource.resource_scope) do yield @@ -1276,30 +1276,30 @@ module ActionDispatch end end - def nested_options + def nested_options #:nodoc: {}.tap do |options| options[:as] = parent_resource.member_name options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? end end - def id_constraint? + def id_constraint? #:nodoc: @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp) end - def id_constraint + def id_constraint #:nodoc: @scope[:constraints][:id] end - def canonical_action?(action, flag) + def canonical_action?(action, flag) #:nodoc: flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? + def shallow_scoping? #:nodoc: shallow? && @scope[:scope_level] == :member end - def path_for_action(action, path) + def path_for_action(action, path) #:nodoc: prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] @@ -1310,11 +1310,11 @@ module ActionDispatch end end - def action_path(name, path = nil) + def action_path(name, path = nil) #:nodoc: path || @scope[:path_names][name.to_sym] || name.to_s end - def prefix_name_for_action(as, action) + def prefix_name_for_action(as, action) #:nodoc: if as as.to_s elsif !canonical_action?(action, @scope[:scope_level]) @@ -1322,12 +1322,14 @@ module ActionDispatch end end - def name_for_action(as, action) + def name_for_action(as, action) #:nodoc: prefix = prefix_name_for_action(as, action) prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource + return nil if as.nil? && action.nil? + collection_name = parent_resource.collection_name member_name = parent_resource.member_name end @@ -1352,7 +1354,7 @@ module ActionDispatch end end - module Shorthand + module Shorthand #:nodoc: def match(*args) if args.size == 1 && args.last.is_a?(Hash) options = args.pop diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 6c3fc5126a..d4db78a25a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -119,8 +119,9 @@ module ActionDispatch # to split the domain from the host. # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ # to split the subdomain from the host. - # * <tt>:tld_length</tt> - Optionally specify the tld length (only used if :subdomain - # or :domain are supplied). + # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if + # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to + # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" 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 15f8e10b7f..e52797042f 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 @@ -79,7 +79,6 @@ module ActionView sources.each do |source| asset_file_path!(path_to_asset(source, false)) end - return sources end def collect_asset_files(*path) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 6c6d659246..831a19654e 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -184,6 +184,11 @@ module ActionView end end + # Used to store template data by template handlers. + def data + @data ||= {} + end + def inspect @inspect ||= if defined?(Rails.root) diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index 0da051822a..82969b2979 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -169,9 +169,6 @@ end class StarStarMimeControllerTest < ActionController::TestCase tests StarStarMimeController - def setup; super; end - def teardown; super; end - def test_javascript_with_format @request.accept = "text/javascript" get :index, :format => 'js' @@ -204,10 +201,8 @@ class RespondToControllerTest < ActionController::TestCase def teardown super - Mime.module_eval { remove_const :IPHONE if const_defined?(:IPHONE) } - Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } - Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} - Mime::LOOKUP.reject!{|key,_| key == 'text/iphone'} + Mime::Type.unregister('text/x-mobile', :iphone) + Mime::Type.unregister('text/iphone', :mobile) end def test_html @@ -604,6 +599,17 @@ class InheritedRespondWithController < RespondWithController end end +class RenderJsonRespondWithController < RespondWithController + clear_respond_to + respond_to :json + + def index + respond_with(resource) do |format| + format.json { render :json => RenderJsonTestException.new('boom') } + end + end +end + class EmptyRespondWithController < ActionController::Base def index respond_with(Customer.new("david", 13)) @@ -620,10 +626,8 @@ class RespondWithControllerTest < ActionController::TestCase def teardown super - Mime.module_eval { remove_const :IPHONE if const_defined?(:IPHONE) } - Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } - Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} - Mime::LOOKUP.reject!{|key,_| key == 'text/iphone'} + Mime::Type.unregister('text/x-mobile', :iphone) + Mime::Type.unregister('text/iphone', :mobile) end def test_using_resource @@ -921,6 +925,13 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal "JSON", @response.body end + def test_render_json_object_responds_to_str_still_produce_json + @controller = RenderJsonRespondWithController.new + @request.accept = "application/json" + get :index, :format => :json + assert_equal %Q{{"message":"boom","error":"RenderJsonTestException"}}, @response.body + end + def test_no_double_render_is_raised @request.accept = "text/html" assert_raise ActionView::MissingTemplate do @@ -1009,10 +1020,8 @@ class MimeControllerLayoutsTest < ActionController::TestCase def teardown super - Mime.module_eval { remove_const :IPHONE if const_defined?(:IPHONE) } - Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } - Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} - Mime::LOOKUP.reject!{|key,_| key == 'text/iphone'} + Mime::Type.unregister('text/x-mobile', :iphone) + Mime::Type.unregister('text/iphone', :mobile) end def test_missing_layout_renders_properly diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 43123e68b2..9424d88498 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -12,6 +12,43 @@ class MimeTypeTest < ActiveSupport::TestCase end end + test "unregister" do + begin + Mime::Type.register("text/x-mobile", :mobile) + assert defined?(Mime::MOBILE) + assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile'] + assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile'] + + Mime::Type.unregister("text/x-mobile", :mobile) + assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined" + assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]" + assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]" + ensure + Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } + Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} + end + end + + test "parse text with trailing star at the beginning" do + accept = "text/*, text/html, application/json, multipart/form-data" + expect = [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::TEXT, Mime::YAML, Mime::JS, Mime::MULTIPART_FORM] + parsed = Mime::Type.parse(accept) + assert_equal expect.size, parsed.size + Range.new(0,expect.size-1).to_a.each do |index| + assert_equal expect[index], parsed[index], "Failed for index number #{index}" + end + end + + test "parse text with trailing star in the end" do + accept = "text/html, application/json, multipart/form-data, text/*" + expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::XML, Mime::ICS, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + parsed = Mime::Type.parse(accept) + assert_equal 10, parsed.size + Range.new(0,expect.size-1).to_a.each do |index| + assert_equal expect[index], parsed[index], "Failed for index number #{index}" + end + end + test "parse text with trailing star" do accept = "text/*" expect = [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT].sort_by(&:to_s) @@ -28,12 +65,6 @@ class MimeTypeTest < ActiveSupport::TestCase assert_equal expect, parsed.sort_by(&:to_s) end - test "parse image with trailing star" do - accept = "image/*" - parsed = Mime::Type.parse(accept) - assert parsed.include?(Mime::PNG) - end - test "parse without q" do accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*" expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL] @@ -68,8 +99,7 @@ class MimeTypeTest < ActiveSupport::TestCase assert_equal Mime::GIF, Mime::SET.last end ensure - Mime.module_eval { remove_const :GIF if const_defined?(:GIF) } - Mime::LOOKUP.reject!{|key,_| key == 'image/gif'} + Mime::Type.unregister('image/gif', :gif) end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index d140ea8358..8f672c1149 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -172,6 +172,14 @@ class RequestTest < ActiveSupport::TestCase assert_equal 8080, request.optional_port end + test "port string" do + request = stub_request 'HTTP_HOST' => 'www.example.org:80' + assert_equal '', request.port_string + + request = stub_request 'HTTP_HOST' => 'www.example.org:8080' + assert_equal ':8080', request.port_string + end + test "full path" do request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/path/of/some/uri', 'QUERY_STRING' => 'mapped=1' assert_equal "/path/of/some/uri?mapped=1", request.fullpath @@ -392,7 +400,7 @@ class RequestTest < ActiveSupport::TestCase mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" } request = nil begin - request = stub_request(mock_rack_env) + request = stub_request(mock_rack_env) request.parameters rescue TypeError => e # rack will raise a TypeError when parsing this query string diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 0ac8c249cb..bbd010ea6d 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -155,6 +155,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :replies do + collection do + get 'page/:page' => 'replies#index', :page => %r{\d+} + get ':page' => 'replies#index', :page => %r{\d+} + end + new do post :preview end @@ -1241,6 +1246,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper + with_test_routes do + assert_equal '/replies', replies_path + end + end + def test_scoped_controller_with_namespace_and_action with_test_routes do assert_equal '/account/twitter/callback', account_callback_path("twitter") diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index ae0c38184d..bd18cdc1b8 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -194,3 +194,9 @@ class ArelLike a.each { |i| yield i } end end + +class RenderJsonTestException < Exception + def to_json(options = nil) + return { :error => self.class.name, :message => self.to_str }.to_json + end +end diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index 6d3b26e131..c9dd27cf2a 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -180,6 +180,16 @@ class LookupContextTest < ActiveSupport::TestCase assert_not_equal template, old_template end + + test "data can be stored in cached templates" do + template = @lookup_context.find("hello_world", "test") + template.data["cached"] = "data" + assert_equal "Hello world!", template.source + + template = @lookup_context.find("hello_world", "test") + assert_equal "data", template.data["cached"] + assert_equal "Hello world!", template.source + end end class LookupContextWithFalseCaching < ActiveSupport::TestCase @@ -205,7 +215,7 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase assert_equal "Bar", template.source end - test "if no template was found in the second lookup, give it higher preference" do + test "if no template was found in the second lookup, with no cache, raise error" do template = @lookup_context.find("foo", "test", true) assert_equal "Foo", template.source @@ -215,7 +225,7 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase end end - test "if no template was cached in the first lookup, do not use the cache in the second" do + test "if no template was cached in the first lookup, retrieval should work in the second call" do @resolver.hash.clear assert_raise ActionView::MissingTemplate do @lookup_context.find("foo", "test", true) @@ -225,4 +235,19 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase template = @lookup_context.find("foo", "test", true) assert_equal "Foo", template.source end + + test "data can be stored as long as template was not updated" do + template = @lookup_context.find("foo", "test", true) + template.data["cached"] = "data" + assert_equal "Foo", template.source + + template = @lookup_context.find("foo", "test", true) + assert_equal "data", template.data["cached"] + assert_equal "Foo", template.source + + @resolver.hash["test/_foo.erb"][1] = Time.now.utc + template = @lookup_context.find("foo", "test", true) + assert_nil template.data["cached"] + assert_equal "Foo", template.source + end end
\ No newline at end of file diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 0372c7a03e..0372c7a03e 100644..100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index a3e3051b96..f46db909ba 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,16 @@ *Rails 3.1.0 (unreleased)* +* ActiveRecord::Base#dup and ActiveRecord::Base#clone semantics have changed +to closer match normal Ruby dup and clone semantics. + +* Calling ActiveRecord::Base#clone will result in a shallow copy of the record, +including copying the frozen state. No callbacks will be called. + +* Calling ActiveRecord::Base#dup will duplicate the record, including calling +after initialize hooks. Frozen state will not be copied, and all associations +will be cleared. A duped record will return true for new_record?, have a nil +id field, and is saveable. + * Migrations can be defined as reversible, meaning that the migration system will figure out how to reverse your migration. To use reversible migrations, just define the "change" method. For example: diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 395c72dfbc..f9b77c1799 100644..100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7da38cd03f..0d9171d876 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -5,6 +5,7 @@ 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' +require 'active_record/associations/class_methods/join_dependency' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -1832,580 +1833,6 @@ module ActiveRecord Array.wrap(extensions) end end - - class JoinDependency # :nodoc: - attr_reader :join_parts, :reflections, :table_aliases - - def initialize(base, associations, joins) - @join_parts = [JoinBase.new(base, joins)] - @associations = {} - @reflections = [] - @table_aliases = Hash.new(0) - @table_aliases[base.table_name] = 1 - build(associations) - 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 - end - - def join_associations - join_parts.last(join_parts.length - 1) - end - - def join_base - join_parts.first - end - - def count_aliases_from_table_joins(name) - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase - join_sql = join_base.table_joins.to_s.downcase - join_sql.blank? ? 0 : - # Table names - join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + - # Table aliases - join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size - end - - def instantiate(rows) - primary_key = join_base.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.dup, model) - parent - }.uniq - - remove_duplicate_results!(join_base.active_record, records, @associations) - records - end - - def remove_duplicate_results!(base, records, associations) - case associations - when Symbol, String - reflection = base.reflections[associations] - remove_uniq_by_reflection(reflection, records) - when Array - associations.each do |association| - remove_duplicate_results!(base, records, association) - end - when Hash - associations.keys.each 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 - end - end - - remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? - end - end - end - - protected - - def cache_joined_association(association) - associations = [] - parent = association.parent - while parent != join_base - associations.unshift(parent.reflection.name) - parent = parent.parent - end - ref = @associations - associations.each do |key| - ref = ref[key] - end - ref[association.reflection.name] ||= {} - end - - def build(associations, parent = nil, join_type = Arel::InnerJoin) - parent ||= join_parts.last - case associations - when Symbol, String - reflection = parent.reflections[associations.to_s.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" - unless join_association = find_join_association(reflection, parent) - @reflections << reflection - join_association = build_join_association(reflection, parent) - join_association.join_type = 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.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - join_association = build(name, parent, join_type) - build(associations[name], join_association, join_type) - end - else - raise ConfigurationError, associations.inspect - 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 - } - end - - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end - end - - def build_join_association(reflection, parent) - JoinAssociation.new(reflection, self, parent) - end - - 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 |name, assoc| - association = construct(parent, name, join_parts, row) - construct(association, assoc, join_parts, row) if association - end - else - raise ConfigurationError, associations.inspect - end - end - - def construct_association(record, join_part, row) - return if record.id.to_s != join_part.parent.record_id(row).to_s - - macro = join_part.reflection.macro - if macro == :has_one - return if record.instance_variable_defined?("@#{join_part.reflection.name}") - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - set_target_and_inverse(join_part, association, record) - else - return if row[join_part.aliased_primary_key].nil? - association = join_part.instantiate(row) - case macro - when :has_many, :has_and_belongs_to_many - collection = record.send(join_part.reflection.name) - collection.loaded - collection.target.push(association) - collection.__send__(:set_inverse_instance, association, record) - when :belongs_to - set_target_and_inverse(join_part, association, record) - else - raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" - end - end - association - end - - def set_target_and_inverse(join_part, association, record) - association_proxy = record.send("set_#{join_part.reflection.name}_target", association) - association_proxy.__send__(:set_inverse_instance, association, record) - end - - # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited - # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which - # everything else is being joined onto. A JoinAssociation represents an association which - # is joining to the base. A JoinAssociation may result in more than one actual join - # 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: - # 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 :active_record - - delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record - - def initialize(active_record) - @active_record = active_record - @cached_record = {} - end - - def ==(other) - raise NotImplementedError - end - - # An Arel::Table for the active_record - def table - raise NotImplementedError - end - - # The prefix to be used when aliasing columns in the active_record's table - def aliased_prefix - raise NotImplementedError - end - - # The alias for the active_record's table - def aliased_table_name - raise NotImplementedError - end - - # The alias for the primary key of the active_record's table - def aliased_primary_key - "#{aliased_prefix}_r0" - end - - # An array of [column_name, alias] pairs for the table - def column_names_with_alias - unless defined?(@column_names_with_alias) - @column_names_with_alias = [] - - ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] - end - end - - @column_names_with_alias - end - - def extract_record(row) - Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] - end - - def record_id(row) - row[aliased_primary_key] - end - - def instantiate(row) - @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) - end - end - - class JoinBase < JoinPart # :nodoc: - # Extra joins provided when the JoinDependency was created - attr_reader :table_joins - - def initialize(active_record, joins = nil) - super(active_record) - @table_joins = joins - end - - def ==(other) - other.class == self.class && - other.active_record == active_record - end - - def aliased_prefix - "t0" - end - - def table - Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) - end - - def aliased_table_name - active_record.table_name - end - end - - class JoinAssociation < JoinPart # :nodoc: - # 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 - - # These implement abstract methods from the superclass - attr_reader :aliased_prefix, :aliased_table_name - - delegate :options, :through_reflection, :source_reflection, :to => :reflection - delegate :table, :table_name, :to => :parent, :prefix => true - - def initialize(reflection, join_dependency, parent = nil) - reflection.check_validity! - - if reflection.options[:polymorphic] - raise EagerLoadPolymorphicError.new(reflection) - end - - super(reflection.klass) - - @reflection = reflection - @join_dependency = join_dependency - @parent = parent - @join_type = Arel::InnerJoin - - # This must be done eagerly upon initialisation because the alias which is produced - # depends on the state of the join dependency, but we want it to work the same way - # every time. - allocate_aliases - end - - 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| - self.parent == join_part - end - end - - def join_to(relation) - send("join_#{reflection.macro}_to", relation) - end - - def join_relation(joining_relation) - self.join_type = Arel::OuterJoin - joining_relation.joins(self) - end - - def table - @table ||= Arel::Table.new( - table_name, :as => aliased_table_name, - :engine => arel_engine, :columns => active_record.columns - ) - end - - # More semantic name given we are talking about associations - alias_method :target_table, :table - - protected - - def aliased_table_name_for(name, suffix = nil) - if @join_dependency.table_aliases[name].zero? - @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) - end - - if !@join_dependency.table_aliases[name].zero? # We need an alias - name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - @join_dependency.table_aliases[name] += 1 - if @join_dependency.table_aliases[name] == 1 # First time we've seen this name - # Also need to count the aliases from the table_aliases to avoid incorrect count - @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) - end - table_index = @join_dependency.table_aliases[name] - name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 - else - @join_dependency.table_aliases[name] += 1 - end - - name - end - - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name - end - - def interpolate_sql(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - - private - - def allocate_aliases - @aliased_prefix = "t#{ join_dependency.join_parts.size }" - @aliased_table_name = aliased_table_name_for(table_name) - - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] - @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") - end - end - - def process_conditions(conditions, table_name) - Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) - end - - def join_target_table(relation, *conditions) - relation = relation.join(target_table, join_type) - - # If the target table is an STI model then we must be sure to only include records of - # its type and its sub-types. - unless active_record.descends_from_active_record? - sti_column = target_table[active_record.inheritance_column] - - sti_condition = sti_column.eq(active_record.sti_name) - active_record.descendants.each do |subclass| - sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) - end - - conditions << sti_condition - end - - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) - end - - relation = relation.on(*conditions) - end - - def join_has_and_belongs_to_many_to(relation) - join_table = Arel::Table.new( - options[:join_table], :engine => arel_engine, - :as => @aliased_join_table_name - ) - - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key - - relation = relation.join(join_table, join_type) - relation = relation.on( - join_table[fk]. - eq(parent_table[reflection.active_record.primary_key]) - ) - - join_target_table( - relation, - target_table[reflection.klass.primary_key]. - eq(join_table[klass_fk]) - ) - end - - def join_has_many_to(relation) - if reflection.options[:through] - join_has_many_through_to(relation) - elsif reflection.options[:as] - join_has_many_polymorphic_to(relation) - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - primary_key = options[:primary_key] || parent.primary_key - - join_target_table( - relation, - target_table[foreign_key]. - eq(parent_table[primary_key]) - ) - end - end - alias :join_has_one_to :join_has_many_to - - def join_has_many_through_to(relation) - join_table = Arel::Table.new( - through_reflection.klass.table_name, :engine => arel_engine, - :as => @aliased_join_table_name - ) - - jt_conditions = [] - jt_foreign_key = first_key = second_key = nil - - if through_reflection.options[:as] # has_many :through against a polymorphic join - as_key = through_reflection.options[:as].to_s - jt_foreign_key = as_key + '_id' - - jt_conditions << - join_table[as_key + '_type']. - eq(parent.active_record.base_class.name) - else - jt_foreign_key = through_reflection.primary_key_name - end - - case source_reflection.macro - when :has_many - second_key = options[:foreign_key] || primary_key - - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - end - - unless through_reflection.klass.descends_from_active_record? - jt_conditions << - join_table[through_reflection.active_record.inheritance_column]. - eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - - jt_conditions << - join_table[reflection.source_reflection.options[:foreign_type]]. - eq(reflection.options[:source_type]) - else - second_key = source_reflection.primary_key_name - end - end - - jt_conditions << - parent_table[parent.primary_key]. - eq(join_table[jt_foreign_key]) - - if through_reflection.options[:conditions] - jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) - end - - relation = relation.join(join_table, join_type).on(*jt_conditions) - - join_target_table( - relation, - target_table[first_key].eq(join_table[second_key]) - ) - end - - def join_has_many_polymorphic_to(relation) - join_target_table( - relation, - target_table["#{reflection.options[:as]}_id"]. - eq(parent_table[parent.primary_key]), - target_table["#{reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name) - ) - end - - def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.primary_key_name - primary_key = options[:primary_key] || reflection.klass.primary_key - - join_target_table( - relation, - target_table[primary_key].eq(parent_table[foreign_key]) - ) - end - end - end end end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 774103342a..ba9373ba6a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -235,12 +235,12 @@ module ActiveRecord # Removes all records from this association. Returns +self+ so method calls may be chained. def clear - return self if length.zero? # forces load_target if it hasn't happened already - - if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy - destroy_all - else - delete_all + unless length.zero? # forces load_target if it hasn't happened already + if @reflection.options[:dependent] == :destroy + destroy_all + else + delete_all + end end self @@ -357,8 +357,7 @@ module ActiveRecord return false unless record.is_a?(@reflection.klass) return include_in_memory?(record) unless record.persisted? load_target if @reflection.options[:finder_sql] && !loaded? - return @target.include?(record) if loaded? - exists?(record) + loaded? ? @target.include?(record) : exists?(record) end def proxy_respond_to?(method, include_private = false) diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb new file mode 100644 index 0000000000..6ab7bd0b06 --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -0,0 +1,225 @@ +require 'active_record/associations/class_methods/join_dependency/join_part' +require 'active_record/associations/class_methods/join_dependency/join_base' +require 'active_record/associations/class_methods/join_dependency/join_association' + +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + attr_reader :join_parts, :reflections, :table_aliases + + def initialize(base, associations, joins) + @join_parts = [JoinBase.new(base, joins)] + @associations = {} + @reflections = [] + @table_aliases = Hash.new(0) + @table_aliases[base.table_name] = 1 + build(associations) + 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 + end + + def join_associations + join_parts.last(join_parts.length - 1) + end + + def join_base + join_parts.first + end + + def columns + join_parts.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) + } + }.flatten + end + + def count_aliases_from_table_joins(name) + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase + join_sql = join_base.table_joins.to_s.downcase + join_sql.blank? ? 0 : + # Table names + join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + + # Table aliases + join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size + end + + def instantiate(rows) + primary_key = join_base.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.dup, model) + parent + }.uniq + + remove_duplicate_results!(join_base.active_record, records, @associations) + records + end + + def remove_duplicate_results!(base, records, associations) + case associations + when Symbol, String + reflection = base.reflections[associations] + remove_uniq_by_reflection(reflection, records) + when Array + associations.each do |association| + remove_duplicate_results!(base, records, association) + end + when Hash + associations.keys.each 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 + end + end + + remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? + end + end + end + + protected + + def cache_joined_association(association) + associations = [] + parent = association.parent + while parent != join_base + associations.unshift(parent.reflection.name) + parent = parent.parent + end + ref = @associations + associations.each do |key| + ref = ref[key] + end + ref[association.reflection.name] ||= {} + end + + def build(associations, parent = nil, join_type = Arel::InnerJoin) + parent ||= join_parts.last + case associations + when Symbol, String + reflection = parent.reflections[associations.to_s.intern] or + raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" + unless join_association = find_join_association(reflection, parent) + @reflections << reflection + join_association = build_join_association(reflection, parent) + join_association.join_type = 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.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| + join_association = build(name, parent, join_type) + build(associations[name], join_association, join_type) + end + else + raise ConfigurationError, associations.inspect + 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 + } + end + + def remove_uniq_by_reflection(reflection, records) + if reflection && reflection.collection? + records.each { |record| record.send(reflection.name).target.uniq! } + end + end + + def build_join_association(reflection, parent) + JoinAssociation.new(reflection, self, parent) + end + + 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 |name, assoc| + association = construct(parent, name, join_parts, row) + construct(association, assoc, join_parts, row) if association + end + else + raise ConfigurationError, associations.inspect + end + end + + def construct_association(record, join_part, row) + return if record.id.to_s != join_part.parent.record_id(row).to_s + + macro = join_part.reflection.macro + if macro == :has_one + return if record.instance_variable_defined?("@#{join_part.reflection.name}") + association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? + set_target_and_inverse(join_part, association, record) + else + return if row[join_part.aliased_primary_key].nil? + association = join_part.instantiate(row) + case macro + when :has_many, :has_and_belongs_to_many + collection = record.send(join_part.reflection.name) + collection.loaded + collection.target.push(association) + collection.__send__(:set_inverse_instance, association, record) + when :belongs_to + set_target_and_inverse(join_part, association, record) + else + raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + end + end + association + end + + def set_target_and_inverse(join_part, association, record) + association_proxy = record.send("set_#{join_part.reflection.name}_target", association) + association_proxy.__send__(:set_inverse_instance, association, record) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb new file mode 100644 index 0000000000..5e5c01c77a --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -0,0 +1,278 @@ +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + class JoinAssociation < JoinPart # :nodoc: + # 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 + + # These implement abstract methods from the superclass + attr_reader :aliased_prefix, :aliased_table_name + + delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :table, :table_name, :to => :parent, :prefix => true + + def initialize(reflection, join_dependency, parent = nil) + reflection.check_validity! + + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) + end + + super(reflection.klass) + + @reflection = reflection + @join_dependency = join_dependency + @parent = parent + @join_type = Arel::InnerJoin + + # This must be done eagerly upon initialisation because the alias which is produced + # depends on the state of the join dependency, but we want it to work the same way + # every time. + allocate_aliases + end + + 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| + self.parent == join_part + end + end + + def join_to(relation) + send("join_#{reflection.macro}_to", relation) + end + + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) + end + + def table + @table ||= Arel::Table.new( + table_name, :as => aliased_table_name, + :engine => arel_engine, :columns => active_record.columns + ) + end + + # More semantic name given we are talking about associations + alias_method :target_table, :table + + protected + + def aliased_table_name_for(name, suffix = nil) + if @join_dependency.table_aliases[name].zero? + @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) + end + + if !@join_dependency.table_aliases[name].zero? # We need an alias + name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" + @join_dependency.table_aliases[name] += 1 + if @join_dependency.table_aliases[name] == 1 # First time we've seen this name + # Also need to count the aliases from the table_aliases to avoid incorrect count + @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) + end + table_index = @join_dependency.table_aliases[name] + name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 + else + @join_dependency.table_aliases[name] += 1 + end + + name + end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + def interpolate_sql(sql) + instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) + end + + private + + def allocate_aliases + @aliased_prefix = "t#{ join_dependency.join_parts.size }" + @aliased_table_name = aliased_table_name_for(table_name) + + if reflection.macro == :has_and_belongs_to_many + @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") + elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] + @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") + end + end + + def process_conditions(conditions, table_name) + Arel.sql(interpolate_sql(sanitize_sql(conditions, table_name))) + end + + def join_target_table(relation, *conditions) + relation = relation.join(target_table, join_type) + + # If the target table is an STI model then we must be sure to only include records of + # its type and its sub-types. + unless active_record.descends_from_active_record? + sti_column = target_table[active_record.inheritance_column] + + sti_condition = sti_column.eq(active_record.sti_name) + active_record.descendants.each do |subclass| + sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) + end + + conditions << sti_condition + end + + # If the reflection has conditions, add them + if options[:conditions] + conditions << process_conditions(options[:conditions], aliased_table_name) + end + + relation = relation.on(*conditions) + end + + def join_has_and_belongs_to_many_to(relation) + join_table = Arel::Table.new( + options[:join_table], :engine => arel_engine, + :as => @aliased_join_table_name + ) + + fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key + klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key + + relation = relation.join(join_table, join_type) + relation = relation.on( + join_table[fk]. + eq(parent_table[reflection.active_record.primary_key]) + ) + + join_target_table( + relation, + target_table[reflection.klass.primary_key]. + eq(join_table[klass_fk]) + ) + end + + def join_has_many_to(relation) + if reflection.options[:through] + join_has_many_through_to(relation) + elsif reflection.options[:as] + join_has_many_polymorphic_to(relation) + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + primary_key = options[:primary_key] || parent.primary_key + + join_target_table( + relation, + target_table[foreign_key]. + eq(parent_table[primary_key]) + ) + end + end + alias :join_has_one_to :join_has_many_to + + def join_has_many_through_to(relation) + join_table = Arel::Table.new( + through_reflection.klass.table_name, :engine => arel_engine, + :as => @aliased_join_table_name + ) + + jt_conditions = [] + jt_foreign_key = first_key = second_key = nil + + if through_reflection.options[:as] # has_many :through against a polymorphic join + as_key = through_reflection.options[:as].to_s + jt_foreign_key = as_key + '_id' + + jt_conditions << + join_table[as_key + '_type']. + eq(parent.active_record.base_class.name) + else + jt_foreign_key = through_reflection.primary_key_name + end + + case source_reflection.macro + when :has_many + second_key = options[:foreign_key] || primary_key + + if source_reflection.options[:as] + first_key = "#{source_reflection.options[:as]}_id" + else + first_key = through_reflection.klass.base_class.to_s.foreign_key + end + + unless through_reflection.klass.descends_from_active_record? + jt_conditions << + join_table[through_reflection.active_record.inheritance_column]. + eq(through_reflection.klass.sti_name) + end + when :belongs_to + first_key = primary_key + + if reflection.options[:source_type] + second_key = source_reflection.association_foreign_key + + jt_conditions << + join_table[reflection.source_reflection.options[:foreign_type]]. + eq(reflection.options[:source_type]) + else + second_key = source_reflection.primary_key_name + end + end + + jt_conditions << + parent_table[parent.primary_key]. + eq(join_table[jt_foreign_key]) + + if through_reflection.options[:conditions] + jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) + end + + relation = relation.join(join_table, join_type).on(*jt_conditions) + + join_target_table( + relation, + target_table[first_key].eq(join_table[second_key]) + ) + end + + def join_has_many_polymorphic_to(relation) + join_target_table( + relation, + target_table["#{reflection.options[:as]}_id"]. + eq(parent_table[parent.primary_key]), + target_table["#{reflection.options[:as]}_type"]. + eq(parent.active_record.base_class.name) + ) + end + + def join_belongs_to_to(relation) + foreign_key = options[:foreign_key] || reflection.primary_key_name + primary_key = options[:primary_key] || reflection.klass.primary_key + + join_target_table( + relation, + target_table[primary_key].eq(parent_table[foreign_key]) + ) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb new file mode 100644 index 0000000000..ed05003f66 --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + class JoinBase < JoinPart # :nodoc: + # Extra joins provided when the JoinDependency was created + attr_reader :table_joins + + def initialize(active_record, joins = nil) + super(active_record) + @table_joins = joins + end + + def ==(other) + other.class == self.class && + other.active_record == active_record + end + + def aliased_prefix + "t0" + end + + def table + Arel::Table.new(table_name, :engine => arel_engine, :columns => active_record.columns) + end + + def aliased_table_name + active_record.table_name + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb new file mode 100644 index 0000000000..0b093b65e9 --- /dev/null +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb @@ -0,0 +1,80 @@ +module ActiveRecord + module Associations + module ClassMethods + class JoinDependency # :nodoc: + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # everything else is being joined onto. A JoinAssociation represents an association which + # is joining to the base. A JoinAssociation may result in more than one actual join + # 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: + # 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 :active_record + + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record + + def initialize(active_record) + @active_record = active_record + @cached_record = {} + @column_names_with_alias = nil + end + + def aliased_table + Arel::Nodes::TableAlias.new aliased_table_name, table + end + + def ==(other) + raise NotImplementedError + end + + # An Arel::Table for the active_record + def table + raise NotImplementedError + end + + # The prefix to be used when aliasing columns in the active_record's table + def aliased_prefix + raise NotImplementedError + end + + # The alias for the active_record's table + def aliased_table_name + raise NotImplementedError + end + + # The alias for the primary key of the active_record's table + def aliased_primary_key + "#{aliased_prefix}_r0" + end + + # An array of [column_name, alias] pairs for the table + def column_names_with_alias + unless @column_names_with_alias + @column_names_with_alias = [] + + ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| + @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] + end + end + @column_names_with_alias + end + + def extract_record(row) + Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] + end + + def record_id(row) + row[aliased_primary_key] + end + + def instantiate(row) + @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index da742fa668..2c72fd0004 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 @@ -67,7 +67,7 @@ module ActiveRecord relation.insert(attributes) end - return true + true end def delete_records(records) @@ -80,7 +80,7 @@ module ActiveRecord ).delete end end - + def construct_joins "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 23831e0b08..685d818ab3 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -42,7 +42,7 @@ module ActiveRecord # documented side-effect of the method that may avoid an extra SELECT. @target ||= [] and loaded if count == 0 - @reflection.options[:limit] ? [@reflection.options[:limit], count].min : count + [@reflection.options[:limit], count].compact.min end def has_cached_counter? @@ -108,7 +108,7 @@ module ActiveRecord end def we_can_set_the_inverse_on_this?(record) - !@reflection.inverse_of.nil? + @reflection.inverse_of end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9e15784f61..9b09b14c87 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1382,30 +1382,6 @@ MSG result end - # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone - # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is - # application specific and is therefore left to the application to implement according to its need. - def initialize_copy(other) - _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - cloned_attributes.delete(self.class.primary_key) - - @attributes = cloned_attributes - - @changed_attributes = {} - attributes_from_column_definition.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) - end - - clear_aggregation_cache - clear_association_cache - @attributes_cache = {} - @persisted = false - ensure_proper_type - - populate_with_current_scope_attributes - end - # Initialize an empty model object from +coder+. +coder+ must contain # the attributes necessary for initializing an empty model object. For # example: @@ -1615,11 +1591,42 @@ MSG @attributes.frozen? end - # Returns duplicated record with unfreezed attributes. - def dup - obj = super - obj.instance_variable_set('@attributes', @attributes.dup) - obj + # Backport dup from 1.9 so that initialize_dup() gets called + unless Object.respond_to?(:initialize_dup) + def dup # :nodoc: + copy = super + copy.initialize_dup(self) + copy + end + end + + # Duped objects have no id assigned and are treated as new records. Note + # that this is a "shallow" copy as it copies the object's attributes + # only, not its associations. The extent of a "deep" copy is application + # specific and is therefore left to the application to implement according + # to its need. + # The dup method does not preserve the timestamps (created|updated)_(at|on). + def initialize_dup(other) + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + cloned_attributes.delete(self.class.primary_key) + + @attributes = cloned_attributes + + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) + + @changed_attributes = {} + attributes_from_column_definition.each do |attr, orig_value| + @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + end + + clear_aggregation_cache + clear_association_cache + @attributes_cache = {} + @persisted = false + + ensure_proper_type + populate_with_current_scope_attributes + clear_timestamp_attributes end # Returns +true+ if the record is read only. Records loaded through joins with piggy-back @@ -1826,6 +1833,16 @@ MSG create_with.each { |att,value| self.respond_to?(:"#{att}=") && self.send("#{att}=", value) } if create_with end end + + # Clear attributes and changed_attributes + def clear_timestamp_attributes + %w(created_at created_on updated_at updated_on).each do |attribute_name| + if has_attribute?(attribute_name) + self[attribute_name] = nil + changed_attributes.delete(attribute_name) + end + end + end end Base.class_eval do diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index ca9314ec99..cffa2387de 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,3 +1,4 @@ +require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/synchronization' diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 9e1a33a6bf..c0e1dda2bd 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -70,7 +70,7 @@ module ActiveRecord result[self.class.locking_column] ||= 0 end - return result + result end def update(attribute_names = @attributes.keys) #:nodoc: @@ -89,7 +89,7 @@ module ActiveRecord affected_rows = relation.where( relation.table[self.class.primary_key].eq(quoted_id).and( - relation.table[self.class.locking_column].eq(quote_value(previous_value)) + relation.table[lock_col].eq(quote_value(previous_value)) ) ).arel.update(arel_attributes_values(false, false, attribute_names)) @@ -111,8 +111,9 @@ module ActiveRecord if persisted? table = self.class.arel_table - predicate = table[self.class.primary_key].eq(id) - predicate = predicate.and(table[self.class.locking_column].eq(send(self.class.locking_column).to_i)) + lock_col = self.class.locking_column + predicate = table[self.class.primary_key].eq(id). + and(table[lock_col].eq(send(lock_col).to_i)) affected_rows = self.class.unscoped.where(predicate).delete_all diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 15f83a8579..050b521b6a 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -417,11 +417,8 @@ module ActiveRecord # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) - if has_destroy_flag?(attributes) && allow_destroy - record.mark_for_destruction - else - record.attributes = attributes.except(*UNASSIGNABLE_KEYS) - end + record.attributes = attributes.except(*UNASSIGNABLE_KEYS) + record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end # Determines if a hash contains a truthy _destroy key. diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 4192456447..23ae0b4325 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -202,7 +202,7 @@ module ActiveRecord end def construct_relation_for_association_find(join_dependency) - relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency)) + relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns) apply_join_dependency(relation, join_dependency) end @@ -349,17 +349,8 @@ module ActiveRecord end end - def column_aliases(join_dependency) - join_dependency.join_parts.collect { |join_part| - join_part.column_names_with_alias.collect{ |column_name, aliased_name| - "#{connection.quote_table_name join_part.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}" - } - }.flatten.join(", ") - end - def using_limitable_reflections?(reflections) reflections.none? { |r| r.collection? } end - end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 32c7d08daa..70d84619a1 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -26,7 +26,7 @@ module ActiveRecord when Range, Arel::Relation attribute.in(value) when ActiveRecord::Base - attribute.eq(value.quoted_id) + attribute.eq(Arel.sql(value.quoted_id)) when Class # FIXME: I think we need to deprecate this behavior attribute.eq(value.name) diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index b13cb2d7a2..fbf7121468 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -163,15 +163,15 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm.account = Account.find(:first) assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } - firm = Firm.find(:first).clone + firm = Firm.find(:first).dup firm.account = Account.find(:first) assert_queries(2) { firm.save! } - firm = Firm.find(:first).clone - firm.account = Account.find(:first).clone + firm = Firm.find(:first).dup + firm.account = Account.find(:first).dup assert_queries(2) { firm.save! } end - + def test_callbacks_firing_order_on_create eye = Eye.create(:iris_attributes => {:color => 'honey'}) assert_equal [true, false], eye.after_create_callbacks_stack diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 26f388ca46..c3ba1f0c35 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -669,64 +669,65 @@ class BasicsTest < ActiveRecord::TestCase assert_equal true, Topic.find(1).persisted? end - def test_clone + def test_dup topic = Topic.find(1) - cloned_topic = nil - assert_nothing_raised { cloned_topic = topic.clone } - assert_equal topic.title, cloned_topic.title - assert !cloned_topic.persisted? + duped_topic = nil + assert_nothing_raised { duped_topic = topic.dup } + assert_equal topic.title, duped_topic.title + assert !duped_topic.persisted? - # test if the attributes have been cloned + # test if the attributes have been duped topic.title = "a" - cloned_topic.title = "b" + duped_topic.title = "b" assert_equal "a", topic.title - assert_equal "b", cloned_topic.title + assert_equal "b", duped_topic.title - # test if the attribute values have been cloned + # test if the attribute values have been duped topic.title = {"a" => "b"} - cloned_topic = topic.clone - cloned_topic.title["a"] = "c" + duped_topic = topic.dup + duped_topic.title["a"] = "c" assert_equal "b", topic.title["a"] - # test if attributes set as part of after_initialize are cloned correctly - assert_equal topic.author_email_address, cloned_topic.author_email_address + # test if attributes set as part of after_initialize are duped correctly + assert_equal topic.author_email_address, duped_topic.author_email_address # test if saved clone object differs from original - cloned_topic.save - assert cloned_topic.persisted? - assert_not_equal cloned_topic.id, topic.id + duped_topic.save + assert duped_topic.persisted? + assert_not_equal duped_topic.id, topic.id - cloned_topic.reload + duped_topic.reload # FIXME: I think this is poor behavior, and will fix it with #5686 - assert_equal({'a' => 'c'}.to_s, cloned_topic.title) + assert_equal({'a' => 'c'}.to_s, duped_topic.title) end - def test_clone_with_aggregate_of_same_name_as_attribute + def test_dup_with_aggregate_of_same_name_as_attribute dev = DeveloperWithAggregate.find(1) assert_kind_of DeveloperSalary, dev.salary - clone = nil - assert_nothing_raised { clone = dev.clone } - assert_kind_of DeveloperSalary, clone.salary - assert_equal dev.salary.amount, clone.salary.amount - assert !clone.persisted? + dup = nil + assert_nothing_raised { dup = dev.dup } + assert_kind_of DeveloperSalary, dup.salary + assert_equal dev.salary.amount, dup.salary.amount + assert !dup.persisted? - # test if the attributes have been cloned - original_amount = clone.salary.amount + # test if the attributes have been dupd + original_amount = dup.salary.amount dev.salary.amount = 1 - assert_equal original_amount, clone.salary.amount + assert_equal original_amount, dup.salary.amount - assert clone.save - assert clone.persisted? - assert_not_equal clone.id, dev.id + assert dup.save + assert dup.persisted? + assert_not_equal dup.id, dev.id end - def test_clone_does_not_clone_associations + def test_dup_does_not_copy_associations author = authors(:david) assert_not_equal [], author.posts + author.send(:clear_association_cache) - author_clone = author.clone - assert_equal [], author_clone.posts + author_dup = author.dup + assert_equal [], author_dup.posts end def test_clone_preserves_subtype @@ -765,22 +766,22 @@ class BasicsTest < ActiveRecord::TestCase assert !cloned_developer.salary_changed? # ... and cloned instance should behave same end - def test_clone_of_saved_object_marks_attributes_as_dirty + def test_dup_of_saved_object_marks_attributes_as_dirty developer = Developer.create! :name => 'Bjorn', :salary => 100000 assert !developer.name_changed? assert !developer.salary_changed? - cloned_developer = developer.clone + cloned_developer = developer.dup assert cloned_developer.name_changed? # both attributes differ from defaults assert cloned_developer.salary_changed? end - def test_clone_of_saved_object_marks_as_dirty_only_changed_attributes + def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes developer = Developer.create! :name => 'Bjorn' assert !developer.name_changed? # both attributes of saved object should be threated as not changed assert !developer.salary_changed? - cloned_developer = developer.clone + cloned_developer = developer.dup assert cloned_developer.name_changed? # ... but on cloned object should be assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be threated as not changed on cloned instance end @@ -1443,10 +1444,6 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = original_logger end - def test_dup - assert !Minimalistic.new.freeze.dup.frozen? - end - def test_compute_type_success assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') end diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb new file mode 100644 index 0000000000..d91646efca --- /dev/null +++ b/activerecord/test/cases/clone_test.rb @@ -0,0 +1,33 @@ +require "cases/helper" +require 'models/topic' + +module ActiveRecord + class CloneTest < ActiveRecord::TestCase + fixtures :topics + + def test_persisted + topic = Topic.first + cloned = topic.clone + assert topic.persisted?, 'topic persisted' + assert cloned.persisted?, 'topic persisted' + assert !cloned.new_record?, 'topic is not new' + end + + def test_stays_frozen + topic = Topic.first + topic.freeze + + cloned = topic.clone + assert cloned.persisted?, 'topic persisted' + assert !cloned.new_record?, 'topic is not new' + assert cloned.frozen?, 'topic should be frozen' + end + + def test_shallow + topic = Topic.first + cloned = topic.clone + topic.author_name = 'Aaron' + assert_equal 'Aaron', cloned.author_name + end + end +end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index b1a54af192..a6738fb654 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -338,13 +338,13 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end - def test_cloned_objects_should_not_copy_dirty_flag_from_creator + def test_dup_objects_should_not_copy_dirty_flag_from_creator pirate = Pirate.create!(:catchphrase => "shiver me timbers") - pirate_clone = pirate.clone - pirate_clone.reset_catchphrase! + pirate_dup = pirate.dup + pirate_dup.reset_catchphrase! pirate.catchphrase = "I love Rum" assert pirate.catchphrase_changed? - assert !pirate_clone.catchphrase_changed? + assert !pirate_dup.catchphrase_changed? end def test_reverted_changes_are_not_dirty diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb new file mode 100644 index 0000000000..0236f9b0a1 --- /dev/null +++ b/activerecord/test/cases/dup_test.rb @@ -0,0 +1,103 @@ +require "cases/helper" +require 'models/topic' + +module ActiveRecord + class DupTest < ActiveRecord::TestCase + fixtures :topics + + def test_dup + assert !Topic.new.freeze.dup.frozen? + end + + def test_not_readonly + topic = Topic.first + + duped = topic.dup + assert !duped.readonly?, 'should not be readonly' + end + + def test_is_readonly + topic = Topic.first + topic.readonly! + + duped = topic.dup + assert duped.readonly?, 'should be readonly' + end + + def test_dup_not_persisted + topic = Topic.first + duped = topic.dup + + assert !duped.persisted?, 'topic not persisted' + assert duped.new_record?, 'topic is new' + end + + def test_dup_has_no_id + topic = Topic.first + duped = topic.dup + assert_nil duped.id + end + + def test_dup_with_modified_attributes + topic = Topic.first + topic.author_name = 'Aaron' + duped = topic.dup + assert_equal 'Aaron', duped.author_name + end + + def test_dup_with_changes + dbtopic = Topic.first + topic = Topic.new + + topic.attributes = dbtopic.attributes + + #duped has no timestamp values + duped = dbtopic.dup + + #clear topic timestamp values + topic.send(:clear_timestamp_attributes) + + assert_equal topic.changes, duped.changes + end + + def test_dup_topics_are_independent + topic = Topic.first + topic.author_name = 'Aaron' + duped = topic.dup + + duped.author_name = 'meow' + + assert_not_equal topic.changes, duped.changes + end + + def test_dup_attributes_are_independent + topic = Topic.first + duped = topic.dup + + duped.author_name = 'meow' + topic.author_name = 'Aaron' + + assert_equal 'Aaron', topic.author_name + assert_equal 'meow', duped.author_name + end + + def test_dup_timestamps_are_cleared + topic = Topic.first + assert_not_nil topic.updated_at + assert_not_nil topic.created_at + + # temporary change to the topic object + topic.updated_at -= 3.days + + #dup should not preserve the timestamps if present + new_topic = topic.dup + assert_nil new_topic.updated_at + assert_nil new_topic.created_at + + new_topic.save + assert_not_nil new_topic.updated_at + assert_not_nil new_topic.created_at + end + + end +end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 31679b2efe..c3da9cdf53 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -16,7 +16,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_class_with_blank_sti_name company = Company.find(:first) - company = company.clone + company = company.dup company.extend(Module.new { def read_attribute(name) return ' ' if name == 'type' diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 4ddcdc010b..f9678cb0c5 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -1,3 +1,4 @@ +require 'thread' require "cases/helper" require 'models/person' require 'models/reader' diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 92af53d56f..fb6a239545 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -827,7 +827,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase fixtures :owners, :pets def setup - Owner.accepts_nested_attributes_for :pets + Owner.accepts_nested_attributes_for :pets, :allow_destroy => true @owner = owners(:ashley) @pet1, @pet2 = pets(:chew), pets(:mochi) @@ -844,6 +844,19 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase @owner.update_attributes(@params) assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name) end + + def test_attr_accessor_of_child_should_be_value_provided_during_update_attributes + @owner = owners(:ashley) + @pet1 = pets(:chew) + assert_equal nil, $current_user + attributes = {:pets_attributes => { "1"=> { :id => @pet1.id, + :name => "Foo2", + :current_user => "John", + :_destroy=>true }}} + @owner.update_attributes(attributes) + assert_equal 'John', $after_destroy_callback_output + end + end class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 535bcd4396..1682f34a1d 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -14,11 +14,18 @@ require 'models/bird' require 'models/car' require 'models/engine' require 'models/tyre' +require 'models/minivan' class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, - :tags, :taggings, :cars + :tags, :taggings, :cars, :minivans + + def test_do_not_double_quote_string_id + van = Minivan.last + assert van + assert_equal van.id, Minivan.where(:minivan_id => van).to_a.first.minivan_id + end def test_bind_values relation = Post.scoped diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index a8bf94dd86..570db4c8d5 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -1,5 +1,13 @@ class Pet < ActiveRecord::Base + + attr_accessor :current_user + set_primary_key :pet_id belongs_to :owner, :touch => true has_many :toys + + after_destroy do |record| + $after_destroy_callback_output = record.current_user + end + end diff --git a/activeresource/Rakefile b/activeresource/Rakefile index 905241acc0..cf01bc1389 100644..100755 --- a/activeresource/Rakefile +++ b/activeresource/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/activesupport/Rakefile b/activesupport/Rakefile index d117ca6356..d117ca6356 100644..100755 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile diff --git a/activesupport/lib/active_support/core_ext/module/synchronization.rb b/activesupport/lib/active_support/core_ext/module/synchronization.rb index 38ce55f26e..ed16c2f71b 100644 --- a/activesupport/lib/active_support/core_ext/module/synchronization.rb +++ b/activesupport/lib/active_support/core_ext/module/synchronization.rb @@ -1,3 +1,4 @@ +require 'thread' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/array/extract_options' diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 320f5c1c92..6a344867ee 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -140,11 +140,10 @@ module ActiveSupport end def convert_value(value) - case value - when Hash + if value.class == Hash self.class.new_from_hash_copying_default(value) - when Array - value.dup.replace(value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e }) + elsif value.is_a?(Array) + value.dup.replace(value.map { |e| convert_value(e) }) else value end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 370f26b0d7..74223dd7f2 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -12,6 +12,9 @@ class HashExtTest < Test::Unit::TestCase class SubclassingArray < Array end + class SubclassingHash < Hash + end + def setup @strings = { 'a' => 1, 'b' => 2 } @symbols = { :a => 1, :b => 2 } @@ -105,6 +108,11 @@ class HashExtTest < Test::Unit::TestCase assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! end + def test_hash_subclass + flash = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of SubclassingHash, flash["foo"] + end + def test_indifferent_assorted @strings = @strings.with_indifferent_access @symbols = @symbols.with_indifferent_access diff --git a/activesupport/test/core_ext/module/synchronization_test.rb b/activesupport/test/core_ext/module/synchronization_test.rb index eb850893f0..6c407e2260 100644 --- a/activesupport/test/core_ext/module/synchronization_test.rb +++ b/activesupport/test/core_ext/module/synchronization_test.rb @@ -1,3 +1,4 @@ +require 'thread' require 'abstract_unit' require 'active_support/core_ext/class/attribute_accessors' diff --git a/railties/Rakefile b/railties/Rakefile index 9504661c0e..5137bee761 100644..100755 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/gempackagetask' diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index d83cafc3be..4dc1023f1f 100644..100755 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -1,7 +1,7 @@ +#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) -require 'rake' <%= app_const %>.load_tasks diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt index 733e321c94..77149ae351 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt @@ -1,4 +1,4 @@ -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' require 'rake/rdoctask' diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile index 88f50f9f04..12350309bf 100644..100755 --- a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile @@ -1,12 +1,10 @@ -# encoding: UTF-8 -require 'rubygems' +#!/usr/bin/env rake begin require 'bundler/setup' rescue LoadError puts 'You must `gem install bundler` and `bundle install` to run rake tasks' end -require 'rake' require 'rake/rdoctask' Rake::RDocTask.new(:rdoc) do |rdoc| |