Skip to content
This repository
Browse code

Add support for nested object forms to ActiveRecord and the helpers i…

…n ActionPack

Signed-Off-By: Michael Koziarski <michael@koziarski.com>

[#1202 state:committed]
  • Loading branch information...
commit ec8f04584479aff895b0b511a7ba1e9d33f84067 1 parent a02d752
Eloy Durán authored February 01, 2009 NZKoz committed February 01, 2009
12  actionpack/CHANGELOG
... ...
@@ -1,5 +1,17 @@
1 1
 *2.3.0 [Edge]*
2 2
 
  3
+* Make the form_for and fields_for helpers support the new Active Record nested update options.  #1202 [Eloy Duran]
  4
+
  5
+		<% form_for @person do |person_form| %>
  6
+		  ...
  7
+		  <% person_form.fields_for :projects do |project_fields| %>
  8
+		    <% if project_fields.object.active? %>
  9
+		      Name: <%= project_fields.text_field :name %>
  10
+		    <% end %>
  11
+		  <% end %>
  12
+		<% end %>
  13
+
  14
+
3 15
 * Added grouped_options_for_select helper method for wrapping option tags in optgroups. #977 [Jon Crawford]
4 16
 
5 17
 * Implement HTTP Digest authentication. #1230 [Gregg Kellogg, Pratik Naik] Example :
196  actionpack/lib/action_view/helpers/form_helper.rb
@@ -269,10 +269,12 @@ def apply_form_for_options!(object_or_array, options) #:nodoc:
269 269
         options[:url] ||= polymorphic_path(object_or_array)
270 270
       end
271 271
 
272  
-      # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
273  
-      # fields_for suitable for specifying additional model objects in the same form:
  272
+      # Creates a scope around a specific model object like form_for, but
  273
+      # doesn't create the form tags themselves. This makes fields_for suitable
  274
+      # for specifying additional model objects in the same form.
  275
+      #
  276
+      # === Generic Examples
274 277
       #
275  
-      # ==== Examples
276 278
       #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
277 279
       #     First name: <%= person_form.text_field :first_name %>
278 280
       #     Last name : <%= person_form.text_field :last_name %>
@@ -282,20 +284,166 @@ def apply_form_for_options!(object_or_array, options) #:nodoc:
282 284
       #     <% end %>
283 285
       #   <% end %>
284 286
       #
285  
-      # ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person:
  287
+      # ...or if you have an object that needs to be represented as a different
  288
+      # parameter, like a Client that acts as a Person:
286 289
       #
287 290
       #   <% fields_for :person, @client do |permission_fields| %>
288 291
       #     Admin?: <%= permission_fields.check_box :admin %>
289 292
       #   <% end %>
290 293
       #
291  
-      # ...or if you don't have an object, just a name of the parameter
  294
+      # ...or if you don't have an object, just a name of the parameter:
292 295
       #
293 296
       #   <% fields_for :person do |permission_fields| %>
294 297
       #     Admin?: <%= permission_fields.check_box :admin %>
295 298
       #   <% end %>
296 299
       #
297  
-      # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base,
298  
-      # like FormOptionHelper#collection_select and DateHelper#datetime_select.
  300
+      # Note: This also works for the methods in FormOptionHelper and
  301
+      # DateHelper that are designed to work with an object as base, like
  302
+      # FormOptionHelper#collection_select and DateHelper#datetime_select.
  303
+      #
  304
+      # === Nested Attributes Examples
  305
+      #
  306
+      # When the object belonging to the current scope has a nested attribute
  307
+      # writer for a certain attribute, fields_for will yield a new scope
  308
+      # for that attribute. This allows you to create forms that set or change
  309
+      # the attributes of a parent object and its associations in one go.
  310
+      #
  311
+      # Nested attribute writers are normal setter methods named after an
  312
+      # association. The most common way of defining these writers is either
  313
+      # with +accepts_nested_attributes_for+ in a model definition or by
  314
+      # defining a method with the proper name. For example: the attribute
  315
+      # writer for the association <tt>:address</tt> is called
  316
+      # <tt>address_attributes=</tt>.
  317
+      #
  318
+      # Whether a one-to-one or one-to-many style form builder will be yielded
  319
+      # depends on whether the normal reader method returns a _single_ object
  320
+      # or an _array_ of objects.
  321
+      #
  322
+      # ==== One-to-one
  323
+      #
  324
+      # Consider a Person class which returns a _single_ Address from the
  325
+      # <tt>address</tt> reader method and responds to the
  326
+      # <tt>address_attributes=</tt> writer method:
  327
+      #
  328
+      #   class Person
  329
+      #     def address
  330
+      #       @address
  331
+      #     end
  332
+      #
  333
+      #     def address_attributes=(attributes)
  334
+      #       # Process the attributes hash
  335
+      #     end
  336
+      #   end
  337
+      #
  338
+      # This model can now be used with a nested fields_for, like so:
  339
+      #
  340
+      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
  341
+      #     ...
  342
+      #     <% person_form.fields_for :address do |address_fields| %>
  343
+      #       Street  : <%= address_fields.text_field :street %>
  344
+      #       Zip code: <%= address_fields.text_field :zip_code %>
  345
+      #     <% end %>
  346
+      #   <% end %>
  347
+      #
  348
+      # When address is already an association on a Person you can use
  349
+      # +accepts_nested_attributes_for+ to define the writer method for you:
  350
+      #
  351
+      #   class Person < ActiveRecord::Base
  352
+      #     has_one :address
  353
+      #     accepts_nested_attributes_for :address
  354
+      #   end
  355
+      #
  356
+      # If you want to destroy the associated model through the form, you have
  357
+      # to enable it first using the <tt>:allow_destroy</tt> option for
  358
+      # +accepts_nested_attributes_for+:
  359
+      #
  360
+      #   class Person < ActiveRecord::Base
  361
+      #     has_one :address
  362
+      #     accepts_nested_attributes_for :address, :allow_destroy => true
  363
+      #   end
  364
+      #
  365
+      # Now, when you use a form element with the <tt>_delete</tt> parameter,
  366
+      # with a value that evaluates to +true+, you will destroy the associated
  367
+      # model (eg. 1, '1', true, or 'true'):
  368
+      #
  369
+      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
  370
+      #     ...
  371
+      #     <% person_form.fields_for :address do |address_fields| %>
  372
+      #       ...
  373
+      #       Delete: <%= address_fields.check_box :_delete %>
  374
+      #     <% end %>
  375
+      #   <% end %>
  376
+      #
  377
+      # ==== One-to-many
  378
+      #
  379
+      # Consider a Person class which returns an _array_ of Project instances
  380
+      # from the <tt>projects</tt> reader method and responds to the
  381
+      # <tt>projects_attributes=</tt> writer method:
  382
+      #
  383
+      #   class Person
  384
+      #     def projects
  385
+      #       [@project1, @project2]
  386
+      #     end
  387
+      #
  388
+      #     def projects_attributes=(attributes)
  389
+      #       # Process the attributes hash
  390
+      #     end
  391
+      #   end
  392
+      #
  393
+      # This model can now be used with a nested fields_for. The block given to
  394
+      # the nested fields_for call will be repeated for each instance in the
  395
+      # collection:
  396
+      #
  397
+      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
  398
+      #     ...
  399
+      #     <% person_form.fields_for :projects do |project_fields| %>
  400
+      #       <% if project_fields.object.active? %>
  401
+      #         Name: <%= project_fields.text_field :name %>
  402
+      #       <% end %>
  403
+      #     <% end %>
  404
+      #   <% end %>
  405
+      #
  406
+      # It's also possible to specify the instance to be used:
  407
+      #
  408
+      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
  409
+      #     ...
  410
+      #     <% @person.projects.each do |project| %>
  411
+      #       <% if project.active? %>
  412
+      #         <% person_form.fields_for :projects, project do |project_fields| %>
  413
+      #           Name: <%= project_fields.text_field :name %>
  414
+      #         <% end %>
  415
+      #       <% end %>
  416
+      #     <% end %>
  417
+      #   <% end %>
  418
+      #
  419
+      # When projects is already an association on Person you can use
  420
+      # +accepts_nested_attributes_for+ to define the writer method for you:
  421
+      #
  422
+      #   class Person < ActiveRecord::Base
  423
+      #     has_many :projects
  424
+      #     accepts_nested_attributes_for :projects
  425
+      #   end
  426
+      #
  427
+      # If you want to destroy any of the associated models through the
  428
+      # form, you have to enable it first using the <tt>:allow_destroy</tt>
  429
+      # option for +accepts_nested_attributes_for+:
  430
+      #
  431
+      #   class Person < ActiveRecord::Base
  432
+      #     has_many :projects
  433
+      #     accepts_nested_attributes_for :projects, :allow_destroy => true
  434
+      #   end
  435
+      #
  436
+      # This will allow you to specify which models to destroy in the
  437
+      # attributes hash by adding a form element for the <tt>_delete</tt>
  438
+      # parameter with a value that evaluates to +true+
  439
+      # (eg. 1, '1', true, or 'true'):
  440
+      #
  441
+      #   <% form_for @person, :url => { :action => "update" } do |person_form| %>
  442
+      #     ...
  443
+      #     <% person_form.fields_for :projects do |project_fields| %>
  444
+      #       Delete: <%= project_fields.check_box :_delete %>
  445
+      #     <% end %>
  446
+      #   <% end %>
299 447
       def fields_for(record_or_name_or_array, *args, &block)
300 448
         raise ArgumentError, "Missing block" unless block_given?
301 449
         options = args.extract_options!
@@ -760,7 +908,11 @@ def fields_for(record_or_name_or_array, *args, &block)
760 908
 
761 909
         case record_or_name_or_array
762 910
         when String, Symbol
763  
-          name = "#{object_name}#{index}[#{record_or_name_or_array}]"
  911
+          if nested_attributes_association?(record_or_name_or_array)
  912
+            return fields_for_with_nested_attributes(record_or_name_or_array, args, block)
  913
+          else
  914
+            name = "#{object_name}#{index}[#{record_or_name_or_array}]"
  915
+          end
764 916
         when Array
765 917
           object = record_or_name_or_array.last
766 918
           name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
@@ -802,6 +954,32 @@ def submit(value = "Save changes", options = {})
802 954
         def objectify_options(options)
803 955
           @default_options.merge(options.merge(:object => @object))
804 956
         end
  957
+
  958
+        def nested_attributes_association?(association_name)
  959
+          @object.respond_to?("#{association_name}_attributes=")
  960
+        end
  961
+
  962
+        def fields_for_with_nested_attributes(association_name, args, block)
  963
+          name = "#{object_name}[#{association_name}_attributes]"
  964
+          association = @object.send(association_name)
  965
+
  966
+          if association.is_a?(Array)
  967
+            children = args.first.respond_to?(:new_record?) ? [args.first] : association
  968
+
  969
+            children.map do |child|
  970
+              child_name = "#{name}[#{ child.new_record? ? new_child_id : child.id }]"
  971
+              @template.fields_for(child_name, child, *args, &block)
  972
+            end.join
  973
+          else
  974
+            @template.fields_for(name, association, *args, &block)
  975
+          end
  976
+        end
  977
+
  978
+        def new_child_id
  979
+          value = (@child_counter ||= 1)
  980
+          @child_counter += 1
  981
+          "new_#{value}"
  982
+        end
805 983
     end
806 984
   end
807 985
 
@@ -809,4 +987,4 @@ class Base
809 987
     cattr_accessor :default_form_builder
810 988
     self.default_form_builder = ::ActionView::Helpers::FormBuilder
811 989
   end
812  
-end
  990
+end
141  actionpack/test/template/form_helper_test.rb
@@ -15,21 +15,31 @@ def new_record=(boolean)
15 15
     def new_record?
16 16
       @new_record
17 17
     end
  18
+
  19
+    attr_accessor :author
  20
+    def author_attributes=(attributes); end
  21
+
  22
+    attr_accessor :comments
  23
+    def comments_attributes=(attributes); end
18 24
   end
19 25
 
20 26
   class Comment
21 27
     attr_reader :id
22 28
     attr_reader :post_id
  29
+    def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end
23 30
     def save; @id = 1; @post_id = 1 end
24 31
     def new_record?; @id.nil? end
25 32
     def to_param; @id; end
26 33
     def name
27  
-      @id.nil? ? 'new comment' : "comment ##{@id}"
  34
+      @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
28 35
     end
29 36
   end
30  
-end
31 37
 
32  
-class Comment::Nested < Comment; end
  38
+  class Author < Comment
  39
+    attr_accessor :post
  40
+    def post_attributes=(attributes); end
  41
+  end
  42
+end
33 43
 
34 44
 class FormHelperTest < ActionView::TestCase
35 45
   tests ActionView::Helpers::FormHelper
@@ -479,7 +489,7 @@ def test_nested_fields_for_with_index_and_parent_fields
479 489
     assert_dom_equal expected, output_buffer
480 490
   end
481 491
 
482  
-  def test_nested_fields_for_with_index
  492
+  def test_form_for_with_index_and_nested_fields_for
483 493
     form_for(:post, @post, :index => 1) do |f|
484 494
       f.fields_for(:comment, @post) do |c|
485 495
         concat c.text_field(:title)
@@ -558,6 +568,127 @@ def test_nested_fields_for_with_index_and_auto_index
558 568
     assert_dom_equal expected, output_buffer
559 569
   end
560 570
 
  571
+  def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association
  572
+    @post.author = Author.new
  573
+
  574
+    form_for(:post, @post) do |f|
  575
+      concat f.text_field(:title)
  576
+      f.fields_for(:author) do |af|
  577
+        concat af.text_field(:name)
  578
+      end
  579
+    end
  580
+
  581
+    expected = '<form action="http://www.example.com" method="post">' +
  582
+               '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
  583
+               '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="new author" />' +
  584
+               '</form>'
  585
+
  586
+    assert_dom_equal expected, output_buffer
  587
+  end
  588
+
  589
+  def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association
  590
+    @post.author = Author.new(321)
  591
+
  592
+    form_for(:post, @post) do |f|
  593
+      concat f.text_field(:title)
  594
+      f.fields_for(:author) do |af|
  595
+        concat af.text_field(:name)
  596
+      end
  597
+    end
  598
+
  599
+    expected = '<form action="http://www.example.com" method="post">' +
  600
+               '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
  601
+               '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
  602
+               '</form>'
  603
+
  604
+    assert_dom_equal expected, output_buffer
  605
+  end
  606
+
  607
+  def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association
  608
+    @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
  609
+
  610
+    form_for(:post, @post) do |f|
  611
+      concat f.text_field(:title)
  612
+      @post.comments.each do |comment|
  613
+        f.fields_for(:comments, comment) do |cf|
  614
+          concat cf.text_field(:name)
  615
+        end
  616
+      end
  617
+    end
  618
+
  619
+    expected = '<form action="http://www.example.com" method="post">' +
  620
+               '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
  621
+               '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #1" />' +
  622
+               '<input id="post_comments_attributes_2_name" name="post[comments_attributes][2][name]" size="30" type="text" value="comment #2" />' +
  623
+               '</form>'
  624
+
  625
+    assert_dom_equal expected, output_buffer
  626
+  end
  627
+
  628
+  def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association
  629
+    @post.comments = [Comment.new, Comment.new]
  630
+
  631
+    form_for(:post, @post) do |f|
  632
+      concat f.text_field(:title)
  633
+      @post.comments.each do |comment|
  634
+        f.fields_for(:comments, comment) do |cf|
  635
+          concat cf.text_field(:name)
  636
+        end
  637
+      end
  638
+    end
  639
+
  640
+    expected = '<form action="http://www.example.com" method="post">' +
  641
+               '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
  642
+               '<input id="post_comments_attributes_new_1_name" name="post[comments_attributes][new_1][name]" size="30" type="text" value="new comment" />' +
  643
+               '<input id="post_comments_attributes_new_2_name" name="post[comments_attributes][new_2][name]" size="30" type="text" value="new comment" />' +
  644
+               '</form>'
  645
+
  646
+    assert_dom_equal expected, output_buffer
  647
+  end
  648
+
  649
+  def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association
  650
+    @post.comments = [Comment.new(321), Comment.new]
  651
+
  652
+    form_for(:post, @post) do |f|
  653
+      concat f.text_field(:title)
  654
+      @post.comments.each do |comment|
  655
+        f.fields_for(:comments, comment) do |cf|
  656
+          concat cf.text_field(:name)
  657
+        end
  658
+      end
  659
+    end
  660
+
  661
+    expected = '<form action="http://www.example.com" method="post">' +
  662
+               '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
  663
+               '<input id="post_comments_attributes_321_name" name="post[comments_attributes][321][name]" size="30" type="text" value="comment #321" />' +
  664
+               '<input id="post_comments_attributes_new_1_name" name="post[comments_attributes][new_1][name]" size="30" type="text" value="new comment" />' +
  665
+               '</form>'
  666
+
  667
+    assert_dom_equal expected, output_buffer
  668
+  end
  669
+
  670
+  def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_only_builder
  671
+    @post.comments = [Comment.new(321), Comment.new]
  672
+    yielded_comments = []
  673
+
  674
+    form_for(:post, @post) do |f|
  675
+      concat f.text_field(:title)
  676
+      f.fields_for(:comments) do |cf|
  677
+        concat cf.text_field(:name)
  678
+        yielded_comments << cf.object
  679
+      end
  680
+    end
  681
+
  682
+    expected = '<form action="http://www.example.com" method="post">' +
  683
+               '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
  684
+               '<input id="post_comments_attributes_321_name" name="post[comments_attributes][321][name]" size="30" type="text" value="comment #321" />' +
  685
+               '<input id="post_comments_attributes_new_1_name" name="post[comments_attributes][new_1][name]" size="30" type="text" value="new comment" />' +
  686
+               '</form>'
  687
+
  688
+    assert_dom_equal expected, output_buffer
  689
+    assert_equal yielded_comments, @post.comments
  690
+  end
  691
+
561 692
   def test_fields_for
562 693
     fields_for(:post, @post) do |f|
563 694
       concat f.text_field(:title)
@@ -974,4 +1105,4 @@ def post_path(post)
974 1105
     def protect_against_forgery?
975 1106
       false
976 1107
     end
977  
-end
  1108
+end
9  activerecord/CHANGELOG
... ...
@@ -1,5 +1,14 @@
1 1
 *2.3.0/3.0*
2 2
 
  3
+* Add Support for updating deeply nested models from a single form. #1202 [Eloy Duran]
  4
+
  5
+	class Book < ActiveRecord::Base
  6
+	  has_one :author
  7
+	  has_many :pages
  8
+
  9
+	  accepts_nested_attributes_for :author, :pages
  10
+	end
  11
+
3 12
 * Make after_save callbacks fire only if the record was successfully saved.  #1735 [Michael Lovitt]
4 13
 
5 14
   Previously the callbacks would fire if a before_save cancelled saving.
2  activerecord/lib/active_record.rb
@@ -46,6 +46,7 @@ def self.load_all!
46 46
   autoload :AssociationPreload, 'active_record/association_preload'
47 47
   autoload :Associations, 'active_record/associations'
48 48
   autoload :AttributeMethods, 'active_record/attribute_methods'
  49
+  autoload :AutosaveAssociation, 'active_record/autosave_association'
49 50
   autoload :Base, 'active_record/base'
50 51
   autoload :Calculations, 'active_record/calculations'
51 52
   autoload :Callbacks, 'active_record/callbacks'
@@ -55,6 +56,7 @@ def self.load_all!
55 56
   autoload :Migration, 'active_record/migration'
56 57
   autoload :Migrator, 'active_record/migration'
57 58
   autoload :NamedScope, 'active_record/named_scope'
  59
+  autoload :NestedAttributes, 'active_record/nested_attributes'
58 60
   autoload :Observing, 'active_record/observer'
59 61
   autoload :QueryCache, 'active_record/query_cache'
60 62
   autoload :Reflection, 'active_record/reflection'
91  activerecord/lib/active_record/associations.rb
@@ -88,6 +88,18 @@ def clear_association_cache #:nodoc:
88 88
       end unless self.new_record?
89 89
     end
90 90
 
  91
+    private
  92
+      # Gets the specified association instance if it responds to :loaded?, nil otherwise.
  93
+      def association_instance_get(name)
  94
+        association = instance_variable_get("@#{name}")
  95
+        association if association.respond_to?(:loaded?)
  96
+      end
  97
+
  98
+      # Set the specified association instance.
  99
+      def association_instance_set(name, association)
  100
+        instance_variable_set("@#{name}", association)
  101
+      end
  102
+
91 103
     # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
92 104
     # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
93 105
     # specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
@@ -256,6 +268,10 @@ def clear_association_cache #:nodoc:
256 268
     # You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
257 269
     # aware of, mostly involving the saving of associated objects.
258 270
     #
  271
+    # Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
  272
+    # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association,
  273
+    # in which case the members are always saved.
  274
+    #
259 275
     # === One-to-one associations
260 276
     #
261 277
     # * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in
@@ -752,6 +768,9 @@ module ClassMethods
752 768
       #   If true, all the associated objects are readonly through the association.
753 769
       # [:validate]
754 770
       #   If false, don't validate the associated objects when saving the parent object. true by default.
  771
+      # [:autosave]
  772
+      #   If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
  773
+      #
755 774
       # Option examples:
756 775
       #   has_many :comments, :order => "posted_on"
757 776
       #   has_many :comments, :include => :author
@@ -865,6 +884,8 @@ def has_many(association_id, options = {}, &extension)
865 884
       #   If true, the associated object is readonly through the association.
866 885
       # [:validate]
867 886
       #   If false, don't validate the associated object when saving the parent object. +false+ by default.
  887
+      # [:autosave]
  888
+      #   If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
868 889
       #
869 890
       # Option examples:
870 891
       #   has_one :credit_card, :dependent => :destroy  # destroys the associated credit card
@@ -882,13 +903,10 @@ def has_one(association_id, options = {})
882 903
         else
883 904
           reflection = create_has_one_reflection(association_id, options)
884 905
 
885  
-          ivar = "@#{reflection.name}"
886  
-
887 906
           method_name = "has_one_after_save_for_#{reflection.name}".to_sym
888 907
           define_method(method_name) do
889  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
890  
-
891  
-            if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
  908
+            association = association_instance_get(reflection.name)
  909
+            if association && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
892 910
               association[reflection.primary_key_name] = id
893 911
               association.save(true)
894 912
             end
@@ -979,6 +997,8 @@ def has_one(association_id, options = {})
979 997
       #   If true, the associated object is readonly through the association.
980 998
       # [:validate]
981 999
       #   If false, don't validate the associated objects when saving the parent object. +false+ by default.
  1000
+      # [:autosave]
  1001
+      #   If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
982 1002
       #
983 1003
       # Option examples:
984 1004
       #   belongs_to :firm, :foreign_key => "client_of"
@@ -991,15 +1011,12 @@ def has_one(association_id, options = {})
991 1011
       def belongs_to(association_id, options = {})
992 1012
         reflection = create_belongs_to_reflection(association_id, options)
993 1013
 
994  
-        ivar = "@#{reflection.name}"
995  
-
996 1014
         if reflection.options[:polymorphic]
997 1015
           association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
998 1016
 
999 1017
           method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
1000 1018
           define_method(method_name) do
1001  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
1002  
-
  1019
+            association = association_instance_get(reflection.name)
1003 1020
             if association && association.target
1004 1021
               if association.new_record?
1005 1022
                 association.save(true)
@@ -1019,9 +1036,7 @@ def belongs_to(association_id, options = {})
1019 1036
 
1020 1037
           method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
1021 1038
           define_method(method_name) do
1022  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
1023  
-
1024  
-            if !association.nil?
  1039
+            if association = association_instance_get(reflection.name)
1025 1040
               if association.new_record?
1026 1041
                 association.save(true)
1027 1042
               end
@@ -1196,6 +1211,8 @@ def belongs_to(association_id, options = {})
1196 1211
       #   If true, all the associated objects are readonly through the association.
1197 1212
       # [:validate]
1198 1213
       #   If false, don't validate the associated objects when saving the parent object. +true+ by default.
  1214
+      # [:autosave]
  1215
+      #   If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
1199 1216
       #
1200 1217
       # Option examples:
1201 1218
       #   has_and_belongs_to_many :projects
@@ -1243,33 +1260,30 @@ def join_table_name(first_table_name, second_table_name)
1243 1260
         end
1244 1261
 
1245 1262
         def association_accessor_methods(reflection, association_proxy_class)
1246  
-          ivar = "@#{reflection.name}"
1247  
-
1248 1263
           define_method(reflection.name) do |*params|
1249 1264
             force_reload = params.first unless params.empty?
1250  
-
1251  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1265
+            association = association_instance_get(reflection.name)
1252 1266
 
1253 1267
             if association.nil? || force_reload
1254 1268
               association = association_proxy_class.new(self, reflection)
1255 1269
               retval = association.reload
1256 1270
               if retval.nil? and association_proxy_class == BelongsToAssociation
1257  
-                instance_variable_set(ivar, nil)
  1271
+                association_instance_set(reflection.name, nil)
1258 1272
                 return nil
1259 1273
               end
1260  
-              instance_variable_set(ivar, association)
  1274
+              association_instance_set(reflection.name, association)
1261 1275
             end
1262 1276
 
1263 1277
             association.target.nil? ? nil : association
1264 1278
           end
1265 1279
 
1266 1280
           define_method("loaded_#{reflection.name}?") do
1267  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1281
+            association = association_instance_get(reflection.name)
1268 1282
             association && association.loaded?
1269 1283
           end
1270 1284
 
1271 1285
           define_method("#{reflection.name}=") do |new_value|
1272  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1286
+            association = association_instance_get(reflection.name)
1273 1287
 
1274 1288
             if association.nil? || association.target != new_value
1275 1289
               association = association_proxy_class.new(self, reflection)
@@ -1280,7 +1294,7 @@ def association_accessor_methods(reflection, association_proxy_class)
1280 1294
               self.send(reflection.name, new_value)
1281 1295
             else
1282 1296
               association.replace(new_value)
1283  
-              instance_variable_set(ivar, new_value.nil? ? nil : association)
  1297
+              association_instance_set(reflection.name, new_value.nil? ? nil : association)
1284 1298
             end
1285 1299
           end
1286 1300
 
@@ -1288,20 +1302,18 @@ def association_accessor_methods(reflection, association_proxy_class)
1288 1302
             return if target.nil? and association_proxy_class == BelongsToAssociation
1289 1303
             association = association_proxy_class.new(self, reflection)
1290 1304
             association.target = target
1291  
-            instance_variable_set(ivar, association)
  1305
+            association_instance_set(reflection.name, association)
1292 1306
           end
1293 1307
         end
1294 1308
 
1295 1309
         def collection_reader_method(reflection, association_proxy_class)
1296 1310
           define_method(reflection.name) do |*params|
1297  
-            ivar = "@#{reflection.name}"
1298  
-
1299 1311
             force_reload = params.first unless params.empty?
1300  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1312
+            association = association_instance_get(reflection.name)
1301 1313
 
1302  
-            unless association.respond_to?(:loaded?)
  1314
+            unless association
1303 1315
               association = association_proxy_class.new(self, reflection)
1304  
-              instance_variable_set(ivar, association)
  1316
+              association_instance_set(reflection.name, association)
1305 1317
             end
1306 1318
 
1307 1319
             association.reload if force_reload
@@ -1339,8 +1351,7 @@ def collection_accessor_methods(reflection, association_proxy_class, writer = tr
1339 1351
         def add_single_associated_validation_callbacks(association_name)
1340 1352
           method_name = "validate_associated_records_for_#{association_name}".to_sym
1341 1353
           define_method(method_name) do
1342  
-            association = instance_variable_get("@#{association_name}")
1343  
-            if !association.nil?
  1354
+            if association = association_instance_get(association_name)
1344 1355
               errors.add association_name unless association.target.nil? || association.valid?
1345 1356
             end
1346 1357
           end
@@ -1350,12 +1361,10 @@ def add_single_associated_validation_callbacks(association_name)
1350 1361
 
1351 1362
         def add_multiple_associated_validation_callbacks(association_name)
1352 1363
           method_name = "validate_associated_records_for_#{association_name}".to_sym
1353  
-          ivar = "@#{association_name}"
1354  
-
1355 1364
           define_method(method_name) do
1356  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1365
+            association = association_instance_get(association_name)
1357 1366
 
1358  
-            if association.respond_to?(:loaded?)
  1367
+            if association
1359 1368
               if new_record?
1360 1369
                 association
1361 1370
               elsif association.loaded?
@@ -1372,8 +1381,6 @@ def add_multiple_associated_validation_callbacks(association_name)
1372 1381
         end
1373 1382
 
1374 1383
         def add_multiple_associated_save_callbacks(association_name)
1375  
-          ivar = "@#{association_name}"
1376  
-
1377 1384
           method_name = "before_save_associated_records_for_#{association_name}".to_sym
1378 1385
           define_method(method_name) do
1379 1386
             @new_record_before_save = new_record?
@@ -1383,13 +1390,13 @@ def add_multiple_associated_save_callbacks(association_name)
1383 1390
 
1384 1391
           method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
1385 1392
           define_method(method_name) do
1386  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1393
+            association = association_instance_get(association_name)
1387 1394
 
1388 1395
             records_to_save = if @new_record_before_save
1389 1396
               association
1390  
-            elsif association.respond_to?(:loaded?) && association.loaded?
  1397
+            elsif association && association.loaded?
1391 1398
               association.select { |record| record.new_record? }
1392  
-            elsif association.respond_to?(:loaded?) && !association.loaded?
  1399
+            elsif association && !association.loaded?
1393 1400
               association.target.select { |record| record.new_record? }
1394 1401
             else
1395 1402
               []
@@ -1407,15 +1414,13 @@ def add_multiple_associated_save_callbacks(association_name)
1407 1414
 
1408 1415
         def association_constructor_method(constructor, reflection, association_proxy_class)
1409 1416
           define_method("#{constructor}_#{reflection.name}") do |*params|
1410  
-            ivar = "@#{reflection.name}"
1411  
-
1412 1417
             attributees      = params.first unless params.empty?
1413 1418
             replace_existing = params[1].nil? ? true : params[1]
1414  
-            association      = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1419
+            association      = association_instance_get(reflection.name)
1415 1420
 
1416  
-            if association.nil?
  1421
+            unless association
1417 1422
               association = association_proxy_class.new(self, reflection)
1418  
-              instance_variable_set(ivar, association)
  1423
+              association_instance_set(reflection.name, association)
1419 1424
             end
1420 1425
 
1421 1426
             if association_proxy_class == HasOneAssociation
213  activerecord/lib/active_record/autosave_association.rb
... ...
@@ -0,0 +1,213 @@
  1
+module ActiveRecord
  2
+  # AutosaveAssociation is a module that takes care of automatically saving
  3
+  # your associations when the parent is saved. In addition to saving, it
  4
+  # also destroys any associations that were marked for destruction.
  5
+  # (See mark_for_destruction and marked_for_destruction?)
  6
+  #
  7
+  # Saving of the parent, its associations, and the destruction of marked
  8
+  # associations, all happen inside 1 transaction. This should never leave the
  9
+  # database in an inconsistent state after, for instance, mass assigning
  10
+  # attributes and saving them.
  11
+  #
  12
+  # If validations for any of the associations fail, their error messages will
  13
+  # be applied to the parent.
  14
+  #
  15
+  # Note that it also means that associations marked for destruction won't
  16
+  # be destroyed directly. They will however still be marked for destruction.
  17
+  #
  18
+  # === One-to-one Example
  19
+  #
  20
+  # Consider a Post model with one Author:
  21
+  #
  22
+  #   class Post
  23
+  #     has_one :author, :autosave => true
  24
+  #   end
  25
+  #
  26
+  # Saving changes to the parent and its associated model can now be performed
  27
+  # automatically _and_ atomically:
  28
+  #
  29
+  #   post = Post.find(1)
  30
+  #   post.title # => "The current global position of migrating ducks"
  31
+  #   post.author.name # => "alloy"
  32
+  #
  33
+  #   post.title = "On the migration of ducks"
  34
+  #   post.author.name = "Eloy Duran"
  35
+  #
  36
+  #   post.save
  37
+  #   post.reload
  38
+  #   post.title # => "On the migration of ducks"
  39
+  #   post.author.name # => "Eloy Duran"
  40
+  #
  41
+  # Destroying an associated model, as part of the parent's save action, is as
  42
+  # simple as marking it for destruction:
  43
+  #
  44
+  #   post.author.mark_for_destruction
  45
+  #   post.author.marked_for_destruction? # => true
  46
+  #
  47
+  # Note that the model is _not_ yet removed from the database:
  48
+  #   id = post.author.id
  49
+  #   Author.find_by_id(id).nil? # => false
  50
+  #
  51
+  #   post.save
  52
+  #   post.reload.author # => nil
  53
+  #
  54
+  # Now it _is_ removed from the database:
  55
+  #   Author.find_by_id(id).nil? # => true
  56
+  #
  57
+  # === One-to-many Example
  58
+  #
  59
+  # Consider a Post model with many Comments:
  60
+  #
  61
+  #   class Post
  62
+  #     has_many :comments, :autosave => true
  63
+  #   end
  64
+  #
  65
+  # Saving changes to the parent and its associated model can now be performed
  66
+  # automatically _and_ atomically:
  67
+  #
  68
+  #   post = Post.find(1)
  69
+  #   post.title # => "The current global position of migrating ducks"
  70
+  #   post.comments.first.body # => "Wow, awesome info thanks!"
  71
+  #   post.comments.last.body # => "Actually, your article should be named differently."
  72
+  #
  73
+  #   post.title = "On the migration of ducks"
  74
+  #   post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
  75
+  #
  76
+  #   post.save
  77
+  #   post.reload
  78
+  #   post.title # => "On the migration of ducks"
  79
+  #   post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
  80
+  #
  81
+  # Destroying one of the associated models members, as part of the parent's
  82
+  # save action, is as simple as marking it for destruction:
  83
+  #
  84
+  #   post.comments.last.mark_for_destruction
  85
+  #   post.comments.last.marked_for_destruction? # => true
  86
+  #   post.comments.length # => 2
  87
+  #
  88
+  # Note that the model is _not_ yet removed from the database:
  89
+  #   id = post.comments.last.id
  90
+  #   Comment.find_by_id(id).nil? # => false
  91
+  #
  92
+  #   post.save
  93
+  #   post.reload.comments.length # => 1
  94
+  #
  95
+  # Now it _is_ removed from the database:
  96
+  #   Comment.find_by_id(id).nil? # => true
  97
+  #
  98
+  # === Validation
  99
+  #
  100
+  # Validation is performed on the parent as usual, but also on all autosave
  101
+  # enabled associations. If any of the associations fail validation, its
  102
+  # error messages will be applied on the parents errors object and validation
  103
+  # of the parent will fail.
  104
+  #
  105
+  # Consider a Post model with Author which validates the presence of its name
  106
+  # attribute:
  107
+  #
  108
+  #   class Post
  109
+  #     has_one :author, :autosave => true
  110
+  #   end
  111
+  #
  112
+  #   class Author
  113
+  #     validates_presence_of :name
  114
+  #   end
  115
+  #
  116
+  #   post = Post.find(1)
  117
+  #   post.author.name = ''
  118
+  #   post.save # => false
  119
+  #   post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
  120
+  #
  121
+  # No validations will be performed on the associated models when validations
  122
+  # are skipped for the parent:
  123
+  #
  124
+  #   post = Post.find(1)
  125
+  #   post.author.name = ''
  126
+  #   post.save(false) # => true
  127
+  module AutosaveAssociation
  128
+    def self.included(base)
  129
+      base.class_eval do
  130
+        alias_method_chain :reload, :autosave_associations
  131
+        alias_method_chain :save,   :autosave_associations
  132
+        alias_method_chain :valid?, :autosave_associations
  133
+
  134
+        %w{ has_one belongs_to has_many has_and_belongs_to_many }.each do |type|
  135
+          base.send("valid_keys_for_#{type}_association") << :autosave
  136
+        end
  137
+      end
  138
+    end
  139
+
  140
+    # Saves the parent, <tt>self</tt>, and any loaded autosave associations.
  141
+    # In addition, it destroys all children that were marked for destruction
  142
+    # with mark_for_destruction.
  143
+    #
  144
+    # This all happens inside a transaction, _if_ the Transactions module is included into
  145
+    # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
  146
+    def save_with_autosave_associations(perform_validation = true)
  147
+      returning(save_without_autosave_associations(perform_validation)) do |valid|
  148
+        if valid
  149
+          self.class.reflect_on_all_autosave_associations.each do |reflection|
  150
+            if (association = association_instance_get(reflection.name)) && association.loaded?
  151
+              if association.is_a?(Array)
  152
+                association.proxy_target.each do |child|
  153
+                  child.marked_for_destruction? ? child.destroy : child.save(perform_validation)
  154
+                end
  155
+              else
  156
+                association.marked_for_destruction? ? association.destroy : association.save(perform_validation)
  157
+              end
  158
+            end
  159
+          end
  160
+        end
  161
+      end
  162
+    end
  163
+
  164
+    # Returns whether or not the parent, <tt>self</tt>, and any loaded autosave associations are valid.
  165
+    def valid_with_autosave_associations?
  166
+      if valid_without_autosave_associations?
  167
+        self.class.reflect_on_all_autosave_associations.all? do |reflection|
  168
+          if (association = association_instance_get(reflection.name)) && association.loaded?
  169
+            if association.is_a?(Array)
  170
+              association.proxy_target.all? { |child| autosave_association_valid?(reflection, child) }
  171
+            else
  172
+              autosave_association_valid?(reflection, association)
  173
+            end
  174
+          else
  175
+            true # association not loaded yet, so it should be valid
  176
+          end
  177
+        end
  178
+      else
  179
+        false # self was not valid
  180
+      end
  181
+    end
  182
+
  183
+    # Returns whether or not the association is valid and applies any errors to the parent, <tt>self</tt>, if it wasn't.
  184
+    def autosave_association_valid?(reflection, association)
  185
+      returning(association.valid?) do |valid|
  186
+        association.errors.each do |attribute, message|
  187
+          errors.add "#{reflection.name}_#{attribute}", message
  188
+        end unless valid
  189
+      end
  190
+    end
  191
+
  192
+    # Reloads the attributes of the object as usual and removes a mark for destruction.
  193
+    def reload_with_autosave_associations(options = nil)
  194
+      @marked_for_destruction = false
  195
+      reload_without_autosave_associations(options)
  196
+    end
  197
+
  198
+    # Marks this record to be destroyed as part of the parents save transaction.
  199
+    # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
  200
+    #
  201
+    # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
  202
+    def mark_for_destruction
  203
+      @marked_for_destruction = true
  204
+    end
  205
+
  206
+    # Returns whether or not this record will be destroyed as part of the parents save transaction.
  207
+    #
  208
+    # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
  209
+    def marked_for_destruction?
  210
+      @marked_for_destruction
  211
+    end
  212
+  end
  213
+end
5  activerecord/lib/active_record/base.rb
@@ -3136,6 +3136,11 @@ def clone_attribute_value(reader_method, attribute_name)
3136 3136
     include Dirty
3137 3137
     include Callbacks, Observing, Timestamp
3138 3138
     include Associations, AssociationPreload, NamedScope
  3139
+
  3140
+    # AutosaveAssociation needs to be included before Transactions, because we want
  3141
+    # #save_with_autosave_associations to be wrapped inside a transaction.
  3142
+    include AutosaveAssociation, NestedAttributes
  3143
+
3139 3144
     include Aggregations, Transactions, Reflection, Calculations, Serialization
3140 3145
   end
3141 3146
 end
279  activerecord/lib/active_record/nested_attributes.rb
... ...
@@ -0,0 +1,279 @@
  1
+module ActiveRecord
  2
+  module NestedAttributes #:nodoc:
  3
+    def self.included(base)
  4
+      base.extend(ClassMethods)
  5
+      base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
  6
+      base.reject_new_nested_attributes_procs = {}
  7
+    end
  8
+
  9
+    # == Nested Attributes
  10
+    #
  11
+    # Nested attributes allow you to save attributes on associated records
  12
+    # through the parent. By default nested attribute updating is turned off,
  13
+    # you can enable it using the accepts_nested_attributes_for class method.
  14
+    # When you enable nested attributes an attribute writer is defined on
  15
+    # the model.
  16
+    #
  17
+    # The attribute writer is named after the association, which means that
  18
+    # in the following example, two new methods are added to your model:
  19
+    # <tt>author_attributes=(attributes)</tt> and
  20
+    # <tt>pages_attributes=(attributes)</tt>.
  21
+    #
  22
+    #   class Book < ActiveRecord::Base
  23
+    #     has_one :author
  24
+    #     has_many :pages
  25
+    #
  26
+    #     accepts_nested_attributes_for :author, :pages
  27
+    #   end
  28
+    #
  29
+    # Note that the <tt>:autosave</tt> option is automatically enabled on every
  30
+    # association that accepts_nested_attributes_for is used for.
  31
+    #
  32
+    # === One-to-one
  33
+    #
  34
+    # Consider a Member model that has one Avatar:
  35
+    #
  36
+    #   class Member < ActiveRecord::Base
  37
+    #     has_one :avatar
  38
+    #     accepts_nested_attributes_for :avatar
  39
+    #   end
  40
+    #
  41
+    # Enabling nested attributes on a one-to-one association allows you to
  42
+    # create the member and avatar in one go:
  43
+    #
  44
+    #   params = { 'member' => { 'name' => 'Jack', 'avatar_attributes' => { 'icon' => 'smiling' } } }
  45
+    #   member = Member.create(params)
  46
+    #   member.avatar.icon #=> 'smiling'
  47
+    #
  48
+    # It also allows you to update the avatar through the member:
  49
+    #
  50
+    #   params = { 'member' => { 'avatar_attributes' => { 'icon' => 'sad' } } }
  51
+    #   member.update_attributes params['member']
  52
+    #   member.avatar.icon #=> 'sad'
  53
+    #
  54
+    # By default you will only be able to set and update attributes on the
  55
+    # associated model. If you want to destroy the associated model through the
  56
+    # attributes hash, you have to enable it first using the
  57
+    # <tt>:allow_destroy</tt> option.
  58
+    #
  59
+    #   class Member < ActiveRecord::Base
  60
+    #     has_one :avatar
  61
+    #     accepts_nested_attributes_for :avatar, :allow_destroy => true
  62
+    #   end
  63
+    #
  64
+    # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
  65
+    # value that evaluates to +true+, you will destroy the associated model:
  66
+    #
  67
+    #   member.avatar_attributes = { '_delete' => '1' }
  68
+    #   member.avatar.marked_for_destruction? # => true
  69
+    #   member.save
  70
+    #   member.avatar #=> nil
  71
+    #
  72
+    # Note that the model will _not_ be destroyed until the parent is saved.
  73
+    #
  74
+    # === One-to-many
  75
+    #
  76
+    # Consider a member that has a number of posts:
  77
+    #
  78
+    #   class Member < ActiveRecord::Base
  79
+    #     has_many :posts
  80
+    #     accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
  81
+    #   end
  82
+    #
  83
+    # You can now set or update attributes on an associated post model through
  84
+    # the attribute hash.
  85
+    #
  86
+    # For each key in the hash that starts with the string 'new' a new model
  87
+    # will be instantiated. When the proc given with the <tt>:reject_if</tt>
  88
+    # option evaluates to +false+ for a certain attribute hash no record will
  89
+    # be built for that hash.
  90
+    #
  91
+    #   params = { 'member' => {
  92
+    #     'name' => 'joe', 'posts_attributes' => {
  93
+    #       'new_12345' => { 'title' => 'Kari, the awesome Ruby documentation browser!' },
  94
+    #       'new_54321' => { 'title' => 'The egalitarian assumption of the modern citizen' },
  95
+    #       'new_67890' => { 'title' => '' } # This one matches the :reject_if proc and will not be instantiated.
  96
+    #     }
  97
+    #   }}
  98
+    #
  99
+    #   member = Member.create(params['member'])
  100
+    #   member.posts.length #=> 2
  101
+    #   member.posts.first.title #=> 'Kari, the awesome Ruby documentation browser!'
  102
+    #   member.posts.second.title #=> 'The egalitarian assumption of the modern citizen'
  103
+    #
  104
+    # When the key for post attributes is an integer, the associated post with
  105
+    # that ID will be updated:
  106
+    #
  107
+