Always validate record if validating a virtual attribute
Fixes #23645 When you're using an `attr_accessor` for a record instead of an attribute in the database there's no way for the record to know if it has `changed?` unless you tell it `attribute_will_change!("attribute")`. The change made in 27aa4dd updated validations to check if a record was `changed?` or `marked_for_destruction?` or not `persisted?`. It did not take into account virtual attributes that do not affect the model's dirty status. The only way to fix this is to always validate the record if the attribute does not belong to the set of attributes the record expects (in `record.attributes`) because virtual attributes will not be in that hash. I think we should consider deprecating this particular behavior in the future and requiring that the user mark the record dirty by noting that the virtual attribute will change. Unfortunately this isn't easy because we have no way of knowing that you did the "right thing" in your application by marking it dirty and will get the deprecation warning even if you are doing the correct thing. For now this restores expected behavior when using a virtual attribute by always validating the record, as well as adds tests for this case. I was going to add the `!record.attributes.include?(attribute)` to the `should_validate?` method but `uniqueness` cannot validate a virtual attribute with nothing to hold on to the attribute. Because of this `should_validate?` was about to become a very messy method so I decided to split them up so we can handle it specifically for each case.
This commit is contained in:
parent
3156a7692c
commit
6f15b276cb
@ -167,6 +167,13 @@ def check_validity!
|
||||
def should_validate?(record) # :nodoc:
|
||||
!record.persisted? || record.changed? || record.marked_for_destruction?
|
||||
end
|
||||
|
||||
# Always validate the record if the attribute is a virtual attribute.
|
||||
# We have no way of knowing that the record was changed if the attribute
|
||||
# does not exist in the database.
|
||||
def unknown_attribute?(record, attribute) # :nodoc:
|
||||
!record.attributes.include?(attribute.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
# +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
|
||||
|
@ -2,7 +2,7 @@ module ActiveRecord
|
||||
module Validations
|
||||
class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
|
||||
def validate_each(record, attribute, association_or_value)
|
||||
return unless should_validate?(record)
|
||||
return unless should_validate?(record) || unknown_attribute?(record, attribute)
|
||||
if record.class._reflect_on_association(attribute)
|
||||
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ module ActiveRecord
|
||||
module Validations
|
||||
class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
|
||||
def validate_each(record, attribute, association_or_value)
|
||||
return unless should_validate?(record) || associations_are_dirty?(record)
|
||||
return unless should_validate?(record) || unknown_attribute?(record, attribute) || associations_are_dirty?(record)
|
||||
if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?
|
||||
association_or_value = association_or_value.target.reject(&:marked_for_destruction?)
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ module ActiveRecord
|
||||
module Validations
|
||||
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
|
||||
def validate_each(record, attribute, association_or_value)
|
||||
return unless should_validate?(record)
|
||||
return unless should_validate?(record) || unknown_attribute?(record, attribute)
|
||||
if record.class._reflect_on_association(attribute)
|
||||
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
|
||||
end
|
||||
|
@ -72,4 +72,18 @@ def test_does_not_validate_if_parent_record_is_validate_false
|
||||
assert man.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_absence_of_virtual_attribute_on_model
|
||||
repair_validations(Interest) do
|
||||
Interest.send(:attr_accessor, :token)
|
||||
Interest.validates_absence_of(:token)
|
||||
|
||||
interest = Interest.create!(topic: 'Thought Leadering')
|
||||
assert interest.valid?
|
||||
|
||||
interest.token = 'tl'
|
||||
|
||||
assert interest.invalid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -74,4 +74,20 @@ def test_does_not_validate_length_of_if_parent_record_is_validate_false
|
||||
assert owner.valid?
|
||||
assert pet.valid?
|
||||
end
|
||||
|
||||
def test_validates_presence_of_virtual_attribute_on_model
|
||||
repair_validations(Pet) do
|
||||
Pet.send(:attr_accessor, :nickname)
|
||||
Pet.validates_length_of(:name, minimum: 1)
|
||||
Pet.validates_length_of(:nickname, minimum: 1)
|
||||
|
||||
pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy')
|
||||
|
||||
assert pet.valid?
|
||||
|
||||
pet.nickname = ''
|
||||
|
||||
assert pet.invalid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -80,4 +80,19 @@ def test_does_not_validate_presence_of_if_parent_record_is_validate_false
|
||||
assert man.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_presence_of_virtual_attribute_on_model
|
||||
repair_validations(Interest) do
|
||||
Interest.send(:attr_accessor, :abbreviation)
|
||||
Interest.validates_presence_of(:topic)
|
||||
Interest.validates_presence_of(:abbreviation)
|
||||
|
||||
interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl')
|
||||
assert interest.valid?
|
||||
|
||||
interest.abbreviation = ''
|
||||
|
||||
assert interest.invalid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user