Skip to content

Commit b6166e8

Browse files
authored
Merge pull request rails#49056 from joshuay03/raise-on-duplicate-accepts-nested-attributes-for
[Fix rails#49055] Raise an `ArgumentError` when `#accepts_nested_attributes_for` is redeclared for an association
2 parents 5415d3a + 0a54155 commit b6166e8

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

activerecord/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
* Raise an `ArgumentError` when `#accepts_nested_attributes_for` is declared more than once for an association in
2+
the same class. Previously, the last declaration would silently override the previous one. Overriding in a subclass
3+
is still allowed.
4+
5+
*Joshua Young*
6+
17
* Deprecate `rewhere` argument on `#merge`.
28

39
The `rewhere` argument on `#merge`is deprecated without replacement and

activerecord/lib/active_record/nested_attributes.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,17 @@ def accepts_nested_attributes_for(*attr_names)
355355
options.update(attr_names.extract_options!)
356356
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
357357
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
358+
options[:class] = self
358359

359360
attr_names.each do |association_name|
360361
if reflection = _reflect_on_association(association_name)
361362
reflection.autosave = true
362363
define_autosave_validation_callbacks(reflection)
363364

365+
if nested_attributes_options.dig(association_name.to_sym, :class) == self
366+
raise ArgumentError, "Already declared #{association_name} as an accepts_nested_attributes association for this class."
367+
end
368+
364369
nested_attributes_options = self.nested_attributes_options.dup
365370
nested_attributes_options[association_name.to_sym] = options
366371
self.nested_attributes_options = nested_attributes_options

activerecord/test/cases/nested_attributes_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
2020
teardown do
21+
Pirate.nested_attributes_options.delete :ship
2122
Pirate.accepts_nested_attributes_for :ship, allow_destroy: true, reject_if: proc(&:empty?)
2223
end
2324

@@ -74,6 +75,7 @@ def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attribute
7475
end
7576

7677
def test_should_disable_allow_destroy_by_default
78+
Pirate.nested_attributes_options.delete :ship
7779
Pirate.accepts_nested_attributes_for :ship
7880

7981
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
@@ -92,6 +94,7 @@ def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked
9294
end
9395

9496
def test_reject_if_method_without_arguments
97+
Pirate.nested_attributes_options.delete :ship
9598
Pirate.accepts_nested_attributes_for :ship, reject_if: :new_record?
9699

97100
pirate = Pirate.new(catchphrase: "Stop wastin' me time")
@@ -100,6 +103,7 @@ def test_reject_if_method_without_arguments
100103
end
101104

102105
def test_reject_if_method_with_arguments
106+
Pirate.nested_attributes_options.delete :ship
103107
Pirate.accepts_nested_attributes_for :ship, reject_if: :reject_empty_ships_on_create
104108

105109
pirate = Pirate.new(catchphrase: "Stop wastin' me time")
@@ -113,6 +117,7 @@ def test_reject_if_method_with_arguments
113117
end
114118

115119
def test_reject_if_with_indifferent_keys
120+
Pirate.nested_attributes_options.delete :ship
116121
Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| attributes[:name].blank? }
117122

118123
pirate = Pirate.new(catchphrase: "Stop wastin' me time")
@@ -121,6 +126,7 @@ def test_reject_if_with_indifferent_keys
121126
end
122127

123128
def test_reject_if_with_a_proc_which_returns_true_always_for_has_one
129+
Pirate.nested_attributes_options.delete :ship
124130
Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| true }
125131
pirate = Pirate.create(catchphrase: "Stop wastin' me time")
126132
ship = pirate.create_ship(name: "s1")
@@ -143,6 +149,7 @@ def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record
143149
end
144150

145151
def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
152+
Human.nested_attributes_options.delete :interests
146153
Human.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }
147154
human = Human.create(name: "John")
148155
interest = human.interests.create(topic: "photography")
@@ -151,6 +158,7 @@ def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
151158
end
152159

153160
def test_destroy_works_independent_of_reject_if
161+
Human.nested_attributes_options.delete :interests
154162
Human.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true
155163
human = Human.create(name: "Jon")
156164
interest = human.interests.create(topic: "the ladies")
@@ -159,6 +167,7 @@ def test_destroy_works_independent_of_reject_if
159167
end
160168

161169
def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
170+
Pirate.nested_attributes_options.delete :ship
162171
Pirate.accepts_nested_attributes_for :ship, reject_if: ->(a) { a[:name] == "The Golden Hind" }, allow_destroy: false
163172

164173
pirate = Pirate.create!(catchphrase: "Stop wastin' me time", ship_attributes: { name: "White Pearl", _destroy: "1" })
@@ -172,6 +181,7 @@ def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
172181
end
173182

174183
def test_has_many_association_updating_a_single_record
184+
Human.nested_attributes_options.delete(:interests)
175185
Human.accepts_nested_attributes_for(:interests)
176186
human = Human.create(name: "John")
177187
interest = human.interests.create(topic: "photography")
@@ -180,6 +190,7 @@ def test_has_many_association_updating_a_single_record
180190
end
181191

182192
def test_reject_if_with_blank_nested_attributes_id
193+
Pirate.nested_attributes_options.delete :ship
183194
# When using a select list to choose an existing 'ship' id, with include_blank: true
184195
Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| attributes[:id].blank? }
185196

@@ -189,6 +200,7 @@ def test_reject_if_with_blank_nested_attributes_id
189200
end
190201

191202
def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record
203+
Human.nested_attributes_options.delete(:interests)
192204
Human.accepts_nested_attributes_for(:interests)
193205
human = Human.create(name: "John")
194206
interest = human.interests.create topic: "gardening"
@@ -198,6 +210,7 @@ def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_at
198210
end
199211

200212
def test_allows_class_to_override_setter_and_call_super
213+
Pirate.nested_attributes_options.delete :parrot
201214
mean_pirate_class = Class.new(Pirate) do
202215
accepts_nested_attributes_for :parrot
203216
def parrot_attributes=(attrs)
@@ -222,6 +235,7 @@ def test_accepts_nested_attributes_for_can_be_overridden_in_subclasses
222235
end
223236

224237
def test_should_not_create_duplicates_with_create_with
238+
Human.nested_attributes_options.delete(:interests)
225239
Human.accepts_nested_attributes_for(:interests)
226240

227241
assert_difference("Interest.count", 1) do
@@ -338,12 +352,14 @@ def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
338352
end
339353

340354
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
355+
Pirate.nested_attributes_options.delete :ship
341356
Pirate.accepts_nested_attributes_for :ship, allow_destroy: false, reject_if: proc(&:empty?)
342357

343358
@pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: "1" })
344359

345360
assert_equal @ship, @pirate.reload.ship
346361

362+
Pirate.nested_attributes_options.delete :ship
347363
Pirate.accepts_nested_attributes_for :ship, allow_destroy: true, reject_if: proc(&:empty?)
348364
end
349365

@@ -411,6 +427,7 @@ def test_should_update_existing_when_update_only_is_true_and_id_is_given
411427
end
412428

413429
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
430+
Pirate.nested_attributes_options.delete :update_only_ship
414431
Pirate.accepts_nested_attributes_for :update_only_ship, update_only: true, allow_destroy: true
415432
@ship.delete
416433
@ship = @pirate.create_update_only_ship(name: "Nights Dirty Lightning")
@@ -420,6 +437,7 @@ def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is
420437
assert_nil @pirate.reload.ship
421438
assert_raise(ActiveRecord::RecordNotFound) { Ship.find(@ship.id) }
422439

440+
Pirate.nested_attributes_options.delete :update_only_ship
423441
Pirate.accepts_nested_attributes_for :update_only_ship, update_only: true, allow_destroy: false
424442
end
425443
end
@@ -532,11 +550,13 @@ def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
532550
end
533551

534552
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
553+
Ship.nested_attributes_options.delete :pirate
535554
Ship.accepts_nested_attributes_for :pirate, allow_destroy: false, reject_if: proc(&:empty?)
536555

537556
@ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: "1" })
538557
assert_nothing_raised { @ship.pirate.reload }
539558
ensure
559+
Ship.nested_attributes_options.delete :pirate
540560
Ship.accepts_nested_attributes_for :pirate, allow_destroy: true, reject_if: proc(&:empty?)
541561
end
542562

@@ -588,6 +608,7 @@ def test_should_update_existing_when_update_only_is_true_and_id_is_given
588608
end
589609

590610
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
611+
Ship.nested_attributes_options.delete :update_only_pirate
591612
Ship.accepts_nested_attributes_for :update_only_pirate, update_only: true, allow_destroy: true
592613
@pirate.delete
593614
@pirate = @ship.create_update_only_pirate(catchphrase: "Aye")
@@ -596,6 +617,7 @@ def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is
596617

597618
assert_raise(ActiveRecord::RecordNotFound) { @pirate.reload }
598619

620+
Ship.nested_attributes_options.delete :update_only_pirate
599621
Ship.accepts_nested_attributes_for :update_only_pirate, update_only: true, allow_destroy: false
600622
end
601623
end
@@ -820,6 +842,7 @@ def test_should_automatically_enable_autosave_on_the_association
820842
end
821843

822844
def test_validate_presence_of_parent_works_with_inverse_of
845+
Human.nested_attributes_options.delete(:interests)
823846
Human.accepts_nested_attributes_for(:interests)
824847
assert_equal :human, Human.reflect_on_association(:interests).options[:inverse_of]
825848
assert_equal :interests, Interest.reflect_on_association(:human).options[:inverse_of]
@@ -842,6 +865,7 @@ def test_can_use_symbols_as_object_identifier
842865
end
843866

844867
def test_numeric_column_changes_from_zero_to_no_empty_string
868+
Human.nested_attributes_options.delete(:interests)
845869
Human.accepts_nested_attributes_for(:interests)
846870

847871
repair_validations(Interest) do
@@ -909,6 +933,7 @@ def setup
909933

910934
module NestedAttributesLimitTests
911935
def teardown
936+
Pirate.nested_attributes_options.delete :parrots
912937
Pirate.accepts_nested_attributes_for :parrots, allow_destroy: true, reject_if: proc(&:empty?)
913938
end
914939

@@ -933,6 +958,7 @@ def test_limit_with_exceeding_records
933958

934959
class TestNestedAttributesLimitNumeric < ActiveRecord::TestCase
935960
def setup
961+
Pirate.nested_attributes_options.delete :parrots
936962
Pirate.accepts_nested_attributes_for :parrots, limit: 2
937963

938964
@pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
@@ -943,6 +969,7 @@ def setup
943969

944970
class TestNestedAttributesLimitSymbol < ActiveRecord::TestCase
945971
def setup
972+
Pirate.nested_attributes_options.delete :parrots
946973
Pirate.accepts_nested_attributes_for :parrots, limit: :parrots_limit
947974

948975
@pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?", parrots_limit: 2)
@@ -953,6 +980,7 @@ def setup
953980

954981
class TestNestedAttributesLimitProc < ActiveRecord::TestCase
955982
def setup
983+
Pirate.nested_attributes_options.delete :parrots
956984
Pirate.accepts_nested_attributes_for :parrots, limit: proc { 2 }
957985

958986
@pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
@@ -965,6 +993,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase
965993
fixtures :owners, :pets
966994

967995
def setup
996+
Owner.nested_attributes_options.delete :pets
968997
Owner.accepts_nested_attributes_for :pets, allow_destroy: true
969998

970999
@owner = owners(:ashley)
@@ -1132,3 +1161,22 @@ def test_should_build_a_new_record_based_on_the_delegated_type
11321161
assert_equal "Hello world!", @entry.entryable.subject
11331162
end
11341163
end
1164+
1165+
class TestPreDeclaredNestedAttributesAssociation < ActiveRecord::TestCase
1166+
setup do
1167+
assert @current_options = Developer.nested_attributes_options[:projects]
1168+
end
1169+
1170+
def test_should_raise_an_argument_error_with_similar_options
1171+
assert_raises ArgumentError do
1172+
Developer.accepts_nested_attributes_for :projects, **@current_options
1173+
end
1174+
end
1175+
1176+
def test_should_raise_an_argument_error_with_varying_options
1177+
assert_equal false, @current_options[:update_only]
1178+
assert_raises ArgumentError do
1179+
Developer.accepts_nested_attributes_for :projects, **@current_options.merge(update_only: true)
1180+
end
1181+
end
1182+
end

0 commit comments

Comments
 (0)