Skip to content
This repository
Browse code

Added :touch option to belongs_to associations that will touch the pa…

…rent record when the current record is saved or destroyed [DHH]
  • Loading branch information...
commit fa750e08a8d8c9f07afd88e616284549598926e2 1 parent 50e8674
David Heinemeier Hansson authored
4  activerecord/CHANGELOG
... ...
@@ -1,6 +1,8 @@
1 1
 *Edge*
2 2
 
3  
-* Added ActiveRecord::Base#touch to update the updated_at/on attributes with the current time [DHH]
  3
+* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH]
  4
+
  5
+* Added ActiveRecord::Base#touch to update the updated_at/on attributes (or another specified timestamp) with the current time [DHH]
4 6
 
5 7
 
6 8
 *2.3.2 [Final] (March 15, 2009)*
68  activerecord/lib/active_record/associations.rb
@@ -981,6 +981,9 @@ def has_one(association_id, options = {})
981 981
       #   If false, don't validate the associated objects when saving the parent object. +false+ by default.
982 982
       # [:autosave]
983 983
       #   If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
  984
+      # [:touch]
  985
+      #   If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or
  986
+      #   destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute.
984 987
       #
985 988
       # Option examples:
986 989
       #   belongs_to :firm, :foreign_key => "client_of"
@@ -990,6 +993,8 @@ def has_one(association_id, options = {})
990 993
       #   belongs_to :attachable, :polymorphic => true
991 994
       #   belongs_to :project, :readonly => true
992 995
       #   belongs_to :post, :counter_cache => true
  996
+      #   belongs_to :company, :touch => true
  997
+      #   belongs_to :company, :touch => :employees_last_updated_at
993 998
       def belongs_to(association_id, options = {})
994 999
         reflection = create_belongs_to_reflection(association_id, options)
995 1000
 
@@ -1001,28 +1006,8 @@ def belongs_to(association_id, options = {})
1001 1006
           association_constructor_method(:create, reflection, BelongsToAssociation)
1002 1007
         end
1003 1008
 
1004  
-        # Create the callbacks to update counter cache
1005  
-        if options[:counter_cache]
1006  
-          cache_column = reflection.counter_cache_column
1007  
-
1008  
-          method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
1009  
-          define_method(method_name) do
1010  
-            association = send(reflection.name)
1011  
-            association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
1012  
-          end
1013  
-          after_create method_name
1014  
-
1015  
-          method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
1016  
-          define_method(method_name) do
1017  
-            association = send(reflection.name)
1018  
-            association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
1019  
-          end
1020  
-          before_destroy method_name
1021  
-
1022  
-          module_eval(
1023  
-            "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
1024  
-          )
1025  
-        end
  1009
+        add_counter_cache_callbacks(reflection)          if options[:counter_cache]
  1010
+        add_touch_callbacks(reflection, options[:touch]) if options[:touch]
1026 1011
 
1027 1012
         configure_dependency_for_belongs_to(reflection)
1028 1013
       end
@@ -1329,6 +1314,43 @@ def association_constructor_method(constructor, reflection, association_proxy_cl
1329 1314
           end
1330 1315
         end
1331 1316
 
  1317
+        def add_counter_cache_callbacks(reflection)
  1318
+          cache_column = reflection.counter_cache_column
  1319
+
  1320
+          method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
  1321
+          define_method(method_name) do
  1322
+            association = send(reflection.name)
  1323
+            association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
  1324
+          end
  1325
+          after_create(method_name)
  1326
+
  1327
+          method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
  1328
+          define_method(method_name) do
  1329
+            association = send(reflection.name)
  1330
+            association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
  1331
+          end
  1332
+          before_destroy(method_name)
  1333
+
  1334
+          module_eval(
  1335
+            "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
  1336
+          )
  1337
+        end
  1338
+        
  1339
+        def add_touch_callbacks(reflection, touch_attribute)
  1340
+          method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym
  1341
+          define_method(method_name) do
  1342
+            association = send(reflection.name)
  1343
+            
  1344
+            if touch_attribute == true
  1345
+              association.touch unless association.nil?
  1346
+            else
  1347
+              association.touch(touch_attribute) unless association.nil?
  1348
+            end
  1349
+          end
  1350
+          after_save(method_name)
  1351
+          after_destroy(method_name)
  1352
+        end
  1353
+
1332 1354
         def find_with_associations(options = {})
1333 1355
           catch :invalid_query do
1334 1356
             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)
1499 1521
         @@valid_keys_for_belongs_to_association = [
1500 1522
           :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
1501 1523
           :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
1502  
-          :validate
  1524
+          :validate, :touch
1503 1525
         ]
1504 1526
 
1505 1527
         def create_belongs_to_reflection(association_id, options)
16  activerecord/lib/active_record/timestamp.rb
@@ -18,11 +18,21 @@ def self.included(base) #:nodoc:
18 18
     
19 19
     # Saves the record with the updated_at/on attributes set to the current time.
20 20
     # If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised.
21  
-    def touch
  21
+    # If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes.
  22
+    #
  23
+    # Examples:
  24
+    #
  25
+    #   product.touch               # updates updated_at
  26
+    #   product.touch(:designed_at) # updates the designed_at attribute
  27
+    def touch(attribute = nil)
22 28
       current_time = current_time_from_proper_timezone
23 29
 
24  
-      write_attribute('updated_at', current_time) if respond_to?(:updated_at)
25  
-      write_attribute('updated_on', current_time) if respond_to?(:updated_on)
  30
+      if attribute
  31
+        write_attribute(attribute, current_time)
  32
+      else
  33
+        write_attribute('updated_at', current_time) if respond_to?(:updated_at)
  34
+        write_attribute('updated_on', current_time) if respond_to?(:updated_on)
  35
+      end
26 36
 
27 37
       save!
28 38
     end
47  activerecord/test/cases/timestamp_test.rb
... ...
@@ -1,8 +1,10 @@
1 1
 require 'cases/helper'
2 2
 require 'models/developer'
  3
+require 'models/owner'
  4
+require 'models/pet'
3 5
 
4 6
 class TimestampTest < ActiveRecord::TestCase
5  
-  fixtures :developers
  7
+  fixtures :developers, :owners, :pets
6 8
 
7 9
   def setup
8 10
     @developer = Developer.first
@@ -27,4 +29,47 @@ def test_touching_a_record_updates_its_timestamp
27 29
     
28 30
     assert @previously_updated_at != @developer.updated_at
29 31
   end
  32
+  
  33
+  def test_touching_a_different_attribute
  34
+    previously_created_at = @developer.created_at
  35
+    @developer.touch(:created_at)
  36
+
  37
+    assert previously_created_at != @developer.created_at
  38
+  end
  39
+  
  40
+  def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
  41
+    pet   = Pet.first
  42
+    owner = pet.owner
  43
+    previously_owner_updated_at = owner.updated_at
  44
+    
  45
+    pet.name = "Fluffy the Third"
  46
+    pet.save
  47
+    
  48
+    assert previously_owner_updated_at != pet.owner.updated_at
  49
+  end
  50
+
  51
+  def test_destroying_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
  52
+    pet   = Pet.first
  53
+    owner = pet.owner
  54
+    previously_owner_updated_at = owner.updated_at
  55
+    
  56
+    pet.destroy
  57
+    
  58
+    assert previously_owner_updated_at != pet.owner.updated_at
  59
+  end
  60
+  
  61
+  def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
  62
+    Pet.belongs_to :owner, :touch => :happy_at
  63
+
  64
+    pet   = Pet.first
  65
+    owner = pet.owner
  66
+    previously_owner_happy_at = owner.happy_at
  67
+    
  68
+    pet.name = "Fluffy the Third"
  69
+    pet.save
  70
+    
  71
+    assert previously_owner_happy_at != pet.owner.happy_at
  72
+  ensure
  73
+    Pet.belongs_to :owner, :touch => true
  74
+  end
30 75
 end
2  activerecord/test/models/pet.rb
... ...
@@ -1,5 +1,5 @@
1 1
 class Pet < ActiveRecord::Base
2 2
   set_primary_key :pet_id
3  
-  belongs_to :owner
  3
+  belongs_to :owner, :touch => true
4 4
   has_many :toys
5 5
 end
2  activerecord/test/schema/schema.rb
@@ -281,6 +281,8 @@ def create_table(*args, &block)
281 281
 
282 282
   create_table :owners, :primary_key => :owner_id ,:force => true do |t|
283 283
     t.string :name
  284
+    t.column :updated_at, :datetime
  285
+    t.column :happy_at,   :datetime
284 286
   end
285 287
 
286 288
 

0 notes on commit fa750e0

Please sign in to comment.
Something went wrong with that request. Please try again.