Skip to content
This repository
Browse code

Nested records (re: autosave) are now updated even when the intermedi…

…ate parent record is unchanged [#4242]

Signed-off-by: José Valim <jose.valim@gmail.com>
  • Loading branch information...
commit a5696e36c6b49381e84184fcc2f164285a26a166 1 parent 94878c6
Ian White authored May 18, 2010 josevalim committed May 18, 2010
25  activerecord/lib/active_record/autosave_association.rb
@@ -217,6 +217,12 @@ def marked_for_destruction?
217 217
       @marked_for_destruction
218 218
     end
219 219
 
  220
+    # Returns whether or not this record has been changed in any way (including whether
  221
+    # any of its nested autosave associations are likewise changed)
  222
+    def changed_for_autosave?
  223
+      new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
  224
+    end
  225
+    
220 226
     private
221 227
 
222 228
     # Returns the record for an association collection that should be validated
@@ -226,12 +232,27 @@ def associated_records_to_validate_or_save(association, new_record, autosave)
226 232
       if new_record
227 233
         association
228 234
       elsif autosave
229  
-        association.target.select { |record| record.new_record? || record.changed? || record.marked_for_destruction? }
  235
+        association.target.select { |record| record.changed_for_autosave? }
230 236
       else
231 237
         association.target.select { |record| record.new_record? }
232 238
       end
233 239
     end
234  
-
  240
+    
  241
+    # go through nested autosave associations that are loaded in memory (without loading
  242
+    # any new ones), and return true if is changed for autosave
  243
+    def nested_records_changed_for_autosave?
  244
+      self.class.reflect_on_all_autosave_associations.each do |reflection|
  245
+        if association = association_instance_get(reflection.name)
  246
+          if [:belongs_to, :has_one].include?(reflection.macro)
  247
+            return true if association.target && association.target.changed_for_autosave?
  248
+          else
  249
+            association.target.each {|record| return true if record.changed_for_autosave? }
  250
+          end
  251
+        end
  252
+      end
  253
+      false
  254
+    end
  255
+    
235 256
     # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
236 257
     # turned on for the association specified by +reflection+.
237 258
     def validate_single_association(reflection)
80  activerecord/test/cases/nested_attributes_test.rb
... ...
@@ -1,6 +1,7 @@
1 1
 require "cases/helper"
2 2
 require "models/pirate"
3 3
 require "models/ship"
  4
+require "models/ship_part"
4 5
 require "models/bird"
5 6
 require "models/parrot"
6 7
 require "models/treasure"
@@ -731,3 +732,82 @@ def test_should_update_existing_records_with_non_standard_primary_key
731 732
     assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name)
732 733
   end
733 734
 end
  735
+
  736
+class TestHasOneAutosaveAssoictaionWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
  737
+  self.use_transactional_fixtures = false
  738
+
  739
+  def setup
  740
+    @pirate = Pirate.create!(:catchphrase => "My baby takes tha mornin' train!")
  741
+    @ship = @pirate.create_ship(:name => "The good ship Dollypop")
  742
+    @part = @ship.parts.create!(:name => "Mast")
  743
+    @trinket = @part.trinkets.create!(:name => "Necklace")
  744
+  end
  745
+  
  746
+  test "when great-grandchild changed in memory, saving parent should save great-grandchild" do
  747
+    @trinket.name = "changed"
  748
+    @pirate.save
  749
+    assert_equal "changed", @trinket.reload.name
  750
+  end
  751
+  
  752
+  test "when great-grandchild changed via attributes, saving parent should save great-grandchild" do
  753
+    @pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :name => "changed"}]}]}}
  754
+    @pirate.save
  755
+    assert_equal "changed", @trinket.reload.name
  756
+  end
  757
+  
  758
+  test "when great-grandchild marked_for_destruction via attributes, saving parent should destroy great-grandchild" do
  759
+    @pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :_destroy => true}]}]}}
  760
+    assert_difference('@part.trinkets.count', -1) { @pirate.save }
  761
+  end
  762
+  
  763
+  test "when great-grandchild added via attributes, saving parent should create great-grandchild" do
  764
+    @pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:name => "created"}]}]}}
  765
+    assert_difference('@part.trinkets.count', 1) { @pirate.save }
  766
+  end
  767
+  
  768
+  test "when extra records exist for associations, validate (which calls nested_records_changed_for_autosave?) should not load them up" do
  769
+    @trinket.name = "changed"
  770
+    Ship.create!(:pirate => @pirate, :name => "The Black Rock")
  771
+    ShipPart.create!(:ship => @ship, :name => "Stern")
  772
+    assert_no_queries { @pirate.valid? }
  773
+  end
  774
+end
  775
+
  776
+class TestHasManyAutosaveAssoictaionWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
  777
+  self.use_transactional_fixtures = false
  778
+
  779
+  def setup
  780
+    @ship = Ship.create!(:name => "The good ship Dollypop")
  781
+    @part = @ship.parts.create!(:name => "Mast")
  782
+    @trinket = @part.trinkets.create!(:name => "Necklace")
  783
+  end
  784
+  
  785
+  test "when grandchild changed in memory, saving parent should save grandchild" do
  786
+    @trinket.name = "changed"
  787
+    @ship.save
  788
+    assert_equal "changed", @trinket.reload.name
  789
+  end
  790
+  
  791
+  test "when grandchild changed via attributes, saving parent should save grandchild" do
  792
+    @ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :name => "changed"}]}]}
  793
+    @ship.save
  794
+    assert_equal "changed", @trinket.reload.name
  795
+  end
  796
+  
  797
+  test "when grandchild marked_for_destruction via attributes, saving parent should destroy grandchild" do
  798
+    @ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :_destroy => true}]}]}
  799
+    assert_difference('@part.trinkets.count', -1) { @ship.save }
  800
+  end
  801
+  
  802
+  test "when grandchild added via attributes, saving parent should create grandchild" do
  803
+    @ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:name => "created"}]}]}
  804
+    assert_difference('@part.trinkets.count', 1) { @ship.save }
  805
+  end
  806
+  
  807
+  test "when extra records exist for associations, validate (which calls nested_records_changed_for_autosave?) should not load them up" do
  808
+    @trinket.name = "changed"
  809
+    Ship.create!(:name => "The Black Rock")
  810
+    ShipPart.create!(:ship => @ship, :name => "Stern")
  811
+    assert_no_queries { @ship.valid? }
  812
+  end
  813
+end
3  activerecord/test/models/ship.rb
@@ -3,8 +3,9 @@ class Ship < ActiveRecord::Base
3 3
 
4 4
   belongs_to :pirate
5 5
   belongs_to :update_only_pirate, :class_name => 'Pirate'
6  
-  has_many :parts, :class_name => 'ShipPart', :autosave => true
  6
+  has_many :parts, :class_name => 'ShipPart'
7 7
 
  8
+  accepts_nested_attributes_for :parts, :allow_destroy => true
8 9
   accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
9 10
   accepts_nested_attributes_for :update_only_pirate, :update_only => true
10 11
 
4  activerecord/test/models/ship_part.rb
... ...
@@ -1,5 +1,7 @@
1 1
 class ShipPart < ActiveRecord::Base
2 2
   belongs_to :ship
3  
-
  3
+  has_many :trinkets, :class_name => "Treasure", :as => :looter
  4
+  accepts_nested_attributes_for :trinkets, :allow_destroy => true
  5
+  
4 6
   validates_presence_of :name
5 7
 end

0 notes on commit a5696e3

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