Removendo ou substituindo uma validação do ActiveRecord adicionada por uma superclasse ou mixin
-
22-09-2019 - |
Pergunta
Estou usando autorização para autenticação no meu aplicativo Rails. o Clearance::User
Mixin adiciona algumas validações ao meu User
Modelo, mas há um deles que eu gostaria de remover ou substituir. Qual a melhor maneira para fazer isto?
A validação em questão é
validates_uniqueness_of :email, :case_sensitive => false
o que por si só não é ruim, mas eu precisaria adicionar :scope => :account_id
. O problema é que, se eu adicionar isso ao meu User
modelo
validates_uniqueness_of :email, :scope => :account_id
eu recebo Ambas As validações, e a única autorização adicionada é mais restritiva que a minha, então a minha não tem efeito. Preciso ter certeza de que apenas as minhas correm. Como eu faço isso?
Solução 2
Acabei "resolvendo" o problema com o seguinte hack:
- Procure um erro no
:email
atributo do tipo:taken
- Verifique se o email é exclusivo para esta conta (que é a validação que eu queria fazer)
- Remova o erro se o email for exclusivo para esta conta.
Parece razoável até você ler o código e descobrir como remover um erro. ActiveRecord::Errors
Não tem métodos para remover erros uma vez adicionados, por isso tenho que agarrar seus internos e fazer isso sozinho. Super Duper Mega Ugly.
Este é o código:
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
Se alguém souber de uma maneira melhor, eu ficaria muito grato.
Outras dicas
Eu bifurcaria a jóia e adicionava uma verificação simples, que pode ser substituída. Meu exemplo usa uma preocupação.
Interesse:
module Slugify
extend ActiveSupport::Concern
included do
validates :slug, uniqueness: true, unless: :skip_uniqueness?
end
protected
def skip_uniqueness?
false
end
end
Modelo:
class Category < ActiveRecord::Base
include Slugify
belongs_to :section
validates :slug, uniqueness: { scope: :section_id }
protected
def skip_uniqueness?
true
end
end
Recentemente, tive esse problema e depois que o Google não me deu as respostas rapidamente, encontrei uma solução mais limpa, mas ainda não ideal para esse problema. Agora, isso não funcionará necessariamente no seu caso, pois parece que você está usando super classes pré-existentes, mas para mim era meu próprio código, então eu apenas usei um: se param com uma verificação de tipo na super classe.
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
No caso de sub -classes de várias piles
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
Eu precisava remover a propriedade do produto Spree :value
validação e parece que há uma solução mais simples com Klass.class_eval
e clear_validators!
do AciveRecord::Base
module Spree
class ProductProperty < Spree::Base
#spree logic
validates :property, presence: true
validates :value, length: { maximum: 255 }
#spree logic
end
end
E substitui aqui
Spree::ProductProperty.class_eval do
clear_validators!
validates :property, presence: true
end
Eu sei que estou atrasado para o jogo, mas que tal:
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
em um inicializador?
Errors.Delete (chave) remove todos os erros para um atributo e eu só quero remover um tipo específico de erro pertencente a um atributo. Este método a seguir pode ser adicionado a qualquer modelo.
Retorna a mensagem se removida, nil caso contrário. As estruturas de dados internas são modificadas para que todos os outros métodos funcionem conforme o esperado após a remoção de erros.
Liberado sob o MIT Licença
Método para remover o erro do modelo após a execução das validações.
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
Uso:
user.remove_error!(:email, :taken)
Método para verificar a validade, exceto atributos e mensagens especificados.
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
Uso:
user.valid_except?({email: :blank})
user.valid_except?({email: "can't be blank"})
No Rails 4, você deve poder usar skip_callback(:validate, :name_of_validation_method)
... se vocês tenho um método de validação com nome de nome conveniente. (Isenção de responsabilidade: eu não testei isso.) Caso contrário, você precisará invadir a lista de retornos de chamada para encontrar o que você deseja pular e usar seu filter
objeto.
Exemplo:
Estou trabalhando em um site usando o Rails 4.1.11 e o Spree 2.4.11.beta, tendo atualizado o Spree de 2.1.4. Nosso código armazena várias cópias de Spree::Variant
s em uma mesa, para fins históricos.
Desde a atualização, a jóia agora validates_uniqueness_of :sku, allow_blank: true, conditions: -> { where(deleted_at: nil) }
, que quebra nosso código. Como você notará, porém, ele não usa um método nomeado para fazê -lo. Isso é o que eu fiz em um Spree::Variant.class_eval
quadra:
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)
Isso parece remover o retorno de chamada de Variant
é completamente a cadeia.
Nb. Eu tive que usar instance_variable_get
por @attributes
, porque não tem um acessador. Você pode checar c.filter.options
no find
bloco também; No exemplo acima, isso se parece:
c.filter.options
#=> {:case_sensitive=>true, :allow_blank=>true, :conditions=>#<Proc:... (lambda)>}
Aqui está uma "solução" do Rails 3 que funcionou para mim (novamente se alguém tiver uma maneira melhor, por favor, ofereça!)
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
Para mim, no meu modelo abaixo, o código foi suficiente. Não quero validar o ZipCode.
after_validation :remove_nonrequired
def remove_nonrequired
errors.messages.delete(:zipcode)
end