Removing or overriding an ActiveRecord validation added by a superclass or mixin

非 Y 不嫁゛ 提交于 2019-11-30 16:29:02

问题


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?


回答1:


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



回答2:


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.




回答3:


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



回答4:


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



回答5:


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?




回答6:


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"})



回答7:


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



回答8:


Here's a Rails 3 "solution" that worked for me (again if anyone has a better way please offer it!)

class NameUniqueForTypeValidator < ActiveModel::Validator

  def validate(record)
    remove_name_taken_error!(record)
  end

  def remove_name_taken_error!(record)
    errors = record.errors
    errors.each do |attribute, error|
      if attribute == :name && error.include?("taken") && record.name_unique_for_type?
        errors.messages[attribute].each do |msg|
          errors.messages[attribute].delete_at(errors.messages[attribute].index(msg)) if msg.include?("taken")
        end
        errors.messages.delete(attribute) if errors.messages[attribute].empty?
      end
    end
  end

end


ActsAsTaggableOn::Tag.class_eval do
  validates_with NameUniqueForTypeValidator

  def name_unique_for_type?
    !ActsAsTaggableOn::Tag.where(:name => name, :type => type).exists?
  end
end



回答9:


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


来源:https://stackoverflow.com/questions/2309757/removing-or-overriding-an-activerecord-validation-added-by-a-superclass-or-mixin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!