toolmantim

Different validations for the same ActiveRecord model

January 30, 2008 10:25 (Sydney Australia), updated about 12 hours later

Last night Mike was adding a feature to the Cultural Awards 2008 website where an admin could manually create a new user with any validly formatted email address, different to the public user registration where users can only use nsw.gov.au email addresses when registering.

One approach to solving this might be to store a global “current user” from a controller’s before/around filter which you’d then check when performing validations. I don’t particularly like this approach for the simple problem at hand because it pushes the request/response state into the model and makes the interaction between model objects less explicit.

My preferred method would be to simply have the creator of the User object specify the state under which validation should occur, and the simplest way to accomplish this is with an accessor method and the validation conditional options :if and :unless.

Below we’ll use :if and :unless to specify an instance method defining whether we perform this validation or not. The controller simply sets the flag on the model before requesting validation.


class User < ActiveRecord::Base
  attr_accessor :validate_as_admin
  attr_protected :validate_as_admin # Protect from mass-assignment

  validates_format_of :email,
                      :with => Format::NSW_GOV_EMAIL,
                      :unless => :validate_as_admin,
                      :message => "is not a valid nsw.gov.au address" 

  validates_format_of :email,
                      :with => Format::EMAIL,
                      :if => :validate_as_admin,
                      :message => "is not a valid email address" 
end

The existing public user registration doesn’t need to change:


class UsersController < ApplicationController
  def create
    @user = User.new(params[:user])
    # @user.validate_as_admin == nil by default
    @user.save!
    redirect_to members_path
  rescue ActiveRecord::RecordInvalid
    render :action => "new" 
  end
end

The admin section now simply sets validate_as_admin = true before triggering the validation:


class Admin::UsersController < Admin::BaseController
  def create
    @user = User.new(params[:user])
    @user.validate_as_admin = true
    @user.save!
    redirect_to members_path
  rescue ActiveRecord::RecordInvalid
    render :action => "new" 
  end
end

and that’s it.

It’s a similar technique we used when creating simple step-based validation.

If the problem you’re solving is more complex then you may consider using Jay Field’s excellent Validatable plugin which does a whole bunch of things, one of which is validation grouping.

Comments

Dr Nic

I like the use of instance variables to represent controller state. I think I’ve coded around this problem in too many different ways in the past; e.g. pass current_user into model methods. I’ll perhaps never find a happy one-hole-for-all-golf-balls approach.

Matthijs Langenberg

Be aware of the fact that a user (not an admin!) can simply add “user[validate_as_admin]=1” to the post data when calling UsersController#create.

Be sure to add attr_protected :validate_as_admin to the User model.

Tim Lucas

@DrNic: yeah i’ve tried some funky things too, most too tricky for their own good in the end.

@Matthijs: ahhh nice catch! Thanks. I’ve updated the article.

Pratik

Hey Tim

Nice post. Another way to do this would be something like http://pastie.org/145293

Henrik N

Nice and simple.

I also like your use of a Format module as opposed to just doing User::EMAIL.

StarTrader

This is fine if a User instance is only written when the user is created, but what about the situation where an admin creates a user, then the user whats to change some of the information in their User entry? This would cause the validation to fail.

I think then that this validation is only good during creation.

Tim Lucas

@StarTrader: sure sure, sorry for not pointing that out. You’d need to do a similar thing in the update action too.

To comment on this article you must have javascript enabled.