From d28f8c779676204e2c53eda57b8506fdf4db9201 Mon Sep 17 00:00:00 2001 From: Diego Carrion Date: Wed, 14 Apr 2010 03:21:28 -0300 Subject: [PATCH 1/4] add :touch option to has_many associations [#4392 state:resolved] --- activerecord/lib/active_record/associations.rb | 15 ++++++++------- activerecord/test/cases/timestamp_test.rb | 17 ++++++++++++++++- activerecord/test/models/pet.rb | 2 +- activerecord/test/schema/schema.rb | 2 ++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d94cc03..7689839 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -885,6 +885,9 @@ module ActiveRecord # If false, don't validate the associated objects when saving the parent object. true by default. # [:autosave] # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default. + # [:touch] + # If true, the associated objects will be touched (the updated_at/on attributes set to now) when this record is either saved or + # destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute. # [:inverse_of] # Specifies the name of the belongs_to association on the associated object that is the inverse of this has_many # association. Does not work in combination with :through or :as options. @@ -907,7 +910,9 @@ module ActiveRecord def has_many(association_id, options = {}, &extension) reflection = create_has_many_reflection(association_id, options, &extension) configure_dependency_for_has_many(reflection) + add_association_callbacks(reflection.name, reflection.options) + add_touch_callbacks(reflection, options[:touch]) if options[:touch] if options[:through] collection_accessor_methods(reflection, HasManyThroughAssociation) @@ -1467,12 +1472,8 @@ module ActiveRecord def add_touch_callbacks(reflection, touch_attribute) method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym define_method(method_name) do - association = send(reflection.name) - - if touch_attribute == true - association.touch unless association.nil? - else - association.touch(touch_attribute) unless association.nil? + [send(reflection.name)].flatten.each do |model| + model.touch touch_attribute end end after_save(method_name) @@ -1611,7 +1612,7 @@ module ActiveRecord :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate, :inverse_of + :validate, :inverse_of, :touch ] def create_has_many_reflection(association_id, options, &extension) diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 24b237a..db5a025 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -37,6 +37,21 @@ class TimestampTest < ActiveRecord::TestCase assert previously_created_at != @developer.created_at end + def test_saving_a_record_with_a_has_many_that_specifies_touching_the_children_should_update_the_children_updated_at + pet = Pet.first + previously_children_updated_at = pet.toys.map(&:updated_at) + pet.update_attribute :name, "Fluffy the Second" + assert previously_children_updated_at != pet.toys.map(&:updated_at) + end + + def test_saving_a_record_with_a_has_many_that_specifies_touching_a_specific_attribute_from_the_children_should_update_that_attribute + Pet.has_many :toys, :touch => :created_at + pet = Pet.first + previously_children_created_at = pet.toys.map(&:created_at) + pet.update_attribute :name, "Fluffy the Fourth" + assert previously_children_created_at != pet.toys.map(&:created_at) + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at pet = Pet.first owner = pet.owner @@ -72,4 +87,4 @@ class TimestampTest < ActiveRecord::TestCase ensure Pet.belongs_to :owner, :touch => true end -end \ No newline at end of file +end diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index a8bf94d..51ee967 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -1,5 +1,5 @@ class Pet < ActiveRecord::Base set_primary_key :pet_id belongs_to :owner, :touch => true - has_many :toys + has_many :toys, :touch => true end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7a0cf55..701942f 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -493,6 +493,8 @@ ActiveRecord::Schema.define do create_table :toys, :primary_key => :toy_id ,:force => true do |t| t.string :name t.integer :pet_id, :integer + t.datetime :created_at + t.datetime :updated_at end create_table :traffic_lights, :force => true do |t| -- 1.6.5.2 From 2659b9ae1fa1cdf6986cdfb12098b9307bfc998b Mon Sep 17 00:00:00 2001 From: Diego Carrion Date: Sun, 18 Apr 2010 21:28:36 -0300 Subject: [PATCH 2/4] add :touch option to has_one associations --- activerecord/lib/active_record/associations.rb | 3 ++- activerecord/test/cases/timestamp_test.rb | 18 +++++++++++++++++- activerecord/test/models/club.rb | 2 +- activerecord/test/models/pet.rb | 1 + 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7689839..54a5bd6 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1030,6 +1030,7 @@ module ActiveRecord association_constructor_method(:create, reflection, HasOneAssociation) configure_dependency_for_has_one(reflection) end + add_touch_callbacks(reflection, options[:touch]) if options[:touch] end # Specifies a one-to-one association with another class. This method should only be used @@ -1626,7 +1627,7 @@ 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, :inverse_of + :validate, :primary_key, :inverse_of, :touch ] def create_has_one_reflection(association_id, options) diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index db5a025..02764a2 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -2,9 +2,10 @@ require 'cases/helper' require 'models/developer' require 'models/owner' require 'models/pet' +require 'models/toy' class TimestampTest < ActiveRecord::TestCase - fixtures :developers, :owners, :pets + fixtures :developers, :owners, :pets, :toys def setup @developer = Developer.first @@ -36,6 +37,21 @@ class TimestampTest < ActiveRecord::TestCase assert previously_created_at != @developer.created_at end + + def test_saving_a_record_with_a_has_one_that_specifies_touching_the_children_should_update_the_children_updated_at + pet = Pet.first + previously_children_updated_at = pet.toy.updated_at + pet.update_attribute :name, "Clash" + assert previously_children_updated_at != pet.toy.updated_at + end + + def test_saving_a_record_with_a_has_one_that_specifies_touching_a_specific_attribute_from_the_children_should_update_that_attribute + Pet.has_one :toy, :touch => :created_at + pet = Pet.first + previously_children_updated_at = pet.toy.created_at + pet.update_attribute :name, "Clash" + assert previously_children_updated_at != pet.toy.created_at + end def test_saving_a_record_with_a_has_many_that_specifies_touching_the_children_should_update_the_children_updated_at pet = Pet.first diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 6e7cdd6..d258850 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -10,4 +10,4 @@ class Club < ActiveRecord::Base def private_method "I'm sorry sir, this is a *private* club, not a *pirate* club" end -end \ No newline at end of file +end diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index 51ee967..509f319 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -2,4 +2,5 @@ class Pet < ActiveRecord::Base set_primary_key :pet_id belongs_to :owner, :touch => true has_many :toys, :touch => true + has_one :toy, :touch => true end -- 1.6.5.2 From 999919dd34043930a6039e32806ed6a620e212a1 Mon Sep 17 00:00:00 2001 From: Diego Carrion Date: Sun, 18 Apr 2010 21:33:54 -0300 Subject: [PATCH 3/4] touch callbacks validates if the touch attribute is not nil, so we dont repeat this in each association --- activerecord/lib/active_record/associations.rb | 20 +++++++++++--------- 1 files changed, 11 insertions(+), 9 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 54a5bd6..dc81923 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -912,7 +912,7 @@ module ActiveRecord configure_dependency_for_has_many(reflection) add_association_callbacks(reflection.name, reflection.options) - add_touch_callbacks(reflection, options[:touch]) if options[:touch] + add_touch_callbacks(reflection, options[:touch]) if options[:through] collection_accessor_methods(reflection, HasManyThroughAssociation) @@ -1030,7 +1030,7 @@ module ActiveRecord association_constructor_method(:create, reflection, HasOneAssociation) configure_dependency_for_has_one(reflection) end - add_touch_callbacks(reflection, options[:touch]) if options[:touch] + add_touch_callbacks(reflection, options[:touch]) end # Specifies a one-to-one association with another class. This method should only be used @@ -1139,7 +1139,7 @@ module ActiveRecord end add_counter_cache_callbacks(reflection) if options[:counter_cache] - add_touch_callbacks(reflection, options[:touch]) if options[:touch] + add_touch_callbacks(reflection, options[:touch]) configure_dependency_for_belongs_to(reflection) end @@ -1471,14 +1471,16 @@ module ActiveRecord end def add_touch_callbacks(reflection, touch_attribute) - method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do - [send(reflection.name)].flatten.each do |model| - model.touch touch_attribute + if touch_attribute + method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym + define_method(method_name) do + [send(reflection.name)].flatten.each do |model| + model.touch touch_attribute + end end + after_save(method_name) + after_destroy(method_name) end - after_save(method_name) - after_destroy(method_name) end # Creates before_destroy callback methods that nullify, delete or destroy -- 1.6.5.2 From cc17bf6e8245163def9a715523a3dc672f0dca1f Mon Sep 17 00:00:00 2001 From: Diego Carrion Date: Sun, 18 Apr 2010 21:49:37 -0300 Subject: [PATCH 4/4] added missed rdoc documentation about the touch option in has_one association --- activerecord/lib/active_record/associations.rb | 3 +++ 1 files changed, 3 insertions(+), 0 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index dc81923..ddfdc6b 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1005,6 +1005,9 @@ module ActiveRecord # If false, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default. + # [:touch] + # If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or + # destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute. # [:inverse_of] # Specifies the name of the belongs_to association on the associated object that is the inverse of this has_one # association. Does not work in combination with :through or :as options. -- 1.6.5.2