Permalink
Browse files

Ensure that mass assignment options are preserved

There are two possible scenarios where the @mass_assignment_options
instance variable can become corrupted:

1. If the assign_attributes doesn't complete correctly, then
   subsequent calls to a nested attribute assignment method will use
   whatever options were passed to the previous assign_attributes call.

2. With nested assign_attributes calls, the inner call will overwrite
   the current options. This will only affect nested attributes as the
   attribute hash is sanitized before any methods are called.

To fix this we save the current options in a local variable and then
restore these options in an ensure block.
  • Loading branch information...
1 parent 42259f6 commit c2e61aa61540802e5a5d9b54ff04b803d770eff6 @pixeltrix pixeltrix committed Jun 10, 2012
@@ -69,6 +69,7 @@ def assign_attributes(new_attributes, options = {})
attributes = new_attributes.stringify_keys
multi_parameter_attributes = []
nested_parameter_attributes = []
+ previous_options = @mass_assignment_options
@mass_assignment_options = options
unless options[:without_protection]
@@ -94,8 +95,9 @@ def assign_attributes(new_attributes, options = {})
send("#{k}=", v)
end
- @mass_assignment_options = nil
assign_multiparameter_attributes(multi_parameter_attributes)
+ ensure
+ @mass_assignment_options = previous_options
end
protected
@@ -380,15 +380,16 @@ def init_internals
@attributes[pk] = nil unless @attributes.key?(pk)
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @new_record = true
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @previously_changed = {}
+ @changed_attributes = {}
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @new_record = true
+ @mass_assignment_options = nil
end
end
end
@@ -878,4 +878,26 @@ def test_has_many_create_with_bang_without_protection
assert_all_attributes(person.best_friends.first)
end
+ def test_mass_assignment_options_are_reset_after_exception
+ person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin)
+ person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin)
+
+ attributes = { :best_friend_attributes => { :comments => 'rides a sweet bike' } }
+ assert_raises(RuntimeError) { person.assign_attributes(attributes, :as => :admin) }
+ assert_equal 'm', person.best_friend.gender
+
+ person.best_friend_attributes = { :gender => 'f' }
+ assert_equal 'm', person.best_friend.gender
+ end
+
+ def test_mass_assignment_options_are_nested_correctly
+ person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin)
+ person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin)
+
+ attributes = { :best_friend_first_name => 'Josh', :best_friend_attributes => { :gender => 'f' } }
+ person.assign_attributes(attributes, :as => :admin)
+ assert_equal 'Josh', person.best_friend.first_name
+ assert_equal 'f', person.best_friend.gender
+ end
+
end
@@ -88,6 +88,25 @@ class TightDescendant < TightPerson; end
class RichPerson < ActiveRecord::Base
self.table_name = 'people'
-
+
has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures'
end
+
+class NestedPerson < ActiveRecord::Base
+ self.table_name = 'people'
+
+ attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes
+ attr_accessible :first_name, :gender, :comments, :as => :admin
+ attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin
+
+ has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id
+ accepts_nested_attributes_for :best_friend, :update_only => true
+
+ def comments=(new_comments)
+ raise RuntimeError
+ end
+
+ def best_friend_first_name=(new_name)
+ assign_attributes({ :best_friend_attributes => { :first_name => new_name } })
+ end
+end

0 comments on commit c2e61aa

Please sign in to comment.