From 41efd73887c00ffd228b05d9346ec47a1f3759b9 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Sat, 6 Sep 2008 21:01:45 -0700 Subject: Revert "Raise UnknownAttributeError when unknown attributes are supplied via mass assignment" This reverts commit 108db00aa90fe266564483ab301cf0669cae600f. --- activerecord/lib/active_record/base.rb | 10 +--------- activerecord/test/cases/base_test.rb | 8 -------- 2 files changed, 1 insertion(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 907495a416..bca2db70f8 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -122,10 +122,6 @@ module ActiveRecord #:nodoc: class MissingAttributeError < NoMethodError end - # Raised when unknown attributes are supplied via mass assignment. - class UnknownAttributeError < NoMethodError - end - # Raised when an error occurred while doing a mass assignment to an attribute through the # attributes= method. The exception has an +attribute+ property that is the name of the # offending attribute. @@ -2441,11 +2437,7 @@ module ActiveRecord #:nodoc: attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes attributes.each do |k, v| - if k.include?("(") - multi_parameter_attributes << [ k, v ] - else - respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") - end + k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v) end assign_multiparameter_attributes(multi_parameter_attributes) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 67358fedd6..0038174ad8 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -904,14 +904,6 @@ class BasicsTest < ActiveRecord::TestCase assert_nil keyboard.id end - def test_mass_assigning_invalid_attribute - firm = Firm.new - - assert_raises(ActiveRecord::UnknownAttributeError) do - firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } - end - end - def test_mass_assignment_protection_on_defaults firm = Firm.new firm.attributes = { "id" => 5, "type" => "Client" } -- cgit v1.2.3 From 4f6875296f6e6ea123130582923284bfd24cd7f1 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 8 Sep 2008 09:45:26 -0700 Subject: Revert "Revert "Raise UnknownAttributeError when unknown attributes are supplied via mass assignment"" This reverts commit 41efd73887c00ffd228b05d9346ec47a1f3759b9. --- activerecord/lib/active_record/base.rb | 10 +++++++++- activerecord/test/cases/base_test.rb | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index bca2db70f8..907495a416 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -122,6 +122,10 @@ module ActiveRecord #:nodoc: class MissingAttributeError < NoMethodError end + # Raised when unknown attributes are supplied via mass assignment. + class UnknownAttributeError < NoMethodError + end + # Raised when an error occurred while doing a mass assignment to an attribute through the # attributes= method. The exception has an +attribute+ property that is the name of the # offending attribute. @@ -2437,7 +2441,11 @@ module ActiveRecord #:nodoc: attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes attributes.each do |k, v| - k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v) + if k.include?("(") + multi_parameter_attributes << [ k, v ] + else + respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") + end end assign_multiparameter_attributes(multi_parameter_attributes) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 0038174ad8..67358fedd6 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -904,6 +904,14 @@ class BasicsTest < ActiveRecord::TestCase assert_nil keyboard.id end + def test_mass_assigning_invalid_attribute + firm = Firm.new + + assert_raises(ActiveRecord::UnknownAttributeError) do + firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } + end + end + def test_mass_assignment_protection_on_defaults firm = Firm.new firm.attributes = { "id" => 5, "type" => "Client" } -- cgit v1.2.3 From 07913788f99f45a3c4ca41b518885468ac3a3915 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Tue, 9 Sep 2008 16:06:15 +0200 Subject: Interpolation requires double quotes --- activerecord/lib/active_record/calculations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index a675af4787..80992dd34b 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -217,7 +217,7 @@ module ActiveRecord sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) - sql << ') AS #{aggregate_alias}_subquery' if use_workaround + sql << ") AS #{aggregate_alias}_subquery" if use_workaround sql end -- cgit v1.2.3 From 16929404417205792165153958ce5effa0b54645 Mon Sep 17 00:00:00 2001 From: "Hongli Lai (Phusion)" Date: Sat, 6 Sep 2008 18:37:31 +0200 Subject: Make the options that has_many, belongs_to and other association generation methods can accept, configurable. [#985 state:resolved] Signed-off-by: Jeremy Kemper --- activerecord/CHANGELOG | 2 ++ activerecord/lib/active_record/associations.rb | 48 ++++++++++++++++---------- 2 files changed, 32 insertions(+), 18 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index f6b9913790..74b3db75ae 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Internal API: configurable association options so plugins may extend and override. #985 [Hongli Lai] + * Changed benchmarks to be reported in milliseconds [DHH] * Connection pooling. #936 [Nick Sieger] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6405071354..990eda3ee3 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1509,28 +1509,36 @@ module ActiveRecord end end + mattr_accessor :valid_keys_for_has_many_association + @@valid_keys_for_has_many_association = [ + :class_name, :table_name, :foreign_key, :primary_key, + :dependent, + :select, :conditions, :include, :order, :group, :limit, :offset, + :as, :through, :source, :source_type, + :uniq, + :finder_sql, :counter_sql, + :before_add, :after_add, :before_remove, :after_remove, + :extend, :readonly, + :validate, :accessible + ] + def create_has_many_reflection(association_id, options, &extension) - options.assert_valid_keys( - :class_name, :table_name, :foreign_key, :primary_key, - :dependent, - :select, :conditions, :include, :order, :group, :limit, :offset, - :as, :through, :source, :source_type, - :uniq, - :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate, :accessible - ) + options.assert_valid_keys(valid_keys_for_has_many_association) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) create_reflection(:has_many, association_id, options, self) end + mattr_accessor :valid_keys_for_has_one_association + @@valid_keys_for_has_one_association = [ + :class_name, :foreign_key, :remote, :select, :conditions, :order, + :include, :dependent, :counter_cache, :extend, :as, :readonly, + :validate, :primary_key, :accessible + ] + def create_has_one_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key, :accessible - ) + options.assert_valid_keys(valid_keys_for_has_one_association) create_reflection(:has_one, association_id, options, self) end @@ -1542,11 +1550,15 @@ module ActiveRecord create_reflection(:has_one, association_id, options, self) end + mattr_accessor :valid_keys_for_belongs_to_association + @@valid_keys_for_belongs_to_association = [ + :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, + :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, + :validate, :accessible + ] + def create_belongs_to_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, - :counter_cache, :extend, :polymorphic, :readonly, :validate, :accessible - ) + options.assert_valid_keys(valid_keys_for_belongs_to_association) reflection = create_reflection(:belongs_to, association_id, options, self) -- cgit v1.2.3 From 1398db0128be7ae01700712eafc95be5de430f7c Mon Sep 17 00:00:00 2001 From: "Hongli Lai (Phusion)" Date: Sat, 6 Sep 2008 19:43:14 +0200 Subject: Add special AssociationReflection methods for creating association objects, and modify the code base to use those methods instead of creating association objects directly. This allows plugins to hook into association object creation behavior. [#986 state:resolved] Signed-off-by: Jeremy Kemper --- activerecord/CHANGELOG | 2 +- activerecord/lib/active_record/associations.rb | 2 +- .../associations/association_collection.rb | 10 ++++--- .../associations/belongs_to_association.rb | 4 +-- .../associations/has_many_through_association.rb | 9 +++--- .../associations/has_one_association.rb | 16 +++++++--- activerecord/lib/active_record/reflection.rb | 35 ++++++++++++++++++++++ 7 files changed, 62 insertions(+), 16 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 74b3db75ae..3e6bae7a67 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,6 +1,6 @@ *Edge* -* Internal API: configurable association options so plugins may extend and override. #985 [Hongli Lai] +* Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 [Hongli Lai] * Changed benchmarks to be reported in milliseconds [DHH] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 990eda3ee3..f9a728d49e 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1266,7 +1266,7 @@ module ActiveRecord association = association_proxy_class.new(self, reflection) end - new_value = reflection.klass.new(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) + new_value = reflection.build_association(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) if association_proxy_class == HasOneThroughAssociation association.create_through_record(new_value) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 168443e092..d4bb43970f 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -110,7 +110,7 @@ module ActiveRecord @owner.transaction do flatten_deeper(records).each do |record| - record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash) + record = @reflection.build_association(record) if @reflection.options[:accessible] && record.is_a?(Hash) raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| @@ -287,7 +287,7 @@ module ActiveRecord # This will perform a diff and delete/add only records that have changed. def replace(other_array) other_array.map! do |val| - val.is_a?(Hash) ? @reflection.klass.new(val) : val + val.is_a?(Hash) ? @reflection.build_association(val) : val end if @reflection.options[:accessible] other_array.each { |val| raise_on_type_mismatch(val) } @@ -377,7 +377,9 @@ module ActiveRecord def create_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) ensure_owner_is_not_new - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + @reflection.build_association(attrs) + end if block_given? add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } else @@ -387,7 +389,7 @@ module ActiveRecord def build_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.klass.new(attrs) + record = @reflection.build_association(attrs) if block_given? add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } else diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 7c28cbdd07..f05c6be075 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,11 +2,11 @@ module ActiveRecord module Associations class BelongsToAssociation < AssociationProxy #:nodoc: def create(attributes = {}) - replace(@reflection.klass.create(attributes)) + replace(@reflection.create_association(attributes)) end def build(attributes = {}) - replace(@reflection.klass.new(attributes)) + replace(@reflection.build_association(attributes)) end def replace(record) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 84fa900f46..ebd2bf768c 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -10,14 +10,14 @@ module ActiveRecord def create!(attrs = nil) @reflection.klass.transaction do - self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!) + self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!) object end end def create(attrs = nil) @reflection.klass.transaction do - self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create) + self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association) object end end @@ -47,8 +47,9 @@ module ActiveRecord return false unless record.save end end - klass = @reflection.through_reflection.klass - @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! } + through_reflection = @reflection.through_reflection + klass = through_reflection.klass + @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! } end # TODO - add dependent option support diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 18733255d2..c92ef5c2c9 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -7,15 +7,21 @@ module ActiveRecord end def create(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.create(attrs) } + new_record(replace_existing) do |reflection| + reflection.create_association(attrs) + end end def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.create!(attrs) } + new_record(replace_existing) do |reflection| + reflection.create_association!(attrs) + end end def build(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.new(attrs) } + new_record(replace_existing) do |reflection| + reflection.build_association(attrs) + end end def replace(obj, dont_save = false) @@ -91,7 +97,9 @@ module ActiveRecord # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + yield @reflection + end if replace_existing replace(record, true) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 935b1939d8..a1b498eceb 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -129,10 +129,45 @@ module ActiveRecord # Holds all the meta-data about an association as it was specified in the Active Record class. class AssociationReflection < MacroReflection #:nodoc: + # Returns the target association's class: + # + # class Author < ActiveRecord::Base + # has_many :books + # end + # + # Author.reflect_on_association(:books).klass + # # => Book + # + # Note: do not call +klass.new+ or +klass.create+ to instantiate + # a new association object. Use +build_association+ or +create_association+ + # instead. This allows plugins to hook into association object creation. def klass @klass ||= active_record.send(:compute_type, class_name) end + # Returns a new, unsaved instance of the associated class. +options+ will + # be passed to the class's constructor. + def build_association(*options) + klass.new(*options) + end + + # Creates a new instance of the associated class, and immediates saves it + # with ActiveRecord::Base#save. +options+ will be passed to the class's + # creation method. Returns the newly created object. + def create_association(*options) + klass.create(*options) + end + + # Creates a new instance of the associated class, and immediates saves it + # with ActiveRecord::Base#save!. +options+ will be passed to the class's + # creation method. If the created record doesn't pass validations, then an + # exception will be raised. + # + # Returns the newly created object. + def create_association!(*options) + klass.create!(*options) + end + def table_name @table_name ||= klass.table_name end -- cgit v1.2.3 From 567392bff32acd5cf357565415d33c2662004cf5 Mon Sep 17 00:00:00 2001 From: miloops Date: Mon, 1 Sep 2008 13:36:42 -0300 Subject: Added find_last_by dynamic finder [status:committed #762] Signed-off-by: David Heinemeier Hansson --- activerecord/lib/active_record/base.rb | 1 + .../lib/active_record/dynamic_finder_match.rb | 3 +- activerecord/test/cases/finder_test.rb | 40 +++++++++++++++++++--- 3 files changed, 39 insertions(+), 5 deletions(-) mode change 100755 => 100644 activerecord/lib/active_record/base.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb old mode 100755 new mode 100644 index 907495a416..0fe9e12549 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -287,6 +287,7 @@ module ActiveRecord #:nodoc: # It's even possible to use all the additional parameters to find. For example, the full interface for Payment.find_all_by_amount # is actually Payment.find_all_by_amount(amount, options). And the full interface to Person.find_by_user_name is # actually Person.find_by_user_name(user_name, options). So you could call Payment.find_all_by_amount(50, :order => "created_on"). + # Also you may call Payment.find_last_by_amount(amount, options) returning the last record matching that amount and options. # # The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with # find_or_create_by_ and will return the object if it already exists and otherwise creates it, then returns it. Protected attributes won't be set unless they are given in a block. For example: diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index b105b919f5..f4a5712981 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -8,7 +8,8 @@ module ActiveRecord def initialize(method) @finder = :find_initial case method.to_s - when /^find_(all_by|by)_([_a-zA-Z]\w*)$/ + when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/ + @finder = :find_last if $1 == 'last_by' @finder = :find_every if $1 == 'all_by' names = $2 when /^find_by_([_a-zA-Z]\w*)\!$/ diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 2ce49ed76f..3eee2056dd 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -197,11 +197,11 @@ class FinderTest < ActiveRecord::TestCase first = Topic.find(:first, :conditions => "title = 'The First Topic!'") assert_nil(first) end - + def test_first assert_equal topics(:second).title, Topic.first(:conditions => "title = 'The Second Topic of the day'").title end - + def test_first_failing assert_nil Topic.first(:conditions => "title = 'The Second Topic of the day!'") end @@ -418,7 +418,7 @@ class FinderTest < ActiveRecord::TestCase def test_named_bind_variables assert_equal '1', bind(':a', :a => 1) # ' ruby-mode assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode - + assert_nothing_raised { bind("'+00:00'", :foo => "bar") } assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }]) @@ -589,6 +589,38 @@ class FinderTest < ActiveRecord::TestCase assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary") end + def test_find_last_by_one_attribute + assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title) + assert_nil Topic.find_last_by_title("A title with no matches") + end + + def test_find_last_by_one_attribute_caches_dynamic_finder + # ensure this test can run independently of order + class << Topic; self; end.send(:remove_method, :find_last_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } + assert !Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } + t = Topic.find_last_by_title(Topic.last) + assert Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } + end + + def test_find_last_by_invalid_method_syntax + assert_raises(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") } + assert_raises(NoMethodError) { Topic.find_last_by_title?("The First Topic") } + end + + def test_find_last_by_one_attribute_with_several_options + assert_equal accounts(:signals37), Account.find_last_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + end + + def test_find_last_by_one_missing_attribute + assert_raises(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") } + end + + def test_find_last_by_two_attributes + topic = Topic.last + assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name) + assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous") + end + def test_find_all_by_one_attribute topics = Topic.find_all_by_content("Have a nice day") assert_equal 2, topics.size @@ -782,7 +814,7 @@ class FinderTest < ActiveRecord::TestCase assert c.valid? assert !c.new_record? end - + def test_dynamic_find_or_initialize_from_one_attribute_caches_method class << Company; self; end.send(:remove_method, :find_or_initialize_by_name) if Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' } assert !Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' } -- cgit v1.2.3 From 6dc9173a63fdd8510b95e6de189139046b2a0a30 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 9 Sep 2008 23:59:54 -0500 Subject: Missing doc updates --- activerecord/CHANGELOG | 2 ++ activerecord/lib/active_record/base.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 3e6bae7a67..4e38b6b688 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Added find_last_by dynamic finder #762 [miloops] + * Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 [Hongli Lai] * Changed benchmarks to be reported in milliseconds [DHH] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 0fe9e12549..d7c67bc10d 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -274,7 +274,7 @@ module ActiveRecord #:nodoc: # == Dynamic attribute-based finders # # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by - # appending the name of an attribute to find_by_ or find_all_by_, so you get finders like Person.find_by_user_name, + # appending the name of an attribute to find_by_, find_last_by_, or find_all_by_, so you get finders like Person.find_by_user_name, # Person.find_all_by_last_name, and Payment.find_by_transaction_id. So instead of writing # Person.find(:first, :conditions => ["user_name = ?", user_name]), you just do Person.find_by_user_name(user_name). # And instead of writing Person.find(:all, :conditions => ["last_name = ?", last_name]), you just do Person.find_all_by_last_name(last_name). -- cgit v1.2.3 From 14d1560e85d5c4795c21ddb00953cf55e0525ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Wed, 10 Sep 2008 13:23:08 +0300 Subject: Fixed test_find_last_by_one_attribute_caches_dynamic_finder for postgresql 8.3 Signed-off-by: Michael Koziarski --- activerecord/test/cases/finder_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 3eee2056dd..1cbc5182d6 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -598,7 +598,7 @@ class FinderTest < ActiveRecord::TestCase # ensure this test can run independently of order class << Topic; self; end.send(:remove_method, :find_last_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } assert !Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } - t = Topic.find_last_by_title(Topic.last) + t = Topic.find_last_by_title(Topic.last.title) assert Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } end -- cgit v1.2.3 From 7c9851dbb6f841ffbf3ebd16e9f57c04319d3a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20T=C3=A4nav?= Date: Wed, 10 Sep 2008 13:39:50 +0300 Subject: Support :limit on update_all so that has_many with :limit can be safely updated Signed-off-by: Michael Koziarski --- activerecord/lib/active_record/base.rb | 20 +++++++++++++++++--- .../abstract/database_statements.rb | 4 ++++ .../connection_adapters/mysql_adapter.rb | 4 ++++ activerecord/test/cases/base_test.rb | 19 +++++++++++++++---- activerecord/test/models/author.rb | 1 + 5 files changed, 41 insertions(+), 7 deletions(-) mode change 100644 => 100755 activerecord/lib/active_record/base.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb old mode 100644 new mode 100755 index d7c67bc10d..fc6d762fcd --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -769,10 +769,24 @@ module ActiveRecord #:nodoc: # :order => 'created_at', :limit => 5 ) def update_all(updates, conditions = nil, options = {}) sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} " + scope = scope(:find) - add_conditions!(sql, conditions, scope) - add_order!(sql, options[:order], nil) - add_limit!(sql, options, nil) + + select_sql = "" + add_conditions!(select_sql, conditions, scope) + + if options.has_key?(:limit) || (scope && scope[:limit]) + # Only take order from scope if limit is also provided by scope, this + # is useful for updating a has_many association with a limit. + add_order!(select_sql, options[:order], scope) + + add_limit!(select_sql, options, scope) + sql.concat(connection.limited_update_conditions(select_sql, quoted_table_name, connection.quote_column_name(primary_key))) + else + add_order!(select_sql, options[:order], nil) + sql.concat(select_sql) + end + connection.update(sql, "#{name} Update") end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index aaf9e2e73f..8fc89de22b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -153,6 +153,10 @@ module ActiveRecord "=" end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index f1d13698c3..c2a0fb72bf 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -523,6 +523,10 @@ module ActiveRecord "= BINARY" end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + private def connect @connection.reconnect = true if @connection.respond_to?(:reconnect=) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 67358fedd6..bda6f346f0 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -76,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts def test_table_exists assert !NonExistentTable.table_exists? @@ -664,10 +664,21 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_update_all_ignores_order_limit_from_association - author = Author.find(1) + def test_update_all_ignores_order_without_limit_from_association + author = authors(:david) assert_nothing_raised do - assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all("body = 'bulk update!'") + assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) + end + end + + def test_update_all_with_order_and_limit_updates_subset_only + author = authors(:david) + assert_nothing_raised do + assert_equal 1, author.posts_sorted_by_id_limited.size + assert_equal 2, author.posts_sorted_by_id_limited.find(:all, :limit => 2).size + assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:welcome).body + assert_not_equal "bulk update!", posts(:thinking).body end end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index c6aa0293c2..ad0123cf2c 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -2,6 +2,7 @@ class Author < ActiveRecord::Base has_many :posts, :accessible => true has_many :posts_with_comments, :include => :comments, :class_name => "Post" has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id' + has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1 has_many :posts_with_categories, :include => :categories, :class_name => "Post" has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post" has_many :posts_containing_the_letter_a, :class_name => "Post" -- cgit v1.2.3 From 2cee51d5c1d143f6fe0096ba6cbd1db1ecbe2d90 Mon Sep 17 00:00:00 2001 From: Rob Anderton Date: Mon, 18 Aug 2008 19:13:01 +0100 Subject: Added :constructor and :converter options to composed_of and deprecated the conversion block Signed-off-by: Michael Koziarski --- activerecord/lib/active_record/aggregations.rb | 91 +++++++++++++++++--------- activerecord/test/cases/aggregations_test.rb | 35 ++++++++++ activerecord/test/fixtures/customers.yml | 11 +++- activerecord/test/models/customer.rb | 20 +++++- 4 files changed, 124 insertions(+), 33 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index a5d3a50ef1..f3d73591e5 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -10,10 +10,10 @@ module ActiveRecord end unless self.new_record? end - # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes + # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is] - # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the - # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) + # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the + # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) # and how it can be turned back into attributes (when the entity is saved to the database). Example: # # class Customer < ActiveRecord::Base @@ -30,10 +30,10 @@ module ActiveRecord # class Money # include Comparable # attr_reader :amount, :currency - # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } - # - # def initialize(amount, currency = "USD") - # @amount, @currency = amount, currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency # end # # def exchange_to(other_currency) @@ -56,19 +56,19 @@ module ActiveRecord # # class Address # attr_reader :street, :city - # def initialize(street, city) - # @street, @city = street, city + # def initialize(street, city) + # @street, @city = street, city # end # - # def close_to?(other_address) - # city == other_address.city + # def close_to?(other_address) + # city == other_address.city # end # # def ==(other_address) # city == other_address.city && street == other_address.street # end # end - # + # # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the # composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our # +balance+ attribute. You interact with the value objects just like you would any other attribute, though: @@ -87,8 +87,8 @@ module ActiveRecord # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") # customer.address = Address.new("May Street", "Chicago") - # customer.address_street # => "May Street" - # customer.address_city # => "Chicago" + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" # # == Writing value objects # @@ -103,9 +103,9 @@ module ActiveRecord # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been # changed through means other than the writer method. # - # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to # change it afterwards will result in a ActiveSupport::FrozenObjectError. - # + # # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable # @@ -130,39 +130,59 @@ module ActiveRecord # * :allow_nil - specifies that the aggregate object will not be instantiated when all mapped # attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes. # This defaults to +false+. - # - # An optional block can be passed to convert the argument that is passed to the writer method into an instance of - # :class_name. The block will only be called if the argument is not already an instance of :class_name. + # * :constructor - a symbol specifying the name of the constructor method or a Proc that will be used to convert the + # attributes that are mapped to the aggregation to instantiate a :class_name object. The default is +:new+. + # * :converter - a symbol specifying the name of a class method of :class_name or a Proc that will be used to convert + # the argument that is passed to the writer method into an instance of :class_name. The converter will only be called + # if the argument is not already an instance of :class_name. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) - # composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money } + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, :allow_nil => true + # composed_of :ip_address, + # :class_name => 'IPAddr', + # :mapping => %w(ip to_i), + # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, + # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } # def composed_of(part_id, options = {}, &block) - options.assert_valid_keys(:class_name, :mapping, :allow_nil) + options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) name = part_id.id2name - class_name = options[:class_name] || name.camelize - mapping = options[:mapping] || [ name, name ] + class_name = options[:class_name] || name.camelize + mapping = options[:mapping] || [ name, name ] mapping = [ mapping ] unless mapping.first.is_a?(Array) - allow_nil = options[:allow_nil] || false + allow_nil = options[:allow_nil] || false + constructor = options[:constructor] || :new + converter = options[:converter] || block + + ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given? + + reader_method(name, class_name, mapping, allow_nil, constructor) + writer_method(name, class_name, mapping, allow_nil, converter) - reader_method(name, class_name, mapping, allow_nil) - writer_method(name, class_name, mapping, allow_nil, block) - create_reflection(:composed_of, part_id, options, self) end private - def reader_method(name, class_name, mapping, allow_nil) + def reader_method(name, class_name, mapping, allow_nil, constructor) module_eval do define_method(name) do |*args| force_reload = args.first || false if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)})) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = case constructor + when Symbol + class_name.constantize.send(constructor, *attrs) + when Proc, Method + constructor.call(*attrs) + else + raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' + end + instance_variable_set("@#{name}", object) end instance_variable_get("@#{name}") end @@ -170,14 +190,23 @@ module ActiveRecord end - def writer_method(name, class_name, mapping, allow_nil, conversion) + def writer_method(name, class_name, mapping, allow_nil, converter) module_eval do define_method("#{name}=") do |part| if part.nil? && allow_nil mapping.each { |pair| self[pair.first] = nil } instance_variable_set("@#{name}", nil) else - part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil? + unless part.is_a?(class_name.constantize) || converter.nil? + part = case converter + when Symbol + class_name.constantize.send(converter, part) + when Proc, Method + converter.call(part) + else + raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' + end + end mapping.each { |pair| self[pair.first] = part.send(pair.last) } instance_variable_set("@#{name}", part.freeze) end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 75d1f27e07..b6656c8631 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -107,6 +107,41 @@ class AggregationsTest < ActiveRecord::TestCase customers(:david).gps_location = nil assert_equal nil, customers(:david).gps_location end + + def test_custom_constructor + assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s + assert_kind_of Fullname, customers(:barney).fullname + end + + def test_custom_converter + customers(:barney).fullname = 'Barnoit Gumbleau' + assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s + assert_kind_of Fullname, customers(:barney).fullname + end +end + +class DeprecatedAggregationsTest < ActiveRecord::TestCase + class Person < ActiveRecord::Base; end + + def test_conversion_block_is_deprecated + assert_deprecated 'conversion block has been deprecated' do + Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } + end + end + + def test_conversion_block_used_when_converter_option_is_nil + Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } + assert_raise(NoMethodError) { Person.new.balance = 5 } + end + + def test_converter_option_overrides_conversion_block + Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| Money.new(balance) }) { |balance| balance.to_money } + + person = Person.new + assert_nothing_raised { person.balance = 5 } + assert_equal 5, person.balance.amount + assert_kind_of Money, person.balance + end end class OverridingAggregationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/fixtures/customers.yml b/activerecord/test/fixtures/customers.yml index f802aac5ea..0399ff83b9 100644 --- a/activerecord/test/fixtures/customers.yml +++ b/activerecord/test/fixtures/customers.yml @@ -6,7 +6,7 @@ david: address_city: Scary Town address_country: Loony Land gps_location: 35.544623640962634x-105.9309951055148 - + zaphod: id: 2 name: Zaphod @@ -14,4 +14,13 @@ zaphod: address_street: Avenue Road address_city: Hamlet Town address_country: Nation Land + gps_location: NULL + +barney: + id: 3 + name: Barney Gumble + balance: 1 + address_street: Quiet Road + address_city: Peaceful Town + address_country: Tranquil Land gps_location: NULL \ No newline at end of file diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 030bbc6237..e258ccdb6c 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -1,7 +1,8 @@ class Customer < ActiveRecord::Base composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true - composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } + composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } composed_of :gps_location, :allow_nil => true + composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse end class Address @@ -53,3 +54,20 @@ class GpsLocation self.latitude == other.latitude && self.longitude == other.longitude end end + +class Fullname + attr_reader :first, :last + + def self.parse(str) + return nil unless str + new(*str.to_s.split) + end + + def initialize(first, last = nil) + @first, @last = first, last + end + + def to_s + "#{first} #{last.upcase}" + end +end \ No newline at end of file -- cgit v1.2.3 From b518b6c0d3e7796e303c2396de97a8d901aeb308 Mon Sep 17 00:00:00 2001 From: Rob Anderton Date: Wed, 10 Sep 2008 17:04:55 +0100 Subject: Expanded documentation for new composed_of options Signed-off-by: Michael Koziarski [#892 state:committed] --- activerecord/lib/active_record/aggregations.rb | 63 ++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index f3d73591e5..9eee7f2d98 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -109,6 +109,45 @@ module ActiveRecord # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable # + # == Custom constructors and converters + # + # By default value objects are initialized by calling the new constructor of the value class passing each of the + # mapped attributes, in the order specified by the :mapping option, as arguments. If the value class doesn't support + # this convention then +composed_of+ allows a custom constructor to be specified. + # + # When a new value is assigned to the value object the default assumption is that the new value is an instance of the value + # class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if + # necessary. + # + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the + # NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it + # expects a CIDR address string as a parameter. New values can be assigned to the value object using either another + # NetAddr::CIDR object, a string or an array. The :constructor and :converter options can be used to + # meet these requirements: + # + # class NetworkResource < ActiveRecord::Base + # composed_of :cidr, + # :class_name => 'NetAddr::CIDR', + # :mapping => [ %w(network_address network), %w(cidr_range bits) ], + # :allow_nil => true, + # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, + # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } + # end + # + # # This calls the :constructor + # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24) + # + # # These assignments will both use the :converter + # network_resource.cidr = [ '192.168.2.1', 8 ] + # network_resource.cidr = '192.168.0.1/24' + # + # # This assignment won't use the :converter as the value is already an instance of the value class + # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') + # + # # Saving and then reloading will use the :constructor on reload + # network_resource.save + # network_resource.reload + # # == Finding records by a value object # # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance @@ -122,19 +161,23 @@ module ActiveRecord # composed_of :address adds address and address=(new_address) methods. # # Options are: - # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # * :class_name - Specifies the class name of the association. Use it only if that name can't be inferred # from the part id. So composed_of :address will by default be linked to the Address class, but # if the real class name is CompanyAddress, you'll have to specify it with this option. - # * :mapping - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name - # to a constructor parameter on the value class. - # * :allow_nil - specifies that the aggregate object will not be instantiated when all mapped - # attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes. + # * :mapping - Specifies the mapping of entity attributes to attributes of the value object. Each mapping + # is represented as an array where the first item is the name of the entity attribute and the second item is the + # name the attribute in the value object. The order in which mappings are defined determine the order in which + # attributes are sent to the value class constructor. + # * :allow_nil - Specifies that the value object will not be instantiated when all mapped + # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes. # This defaults to +false+. - # * :constructor - a symbol specifying the name of the constructor method or a Proc that will be used to convert the - # attributes that are mapped to the aggregation to instantiate a :class_name object. The default is +:new+. - # * :converter - a symbol specifying the name of a class method of :class_name or a Proc that will be used to convert - # the argument that is passed to the writer method into an instance of :class_name. The converter will only be called - # if the argument is not already an instance of :class_name. + # * :constructor - A symbol specifying the name of the constructor method or a Proc that is called to + # initialize the value object. The constructor is passed all of the mapped attributes, in the order that they + # are defined in the :mapping option, as arguments and uses them to instantiate a :class_name object. + # The default is :new. + # * :converter - A symbol specifying the name of a class method of :class_name or a Proc that is + # called when a new value is assigned to the value object. The converter is passed the single value that is used + # in the assignment and is only called if the new value is not an instance of :class_name. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) -- cgit v1.2.3 From 9994f0d90248db7d7eae36f0b597a15e8a427612 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Wed, 10 Sep 2008 18:50:01 +0100 Subject: Revert "Add :accessible option to Associations for allowing mass assignments using hash. [#474 state:resolved]" This reverts commit e0750d6a5c7f621e4ca12205137c0b135cab444a. Conflicts: activerecord/CHANGELOG activerecord/lib/active_record/associations.rb activerecord/lib/active_record/associations/association_collection.rb --- activerecord/CHANGELOG | 18 ---- activerecord/lib/active_record/associations.rb | 22 +---- .../associations/association_collection.rb | 6 -- activerecord/test/cases/associations_test.rb | 108 --------------------- activerecord/test/models/author.rb | 2 +- activerecord/test/models/post.rb | 6 -- 6 files changed, 5 insertions(+), 157 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 4e38b6b688..5bcc7ad3a9 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -20,24 +20,6 @@ * Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334] -* Add :accessible option to associations for allowing (opt-in) mass assignment. #474. [David Dollar] Example : - - class Post < ActiveRecord::Base - belongs_to :author, :accessible => true - has_many :comments, :accessible => true - end - - post = Post.create({ - :title => 'Accessible Attributes', - :author => { :name => 'David Dollar' }, - :comments => [ - { :body => 'First Post!' }, - { :body => 'Nested Hashes are great!' } - ] - }) - - post.comments << { :body => 'Another Comment' } - * Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example : # Ensure essay contains at least 100 words. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f9a728d49e..aca2d770fc 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -739,9 +739,6 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). - # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -855,8 +852,6 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -973,8 +968,6 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -1190,8 +1183,6 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. - # [:accessible<] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_and_belongs_to_many :projects @@ -1266,8 +1257,6 @@ module ActiveRecord association = association_proxy_class.new(self, reflection) end - new_value = reflection.build_association(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) - if association_proxy_class == HasOneThroughAssociation association.create_through_record(new_value) self.send(reflection.name, new_value) @@ -1519,12 +1508,11 @@ module ActiveRecord :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate, :accessible + :validate ] def create_has_many_reflection(association_id, options, &extension) options.assert_valid_keys(valid_keys_for_has_many_association) - options[:extend] = create_extension_modules(association_id, extension, options[:extend]) create_reflection(:has_many, association_id, options, self) @@ -1534,12 +1522,11 @@ module ActiveRecord @@valid_keys_for_has_one_association = [ :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, - :validate, :primary_key, :accessible + :validate, :primary_key ] def create_has_one_reflection(association_id, options) options.assert_valid_keys(valid_keys_for_has_one_association) - create_reflection(:has_one, association_id, options, self) end @@ -1554,12 +1541,11 @@ module ActiveRecord @@valid_keys_for_belongs_to_association = [ :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, - :validate, :accessible + :validate ] def create_belongs_to_reflection(association_id, options) options.assert_valid_keys(valid_keys_for_belongs_to_association) - reflection = create_reflection(:belongs_to, association_id, options, self) if options[:polymorphic] @@ -1577,7 +1563,7 @@ module ActiveRecord :finder_sql, :delete_sql, :insert_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate, :accessible + :validate ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index d4bb43970f..8de528f638 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -110,8 +110,6 @@ module ActiveRecord @owner.transaction do flatten_deeper(records).each do |record| - record = @reflection.build_association(record) if @reflection.options[:accessible] && record.is_a?(Hash) - raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| result &&= insert_record(record) unless @owner.new_record? @@ -286,10 +284,6 @@ module ActiveRecord # Replace this collection with +other_array+ # This will perform a diff and delete/add only records that have changed. def replace(other_array) - other_array.map! do |val| - val.is_a?(Hash) ? @reflection.build_association(val) : val - end if @reflection.options[:accessible] - other_array.each { |val| raise_on_type_mismatch(val) } load_target diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 0b2731ecd7..056a29438a 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -189,114 +189,6 @@ class AssociationProxyTest < ActiveRecord::TestCase end end - def test_belongs_to_mass_assignment - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - author_attributes = { :name => 'David Dollar' } - - assert_no_difference 'Author.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:author => author_attributes})) - end - end - - assert_difference 'Author.count' do - post = Post.create(post_attributes.merge({:creatable_author => author_attributes})) - assert_equal post.creatable_author.name, author_attributes[:name] - end - end - - def test_has_one_mass_assignment - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - comment_attributes = { :body => 'Setter Takes Hash' } - - assert_no_difference 'Comment.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:uncreatable_comment => comment_attributes})) - end - end - - assert_difference 'Comment.count' do - post = Post.create(post_attributes.merge({:creatable_comment => comment_attributes})) - assert_equal post.creatable_comment.body, comment_attributes[:body] - end - end - - def test_has_many_mass_assignment - post = posts(:welcome) - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - comment_attributes = { :body => 'Setter Takes Hash' } - - assert_no_difference 'Comment.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:comments => [comment_attributes]})) - end - assert_raise(ActiveRecord::AssociationTypeMismatch) do - post.comments << comment_attributes - end - end - - assert_difference 'Comment.count' do - post = Post.create(post_attributes.merge({:creatable_comments => [comment_attributes]})) - assert_equal post.creatable_comments.last.body, comment_attributes[:body] - end - - assert_difference 'Comment.count' do - post.creatable_comments << comment_attributes - assert_equal post.comments.last.body, comment_attributes[:body] - end - - post.creatable_comments = [comment_attributes, comment_attributes] - assert_equal post.creatable_comments.count, 2 - end - - def test_has_and_belongs_to_many_mass_assignment - post = posts(:welcome) - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - category_attributes = { :name => 'Accessible Association', :type => 'Category' } - - assert_no_difference 'Category.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:categories => [category_attributes]})) - end - assert_raise(ActiveRecord::AssociationTypeMismatch) do - post.categories << category_attributes - end - end - - assert_difference 'Category.count' do - post = Post.create(post_attributes.merge({:creatable_categories => [category_attributes]})) - assert_equal post.creatable_categories.last.name, category_attributes[:name] - end - - assert_difference 'Category.count' do - post.creatable_categories << category_attributes - assert_equal post.creatable_categories.last.name, category_attributes[:name] - end - - post.creatable_categories = [category_attributes, category_attributes] - assert_equal post.creatable_categories.count, 2 - end - - def test_association_proxy_setter_can_take_hash - special_comment_attributes = { :body => 'Setter Takes Hash' } - - post = posts(:welcome) - post.creatable_comment = { :body => 'Setter Takes Hash' } - - assert_equal post.creatable_comment.body, special_comment_attributes[:body] - end - - def test_association_collection_can_take_hash - post_attributes = { :title => 'Setter Takes', :body => 'Hash' } - david = authors(:david) - - post = (david.posts << post_attributes).last - assert_equal post.title, post_attributes[:title] - - david.posts = [post_attributes, post_attributes] - assert_equal david.posts.count, 2 - end - def setup_dangling_association josh = Author.create(:name => "Josh") p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh) diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index ad0123cf2c..37551c8157 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,5 +1,5 @@ class Author < ActiveRecord::Base - has_many :posts, :accessible => true + has_many :posts has_many :posts_with_comments, :include => :comments, :class_name => "Post" has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id' has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1 diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index e23818eb33..3adbc0ce1f 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -33,12 +33,6 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - belongs_to :creatable_author, :class_name => 'Author', :accessible => true - has_one :uncreatable_comment, :class_name => 'Comment', :accessible => false, :order => 'id desc' - has_one :creatable_comment, :class_name => 'Comment', :accessible => true, :order => 'id desc' - has_many :creatable_comments, :class_name => 'Comment', :accessible => true, :dependent => :destroy - has_and_belongs_to_many :creatable_categories, :class_name => 'Category', :accessible => true - has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select -- cgit v1.2.3