Controlling the order of rails validations
Asked Answered
L

5

55

I have a rails model which has 7 numeric attributes filled in by the user via a form.

I need to validate the presence of each of these attributes which is obviously easy using

validates :attribute1, :presence => true
validates :attribute2, :presence => true
# and so on through the attributes

However I also need to run a custom validator which takes a number of the attributes and does some calculations with them. If the result of these calculations is not within a certain range then the model should be declared invalid.

On it's own, this too is easy

validate :calculations_ok?

def calculations_ok?
  errors[:base] << "Not within required range" unless within_required_range?
end

def within_required_range?
  # check the calculations and return true or false here
end

However the problem is that the method "validate" always gets run before the method "validates". This means that if the user leaves one of the required fields blank, rails throws an error when it tries to do a calculation with a blank attribute.

So how can I check the presence of all the required attributes first?

Lampert answered 11/5, 2011 at 14:38 Comment(4)
If I'm not mistaken, Rails always runs all validations even if the first one is invalid. So even if you can change the order of the validations, wont the calculations still cause an error if it in fact tries to do something invalid? Would probably be better to check if it is blank manually before performing calculations.Deerskin
So just catch the error and add to errors[:base] as before.Affix
@Affix If I just catch it, I don't think I am unable to relay any information about what caused the problem to the user (ie. which field they left blank)?Lampert
@Deerskin I think that may be true. I wish there was a way I could halt validation in the middle and render a page of error messages.Lampert
B
23

I'm not sure it's guaranteed what order these validations get run in, as it might depend on how the attributes hash itself ends up ordered. You may be better off making your validate method more resilient and simply not run if some of the required data is missing. For example:

def within_required_range?
  return if ([ a, b, c, d ].any?(&:blank?))

  # ...
end

This will bail out if any of the variables a through d are blank, which includes nil, empty arrays or strings, and so forth.

Bellicose answered 11/5, 2011 at 14:55 Comment(6)
Seems very un-Railsy though. Doing it this way means that not only do I have to check for the presence of each attribute, but also other things like numericallity. It basically means I'm running a lot of validations twice.Lampert
I played around with it a bit more and it looks like this is what I need to do after all. I think that what DanneManne said is true, Rails runs all the validations even if an early one fails.Lampert
Does custom validation method fail when return? Rails guide mentions errors.add as the only way to trigger a validation to fail. Could you point me towards some readings?Pereira
@Pereira Yes, you do need to use errors.add to include any messages there after the return. That's just expressing the logic to avoid triggering the error.Bellicose
I know I'm late, but shouldn't the find here be replaced with any?? find returns the element, which could be nil in this case.Lindeberg
@Lindeberg Probably the better way to do it, good point.Bellicose
D
9

An alternative for slightly more complex situations would be to create a helper method which runs the validations for the dependent attributes first. Then you can make your :calculations_ok? validation run conditionally.

validates :attribute1, :presence => true
validates :attribute2, :presence => true
...
validates :attribute7, :presence => true

validate :calculations_ok?, :unless => Proc.new { |a| a.dependent_attributes_valid? }

def dependent_attributes_valid?
  [:attribute1, ..., :attribute7].each do |field|
    self.class.validators_on(field).each { |v| v.validate(self) }
    return false if self.errors.messages[field].present?
  end
  return true
end

I had to create something like this for a project because the validations on the dependent attributes were quite complex. My equivalent of :calculations_ok? would throw an exception if the dependent attributes didn't validate properly.

Advantages:

  • relatively DRY, especially if your validations are complex
  • ensures that your errors array reports the right failed validation instead of the macro-validation
  • automatically includes any additional validations on the dependent attributes you add later

Caveats:

  • potentially runs all validations twice
  • you may not want all validations to run on the dependent attributes
Designing answered 16/7, 2012 at 20:47 Comment(3)
While this might be overkill for some applications, I don't understand why no more people like this solution. I found it awesome for a similarly complex scenario!Ninetieth
@james-h Will it work if I have nested_attributes with their own validations?Zampino
@Zampino I wouldn't trust it without a thorough unit test. Validations on associations are notoriously buggy.Designing
O
2

Check out http://railscasts.com/episodes/211-validations-in-rails-3

After implementing a custom validator, you'll simply do

validates :attribute1, :calculations_ok => true

That should solve your problem.

Omnirange answered 11/5, 2011 at 14:50 Comment(3)
Wile in one single validates call, the validations do occur in the order they are listed, rails will still plough ahead with checking calculations_ok? even if attribute2 is blank. So I think I will still have a problem?Lampert
Yes, but your example shows you're validating their present first, so it shouldn't be a problem: validate for the presence of all required attributes, then validate with calculations_ok: validates :attribute1, :presence => true; validates :attribute2, :presence => true validates :attribute1, :calculations_ok => trueOmnirange
Unfortunately, using an ActiveModel::EachValidator doesn't work for a custom validator that inspects more than one attribute. For example, a validator that verifies correct ordering of a starting and ending timestamp and therefore must compare the two. Probably best to just bail on Rails' presence validator for special cases like this and bake the logic into the custom validator.Thomasina
N
2

I recall running into this issue quite some time ago, still unclear if validations order can be set and execution chain halted if a validation returns error.

I don't think Rails offers this option. It makes sense; we want to show all of the errors on the record (including those that come after a failing, due to invalid input, validation).

One possible approach is to validate only if the input to validate is present:

def within_required_range?
  return unless [attribute1, attribute2, ..].all?(&:present?)
  
  # check the calculations and return true or false here
end

Make it pretty & better structured (single responsibility) with Rails idiomatic validation options:

validates :attribute1, :presence => true
validates :attribute2, :presence => true
# and so on through the attributes

validate :calculations_ok?, if: :attributes_present?

private
  def attributes_present?
    [attribute1, attribute2, ..].all?(&:present?)
  end

  def calculations_ok?
    errors[:base] << "Not within required range" unless within_required_range?
  end

  def within_required_range?
    # check the calculations and return true or false here
  end
Neighboring answered 24/3, 2020 at 15:59 Comment(0)
U
1

The James H solution makes the most sense to me. One extra thing to consider however, is that if you have conditions on the dependent validations, they need to be checked also in order for the dependent_attributes_valid? call to work.

ie.

    validates :attribute1, presence: true
    validates :attribute1, uniqueness: true, if: :attribute1?
    validates :attribute1, numericality: true, unless: Proc.new {|r| r.attribute1.index("@") }
    validates :attribute2, presence: true
    ...
    validates :attribute7, presence: true

    validate :calculations_ok?, unless: Proc.new { |a| a.dependent_attributes_valid? }

    def dependent_attributes_valid?
      [:attribute1, ..., :attribute7].each do |field|
        self.class.validators_on(field).each do |v|
          # Surely there is a better way with rails?
          existing_error = v.attributes.select{|a| self.errors[a].present? }.present?

          if_condition = v.options[:if]
          validation_if_condition_passes = if_condition.blank?
          validation_if_condition_passes ||= if_condition.class == Proc ? if_condition.call(self) : !!self.send(if_condition)

          unless_condition = v.options[:unless]
          validation_unless_condition_passes = unless_condition.blank?
          validation_unless_condition_passes ||= unless_condition.class == Proc ? unless_condition.call(self) : !!self.send(unless_condition)

          if !existing_error and validation_if_condition_passes and validation_unless_condition_passes
            v.validate(self)
          end
        end
        return false if self.errors.messages[field].present?
      end
      return true
    end
Unto answered 3/4, 2016 at 23:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.