Skip to content
This repository
Browse code

Merge commit 'mainstream/master'

  • Loading branch information...
commit a390b2629a7de7cb8a8a5370aa493283c4a3f066 2 parents a4ff4fd + 3be0ad6
Pratik authored February 01, 2009

Showing 40 changed files with 1,992 additions and 235 deletions. Show diff stats Hide diff stats

  1. 12  actionpack/CHANGELOG
  2. 15  actionpack/lib/action_controller/integration.rb
  3. 1  actionpack/lib/action_controller/middlewares.rb
  4. 1  actionpack/lib/action_controller/rack_ext/parse_query.rb
  5. 35  actionpack/lib/action_controller/resources.rb
  6. 23  actionpack/lib/action_controller/test_process.rb
  7. 2  actionpack/lib/action_controller/url_encoded_pair_parser.rb
  8. 196  actionpack/lib/action_view/helpers/form_helper.rb
  9. 2  actionpack/test/abstract_unit.rb
  10. 14  actionpack/test/controller/integration_test.rb
  11. 36  actionpack/test/controller/request/url_encoded_params_parsing_test.rb
  12. 43  actionpack/test/controller/resources_test.rb
  13. 58  actionpack/test/controller/session/test_session_test.rb
  14. 23  actionpack/test/controller/test_test.rb
  15. 141  actionpack/test/template/form_helper_test.rb
  16. 2  actionpack/test/template/render_test.rb
  17. 9  activerecord/CHANGELOG
  18. 2  activerecord/lib/active_record.rb
  19. 91  activerecord/lib/active_record/associations.rb
  20. 213  activerecord/lib/active_record/autosave_association.rb
  21. 5  activerecord/lib/active_record/base.rb
  22. 279  activerecord/lib/active_record/nested_attributes.rb
  23. 5  activerecord/lib/active_record/reflection.rb
  24. 1  activerecord/lib/active_record/test_case.rb
  25. 9  activerecord/lib/active_record/validations.rb
  26. 386  activerecord/test/cases/autosave_association_test.rb
  27. 2  activerecord/test/cases/dirty_test.rb
  28. 359  activerecord/test/cases/nested_attributes_test.rb
  29. 9  activerecord/test/cases/reflection_test.rb
  30. 13  activerecord/test/cases/validations_test.rb
  31. 3  activerecord/test/models/bird.rb
  32. 2  activerecord/test/models/parrot.rb
  33. 7  activerecord/test/models/pirate.rb
  34. 7  activerecord/test/models/ship.rb
  35. 5  activerecord/test/models/ship_part.rb
  36. 11  activerecord/test/schema/schema.rb
  37. 19  activesupport/lib/active_support/dependencies.rb
  38. 182  activesupport/lib/active_support/xml_mini.rb
  39. 3  railties/configs/databases/mysql.yml
  40. 1  railties/html/index.html
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 :
15  actionpack/lib/action_controller/integration.rb
@@ -26,6 +26,9 @@ class Session
26 26
       # The status message that accompanied the status code of the last request.
27 27
       attr_reader :status_message
28 28
 
  29
+      # The body of the last request.
  30
+      attr_reader :body
  31
+
29 32
       # The URI of the last request.
30 33
       attr_reader :path
31 34
 
@@ -308,7 +311,11 @@ def process(method, path, parameters = nil, headers = nil)
308 311
 
309 312
           ActionController::Base.clear_last_instantiation!
310 313
 
311  
-          app = Rack::Lint.new(@application)
  314
+          app = @application
  315
+          # Rack::Lint doesn't accept String headers or bodies in Ruby 1.9
  316
+          unless RUBY_VERSION >= '1.9.0' && Rack.release <= '0.9.0'
  317
+            app = Rack::Lint.new(app)
  318
+          end
312 319
 
313 320
           status, headers, body = app.call(env)
314 321
           @request_count += 1
@@ -326,7 +333,11 @@ def process(method, path, parameters = nil, headers = nil)
326 333
           end
327 334
 
328 335
           @body = ""
329  
-          body.each { |part| @body << part }
  336
+          if body.is_a?(String)
  337
+            @body << body
  338
+          else
  339
+            body.each { |part| @body << part }
  340
+          end
330 341
 
331 342
           if @controller = ActionController::Base.last_instantiation
332 343
             @request = @controller.request
1  actionpack/lib/action_controller/middlewares.rb
@@ -19,3 +19,4 @@
19 19
 use "ActionController::RewindableInput"
20 20
 use "ActionController::ParamsParser"
21 21
 use "Rack::MethodOverride"
  22
+use "Rack::Head"
1  actionpack/lib/action_controller/rack_ext/parse_query.rb
@@ -10,7 +10,6 @@ module Utils
10 10
     def parse_query(qs, d = '&;')
11 11
       qs = qs.dup
12 12
       qs.chop! if qs[-1] == 0
13  
-      qs.gsub!(/&_=$/, '')
14 13
       parse_query_without_ajax_body_cleanup(qs, d)
15 14
     end
16 15
     module_function :parse_query
35  actionpack/lib/action_controller/resources.rb
@@ -42,7 +42,7 @@ module ActionController
42 42
   #
43 43
   # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer
44 44
   module Resources
45  
-    INHERITABLE_OPTIONS = :namespace, :shallow, :actions
  45
+    INHERITABLE_OPTIONS = :namespace, :shallow
46 46
 
47 47
     class Resource #:nodoc:
48 48
       DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy
@@ -119,7 +119,7 @@ def uncountable?
119 119
       end
120 120
 
121 121
       def has_action?(action)
122  
-        !DEFAULT_ACTIONS.include?(action) || @options[:actions].nil? || @options[:actions].include?(action)
  122
+        !DEFAULT_ACTIONS.include?(action) || action_allowed?(action)
123 123
       end
124 124
 
125 125
       protected
@@ -135,24 +135,29 @@ def add_default_actions
135 135
         end
136 136
 
137 137
         def set_allowed_actions
138  
-          only    = @options.delete(:only)
139  
-          except  = @options.delete(:except)
  138
+          only, except = @options.values_at(:only, :except)
  139
+          @allowed_actions ||= {}
140 140
 
141  
-          if only && except
142  
-            raise ArgumentError, 'Please supply either :only or :except, not both.'
143  
-          elsif only == :all || except == :none
144  
-            options[:actions] = DEFAULT_ACTIONS
  141
+          if only == :all || except == :none
  142
+            only = nil
  143
+            except = []
145 144
           elsif only == :none || except == :all
146  
-            options[:actions] = []
147  
-          elsif only
148  
-            options[:actions] = DEFAULT_ACTIONS & Array(only).map(&:to_sym)
  145
+            only = []
  146
+            except = nil
  147
+          end
  148
+
  149
+          if only
  150
+            @allowed_actions[:only] = Array(only).map(&:to_sym)
149 151
           elsif except
150  
-            options[:actions] = DEFAULT_ACTIONS - Array(except).map(&:to_sym)
151  
-          else
152  
-            # leave options[:actions] alone
  152
+            @allowed_actions[:except] = Array(except).map(&:to_sym)
153 153
           end
154 154
         end
155 155
 
  156
+        def action_allowed?(action)
  157
+          only, except = @allowed_actions.values_at(:only, :except)
  158
+          (!only || only.include?(action)) && (!except || !except.include?(action))
  159
+        end
  160
+
156 161
         def set_prefixes
157 162
           @path_prefix = options.delete(:path_prefix)
158 163
           @name_prefix = options.delete(:name_prefix)
@@ -403,8 +408,6 @@ def initialize(entity, options)
403 408
     #   # --> POST /posts/1/comments (maps to the CommentsController#create action)
404 409
     #   # --> PUT /posts/1/comments/1 (fails)
405 410
     #
406  
-    # The <tt>:only</tt> and <tt>:except</tt> options are inherited by any nested resource(s).
407  
-    #
408 411
     # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
409 412
     #
410 413
     # Examples:
23  actionpack/lib/action_controller/test_process.rb
@@ -15,7 +15,7 @@ def initialize
15 15
     end
16 16
 
17 17
     def reset_session
18  
-      @session = TestSession.new
  18
+      @session.reset
19 19
     end
20 20
 
21 21
     # Wraps raw_post in a StringIO.
@@ -284,9 +284,13 @@ class TestSession < Hash #:nodoc:
284 284
     attr_accessor :session_id
285 285
 
286 286
     def initialize(attributes = nil)
287  
-      @session_id = ''
288  
-      attributes ||= {}
289  
-      replace(attributes.stringify_keys)
  287
+      reset_session_id
  288
+      replace_attributes(attributes)
  289
+    end
  290
+
  291
+    def reset
  292
+      reset_session_id
  293
+      replace_attributes({ })
290 294
     end
291 295
 
292 296
     def data
@@ -322,6 +326,17 @@ def delete(key = nil)
322 326
     def close
323 327
       ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller)
324 328
     end
  329
+
  330
+  private
  331
+
  332
+    def reset_session_id
  333
+      @session_id = ''
  334
+    end
  335
+
  336
+    def replace_attributes(attributes = nil)
  337
+      attributes ||= {}
  338
+      replace(attributes.stringify_keys)
  339
+    end
325 340
   end
326 341
 
327 342
   # Essentially generates a modified Tempfile object similar to the object
2  actionpack/lib/action_controller/url_encoded_pair_parser.rb
@@ -46,7 +46,7 @@ def get_typed_value(value)
46 46
           when Array
47 47
             value.map { |v| get_typed_value(v) }
48 48
           when Hash
49  
-            if value.has_key?(:tempfile) && value[:filename].any?
  49
+            if value.has_key?(:tempfile) && !value[:filename].blank?
50 50
               upload = value[:tempfile]
51 51
               upload.extend(UploadedFile)
52 52
               upload.original_path = value[:filename]
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
2  actionpack/test/abstract_unit.rb
@@ -34,7 +34,7 @@
34 34
 
35 35
 # Register danish language for testing
36 36
 I18n.backend.store_translations 'da', {}
37  
-ORIGINAL_LOCALES = I18n.available_locales
  37
+ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort
38 38
 
39 39
 FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
40 40
 ActionController::Base.view_paths = FIXTURE_LOAD_PATH
14  actionpack/test/controller/integration_test.rb
@@ -266,6 +266,7 @@ def test_get
266 266
       assert_response :success
267 267
       assert_response :ok
268 268
       assert_equal({}, cookies)
  269
+      assert_equal "OK", body
269 270
       assert_equal "OK", response.body
270 271
       assert_kind_of HTML::Document, html_document
271 272
       assert_equal 1, request_count
@@ -281,6 +282,7 @@ def test_post
281 282
       assert_response :success
282 283
       assert_response :created
283 284
       assert_equal({}, cookies)
  285
+      assert_equal "Created", body
284 286
       assert_equal "Created", response.body
285 287
       assert_kind_of HTML::Document, html_document
286 288
       assert_equal 1, request_count
@@ -360,6 +362,18 @@ def test_get_with_parameters
360 362
     end
361 363
   end
362 364
 
  365
+  def test_head
  366
+    with_test_route_set do
  367
+      head '/get'
  368
+      assert_equal 200, status
  369
+      assert_equal "", body
  370
+
  371
+      head '/post'
  372
+      assert_equal 201, status
  373
+      assert_equal "", body
  374
+    end
  375
+  end
  376
+
363 377
   private
364 378
     def with_test_route_set
365 379
       with_routing do |set|
36  actionpack/test/controller/request/url_encoded_params_parsing_test.rb
@@ -80,36 +80,6 @@ def teardown
80 80
     assert_parses expected, query
81 81
   end
82 82
 
83  
-  test "parses params with non alphanumeric name" do
84  
-    query     = "a/b[c]=d"
85  
-    expected  = { "a/b" => { "c" => "d" }}
86  
-    assert_parses expected, query
87  
-  end
88  
-
89  
-  test "parses params with single brackets in the middle" do
90  
-    query     = "a/b[c]d=e"
91  
-    expected  = { "a/b" => {} }
92  
-    assert_parses expected, query
93  
-  end
94  
-
95  
-  test "parses params with separated brackets" do
96  
-    query     = "a/b@[c]d[e]=f"
97  
-    expected  = { "a/b@" => { }}
98  
-    assert_parses expected, query
99  
-  end
100  
-
101  
-  test "parses params with separated brackets and array" do
102  
-    query     = "a/b@[c]d[e][]=f"
103  
-    expected  = { "a/b@" => { }}
104  
-    assert_parses expected, query
105  
-  end
106  
-
107  
-  test "parses params with unmatched brackets and array" do
108  
-    query     = "a/b@[c][d[e][]=f"
109  
-    expected  = { "a/b@" => { "c" => { }}}
110  
-    assert_parses expected, query
111  
-  end
112  
-
113 83
   test "parses params with nil key" do
114 84
     query    = "=&test2=value1"
115 85
     expected = { "test2" => "value1" }
@@ -156,12 +126,6 @@ def teardown
156 126
     assert_parses expected, query
157 127
   end
158 128
 
159  
-  test "parses params with Prototype's hack around Safari 2 trailing null character" do
160  
-    query = "selected[]=1&selected[]=2&selected[]=3&_="
161  
-    expected = { "selected" => [ "1", "2", "3" ] }
162  
-    assert_parses expected, query
163  
-  end
164  
-
165 129
   test "passes through rack middleware and parses params" do
166 130
     with_muck_middleware do
167 131
       assert_parses({ "a" => { "b" => "c" } }, "a[b]=c")
43  actionpack/test/controller/resources_test.rb
@@ -942,19 +942,6 @@ def test_singleton_resource_has_only_member_action
942 942
     end
943 943
   end
944 944
 
945  
-  def test_nested_resource_inherits_only_show_action
946  
-    with_routing do |set|
947  
-      set.draw do |map|
948  
-        map.resources :products, :only => :show do |product|
949  
-          product.resources :images
950  
-        end
951  
-      end
952  
-
953  
-      assert_resource_allowed_routes('images', { :product_id => '1' },                    { :id => '2' }, :show, [:index, :new, :create, :edit, :update, :destroy], 'products/1/images')
954  
-      assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' },  { :id => '2' }, :show, [:index, :new, :create, :edit, :update, :destroy], 'products/1/images')
955  
-    end
956  
-  end
957  
-
958 945
   def test_nested_resource_has_only_show_and_member_action
959 946
     with_routing do |set|
960 947
       set.draw do |map|
@@ -971,7 +958,7 @@ def test_nested_resource_has_only_show_and_member_action
971 958
     end
972 959
   end
973 960
 
974  
-  def test_nested_resource_ignores_only_option
  961
+  def test_nested_resource_does_not_inherit_only_option
975 962
     with_routing do |set|
976 963
       set.draw do |map|
977 964
         map.resources :products, :only => :show do |product|
@@ -984,7 +971,20 @@ def test_nested_resource_ignores_only_option
984 971
     end
985 972
   end
986 973
 
987  
-  def test_nested_resource_ignores_except_option
  974
+  def test_nested_resource_does_not_inherit_only_option_by_default
  975
+    with_routing do |set|
  976
+      set.draw do |map|
  977
+        map.resources :products, :only => :show do |product|
  978
+          product.resources :images
  979
+        end
  980
+      end
  981
+
  982
+      assert_resource_allowed_routes('images', { :product_id => '1' },                    { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destory], [], 'products/1/images')
  983
+      assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' },  { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
  984
+    end
  985
+  end
  986
+
  987
+  def test_nested_resource_does_not_inherit_except_option
988 988
     with_routing do |set|
989 989
       set.draw do |map|
990 990
         map.resources :products, :except => :show do |product|
@@ -997,6 +997,19 @@ def test_nested_resource_ignores_except_option
997 997
     end
998 998
   end
999 999
 
  1000
+  def test_nested_resource_does_not_inherit_except_option_by_default
  1001
+    with_routing do |set|
  1002
+      set.draw do |map|
  1003
+        map.resources :products, :except => :show do |product|
  1004
+          product.resources :images
  1005
+        end
  1006
+      end
  1007
+
  1008
+      assert_resource_allowed_routes('images', { :product_id => '1' },                    { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
  1009
+      assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' },  { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
  1010
+    end
  1011
+  end
  1012
+
1000 1013
   def test_default_singleton_restful_route_uses_get
1001 1014
     with_routing do |set|
1002 1015
       set.draw do |map|
58  actionpack/test/controller/session/test_session_test.rb
... ...
@@ -0,0 +1,58 @@
  1
+require 'abstract_unit'
  2
+require 'stringio'
  3
+
  4
+class ActionController::TestSessionTest < ActiveSupport::TestCase
  5
+  
  6
+  def test_calling_delete_without_parameters_raises_deprecation_warning_and_calls_to_clear_test_session
  7
+    assert_deprecated(/use clear instead/){ ActionController::TestSession.new.delete }
  8
+  end
  9
+  
  10
+  def test_calling_update_without_parameters_raises_deprecation_warning_and_calls_to_clear_test_session
  11
+    assert_deprecated(/use replace instead/){ ActionController::TestSession.new.update }
  12
+  end
  13
+  
  14
+  def test_calling_close_raises_deprecation_warning
  15
+    assert_deprecated(/sessions should no longer be closed/){ ActionController::TestSession.new.close }
  16
+  end
  17
+  
  18
+  def test_defaults
  19
+    session = ActionController::TestSession.new
  20
+    assert_equal({}, session.data)
  21
+    assert_equal('', session.session_id)
  22
+  end
  23
+  
  24
+  def test_ctor_allows_setting
  25
+    session = ActionController::TestSession.new({:one => 'one', :two => 'two'})
  26
+    assert_equal('one', session[:one])
  27
+    assert_equal('two', session[:two])
  28
+  end
  29
+  
  30
+  def test_setting_session_item_sets_item
  31
+    session = ActionController::TestSession.new
  32
+    session[:key] = 'value'
  33
+    assert_equal('value', session[:key])
  34
+  end
  35
+  
  36
+  def test_calling_delete_removes item
  37
+    session = ActionController::TestSession.new
  38
+    session[:key] = 'value'
  39
+    assert_equal('value', session[:key])
  40
+    session.delete(:key)
  41
+    assert_nil(session[:key])
  42
+  end
  43
+  
  44
+  def test_calling_update_with_params_passes_to_attributes
  45
+    session = ActionController::TestSession.new()
  46
+    session.update('key' => 'value')
  47
+    assert_equal('value', session[:key])
  48
+  end
  49
+  
  50
+  def test_clear_emptys_session
  51
+    params = {:one => 'one', :two => 'two'}
  52
+    session = ActionController::TestSession.new({:one => 'one', :two => 'two'})
  53
+    session.clear
  54
+    assert_nil(session[:one])
  55
+    assert_nil(session[:two])
  56
+  end
  57
+  
  58
+end
23  actionpack/test/controller/test_test.rb
@@ -23,6 +23,11 @@ def set_session
23 23
       render :text => 'Success'
24 24
     end
25 25
 
  26
+    def reset_the_session
  27
+      reset_session
  28
+      render :text => 'ignore me'
  29
+    end
  30
+
26 31
     def render_raw_post
27 32
       raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank?
28 33
       render :text => request.raw_post
@@ -171,6 +176,24 @@ def test_process_with_session_arg
171 176
     assert_equal 'value2', session[:symbol]
172 177
   end
173 178
 
  179
+  def test_session_is_cleared_from_controller_after_reset_session
  180
+    process :set_session
  181
+    process :reset_the_session
  182
+    assert_equal Hash.new, @controller.session.to_hash
  183
+  end
  184
+
  185
+  def test_session_is_cleared_from_response_after_reset_session
  186
+    process :set_session
  187
+    process :reset_the_session
  188
+    assert_equal Hash.new, @response.session.to_hash
  189
+  end
  190
+
  191
+  def test_session_is_cleared_from_request_after_reset_session
  192
+    process :set_session
  193
+    process :reset_the_session
  194
+    assert_equal Hash.new, @request.session.to_hash
  195
+  end
  196
+
174 197
   def test_process_with_request_uri_with_no_params
175 198
     process :test_uri
176 199
     assert_equal "/test_test/test/test_uri", @response.body
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
2  actionpack/test/template/render_test.rb
@@ -11,7 +11,7 @@ def setup_view(paths)
11 11
     I18n.backend.store_translations 'da', {}
12 12
 
13 13
     # Ensure original are still the same since we are reindexing view paths
14  
-    assert_equal ORIGINAL_LOCALES, I18n.available_locales
  14
+    assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort
15 15
   end
16 16
 
17 17
   def test_render_file
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
@@ -1198,6 +1213,8 @@ def belongs_to(association_id, options = {})
1198 1213
       #   If true, all the associated objects are readonly through the association.
1199 1214
       # [:validate]
1200 1215
       #   If false, don't validate the associated objects when saving the parent object. +true+ by default.
  1216
+      # [:autosave]
  1217
+      #   If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
1201 1218
       #
1202 1219
       # Option examples:
1203 1220
       #   has_and_belongs_to_many :projects
@@ -1245,33 +1262,30 @@ def join_table_name(first_table_name, second_table_name)
1245 1262
         end
1246 1263
 
1247 1264
         def association_accessor_methods(reflection, association_proxy_class)
1248  
-          ivar = "@#{reflection.name}"
1249  
-
1250 1265
           define_method(reflection.name) do |*params|
1251 1266
             force_reload = params.first unless params.empty?
1252  
-
1253  
-            association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
  1267
+            association = association_instance_get(reflection.name)
1254 1268
 
1255 1269
             if association.nil? || force_reload
1256 1270
               association = association_proxy_class.new(self, reflection)