问题
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