Removendo ou substituindo uma validação do ActiveRecord adicionada por uma superclasse ou mixin

StackOverflow https://stackoverflow.com/questions/2309757

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?

Foi útil?

Solução 2

Acabei "resolvendo" o problema com o seguinte hack:

  1. Procure um erro no :email atributo do tipo :taken
  2. Verifique se o email é exclusivo para esta conta (que é a validação que eu queria fazer)
  3. 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::Variants 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
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top