mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Do not overwrite manually built records during one-to-one nested attribute assignment
For one-to-one nested associations, if you build the new (in-memory) child object yourself before assignment, then the NestedAttributes module will not overwrite it, e.g.: class Member < ActiveRecord::Base has_one :avatar accepts_nested_attributes_for :avatar def avatar super || build_avatar(width: 200) end end member = Member.new member.avatar_attributes = {icon: 'sad'} member.avatar.width # => 200
This commit is contained in:
parent
6023a50491
commit
534030cf83
4 changed files with 75 additions and 11 deletions
|
@ -1,3 +1,24 @@
|
|||
* Do not overwrite manually built records during one-to-one nested attribute assignment
|
||||
|
||||
For one-to-one nested associations, if you build the new (in-memory)
|
||||
child object yourself before assignment, then the NestedAttributes
|
||||
module will not overwrite it, e.g.:
|
||||
|
||||
class Member < ActiveRecord::Base
|
||||
has_one :avatar
|
||||
accepts_nested_attributes_for :avatar
|
||||
|
||||
def avatar
|
||||
super || build_avatar(width: 200)
|
||||
end
|
||||
end
|
||||
|
||||
member = Member.new
|
||||
member.avatar_attributes = {icon: 'sad'}
|
||||
member.avatar.width # => 200
|
||||
|
||||
*Olek Janiszewski*
|
||||
|
||||
* fixes bug introduced by #3329. Now, when autosaving associations,
|
||||
deletions happen before inserts and saves. This prevents a 'duplicate
|
||||
unique value' database error that would occur if a record being created had
|
||||
|
|
|
@ -164,6 +164,13 @@ module ActiveRecord
|
|||
@reflection = @owner.class.reflect_on_association(reflection_name)
|
||||
end
|
||||
|
||||
def initialize_attributes(record) #:nodoc:
|
||||
skip_assign = [reflection.foreign_key, reflection.type].compact
|
||||
attributes = create_scope.except(*(record.changed - skip_assign))
|
||||
record.assign_attributes(attributes)
|
||||
set_inverse_instance(record)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_target?
|
||||
|
@ -233,10 +240,7 @@ module ActiveRecord
|
|||
|
||||
def build_record(attributes)
|
||||
reflection.build_association(attributes) do |record|
|
||||
skip_assign = [reflection.foreign_key, reflection.type].compact
|
||||
attributes = create_scope.except(*(record.changed - skip_assign))
|
||||
record.assign_attributes(attributes)
|
||||
set_inverse_instance(record)
|
||||
initialize_attributes(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -229,6 +229,23 @@ module ActiveRecord
|
|||
# belongs_to :member, inverse_of: :posts
|
||||
# validates_presence_of :member
|
||||
# end
|
||||
#
|
||||
# For one-to-one nested associations, if you build the new (in-memory)
|
||||
# child object yourself before assignment, then this module will not
|
||||
# overwrite it, e.g.:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_one :avatar
|
||||
# accepts_nested_attributes_for :avatar
|
||||
#
|
||||
# def avatar
|
||||
# super || build_avatar(width: 200)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# member = Member.new
|
||||
# member.avatar_attributes = {icon: 'sad'}
|
||||
# member.avatar.width # => 200
|
||||
module ClassMethods
|
||||
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
|
||||
|
||||
|
@ -356,20 +373,28 @@ module ActiveRecord
|
|||
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
attributes = attributes.with_indifferent_access
|
||||
existing_record = send(association_name)
|
||||
|
||||
if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
|
||||
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
|
||||
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
|
||||
if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
|
||||
(options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
|
||||
|
||||
elsif attributes['id'].present?
|
||||
raise_nested_attributes_record_not_found!(association_name, attributes['id'])
|
||||
|
||||
elsif !reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
||||
|
||||
if existing_record && existing_record.new_record?
|
||||
existing_record.assign_attributes(assignable_attributes)
|
||||
association(association_name).initialize_attributes(existing_record)
|
||||
else
|
||||
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, assignable_attributes)
|
||||
else
|
||||
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -131,6 +131,20 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
|
|||
assert_equal 's1', ship.reload.name
|
||||
end
|
||||
|
||||
def test_reuse_already_built_new_record
|
||||
pirate = Pirate.new
|
||||
ship_built_first = pirate.build_ship
|
||||
pirate.ship_attributes = { name: 'Ship 1' }
|
||||
assert_equal ship_built_first.object_id, pirate.ship.object_id
|
||||
end
|
||||
|
||||
def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record
|
||||
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
|
||||
pirate.build_ship
|
||||
pirate.ship_attributes = { name: 'Ship 1', pirate_id: pirate.id + 1 }
|
||||
assert_equal pirate.id, pirate.ship.pirate_id
|
||||
end
|
||||
|
||||
def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
|
||||
Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }
|
||||
man = Man.create(name: "John")
|
||||
|
|
Loading…
Reference in a new issue