#41
Jun 06, 2007

Conditional Validations

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.
Download (8.6 MB, 4:59)
alternative download for iPod & Apple TV (6.1 MB, 4:59)
# models/user.rb
validates_presence_of :password, :if => :should_validate_password?
validates_presence_of :country
validates_presence_of :state, :if => :in_us?
attr_accessor :updating_password

def in_us?
  country == 'US'
end

def should_validate_password?
  updating_password || new_record?
end

# in controller
@user.updating_password = true
@user.save

# or...
@user.save(false)

29 comments

chineseGuy Jun 06, 2007 at 00:20

aha,very nice!~


Mr Dougal Jun 06, 2007 at 00:26

Ah, excellent, I was just struggling with this problem, and then your new post turned up in my rss feeds.


RSL Jun 06, 2007 at 04:10

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?


Cristiano Betta Jun 06, 2007 at 04:48

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?


ArthurGeek Jun 06, 2007 at 05:03

Cristiano: Uses the option :allow_nil => true

Eg: validates_format_of :email, :allow_nil => true


Chris Olsen Jun 06, 2007 at 07:15

This was new for me. It's amazing at how well thought out rails is.

Thanks again for the video Ryan.


Ryan Bates Jun 06, 2007 at 07:37

@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.


Geoff Buesing Jun 06, 2007 at 09:19

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? }


Geoff Buesing Jun 06, 2007 at 09:23

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? }


Ryan Bates Jun 06, 2007 at 10:35

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.


James Herdman Jun 06, 2007 at 17:01

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.


peanut Jun 07, 2007 at 00:50

I think this episode was not good, because no real web-browser examples. :-( Only code.


Karl Smith Jun 07, 2007 at 10:19

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.


Ryan Bates Jun 07, 2007 at 17:45

@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.


Thomaz Leite Jun 08, 2007 at 11:23

Ryan, you are the best!
I was having problem with conditional validations just before you made this episode.

Thanks!


Cristiano Betta Jun 09, 2007 at 15:45

@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?


Ryan Bates Jun 09, 2007 at 18:35

@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.


David Parker Jun 11, 2007 at 13:05

Ryan, once again, I just wanted to continue to say good work and thanks for the insight.


Ismael Jun 11, 2007 at 14:59

This is cool. Makes me feel like a fool for previous over-complex solutions I implemented.


João Victor Jun 13, 2007 at 12:42

Very Nice!


Ziggy Jun 18, 2007 at 06:11

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 =)


Ziggy Jun 18, 2007 at 06:17

Oops. Sorry, that was a stupid question. I guess I'm getting tired.
Again - thanks for Railscasts!


Scott Jul 09, 2007 at 09:23

Great stuff. Just what I needed when I needed it... Keep up the spot on tutorials!


Evgeny Jul 15, 2007 at 15:28

@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.


Evgeny Jul 15, 2007 at 21:13

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.


Mahendra Oct 29, 2007 at 06:48

Greate tutorial


kino May 10, 2008 at 08:04

This was new for me. It's amazing at how well thought out rails is.

Thanks again for the video Ryan.


Adam Jun 30, 2008 at 12:21

I am struggling with passing an argument to my condition method. what if your in_us? was in_country? that took a country name?


Jacob Jul 23, 2008 at 02:47

thanks a lot, really helped me a lot... thanks...

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(required)

subscribe:
sponsored by:
if you want to help:
required:
Get Quicktime Player