【问题标题】:How do I make the error message for validates_inclusion_of show the list of allowed options?如何使 validates_inclusion_of 的错误消息显示允许的选项列表?
【发布时间】:2011-12-15 21:51:56
【问题描述】:

我的模型中有一个validates inclusion:,但我认为“不包含在列表中”的默认错误消息完全没有用。

如何让它在错误消息本身中显示允许的选项列表? (例如,"is not one of the allowed options (option 1, option 2, or option 3)"?

更具体地说,让以下测试通过的最优雅的方法是什么:

describe Person do
  describe 'validation' do
    describe 'highest_degree' do
      # Note: Uses matchers from shoulda gem
      it { should     allow_value('High School').       for(:highest_degree) }
      it { should     allow_value('Associates').        for(:highest_degree) }
      it { should     allow_value('Bachelors').         for(:highest_degree) }
      it { should     allow_value('Masters').           for(:highest_degree) }
      it { should     allow_value('Doctorate').         for(:highest_degree) }
      it { should_not allow_value('Elementary School'). for(:highest_degree).with_message('is not one of the allowed options (High School, Associates, Bachelors, Masters, or Doctorate)') }
      it { should_not allow_value(nil).                 for(:highest_degree).with_message('is required') }
      it { subject.valid?; subject.errors[:highest_degree].grep(/is not one of/).should be_empty }
    end
  end
end

,给定以下模型:

class Person
  DegreeOptions = ['High School', 'Associates', 'Bachelors', 'Masters', 'Doctorate']
  validates :highest_degree, inclusion: {in: DegreeOptions}, allow_blank: true, presence: true
end

?

这是我目前在 config/locales/en.yml 中的内容:

en:
  activerecord:
    errors:
      messages:
        blank: "is required"
        inclusion: "is not one of the allowed options (%{in})"

【问题讨论】:

    标签: ruby-on-rails ruby validation internationalization interpolation


    【解决方案1】:

    这是一个 custom Validator,它自动提供 %{allowed_options} 插值变量以在您的错误消息中使用:

    class RestrictToValidator < ActiveModel::EachValidator
      ErrorMessage = "An object with the method #include? or a proc or lambda is required, " <<
                      "and must be supplied as the :allowed_options option of the configuration hash"
    
      def initialize(*args)
        super
        @allowed_options = options[:allowed_options]
      end
    
      def check_validity!
        unless [:include?, :call].any?{ |method| options[:allowed_options].respond_to?(method) }
          raise ArgumentError, ErrorMessage
        end
      end
    
      def allowed_options(record)
        @allowed_options.respond_to?(:call) ? @allowed_options.call(record) : @allowed_options
      end
      def allowed_options_string(record)
        allowed_options = allowed_options(record)
        if allowed_options.is_a?(Range)
          "#{allowed_options}"
        else
          allowed_options.to_sentence(last_word_connector: ', or ')
        end
      end
    
      def validate_each(record, attribute, value)
        allowed_options = allowed_options(record)
        inclusion_method = inclusion_method(allowed_options)
        unless allowed_options.send(inclusion_method, value)
          record.errors.add(attribute, :restrict_to,
                            options.except(:in).merge!(
                              value: value,
                              allowed_options: allowed_options_string(record)
                            )
          )
        end
      end
    
    private
    
      # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
      # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt>
      # uses the previous logic of comparing a value with the range endpoints.
      def inclusion_method(enumerable)
        enumerable.is_a?(Range) ? :cover? : :include?
      end
    end
    

    包含在您的 config/locales/en.yml 中:

    en:
      activerecord:
        errors:
          messages:
            restrict_to: "is not one of the allowed options (%{allowed_options})"
    

    你可以这样使用它:

      DegreeOptions = ['High School', 'Associates', 'Bachelors', 'Masters', 'Doctorate']
      validates :highest_degree, restrict_to: {allowed_options: DegreeOptions},
        allow_blank: true, presence: true
      # => "highest_degree is not one of the allowed options (High School, Associates, Bachelors, Masters, or Doctorate)"
    

    或者有一个范围:

      validates :letter_grade, restrict_to: {allowed_options: 'A'..'F'}
      # => "letter_grade is not one of the allowed options (A..F)"
    

    或者使用 lambda/Proc:

      validates :address_state, restrict_to: {
        allowed_options: ->(person){ Carmen::states(country)
      }
    

    欢迎评论!您认为应该将这样的内容添加到 Rails (ActiveModel) 核心吗?

    这个验证器有更好的名字吗? restrict_to_options? restrict_to?

    【讨论】:

    • 我很惊讶没有其他人对此发表评论。干得好!我认为这是一段很棒的代码。 :+1:
    【解决方案2】:

    嗯!看起来 Rails 明确排除了(使用except(:in))我们在将参数传递给 I18n 之前传入的 :in 选项!

    这里是来自 activemodel/lib/active_model/validations/inclusion.rb 的 rails 源代码:

    class InclusionValidator < EachValidator
      def validate_each(record, attribute, value)
        delimiter = options[:in]
        exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
        unless exclusions.send(inclusion_method(exclusions), value)
          record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value))
        end
      end
    end
    

    为什么要这样做?

    无论如何,将原始 array 插入到错误消息中并不是很有用。我们需要的是一个 string 参数(由数组构建),我们可以直接插值。

    当我将 validates 更改为此时,我设法通过了测试:

      validates :highest_degree, inclusion: {
        in:              DegreeOptions,
        allowed_options: DegreeOptions.to_sentence(last_word_connector: ', or ')}
      }, allow_blank: true, presence: true
    

    并将 en.yml 更改为:

            inclusion: "is not one of the allowed options (%{allowed_options})"
    

    但是不得不通过两个不同的哈希键传递 DegreeOptions 是很丑的。

    我的意见是验证器本身应该为我们构建该密钥(并将其传递给 I18n 以插入到消息中)。

    那么我们有哪些选择?创建自定义验证器,修改现有的 InclusionValidator,或向 Rails 团队提交补丁...

    【讨论】:

      猜你喜欢
      • 2016-08-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-22
      • 2017-04-26
      • 1970-01-01
      相关资源
      最近更新 更多