Removing or overriding an ActiveRecord validation added by a superclass or mixin
Asked Answered
A

8

27

I'm using Clearance for authentication in my Rails application. The Clearance::User mixin adds a couple of validations to my User model, but there's one of these that I would like to remove or override. What is the best way of doing this?

The validation in question is

validates_uniqueness_of :email, :case_sensitive => false

which in itself isn't bad, but I would need to add :scope => :account_id. The problem is that if I add this to my User model

validates_uniqueness_of :email, :scope => :account_id

I get both validations, and the one Clearance adds is more restrictive than mine, so mine has no effect. I need to make sure that only mine runs. How do I do this?

Ankerite answered 22/2, 2010 at 8:54 Comment(0)
A
6

I ended up "solving" the problem with the following hack:

  1. look for an error on the :email attribute of type :taken
  2. check if the email is unique for this account (which is the validation I wanted to do)
  3. remove the error if the email is unique for this account.

Sounds reasonable until you read the code and discover how I remove an error. ActiveRecord::Errors has no methods to remove errors once added, so I have to grab hold of it's internals and do it myself. Super duper mega ugly.

This is the code:

def validate
  super
  remove_spurious_email_taken_error!(errors)
end

def remove_spurious_email_taken_error!(errors)
  errors.each_error do |attribute, error|
    if error.attribute == :email && error.type == :taken && email_unique_for_account?
      errors_hash = errors.instance_variable_get(:@errors)
      if Array == errors_hash[attribute] && errors_hash[attribute].size > 1
        errors_hash[attribute].delete_at(errors_hash[attribute].index(error))
      else
        errors_hash.delete(attribute)
      end
    end
  end
end

def email_unique_for_account?
  match = account.users.find_by_email(email)
  match.nil? or match == self
end

If anyone knows of a better way, I would be very grateful.

Ankerite answered 22/2, 2010 at 12:23 Comment(1)
As of Rails 3, you can use errors.delete(:field) to remove an error from the collection.Illness
A
14

I'd fork the GEM and add a simple check, that can then be overridden. My example uses a Concern.

Concern:

module Slugify

  extend ActiveSupport::Concern

  included do

    validates :slug, uniqueness: true, unless: :skip_uniqueness?
  end

  protected

  def skip_uniqueness?
    false
  end

end

Model:

class Category < ActiveRecord::Base
  include Slugify

  belongs_to :section

  validates :slug, uniqueness: { scope: :section_id }

  protected

  def skip_uniqueness?
    true
  end
end
Aldo answered 24/7, 2014 at 11:4 Comment(1)
just elegant. fits right in with the workings of a concern, which is my current concern lolBenzocaine
R
9

I needed to remove Spree product property :value validation and it seems there's a simplier solution with Klass.class_eval and clear_validators! of AciveRecord::Base

module Spree
  class ProductProperty < Spree::Base

    #spree logic

    validates :property, presence: true
    validates :value, length: { maximum: 255 }

    #spree logic


  end
end

And override it here

Spree::ProductProperty.class_eval do    
  clear_validators!
  validates :property, presence: true
end
Rhamnaceous answered 17/7, 2014 at 0:28 Comment(5)
doesn't this remove the validator on the base class too, and hence all other subclasses?Attaint
according to github.com/rails/rails/blob/… it just resets locally instantiated Set of validations/callbacks. I presume that set of callbacks is set on the individual class not all chain. I did't have issues with validations of derived classes.Rhamnaceous
I think it should go like this: class Subclass < Parent; class_eval { clear_validators!}; end. Perhaps in your app the lexical scope and actual scope happened to match up, but Parent.class_eval will have global runtime scope, so in production if you expect Parent to have its validators somewhere else in your app you will be suprprised.Attaint
Well you might be right but in my case I had to deal with a Rails Engine and rewriting a class wasn't working for me. Also we run shops in production. No problems there so far. Thanks for the tip, however!Rhamnaceous
I sounds like you have a different situation than OP: rather than a subclass or class using a mixin, you are using a library class directly, otherwise there wouldn't be a "rewriting class" issue. If you always want Spree::ProductProperty classes in your app to not have their library validators you are fine. Otherwise the scope of "locally instantiated set of validations" might (probably will) span multiple requests in production.Attaint
A
6

I ended up "solving" the problem with the following hack:

  1. look for an error on the :email attribute of type :taken
  2. check if the email is unique for this account (which is the validation I wanted to do)
  3. remove the error if the email is unique for this account.

Sounds reasonable until you read the code and discover how I remove an error. ActiveRecord::Errors has no methods to remove errors once added, so I have to grab hold of it's internals and do it myself. Super duper mega ugly.

This is the code:

def validate
  super
  remove_spurious_email_taken_error!(errors)
end

def remove_spurious_email_taken_error!(errors)
  errors.each_error do |attribute, error|
    if error.attribute == :email && error.type == :taken && email_unique_for_account?
      errors_hash = errors.instance_variable_get(:@errors)
      if Array == errors_hash[attribute] && errors_hash[attribute].size > 1
        errors_hash[attribute].delete_at(errors_hash[attribute].index(error))
      else
        errors_hash.delete(attribute)
      end
    end
  end
end

def email_unique_for_account?
  match = account.users.find_by_email(email)
  match.nil? or match == self
end

If anyone knows of a better way, I would be very grateful.

Ankerite answered 22/2, 2010 at 12:23 Comment(1)
As of Rails 3, you can use errors.delete(:field) to remove an error from the collection.Illness
I
5

I recently had this problem and after google didn't give me the answers quick enough I found a neater yet still un-ideal solution to this problem. Now this won't necessarily work in your case as it seems your using pre-existing super classes but for me it was my own code so I just used an :if param with a type check in the super class.

def SuperClass
  validates_such_and_such_of :attr, :options => :whatever, :if => Proc.new{|obj| !(obj.is_a? SubClass)}
end

def SubClass < SuperClass
  validates_such_and_such_of :attr
end

In the case of multpile sub classes

def SuperClass
  validates_such_and_such_of :attr, :options => :whatever, :if => Proc.new{|obj| [SubClass1, SubClass2].select{|sub| obj.is_a? sub}.empty?}
end

def SubClass1 < SuperClass
  validates_such_and_such_of :attr
end

def SubClass2 < SuperClass
end
Infirmary answered 22/7, 2010 at 18:23 Comment(2)
Unfortunately for me I can't change the superclass, it's in a gem.Ankerite
This is like putting tape over your check engine light and saying it works.Pedal
G
4

In Rails 4, you should be able to use skip_callback(:validate, :name_of_validation_method)... if you have a conveniently-named validation method. (Disclaimer: I haven't tested that.) If not, you'll need to hack into the list of callbacks to find the one you want to skip, and use its filter object.

Example:

I'm working on a site using Rails 4.1.11 and Spree 2.4.11.beta, having upgraded Spree from 2.1.4. Our code stores multiple copies of Spree::Variants in one table, for historical purposes.

Since the upgrade, the gem now validates_uniqueness_of :sku, allow_blank: true, conditions: -> { where(deleted_at: nil) }, which breaks our code. As you'll notice, though, it doesn't use a named method to do so. This is what I've done in a Spree::Variant.class_eval block:

unique_sku_filter = _validate_callbacks.find do |c|
  c.filter.is_a?(ActiveRecord::Validations::UniquenessValidator) &&
    c.filter.instance_variable_get(:@attributes) == [:sku]
end.filter

skip_callback(:validate, unique_sku_filter)

This appears to remove the callback from Variant's chain entirely.

NB. I've had to use instance_variable_get for @attributes, because it doesn't have an accessor to it. You can check c.filter.options in the find block as well; in the above example, this looks like:

c.filter.options
#=> {:case_sensitive=>true, :allow_blank=>true, :conditions=>#<Proc:... (lambda)>}
Groff answered 29/8, 2017 at 2:55 Comment(0)
L
2

Errors.delete(key) removes all errors for an attribute and I only want to remove a specific type of error belonging to an attribute. This following method can be added to any model.

Returns message if removed, nil otherwise. Internal data structures are modified so all other methods should work as expected after error removal.

Released under the MIT License

Method to remove error from model after validations have been run.

def remove_error!(attribute, message = :invalid, options = {})
  # -- Same code as private method ActiveModel::Errors.normalize_message(attribute, message, options).
  callbacks_options = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
  case message
  when Symbol
    message = self.errors.generate_message(attribute, message, options.except(*callbacks_options))
  when Proc
    message = message.call
  else
    message = message
  end
  # -- end block

  # -- Delete message - based on ActiveModel::Errors.added?(attribute, message = :invalid, options = {}).
  message = self.errors[attribute].delete(message) rescue nil
  # -- Delete attribute from errors if message array is empty.
  self.errors.messages.delete(attribute) if !self.errors.messages[attribute].present?
  return message
end

Usage:

user.remove_error!(:email, :taken)

Method to check validity except specified attributes and messages.

def valid_except?(except={})
  self.valid?
  # -- Use this to call valid? for superclass if self.valid? is overridden.
  # self.class.superclass.instance_method(:valid?).bind(self).call
  except.each do |attribute, message|
    if message.present?
      remove_error!(attribute, message)
    else
      self.errors.delete(attribute)
    end
  end
  !self.errors.present?
end

Usage:

user.valid_except?({email: :blank})
user.valid_except?({email: "can't be blank"})
Latticework answered 19/10, 2013 at 3:39 Comment(0)
R
1

I know I'm late to the game, but how about:

module Clearance
  module User
    module Validations
      extend ActiveSupport::Concern

      included do
        validates :email,
          email: true,
          presence: true,
          uniqueness: { scope: :account, allow_blank: true },
          unless: :email_optional?

        validates :password, presence: true, unless: :password_optional?
      end
    end
  end
end

in an initializer?

Recapitulate answered 22/7, 2013 at 18:28 Comment(0)
E
-2

For me on my model below code was enough. I don't want zipcode to validate.

after_validation :remove_nonrequired

def remove_nonrequired
  errors.messages.delete(:zipcode)
end
Edana answered 14/3, 2014 at 16:28 Comment(1)
For me that removed the error, but not the "invalid" status on the model.Groff

© 2022 - 2024 — McMap. All rights reserved.