Sunday, September 16, 2007

Rails Validation - using Validation Groups

The application I'm currently working on has a few mutli page forms, where a user fills out information for a particular model over several pages, sort of a wizard interface. This has been giving me difficulty with validation, as I prefer to to partial validation on the model and only validate fields as the user is going along, as opposed to doing validation at the end of the process. However, ActiveRecord has no way to specify which validations to run when. People seem to either write custom validation methods for each step, or run the validation at the end of the process. I prefer not to do this, as you are repeating your validation or you can't use the validates_as methods. Ideally, I wanted a way to define which fields to validate when, without having to redefine validations. I was playing around with Rails, and saw a way to override the AR model so that only certain fields are validated. Basically, I overrode the add method on the Errors to only set the error if it is part of the current validation group. I added some methods to define validation groups, and also set the current validation group. Since I was bored, I made it a plugin. Here is more information: Using the plugin, you can define validation_groups on the model level, defining which fields you would like to validate. Then, when you create an instance of model, you can set the desired validation_group to use. The plugin then intercepts Rails before it adds an error message to the errors array, and only adds it if there is no validation group set, or if the field is defined as the current validation group. Here is how the model looks:

class User < fields="">[:name,
                                    :description]
 validation_group :step2, :fields=>[:address]    
end
Here is how you enable the group

   @user = User.new
   @user.enable_validation_group :step1
   @user.valid?
You can reset it by calling this:

   @user.disable_validation_group
The plugin is located at: http://github.com/akira/validationgroup/tree/master To install, type

git clone git://github.com/akira/validationgroup.git vendor/plugins/validationgroup && rm -rf vendor/plugins/validationgroup/.git
Please let me know if you find any issues.

13 comments:

Jack said...

I was working on a multipage form and ran into this. The biggest problem was validation as I didn't want to write my own custom validation. I preferred using AR's validation. I'll give this plugin a try, thanks!

Anonymous said...

I've seen it done like so:

class User < ActiveRecord::Base
  validates_presence_of :name, :description, :if => Proc.new {|u| u.step == :step1}
  validates_presence_of :address, :if => :step2? # alternative

  def step2?
    step == :step2
  end
end

@user = User.new
@user.step = :step1
@user.valid?

akira_atl said...

Yes, I have seen it that way as well. However, I wanted a shorthand way of doing that without all the code..

Jarrett Colby said...

The plugin breaks any models that don't use validation groups. You get a NoMethodError for validation_group_enabled? when a validation fails. Luckily, this is easy to fix.

Change line 56 of validation_group.rb to:

if @base.respond_to? :validation_group_enabled? and @base.validation_group_enabled?

Jarrett Colby said...

3 bugs in the plugin found so far and counting. It's a really useful plugin when it works, but be warned that you may have to debug it. (Luckily, it's a relatively short file.)

akira_atl said...

Jarrett, thanks for the update, I will take a look at it and update the code. Let me know if you have any other updates to it.

Anonymous said...

Updates would be awesome. It would be nice to be able to set this as an external in my project without local bugfixes.

akira_atl said...

Sorry for the bug and delay - a previous patch caused the issue. I have now applied the update from Jarrett (line 56). I have also added some extra tests which can be found here: http://validationgroup.rubyforge.org/svn/trunk/test/validation_group_test.rb

If you see any other issues, please let me know what the issue is, or send me a failing test and I will try to fix the issue.

JL said...

I was getting an error after upgrading from Rails 2.0.2 to 2.2.2. It was the wrong number of arguments(3 for 2) error.

I'm still running an older version of the plugin because I hacked in some support for setting an error to :base - this might be fixed or changed now, not sure. Regardless, I haven't done an update to the latest svn branch.

I did find out tho why I was getting the error. It appears that Rails Errors.add does not take a block as the last arg. Instead they are a options hash. So I changed the method to:

def add_with_validation_group(attribute, msg = @@default_error_messages[:invalid], options={})

and the add_without_validation_call to:

add_without_validation_group(attribute, msg, options)

And now everything works. At some point, I'll probably roll in the latest svn updates, but in case someone ran into the same problem, this might be the possible solution.

Cheers!

Stefan Botzenhart said...

@JL: Perfect! Also worked for me! I made a checkout right now, so it seems that the validation plugin is not rails 2.2.2 ready.

akira_atl said...

@JL, @Stefan, I fixed it to use variable arguments so it will be backwards compatible (using *args). I tested it on Rails 2.2 and it worked with this fixed.

I moved the code to github, it is available at http://github.com/akira/validationgroup/tree/master let me know if this worked

Stefan Botzenhart said...

sorry for the late reply. In my case it works fine. But I didn't test it in another app. Thanks for your efforts!

JL said...

So running the latest 2.2.x Rails, I again came across this error:

NameError: uninitialized class variable @@default_error_messages in ValidationGroup::ActiveRecord::Errors

I looked in the Rails validations.rb file and saw that default_error_messages is no longer a class var and in fact, it's deprecated as a method call. So I updated the validation group method to use the appropriate Rails non-deprecated call:

def add_with_validation_group(attribute,
msg = I18n.translate('activerecord.errors.messages')[:invalid], *args,
&block)

Now it works again.