#41 Conditional Validations
Jun 06, 2007 | 4 minutes | Active Record
By default, validations will take place every time the model is saved. Sometimes you only want a validation to happen when certain conditions are met. See how to do that in this episode.
That last bit about passing false to the save method was really useful-seeming but I can't find anything in the API [official or caboo.se] that verifies that. Can you give a little more information about that? And does it work for create?
I have an additional question about conditional validation:
How do I only check one validation if an other validation returns true? For example, I would like to check the syntax of someones email address and I constructed a nice validator for this, but it doesn't check if the email field exists. Now I would only like to check the validity of the email syntax if the email field is even there. How can I do that?
Cristiano: Uses the option :allow_nil => true
Eg: validates_format_of :email, :allow_nil => true
This was new for me. It's amazing at how well thought out rails is.
Thanks again for the video Ryan.
@RSL, this is difficult to find in the documentation. The "save" method defined in ActiveRecord::Base doesn't accept a parameter. However, there is another method called save_with_validation which accepts a boolean. As it turns out, this method overrides the "save" method through alias_method_chain, so that is what is being called.
AFAIK it only applies to the "save" method, not the "create" method or any other method.
As always, great stuff!
I just looked at the Rdoc for validates_presence_of -- I noticed that the :if option can take a proc object. So maybe you could do something like this?
validates_presence_of :password,
:if => Proc.new { updating_password || new_record? }
Hmm, you might need to pass the user object into the block:
validates_presence_of :password, :if => Proc.new {|user| user.updating_password || user.new_record? }
True, the :if condition does take a Proc object, but I never use it because it's kind of ugly. Having it call a method is a much better solution IMO. It is cleaner, it is easier to extend and it can be shared across multiple validations.
Hi Ryan. Has this changed at all in rails 3? My attr_accessor does not seem to be passing in the true/false value of the variable I set in the controller. I've tried everything and am beginning to think my problem might have to do with changes in Rails. Something is being blocked. Its such a simple thing...it driving me NUTS that its not just working!
THANKS!
For any Googlers or the curious out there, using this sort of conditional validation allows you to implement a multi-step creation process for a model. However, this sort of thing is VERY difficult to fit into the REST paradigm.
I think this episode was not good, because no real web-browser examples. :-( Only code.
I was looking for just something like this yesterday... well, sort of. I need to validate that a :start_date is before the :end_date. I didn't know about the :if => option on validations, so I wrote my own:
def validate
if self.start_date && self.end_date
errors.add( :end_date, "must be after the Start Date") if self.start_date > self.end_date
end
end
Probably not the cleanest method, but i works.
@Karl, sometimes you don't want to only change when a validation takes place, but the logic behind the validation as well. Rails offers various methods for setting validations (such as validates_presence_of), but I don't think there's any for comparing dates. In that case you did the right thing by making it a custom validation in the "validate" method.
Ryan, you are the best!
I was having problem with conditional validations just before you made this episode.
Thanks!
@Arthur:Geek: Thanx for the tip, but I don't want to allow for a nil, right? Or doesn't that matter because I also do the other check?
@Cristiano, I think what you want is an ":if" parameter as I show in the screencast. You would need to create a method to check if the email address is blank. Something like this:
--
validates_format_of ..., :if => :email_is_filled
def email_is_filled
!email.blank?
end
--
I don't think validates_format_of takes the allow_nil option, at least I don't see it in the docs.
Ryan, once again, I just wanted to continue to say good work and thanks for the insight.
Great as always, but I have a question: how do I set the @user.updating_password = true in functional test. I'm testing the changing password page =)
Oops. Sorry, that was a stupid question. I guess I'm getting tired.
Again - thanks for Railscasts!
Great stuff. Just what I needed when I needed it... Keep up the spot on tutorials!
@Ryan Bates
@ArthurGeek
Not only does validates_format_of not take the allow_nil option, it is also a known issue.
http://dev.rubyonrails.org/ticket/840
http://dev.rubyonrails.org/ticket/4208
allow_nil => true is a known method to make validations skip, to make way for validates_presence_of to kick in and put its error. But alas, it does not exist with validates_format_of ... and I have been trying to make my little rSpec pass for several days now with no success. Even the above patch did not help.
And you can find quite a bit of examples on the web where rail coders use this "missing" feature without being aware that its not there, because there is no warning or anything.
Another related problem is that rails will not convert empty strings into nil when posting forms, buts thats a whole other issue.
Ooops, seems that the mistake that prevented me from working (which I describe in the previous comment) was all my own.
Combined a spec for checking a nil email, with a too short email and a too long email into one specification - and thats what I get, errors and comments about the wrong things.
Because :allow_nil => true was at some point moved into validates_each, so now every validation that is using validates_each (most if not all of them) now has the allow_nil property.
Even though its not in the documentation - probably for historical reasons.
I am struggling with passing an argument to my condition method. what if your in_us? was in_country? that took a country name?
actually, this IS useful for multipage forms
even if a instance variable is not shared among requests, you can progressively put/retrieve attributes in the session, and step by step updating a "step" attribute (a virtual attribute of the model), then firing validations only for the correct steps
I noticed that apart save(false) you can check the validity of the attributes with model_instance.valid?
I don't think the original validaiton in rails is suitble for me,I just use Jquery to validate the form in client,not in server.
You should always do both. Never rely on client only :)
Oops. Sorry, that was a stupid question. I guess I'm getting tired.
Again - thanks for Railscasts!
is it just me, or does the question mark on the function name break this?
This works for me:
validates_presence_of :state, :if => :in_us
def in_us
country == 'US'
end
This, would not:
validates_presence_of :state, :if => :in_us?
def in_us?
country == 'US'
end
scratch that. :-$
Do we need to set updating_password back to false to avoid future updates from invoking the validation? If not, why not?
Is this possible when using collection.build for example?
@board.users.adding_board_user = true
@scoreboard.users.build(username: val[:username])
@board.save
adding_board_user is the conditional validation that works when called like the following in the user class:-
@user.adding_board_user = true
Just want to apply the same conditionals when saving a user from the board class.
Wow! Been a RailsCasts member for about 2 weeks and I cannot believe how helpful it has been. Just when you think Google can answer everything, I stumble back here.
I have been looking for a good solution to conditional validations for almost 2 days. Stack Overflow had dozens of potential answers, but no "great" way to get this done.
Thanks Ryan!