Rails errors.add override :invalid without creating a language file entry - ruby-on-rails

I am working to solve my problem to another question and I think I am close to a solution, but I have run into another problem. I am trying to copy code I found in the rails source and modify it to fit my needs:
I have created the following custom validator:
class ExistingTwoValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(:code => value).any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
record.errors.add(attribute, "custom error", options.merge(:value => value))
record.errors.add(attribute, :madeup, options.merge(:value => value))
end
end
end
If I provide a custom error message in my model:
validates :group_code, existing_two: { message: "My custom message %{value} is not okay."}
For invalid input (e.g., "www") I get the following errors:
Group code My custom message www is not okay.
Group code custom error
Group code My custom message www is not okay.
If I do not provide a custom error message:
validates :group_code, existing_two: true
For invalid input (e.g., "www") I get the following errors:
Group code is invalid
Group code custom error
Group code translation missing: en.activerecord.errors.models.XX.attributes.group_code.madeup
The first record.errors.add(attribute, :invalid, ... will allow me to override the default message in :invalid, but if I provide literal string as the second argument (e.g. record.errors.add(attribute, "custom error", it will not let me override the message.
I want to understand the logic that says if it's a symbol it can be replaced but if its a string ignore the message passed in the options. Also, is there any way to provide the custom message without using a symbol or is there a way to define the symbol without adding it to a language file somewhere?

Reviewing the following snippets from active model errors.
# File activemodel/lib/active_model/errors.rb, Line 299
def add(attribute, message = :invalid, options = {})
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
raise exception, full_message(attribute, message)
end
self[attribute] << message
end
#File activemodel/lib/active_model/errors.rb, line 438
def normalize_message(attribute, message, options)
case message
when Symbol
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
when Proc
message.call
else
message
end
end
# File activemodel/lib/active_model/errors.rb, line 412
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)
if #base.class.respond_to?(:i18n_scope)
defaults = #base.class.lookup_ancestors.map do |klass|
[ :"#{#base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
:"#{#base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
else
defaults = []
end
defaults << options.delete(:message)
defaults << :"#{#base.class.i18n_scope}.errors.messages.#{type}" if #base.class.respond_to?(:i18n_scope)
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
defaults.compact!
defaults.flatten!
key = defaults.shift
value = (attribute != :base ? #base.send(:read_attribute_for_validation, attribute) : nil)
options = {
default: defaults,
model: #base.model_name.human,
attribute: #base.class.human_attribute_name(attribute),
value: value
}.merge!(options)
I18n.translate(key, options)
end
You're calling errors.add, which calls the private method normalize_message, the normalise_message method takes the message and if it's a Symbol calls generate_message. In the generate_message method it ends up falling back to the default message because it is unable to find the translation in activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE or activemodel.errors.models.MODEL.MESSAGE
I strongly suggest you read the source code link I've provided as it is well documented and is very in depth.
Active Model Errors

This is how can you can do it simply
class Example < ActiveRecord::Base
validates :group_code, presence: true
validate do |example|
unless Group.where(:code => example.group_code).any?
example.errors.add(:base, "The code you have entered #{example.group_code} is not a valid code, please check with your teacher or group")
end
end
end
Of course, you can also extract the the above logic and put it into custom validator.
Note that , i am passing :base not attribute.
Basically, the interpolation is done with i18n gem.
You can see here

Related

Regex on validates_format_of

I have a field in a form, and I need it to be registered only if its format equals one of the two regex I am using.
I have tried to use methods, the helper structure validates_format_of with Proc.new, but may not be using the correct syntax or the correct data type
private def format_field
errors.add(
:field, "The field does not have a valid format."
)unless[
/[0-9]{3}\.?[0-9]{3}\.?[0-9]{3}\-?[0-9]{2}/,
/[0-9]{2}\.?[0-9]{3}\.?[0-9]{3}\/?[0-9]{4}\-?[0-9]{2}/
].include?(field.format)
end
A simple approach to this is the following:
FORMAT_REGEXES = [
/[0-9]{3}\.?[0-9]{3}\.?[0-9]{3}\-?[0-9]{2}/,
/[0-9]{2}\.?[0-9]{3}\.?[0-9]{3}\/?[0-9]{4}\-?[0-9]{2}/
].freeze
def validate_format
return unless FORMAT_REGEXES.find { |regex| field.format =~ regex }
errors.add(:field, :invalid, message: 'The field does not have a valid format')
end

Custom Validator Test - Contains Errors?

Wrote a custom validator in my rails project and want to write a test for when nil is passed into the record.
The validator code
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value == nil
record.errors[attribute] << "can't be nil"
elsif value <= Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
Possible test
test "future validator rejects nil values" do
#obj.future = nil
assert_includes #obj.errors
end
I'd like to not only write a test that checks that assert_not #obj.valid? but actually demonstrate that the error message is being passed back. If that's asking too much I'll settle for knowing that an error message is coming back but currently my test isn't working.
It is returning
ArgumentError: wrong number of arguments (1 for 2..3)
Update
Getting close to a green test
test "future validator rejects nil values" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages, {future: ["can't be nil"]}
end
Is returning
FutureValidatorTest#test_future_validator_rejects_nil_values [/Users/rwdonnard/WorkSpace/charter-pay-api/test/models/future_validator_test.rb:42]:
Expected {:future=>["can't be nil"]} to include {:future=>["can't be nil"]}.
The issue seems to be the way you test your class and most probably the exception gets raised from assert_includes. assert_includes expects at least 2 arguments with the first being a collection and the second being the object you expect to be included, in your case an array of errors and the error you expect respectively. Also your test is going to fail if you don't populate the #obj.errors collection, something that requires to call #obj.valid?
Your test should look like this:
test "future validator rejects nil values" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages, { future: ["can't be nil"] }
end
This way you make sure that your model is invalid if future.nil? regardless of other validations in place.
I'd also suggest that you don't check for presence in your custom validator since Rails already provide a way for this. You could have your validator look like this:
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value <= Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
and setup validation in your model like this:
class SomeClass < ActiveRecord::Base
validates :future, presence: true, future: true
end
You could also simplify your tests like:
test "is invalid with nil future date" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages[:future], "can't be nil"
end
test "future validator rejects past dates" do
#obj.future = Date.new(1987, 1, 1)
#obj.valid?
assert_includes #obj.errors.messages[:future], "can't be in the past!"
end

How to validate inclusion of array content in rails

Hi I have an array column in my model:
t.text :sphare, array: true, default: []
And I want to validate that it includes only the elements from the list ("Good", "Bad", "Neutral")
My first try was:
validates_inclusion_of :sphare, in: [ ["Good"], ["Bad"], ["Neutral"] ]
But when I wanted to create objects with more then one value in sphare ex(["Good", "Bad"] the validator cut it to just ["Good"].
My question is:
How to write a validation that will check only the values of the passed array, without comparing it to fix examples?
Edit added part of my FactoryGirl and test that failds:
Part of my FactoryGirl:
sphare ["Good", "Bad"]
and my rspec test:
it "is not valid with wrong sphare" do
expect(build(:skill, sphare: ["Alibaba"])).to_not be_valid
end
it "is valid with proper sphare" do
proper_sphare = ["Good", "Bad", "Neutral"]
expect(build(:skill, sphare: [proper_sphare.sample])).to be_valid
end
Do it this way:
validates :sphare, inclusion: { in: ["Good", "Bad", "Neutral"] }
or, you can be fancy by using the short form of creating the array of strings: %w(Good Bad Neutral):
validates :sphare, inclusion: { in: %w(Good Bad Neutral) }
See the Rails Documentation for more usage and example of inclusion.
Update
As the Rails built-in validator does not fit your requirement, you can add a custom validator in your model like following:
validate :correct_sphare_types
private
def correct_sphare_types
if self.sphare.blank?
errors.add(:sphare, "sphare is blank/invalid")
elsif self.sphare.detect { |s| !(%w(Good Bad Neutral).include? s) }
errors.add(:sphare, "sphare is invalid")
end
end
You can implement your own ArrayInclusionValidator:
# app/validators/array_inclusion_validator.rb
class ArrayInclusionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# your code here
record.errors.add(attribute, "#{attribute_name} is not included in the list")
end
end
In the model it looks like this:
# app/models/model.rb
class YourModel < ApplicationRecord
ALLOWED_TYPES = %w[one two three]
validates :type_of_anything, array_inclusion: { in: ALLOWED_TYPES }
end
Examples can be found here:
https://github.com/sciencehistory/kithe/blob/master/app/validators/array_inclusion_validator.rb
https://gist.github.com/bbugh/fadf8c65b7f4d3eaa55e64acfc563ab2

Rails model.valid? flushing custom errors and falsely returning true

I am trying to add a custom error to an instance of my User model, but when I call valid? it is wiping the custom errors and returning true.
[99] pry(main)> u.email = "test#test.com"
"test#test.com"
[100] pry(main)> u.status = 1
1
[101] pry(main)> u.valid?
true
[102] pry(main)> u.errors.add(:status, "must be YES or NO")
[
[0] "must be YES or NO"
]
[103] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={:status=>["must be YES or NO"]}>
[104] pry(main)> u.valid?
true
[105] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={}>
If I use the validate method from within the model, then it works, but this specific validation is being added from within a different method (which requires params to be passed):
User
def do_something_with(arg1, arg2)
errors.add(:field, "etc") if arg1 != arg2
end
Because of the above, user.valid? is returning true even when that error is added to the instance.
In ActiveModel, valid? is defined as following:
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
errors.clear
run_validations!
ensure
self.validation_context = current_context
end
So existing errors are cleared is expected. You have to put all your custom validations into some validate callbacks. Like this:
validate :check_status
def check_status
errors.add(:status, "must be YES or NO") unless ['YES', 'NO'].include?(status)
end
If you want to force your model to show the errors you could do something as dirty as this:
your_object = YourModel.new
your_object.add(:your_field, "your message")
your_object.define_singleton_method(:valid?) { false }
# later on...
your_object.valid?
# => false
your_object.errors
# => {:your_field =>["your message"]}
The define_singleton_method method can override the .valid? behaviour.
This is not a replacement for using the provided validations/framework. However, in some exceptional scenarios, you want to gracefully return an errd model. I would only use this when other alternatives aren't possible. One of the few scenarios I have had to use this approach is inside of a service object creating a model where some portion of the create fails (like resolving a dependent entity). It doesn't make sense for our domain model to be responsible for this type of validation, so we don't store it there (which is why the service object is doing the creation in the first place). However for simplicity of the API design it can be convenient to hang a domain error like 'associated entity foo not found' and return via the normal rails 422/unprocessible entity flow.
class ModelWithErrors
def self.new(*errors)
Module.new do
define_method(:valid?) { false }
define_method(:invalid?) { true }
define_method(:errors) do
errors.each_slice(2).with_object(ActiveModel::Errors.new(self)) do |(name, message), errs|
errs.add(name, message)
end
end
end
end
end
Use as some_instance.extend(ModelWithErrors.new(:name, "is gibberish", :height, "is nonsense")
create new concerns
app/models/concerns/static_error.rb
module StaticError
extend ActiveSupport::Concern
included do
validate :check_static_errors
end
def add_static_error(*args)
#static_errors = [] if #static_errors.nil?
#static_errors << args
true
end
def clear_static_error
#static_errors = nil
end
private
def check_static_errors
#static_errors&.each do |error|
errors.add(*error)
end
end
end
include the model
class Model < ApplicationRecord
include StaticError
end
model = Model.new
model.add_static_error(:base, "STATIC ERROR")
model.valid? #=> false
model.errors.messages #=> {:base=>["STATIC ERROR"]}
A clean way to achieve your needs is contexts, but if you want a quick fix, do:
#in your model
attr_accessor :with_foo_validation
validate :foo_validation, if: :with_foo_validation
def foo_validation
#code
end
#where you need it
your_object.with_foo_validation = true
your_object.valid?

Rails 3 custom formatted validation errors?

With this model:
validates_presence_of :email, :message => "We need your email address"
as a rather contrived example. The error comes out as:
Email We need your email address
How can I provide the format myself?
I looked at the source code of ActiveModel::Errors#full_messages and it does this:
def full_messages
full_messages = []
each do |attribute, messages|
messages = Array.wrap(messages)
next if messages.empty?
if attribute == :base
messages.each {|m| full_messages << m }
else
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
options = { :default => "%{attribute} %{message}", :attribute => attr_name }
messages.each do |m|
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
full_messages
end
Notice the :default format string in the options? So I tried:
validates_presence_of :email, :message => "We need your email address", :default => "something"
But then the error message actually appears as:
Email something
So then I tried including the interpolation string %{message}, thus overriding the %{attribute} %{message} version Rails uses by default. This causes an Exception:
I18n::MissingInterpolationArgument in SubscriptionsController#create
missing interpolation argument in "%{message}" ({:model=>"Subscription", :attribute=>"Email", :value=>""} given
Yet if I use the interpolation string %{attribute}, it doesn't error, it just spits out the humanized attribute name twice.
Anyone got any experience with this? I could always have the attribute name first, but quite often we need some other string (marketing guys always make things more complicated!).
Errors on :base are not specific to any attribute, so the humanized attribute name is not appended to the message. This allows us to add error messages about email, but not attach them to the email attribute, and get the intended result:
class User < ActiveRecord::Base
validate :email_with_custom_message
...
private
def email_with_custom_message
errors.add(:base, "We need your email address") if
email.blank?
end
end
Using internationalization for this is probably your best bet. Take a look at
http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models
Particularly this section:
5.1.2 Error Message Interpolation
The translated model name, translated
attribute name, and value are always
available for interpolation.
So, for example, instead of the
default error message "can not be
blank" you could use the attribute
name like this : "Please fill in your
%{attribute}"

Resources