Skip to content

Commit e201aab

Browse files
authored
Merge pull request rails#54273 from Edouard-chin/ec-no-invalid-persist
Prevent persisting invalid record:
2 parents 2e382ef + 4b80303 commit e201aab

File tree

4 files changed

+101
-24
lines changed

4 files changed

+101
-24
lines changed

activerecord/lib/active_record/autosave_association.rb

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -370,27 +370,29 @@ def validate_collection_association(reflection)
370370
# enabled records if they're marked_for_destruction? or destroyed.
371371
def association_valid?(association, record)
372372
return true if record.destroyed? || (association.options[:autosave] && record.marked_for_destruction?)
373-
if custom_validation_context?
374-
context = validation_context
373+
374+
context = validation_context if custom_validation_context?
375+
return true if record.valid?(context)
376+
377+
if record.changed? || record.new_record? || context
378+
associated_errors = record.errors.objects
375379
else
376-
# If the associated record is unchanged we shouldn't auto validate it.
377-
# Even if a record is invalid you should still be able to create new references
378-
# to it.
379-
return true if !record.new_record? && !record.changed?
380+
# If there are existing invalid records in the DB, we should still be able to reference them.
381+
# Unless a record (no matter where in the association chain) is invalid and is being changed.
382+
associated_errors = record.errors.objects.select { |error| error.is_a?(Associations::NestedError) }
380383
end
381384

382-
unless valid = record.valid?(context)
383-
if association.options[:autosave]
384-
record.errors.each { |error|
385-
self.errors.objects.append(
386-
Associations::NestedError.new(association, error)
387-
)
388-
}
389-
else
390-
errors.add(association.reflection.name)
391-
end
385+
if association.options[:autosave]
386+
associated_errors.each { |error|
387+
errors.objects.append(
388+
Associations::NestedError.new(association, error)
389+
)
390+
}
391+
elsif associated_errors.any?
392+
errors.add(association.reflection.name)
392393
end
393-
valid
394+
395+
errors.any?
394396
end
395397

396398
# Is used as an around_save callback to check while saving a collection

activerecord/test/cases/autosave_association_test.rb

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require "models/parrot"
2020
require "models/pirate"
2121
require "models/project"
22+
require "models/price_estimate"
2223
require "models/ship"
2324
require "models/ship_part"
2425
require "models/squeak"
@@ -1490,7 +1491,7 @@ def test_should_skip_validation_on_habtm_if_destroyed
14901491
assert_predicate @pirate, :valid?
14911492
end
14921493

1493-
def test_should_skip_validation_on_habtm_if_persisted_and_unchanged
1494+
def test_should_be_valid_on_habtm_if_persisted_and_unchanged
14941495
parrot = @pirate.parrots.create!(name: "parrots_1")
14951496
parrot.update_column(:name, "")
14961497
parrot.reload
@@ -1501,6 +1502,73 @@ def test_should_skip_validation_on_habtm_if_persisted_and_unchanged
15011502
new_pirate.save!
15021503
end
15031504

1505+
def test_should_be_invalid_on_habtm_when_any_record_in_the_association_chain_is_invalid_and_was_changed
1506+
treasure = @pirate.treasures.create!(name: "gold")
1507+
estimate = treasure.price_estimates.create!(price: 1)
1508+
estimate.update_columns(price: "not a number")
1509+
1510+
assert_not_predicate estimate, :valid?
1511+
1512+
treasures = @pirate.treasures.eager_load(:price_estimates).to_a
1513+
treasures.first.price_estimates.first.price = "not a price"
1514+
new_pirate = Pirate.new(
1515+
catchphrase: "Arr",
1516+
treasures: treasures,
1517+
)
1518+
1519+
assert_raises(ActiveRecord::RecordInvalid) do
1520+
new_pirate.save!
1521+
end
1522+
assert_equal(["Treasures is invalid"], new_pirate.errors.full_messages)
1523+
end
1524+
1525+
def test_should_be_invalid_on_habtm_when_any_record_in_the_association_chain_is_invalid_and_was_changed_with_autosave
1526+
super_pirate = Class.new(Pirate) do
1527+
self.table_name = "pirates"
1528+
has_many :great_treasures, class_name: "Treasure", foreign_key: "looter_id", autosave: true
1529+
1530+
def self.name
1531+
"SuperPirate"
1532+
end
1533+
end
1534+
@pirate = super_pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
1535+
treasure = @pirate.great_treasures.create!(name: "gold")
1536+
estimate = treasure.price_estimates.create!(price: 1)
1537+
estimate.update_columns(price: "not a number")
1538+
1539+
assert_not_predicate estimate, :valid?
1540+
1541+
treasures = @pirate.great_treasures.eager_load(:price_estimates).to_a
1542+
treasures.first.price_estimates.first.price = "not a price"
1543+
new_pirate = super_pirate.new(
1544+
catchphrase: "Arr",
1545+
great_treasures: treasures,
1546+
)
1547+
1548+
assert_raises(ActiveRecord::RecordInvalid) do
1549+
new_pirate.save!
1550+
end
1551+
assert_equal(["Great treasures price estimates price is not a number"], new_pirate.errors.full_messages)
1552+
end
1553+
1554+
def test_should_be_valid_on_habtm_when_any_record_in_the_association_chain_is_invalid_but_was_not_changed
1555+
treasure = @pirate.treasures.create!(name: "gold")
1556+
estimate = treasure.price_estimates.create!(price: 1)
1557+
estimate.update_columns(price: "not a number")
1558+
1559+
assert_not_predicate estimate, :valid?
1560+
1561+
treasures = @pirate.treasures.eager_load(:price_estimates).to_a
1562+
new_pirate = Pirate.new(
1563+
catchphrase: "Arr",
1564+
treasures: treasures,
1565+
)
1566+
1567+
assert_nothing_raised do
1568+
new_pirate.save!
1569+
end
1570+
end
1571+
15041572
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
15051573
@pirate.parrots.create!(name: "parrots_1")
15061574

activerecord/test/cases/nested_attributes_test.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,12 +1206,19 @@ def self.name; "Guitar"; end
12061206
end
12071207

12081208
class TestNestedAttributesWithExtend < ActiveRecord::TestCase
1209-
setup do
1210-
Pirate.accepts_nested_attributes_for :treasures
1211-
end
1212-
12131209
def test_extend_affects_nested_attributes
1214-
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
1210+
super_pirate = Class.new(ActiveRecord::Base) do
1211+
self.table_name = "pirates"
1212+
1213+
has_many :treasures, as: :looter, extend: Pirate::PostTreasuresExtension
1214+
self.accepts_nested_attributes_for :treasures
1215+
1216+
def self.name
1217+
"SuperPirate"
1218+
end
1219+
end
1220+
1221+
pirate = super_pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
12151222
pirate.treasures_attributes = [{ id: nil }]
12161223
assert_equal "from extension", pirate.treasures[0].name
12171224
end

activerecord/test/models/treasure.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class Treasure < ActiveRecord::Base
66
# No counter_cache option given
77
belongs_to :ship
88

9-
has_many :price_estimates, as: :estimate_of
9+
has_many :price_estimates, as: :estimate_of, autosave: true
1010
has_and_belongs_to_many :rich_people, join_table: "peoples_treasures", validate: false
1111

1212
accepts_nested_attributes_for :looter

0 commit comments

Comments
 (0)