5

In my User model, I have the usual suspects of email, first_name, last_name, password, etc.

I have several cases where I need to skip all or some of the validations.

Currently, I have an unless condition that looks something like:

  validates :first_name, presence: :true, etc..., unless: :skip_first_name_validation?
  validates :last_name, presence: :true, etc..., unless: :skip_last_name_validation?
  validates :email, presence: :true, etc..., unless: :skip_email_validation?

Of course I also have:

  attr_accessor :skip_first_name_validation, :skip_last_name_validation, etc.

And then I use private methods to check the state of each:

  def skip_first_name_validation?
    skip_first_name_validation
  end

  def skip_last_name_validation?
    skip_last_name_validation
  end

  def skip_email_validation?
    skip_email_validation
  end

  etc..

From there, whenever I need to skip validations, I just assign each one of these guys a true value in my controller.


So while all of this works fine, I'm wondering if there's a more elegant way?

Ideally it would be nice if I could use a simple conditional like this for each attribute in my models:

:skip_validation?

And in my controllers, just do something like:

skip_validation(:first_name, :last_name, :password) = true

Can someone offer a suggestion for how I might program this? I'd prefer not using an existing gem/library, but am trying to understand how to program this behavior in rails. Thanks.

Nathan
  • 7,627
  • 11
  • 46
  • 80
  • that's what DCI is all about. Otherwise, won't a state_machine fit? – apneadiving May 03 '12 at 19:17
  • I think it depends on how you are updating your models. For instance, you can use the `update_column` method to update 1 column at a time, which skips validations. – Max May 03 '12 at 22:28
  • Is that like update_attribute? I think this too isn't very clean, because now my controllers would be cluttered with a series of these method calls. Ideally, I would have one method that I can pass params to in the controller like: skip_validation(user_name, email, password, etc). I'm frankly a bit surprised something like this doesn't exist OOTB with rails, but that's not really the issue. I'm wondering how one go about programming this in rails. – Nathan May 04 '12 at 01:12
  • Depending on what are you skiping the validations? – albertopq May 04 '12 at 10:31
  • Mmm... I suppose I didn't add enough clarity when I asked my question, or it's too complex a problem. – Nathan May 04 '12 at 12:41
  • If there is a good reason for skipping the validations, you should maybe set accessor(s) for this reason? – theodorton May 04 '12 at 14:13
  • It would help to know what the reasons are for skipping the validations. In general, I would think the controller should not be telling the model about such details, and that determination should be handled at a lower level, either in the model itself or in some other intermediary. – Steve Jorgensen May 04 '12 at 15:46
  • There are several reasons. During registration I use a wizard that asks for parts the User data at different steps, so I need a way to suppress the validation of the fields that are not being asked. (I know there's lots of different ways to handle this, but in my case each step saves to the DB). Also, when a user is updating his/her details, they can update their email or their password. If I don't allow skipping of validation, it fires off in all cases. – Nathan May 04 '12 at 17:11

2 Answers2

1

This could help you define all the setters and checkers dynamically

private 
def self.skip_validations_for(*args)
  
  # this block dynamically defines a setter method 'set_validations' 
  # e.g.
  #   set_validations(true, :email, :last_name, :first_name)
  #   set_validations(false, :email, :last_name, :first_name)
  define_method('set_validations') do |value, *params|
    params.each do |var|
      self.send("skip_#{var.to_s}_validations=", value)
    end
  end

  # this block walks through the passed in fields and defines a checker
  # method `skip_[field_name]_validation?
  args.each do |arg|
    if self.method_defined? arg
      send :define_method, "skip_#{attr.to_s}_validation?" do 
        send "skip_#{attr.to_s}_validation"  
      end
    end
  end
end

Then in your model regardless of whether this is native in the class or included through a module, you can add:

Class Foo < ActiveRecord::Base
  validate :bar, :presence => true
  validate :baz, :length_of => 10

  skip_validations_for :bar, :baz
end

at which point you'll have access to

set_validations(true, :bar, :baz)

and

skip_bar_validation? 

update:

Where would this code go?

This depends on how widely you'd like to use it. If you just want to override the validations in one model then you could put it all directly in that model. If however you wish to create a Module that allows you to do this in every class then you'll have to create a Module put it in the lib/ directory and the make sure it's in the load path. Perhaps this could help you with that.

Why did things change so drastically?

Originally I was using the _validations method on an instance and that is an ActiveRecord method that returns a hash of validations (field name -> ActiveRecord::Validation). That method of doing things automatically turned on the ability for every field. It's more Rails'ish to allow you to opt in the specific fields you want.

Community
  • 1
  • 1
TCopple
  • 880
  • 7
  • 14
  • This is a good approach :) How about interpolating the `attr.to_s`? – theodorton May 04 '12 at 14:26
  • @theodorton I actually liked your `alias` approach. :) – TCopple May 04 '12 at 14:31
  • Hi @TCopple, I have a few newbie questions. 1. Where would this code go? Would this be added to a lib directory and would I need to include it in each model? 2. Is `self.class._validators.keys` a rails method? I'm a little confused about this last method you've described. Can you help me understand it? Thanks. – Nathan May 04 '12 at 17:27
  • @Nathan I updated my solution and added some answers to your questions. I eliminated the `self.class._validators.keys` call because that was going to create skip_validation methods for every field, instead of just the one's you selected. Rails prefers to contracts to be opt in'able, in general. – TCopple May 04 '12 at 19:36
  • @TCopple Thanks :) I undeleted the post, so it's open. – theodorton May 04 '12 at 20:11
  • Your post helps me also get started with learning meta-programming, which is really my hidden agenda with this enhancement. – Nathan May 04 '12 at 21:30
0

I'm not an expert in meta-programming, but I guess you could implement it something like this.

class User < ActiveRecord::Base
  ...
  %w(email first_name last_name).each do |attr|
    self.send :attr_accessor, "skip_#{attr}_validation".to_sym
    self.send :alias, "skip_#{attr}_validation?".to_sym, "skip_#{attr}_validation".to_sym
    self.send :validates_presence_of, attr, { unless: "skip_#{attr}_validation?".to_sym }
  end

  def skip_validation_for(*attrs)
    attrs.map{|attr| send "skip_#{attr}_validation=", true }
  end
end

Not really elegant mixed into the model, but you could make it into a module - where the use could be something like:

class User < ActiveRecord::Base
  include SkipValidations
  skip_validations :email, :first_name, :last_name
end
theodorton
  • 674
  • 1
  • 6
  • 11