Rails 3 custom formatted validation errors? - ruby-on-rails

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

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

Rails errors.add override :invalid without creating a language file entry

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

rails email validation with name and email

I want to write a validation for email filed .But some different way.
I will allow user to enter email in two format like "name 'email#example.com' " and simple 'email#example.com' .so basically i want to write a validation which will check that is valid email format in present in the value or not.
Just need a custom validation for check the valid email format is present in the input email value.
My model look like :
class Contact < ActiveRecord::Base
validates :email ,presence: true
validate :email_format
def email_format
??? what to write here ???
end
end
How can i write a validation for this.
You need to slightly modify the regular expression in your case.
validates :email, format: { with: /(\A([a-z]*\s*)*\<*([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\>*\Z)/i }
This will match the following formats.
soundar.rathinsamy#gmail.com
Soundar<soundar.rathinsamy#gmail.com>
Soundar <soundar.rathinsamy#gmail.com>
soundar<soundar.rathinsamy#gmail.com>
Soundar Rathinsamy<soundar.rathinsamy#gmail.com>
Soundar Rathinsamy <soundar.rathinsamy#gmail.com>
soundar rathinsamy <soundar.rathinsamy#gmail.com>
If you need some change continue editing this regular expression at rubular.com
validates :email, format: { with: /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i }
Taken from apidock.com: http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates
This code extracts from the email a string between "<" and ">" but if it doesn't find a match then it takes the last word in the email... in either case it then takes that string and tests for a valid email.
So it should work for "John john#example.com" and "John<john#example.com>"
def email_format
test_string = $1 if email =~ /\<([^\>]+)\>/
test_string = email.split(' ').last unless test_string
return if test_string =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
errors.add(:email, "not a valid email")
end

Rails 4.1 enum raises error instead set object as invalid

I am using the Rails 4.1 enum field
class User
enum category: [ :client, :seller, :provider ]
end
When the user signs up, he chooses from a select box his category. The default is empty, because I want to force the user to choose one option.
If the user does not select any option, I would like to return to the form with a validation message. Here is the select box code in sign up form
<%= f.select :category, [], {}, class: "form-control" do %>
<option value="99">Choose an option</option>
<% User.categories.each do |cat,code| %>
<option value="<%= code %>" <% if params["user"] && code.to_s == params["user"]["category"] %>selected='selected'<%end%> ><%= t(cat) %></option>
<% end %>
<% end %>
When the controller creates the user, instead of adding a validation error to the record, it raises an exception. How to avoid this?
ArgumentError - '99' is not a valid category:
(gem) activerecord-4.1.1/lib/active_record/enum.rb:103:in `block (3 levels) in enum'
(gem) activerecord-4.1.1/lib/active_record/attribute_assignment.rb:45:in `_assign_attribute'
(gem) activerecord-4.1.1/lib/active_record/attribute_assignment.rb:32:in `block in assign_attributes
I really dislike the reasoning as stated in the issue listed above. Since the value is coming over the wire, it should be treated the same as a freetext input where the expectation is to validate in the model and not the controller. This is especially true in APIs where the developers have even less of a say as far as expected input coming from form data (for example).
In case anyone wants a hack, here is what I came up with. Passes basic testing, but would love feedback if anyone finds issues with it:
class User
enum category: [ :client, :seller, :provider ]
def category=(val)
super val
rescue
#__bad_cat_val = val
super nil
end
end
This will reset category to nil and we can then validate the field:
class User
...
validates :category, presence: true
end
The problem is that we really want to be validating inclusion, not just presence. To get around that we have to capture the input (e.g. as above in def category=). Then we can output our message using that value:
class User
...
validates :category, inclusion: {
in: categories.keys,
message: ->(record,error) {
val = record.instance_variable_get(:#__bad_cat_val)
return "Category is required" if val.nil?
"#{val.inspect} is not a valid category"
}
}
end
That will give us messages for presence and inclusion. If you need to fine tune it more you would have to use a custom validator (I believe).
Read: https://github.com/rails/rails/issues/13971
Specifically read Senny's comment:
The current focus of AR enums is to map a set of states (labels) to an integer for performance reasons. Currently assigning a wrong state is considered an application level error and not a user input error. That's why you get an ArgumentError.
That being said you can always set nil or an empty string to the enum attribute without raising an error:
<option value="">Choose an option</option>
and add a simple presence validation like:
validates :category, presence: { message: "is required" }
The enum in Rails always raise a error if you try to set an invalid value. There is no such validation and add an error message to base or adding a validation error to the record. You should create own validation by rescue errors.
=> u = User.last
=> User.genders
=> {"male"=>0, "female"=>1}
=> u.gender = 'boy'
#> ArgumentError: 'boy' is not a valid gender
=> u.gender = 'male'
#> "male"
In case someone still struggles with this. You can add a standard inclusion validation like this (in the User model):
validates :category, inclusion: {
in: categories.keys,
message: "%{value} is not a valid category"
}

How do you validate the presence of one field from many

I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""

Resources