diff options
author | Brent Wheeldon & Nick Monje <pair+brent+nmonje@pivotallabs.com> | 2012-06-22 12:09:48 -0400 |
---|---|---|
committer | Ben Moss <pair+bmoss@pivotallabs.com> | 2012-07-20 17:37:57 -0400 |
commit | 9feda929409ab687befaed8d1c9878d94e955adc (patch) | |
tree | 5deafb5e134315050c59e059a71d18ec6915e655 /activerecord | |
parent | 215d41d802637520129cb7551b35faca72873143 (diff) | |
download | rails-9feda929409ab687befaed8d1c9878d94e955adc.tar.gz rails-9feda929409ab687befaed8d1c9878d94e955adc.tar.bz2 rails-9feda929409ab687befaed8d1c9878d94e955adc.zip |
AR has a subclass of AM:PresenceValidator.
This allows us to mark the parent object as invalid if all associated objects
in a presence validated association are marked for destruction.
See: https://github.com/rails/rails/issues/6812
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG.md | 7 | ||||
-rw-r--r-- | activerecord/lib/active_record/validations.rb | 1 | ||||
-rw-r--r-- | activerecord/lib/active_record/validations/presence.rb | 64 | ||||
-rw-r--r-- | activerecord/test/cases/validations/presence_validation_test.rb | 44 |
4 files changed, 116 insertions, 0 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a965fe0494..f41ccfe56f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -31,6 +31,13 @@ *kennyj* +* Changed validates_presence_of on an association so that children objects + do not validate as being present if they are marked for destruction. This + prevents you from saving the parent successfully and thus putting the parent + in an invalid state. + + *Nick Monje & Brent Wheeldon* + * `FinderMethods#exists?` now returns `false` with the `false` argument. *Egor Lynko* diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index d06020b3ce..cef2bbd563 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -81,3 +81,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" +require "active_record/validations/presence" diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb new file mode 100644 index 0000000000..056527b512 --- /dev/null +++ b/activerecord/lib/active_record/validations/presence.rb @@ -0,0 +1,64 @@ +module ActiveRecord + module Validations + class PresenceValidator < ActiveModel::Validations::PresenceValidator + def validate(record) + super + attributes.each do |attribute| + next unless record.class.reflect_on_association(attribute) + value = record.send(attribute) + if Array(value).all? { |r| r.marked_for_destruction? } + record.errors.add(attribute, :blank, options) + end + end + end + end + + module ClassMethods + # Validates that the specified attributes are not blank (as defined by + # Object#blank?), and, if the attribute is an association, that the + # associated object is not marked for destruction. Happens by default + # on save. + # + # class Person < ActiveRecord::Base + # has_one :face + # validates_presence_of :face + # end + # + # The face attribute must be in the object and it cannot be blank or marked + # for destruction. + # + # If you want to validate the presence of a boolean field (where the real values + # are true and false), you will want to use + # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # + # This is due to the way Object#blank? handles boolean values: + # <tt>false.blank? # => true</tt>. + # + # This validator defers to the ActiveModel validation for presence, adding the + # check to see that an associated object is not marked for destruction. This + # prevents the parent object from validating successfully and saving, which then + # deletes the associated object, thus putting the parent object into an invalid + # state. + # + # Configuration options: + # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). + # * <tt>:on</tt> - Specifies when this validation is active. Runs in all + # validation contexts by default (+nil+), other options are <tt>:create</tt> + # and <tt>:update</tt>. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # + def validates_presence_of(*attr_names) + validates_with PresenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb new file mode 100644 index 0000000000..cd9175f454 --- /dev/null +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/man' +require 'models/face' +require 'models/interest' + +class PresenceValidationTest < ActiveRecord::TestCase + class Boy < Man; end + + repair_validations(Boy) + + def test_validates_presence_of_non_association + Boy.validates_presence_of(:name) + b = Boy.new + assert b.invalid? + + b.name = "Alex" + assert b.valid? + end + + def test_validates_presence_of_has_one_marked_for_destruction + Boy.validates_presence_of(:face) + b = Boy.new + f = Face.new + b.face = f + assert b.valid? + + f.mark_for_destruction + assert b.invalid? + end + + def test_validates_presence_of_has_many_marked_for_destruction + Boy.validates_presence_of(:interests) + b = Boy.new + b.interests << [i1 = Interest.new, i2 = Interest.new] + assert b.valid? + + i1.mark_for_destruction + assert b.valid? + + i2.mark_for_destruction + assert b.invalid? + end +end |