How do I make the error message for validates_inclusion_of show the list of allowed options?

风流意气都作罢 提交于 2019-12-22 01:00:37

问题


I have a validates inclusion: in my model, but I think the default error message of "is not included in the list" is entirely unuseful.

How do I make it show the list of allowed options in the error message itself? (for example, "is not one of the allowed options (option 1, option 2, or option 3)"?

More concretely, what is the most elegant way to get the following tests to pass:

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

, given the following model:

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

?

This is what I have in my config/locales/en.yml currently:

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

回答1:


Here's a custom Validator that automatically provides the %{allowed_options} interpolation variable for use in your error messages:

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

Include in your config/locales/en.yml:

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

You can use it like this:

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

Or with a range:

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

Or with a lambda/Proc:

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

Comments are welcome! Do you think something like this should be added to the Rails (ActiveModel) core?

Is there a better name for this validator? restrict_to_options? restrict_to?




回答2:


Huh! It looks like Rails explicitly excludes (with except(:in)) the :in option that we pass in before passing the params to I18n!

Here is the rails source from activemodel/lib/active_model/validations/inclusion.rb:

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

Why does it do that?

Not that it would be very useful to have the raw array interpolated into the error message anyway. What we need is a string param (built from the array) that we can interpolate directly.

I managed to get the tests to pass when I changed the validates to this:

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

and changed en.yml to this:

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

but that's ugly having to pass DegreeOptions via two different hash keys.

My opinion is that the validator itself should build that key for us (and pass it on to I18n for interpolation into the message).

So what are our options? Create a custom Validator, monkey patch existing InclusionValidator, or submit a patch to Rails team...



来源:https://stackoverflow.com/questions/8527124/how-do-i-make-the-error-message-for-validates-inclusion-of-show-the-list-of-allo

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