diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 472d4aa918e8..d58b44144b2e 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,6 +1,8 @@ *Edge* -* Added ActiveRecord::Base#touch to update the updated_at/on attributes with the current time [DHH] +* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH] + +* Added ActiveRecord::Base#touch to update the updated_at/on attributes (or another specified timestamp) with the current time [DHH] *2.3.2 [Final] (March 15, 2009)* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6d25b36aea5a..53a710537fae 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -981,6 +981,9 @@ def has_one(association_id, options = {}) # If false, don't validate the associated objects 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. # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -990,6 +993,8 @@ def has_one(association_id, options = {}) # belongs_to :attachable, :polymorphic => true # belongs_to :project, :readonly => true # belongs_to :post, :counter_cache => true + # belongs_to :company, :touch => true + # belongs_to :company, :touch => :employees_last_updated_at def belongs_to(association_id, options = {}) reflection = create_belongs_to_reflection(association_id, options) @@ -1001,28 +1006,8 @@ def belongs_to(association_id, options = {}) association_constructor_method(:create, reflection, BelongsToAssociation) end - # Create the callbacks to update counter cache - if options[:counter_cache] - cache_column = reflection.counter_cache_column - - method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? - end - after_create method_name - - method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? - end - before_destroy method_name - - module_eval( - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)" - ) - end + add_counter_cache_callbacks(reflection) if options[:counter_cache] + add_touch_callbacks(reflection, options[:touch]) if options[:touch] configure_dependency_for_belongs_to(reflection) end @@ -1329,6 +1314,43 @@ def association_constructor_method(constructor, reflection, association_proxy_cl end end + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column + + method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym + define_method(method_name) do + association = send(reflection.name) + association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? + end + after_create(method_name) + + method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym + define_method(method_name) do + association = send(reflection.name) + association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? + end + before_destroy(method_name) + + module_eval( + "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)" + ) + 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 + association = send(reflection.name) + + if touch_attribute == true + association.touch unless association.nil? + else + association.touch(touch_attribute) unless association.nil? + end + end + after_save(method_name) + after_destroy(method_name) + end + def find_with_associations(options = {}) catch :invalid_query do join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) @@ -1499,7 +1521,7 @@ def create_has_one_through_reflection(association_id, options) @@valid_keys_for_belongs_to_association = [ :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, - :validate + :validate, :touch ] def create_belongs_to_reflection(association_id, options) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 648861f8d4c8..d9e1ef351f7d 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -18,11 +18,21 @@ def self.included(base) #:nodoc: # Saves the record with the updated_at/on attributes set to the current time. # If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised. - def touch + # If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes. + # + # Examples: + # + # product.touch # updates updated_at + # product.touch(:designed_at) # updates the designed_at attribute + def touch(attribute = nil) current_time = current_time_from_proper_timezone - write_attribute('updated_at', current_time) if respond_to?(:updated_at) - write_attribute('updated_on', current_time) if respond_to?(:updated_on) + if attribute + write_attribute(attribute, current_time) + else + write_attribute('updated_at', current_time) if respond_to?(:updated_at) + write_attribute('updated_on', current_time) if respond_to?(:updated_on) + end save! end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index e5f8fb9c1dbd..24b237a72b36 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,8 +1,10 @@ require 'cases/helper' require 'models/developer' +require 'models/owner' +require 'models/pet' class TimestampTest < ActiveRecord::TestCase - fixtures :developers + fixtures :developers, :owners, :pets def setup @developer = Developer.first @@ -27,4 +29,47 @@ def test_touching_a_record_updates_its_timestamp assert @previously_updated_at != @developer.updated_at end + + def test_touching_a_different_attribute + previously_created_at = @developer.created_at + @developer.touch(:created_at) + + assert previously_created_at != @developer.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 + previously_owner_updated_at = owner.updated_at + + pet.name = "Fluffy the Third" + pet.save + + assert previously_owner_updated_at != pet.owner.updated_at + end + + def test_destroying_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at + pet = Pet.first + owner = pet.owner + previously_owner_updated_at = owner.updated_at + + pet.destroy + + assert previously_owner_updated_at != pet.owner.updated_at + end + + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute + Pet.belongs_to :owner, :touch => :happy_at + + pet = Pet.first + owner = pet.owner + previously_owner_happy_at = owner.happy_at + + pet.name = "Fluffy the Third" + pet.save + + assert previously_owner_happy_at != pet.owner.happy_at + ensure + Pet.belongs_to :owner, :touch => true + end end \ No newline at end of file diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index dc1a3c5e94e4..a8bf94dd8679 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 + belongs_to :owner, :touch => true has_many :toys end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ea848a29403d..5640510c96a0 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -281,6 +281,8 @@ def create_table(*args, &block) create_table :owners, :primary_key => :owner_id ,:force => true do |t| t.string :name + t.column :updated_at, :datetime + t.column :happy_at, :datetime end