问题
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:
- look for an error on the
:email
attribute of type:taken
- check if the email is unique for this account (which is the validation I wanted to do)
- 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::Variant
s 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