Avoid duplication in Rails validators - ruby-on-rails

I have two validations:
validates :email, format: { with: /\A(.+)#(aol|gmail|office365|outlook|verizon|yahoo)\.com\Z/i }, if: Proc.new { |user| user.imap_server.blank? }
validates :email, presence: true
validates :imap_server, presence: true, if: Proc.new { |user| user.email.present? && user.email_invalid? }
def email_invalid?
self.email =~ /\A(.+)#(aol|gmail|office365|outlook|verizon|yahoo)\.com\Z/i
end
I show a user a form. It displays an email field but not imap_server field. If the value in email field does not match the specific regex, then I want to show them form again with the imap_server field present as well. If they enter a value for the imap_server field, then I no longer want to validate the regex of email field (although it must still be present).
The problem it feels like I am duplicating a validation. Both email_invalid? and the validates :email, format: ... do the same thing. How can I clean this up?

You could replace the validates :email, format: ... with
validate :email_format
def email_format
errors.add(:email, 'format invalid') if imap_server.blank? && email_invalid?
end
which is slightly more lines but lets you define the format validation in one place.

I suspect that the issue is you're trying to check the result of a validation (email_invalid?) while you're * still doing * the validations... you don't know what order the validations are going to be run (the order on the page is not something I'd trust)... so the best way to solve it is to just write all these things into a single validates method eg a quick-and-dirty way:
validates :email_or_imap_server
def email_or_imap_server
email_valid = false # for scoping
if email.present?
# note: email validation via regex is harder than you think...
# google it...
email_valid = email.match(/#{VALID_EMAIL_FORMATS}/)
if email_invalid
errors.add(:email, "email invalid format should be...")
errors.add(:imap_server, "email or imap-server must be present") unless imap_server.present?
end
else
errors.add(:imap_server, "either email or imap-server must be present") unless imap_server.present?
end
end
etc.
Note: the code above is almost certainly full of bugs and typos... don't copy/paste it almost certainly won't work and the logic doesn't exactly match that of your validations... but do something like this.

Related

Rails form validates with custom method only if any value is provided

I have a conditional form field (which is shown by clicking a checkbox) which I want to validate with a custom method.
validates :number, length: { maximum: 20 }, if: :checksum_is_valid?
def checksum_is_valid?
if !Luhn.valid?(number)
errors.add(number, "is not valid.")
end
end
is my attempt. This technically works fine but the error is also shown, even if I don't enter any number at all (because the field is not mandatory). Any idea how I can check with a custom method, but only if the user provides any number at all.
Thanks
You could use validate instead of validates for custom validators and then move the check if there is a number present into the validator method:
validate :checksum_valid?
private
def checksum_valid?
if number.present?
errors.add(:number, "is not valid.") unless number_valid?
end
end
def number_valid?
number.length < 20 && Luhn.valid?(number)
end
You're mixing two things in your code, so let me help you understanding better what is happening.
1). conditional validation:
validates :number, length: { maximum: 20 }, if: :checksum_is_valid?
By this, Rails expects the model to implement method checksum_is_valid? which returns boolean value, based on which it will perform this validation. Something like would work:
def checksum_is_valid?
Luhn.valid?(number)
end
In case this method returns true, Rails would perform the validation of :number is not more than 20.
2). custom validation:
When using this custom method, you can build your own validation logic and error. So, if you have your method:
def checksum_is_valid?
if !Luhn.valid?(number)
errors.add(number, "is not valid.")
end
end
You should configure your validator with:
validate :checksum_valid?
Unfortunately, from your code is not clear what you would like to achieve (validate the number not to be more than 20, or perform the custom validator), but I hope what I've pointed will help you make the proper decision how to take that further.
Hope that helps!

Rails validation not failing

I have the following validation on a model field:
validates :invoice_date, :presence => true, :unless => Proc.new { |invoice| invoice.invoice_date.future? }
It looks pretty simple, but it doesn't work. There is no error thrown if the date is in the future. And the Proc indeed returns false in that case.
Any idea why isn't there any validation error shown?
The 'unless' condition is for deciding if the validation should run or not, not if it should succeed or fail. So your validation is essentially saying "validate the presence of invoice_date, unless invoice_date is in the future in which case don't validate its presence" (which makes no sense)
It sounds like you want two validations, presence and date fencing.
validate :invoice_date_in_past
def invoice_date_in_past
if invoice_date.future?
errors.add(:invoice_date, 'must be a date in the past')
end
end
validates :invoice_date, :presence => true
validate :is_future_invoice_date?
private
def is_future_invoice_date?
if invoice_date.future?
errors.add(:invoice_date, 'Sorry, your invoice date is in future time.')
end
end
Presence true simply ensures, invoice_date must be present.
for validating whether the date is a future date or not we have specified a custom validation method.(is_future_invoice_date?)
This method, will add error message against our invoice_date attribute if the date is of future date.
More info here: http://guides.rubyonrails.org/active_record_validations.html#custom-methods
Try like that :--
validate check_invoice_date_is_future
def check_invoice_date_is_future
if invoice_date.present?
errors.add(:invoice_date, "Should not be in future.") if invoice_date.future?
else
errors.add(:invoice_date, "can't be blank.")
end
end

Validate presence of one of multiple attributes in rails

In an multilingual application, a user can input their Chinese and English names. The user can input either or both, but must input at least one name.
class Person < ActiveRecord::Base
validates :zh_name, :presence => true
validates :en_name, :presence => true
validates :fr_name, :presence => true
end
Since the built-in :validates_presence_of method can only validate both attributes at once, is there a way to validate the presence of at least one of many attributes in rails?
Like a magical, validates_one_of :zh_name, :en_name, :fr_name
Thank you in advance,
validate :at_least_one_name
def at_least_one_name
if [self.zh_name, self.en_name, self.fr_name].reject(&:blank?).size == 0
errors[:base] << ("Please choose at least one name - any language will do.")
end
end
Taking #micapam's answer a step futher, may I suggest:
validate :has_a_name
def has_a_name
unless [zh_name?, en_name?, fr_name?].include?(true)
errors.add :base, 'You need at least one name in some language!'
end
end
just a quick shot out, you can pass a "if" or "unless" to the validator, maybe you can get it working this way. i have something like this in mind
validates :zh_name, :presence => { :if => (fr_name.blank? && en_name.blank?) }
validate :has_a_name
def has_a_name
unless [zh_name, en_name, fr_name].any?{|val| val.present? }
errors.add :base, 'You need at least one name in some language!'
end
end
Max Williams' answer is fine, but I didn't see the need to count hits when any? returns a boolean.

validate and update single attribute rails

I have the following in my user model
attr_accessible :avatar, :email
validates_presence_of :email
has_attached_file :avatar # paperclip
validates_attachment_size :avatar,
:less_than => 1.megabyte,
:message => 'Image cannot be larger than 1MB in size',
:if => Proc.new { |imports| !imports.avatar_file_name.blank? }
in one of my controllers, I ONLY want to update and validate the avatar field without updating and validating email.
How can I do this?
for example (this won't work)
if #user.update_attributes(params[:user])
# do something...
end
I also tried with update_attribute('avatar', params[:user][:avatar]), but that would skip the validations for avatar field as well.
You could validate the attribute by hand and use update_attribute, that skips validation. If you add this to your User:
def self.valid_attribute?(attr, value)
mock = self.new(attr => value)
if mock.valid?
true
else
!mock.errors.has_key?(attr)
end
end
And then update the attribute thusly:
if(!User.valid_attribute?('avatar', params[:user][:avatar])
# Complain or whatever.
end
#user.update_attribute('avatar', params[:user][:avatar])
You should get your single attribute updated while only (manually) validating that attribute.
If you look at how Milan Novota's valid_attribute? works, you'll see that it performs the validations and then checks to see if the specific attr had issues; it doesn't matter if any of the other validations failed as valid_attribute? only looks at the validation failures for the attribute that you're interested in.
If you're going to be doing a lot of this stuff then you could add a method to User:
def update_just_this_one(attr, value)
raise "Bad #{attr}" if(!User.valid_attribute?(attr, value))
self.update_attribute(attr, value)
end
and use that to update your single attribute.
A condition?
validates_presence_of :email, :if => :email_changed?
Have you tried putting a condition on the validates_presence_of :email ?
http://ar.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#M000083
Configuration options:
if - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The method, proc or string should return or evaluate to a true or false value.
unless - Specifies a method, proc or string to call to determine if the validation should not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The method, proc or string should return or evaluate to a true or false value.
I am assuming you need this, because you have a multi-step wizard, where you first upload the avatar and the e-mail is filled in later.
To my knowledge, with your validations as they are, I see no good working solution. Either you validate all, or you update the avatar without validations. If it would be a simple attribute, you could check if the new value passes the validation seperately, and then update the model without validations (e.g. using update_attribute).
I can suggest two possible alternative approaches:
either you make sure that the e-mail is always entered first, which I believe is not a bad solution. And then, with each save, the validation is met.
otherwise, change the validation. Why would you declare a validation on a model, if there are records in the database that do not meet the validation? That is very counter-intuitive.
So I would propose something like this:
validate :presence_of_email_after_upload_avatar
def presence_of_email_after_upload_avatar
# write some test, when the email should be present
if avatar.present?
errors.add(:email, "Email is required") unless email.present?
end
end
Hope this helps.
Here is my solution.
It keeps the same behaviour than .valid? method, witch returns true or false, and add errors on the model on witch it was called.
class MyModel < ActiveRecord::Base
def valid_attributes?(attributes)
mock = self.class.new(self.attributes)
mock.valid?
mock.errors.to_hash.select { |attribute| attributes.include? attribute }.each do |error_key, error_messages|
error_messages.each do |error_message|
self.errors.add(error_key, error_message)
end
end
self.errors.to_hash.empty?
end
end
> my_model.valid_attributes? [:first_name, :email] # => returns true if first_name and email is valid, returns false if at least one is not valid
> my_modal.errors.messages # => now contain errors of the previous validation
{'first_name' => ["can't be blank"]}

Controlling the order of rails validations

I have a rails model which has 7 numeric attributes filled in by the user via a form.
I need to validate the presence of each of these attributes which is obviously easy using
validates :attribute1, :presence => true
validates :attribute2, :presence => true
# and so on through the attributes
However I also need to run a custom validator which takes a number of the attributes and does some calculations with them. If the result of these calculations is not within a certain range then the model should be declared invalid.
On it's own, this too is easy
validate :calculations_ok?
def calculations_ok?
errors[:base] << "Not within required range" unless within_required_range?
end
def within_required_range?
# check the calculations and return true or false here
end
However the problem is that the method "validate" always gets run before the method "validates". This means that if the user leaves one of the required fields blank, rails throws an error when it tries to do a calculation with a blank attribute.
So how can I check the presence of all the required attributes first?
I'm not sure it's guaranteed what order these validations get run in, as it might depend on how the attributes hash itself ends up ordered. You may be better off making your validate method more resilient and simply not run if some of the required data is missing. For example:
def within_required_range?
return if ([ a, b, c, d ].any?(&:blank?))
# ...
end
This will bail out if any of the variables a through d are blank, which includes nil, empty arrays or strings, and so forth.
An alternative for slightly more complex situations would be to create a helper method which runs the validations for the dependent attributes first. Then you can make your :calculations_ok? validation run conditionally.
validates :attribute1, :presence => true
validates :attribute2, :presence => true
...
validates :attribute7, :presence => true
validate :calculations_ok?, :unless => Proc.new { |a| a.dependent_attributes_valid? }
def dependent_attributes_valid?
[:attribute1, ..., :attribute7].each do |field|
self.class.validators_on(field).each { |v| v.validate(self) }
return false if self.errors.messages[field].present?
end
return true
end
I had to create something like this for a project because the validations on the dependent attributes were quite complex. My equivalent of :calculations_ok? would throw an exception if the dependent attributes didn't validate properly.
Advantages:
relatively DRY, especially if your validations are complex
ensures that your errors array reports the right failed validation instead of the macro-validation
automatically includes any additional validations on the dependent attributes you add later
Caveats:
potentially runs all validations twice
you may not want all validations to run on the dependent attributes
Check out http://railscasts.com/episodes/211-validations-in-rails-3
After implementing a custom validator, you'll simply do
validates :attribute1, :calculations_ok => true
That should solve your problem.
The James H solution makes the most sense to me. One extra thing to consider however, is that if you have conditions on the dependent validations, they need to be checked also in order for the dependent_attributes_valid? call to work.
ie.
validates :attribute1, presence: true
validates :attribute1, uniqueness: true, if: :attribute1?
validates :attribute1, numericality: true, unless: Proc.new {|r| r.attribute1.index("#") }
validates :attribute2, presence: true
...
validates :attribute7, presence: true
validate :calculations_ok?, unless: Proc.new { |a| a.dependent_attributes_valid? }
def dependent_attributes_valid?
[:attribute1, ..., :attribute7].each do |field|
self.class.validators_on(field).each do |v|
# Surely there is a better way with rails?
existing_error = v.attributes.select{|a| self.errors[a].present? }.present?
if_condition = v.options[:if]
validation_if_condition_passes = if_condition.blank?
validation_if_condition_passes ||= if_condition.class == Proc ? if_condition.call(self) : !!self.send(if_condition)
unless_condition = v.options[:unless]
validation_unless_condition_passes = unless_condition.blank?
validation_unless_condition_passes ||= unless_condition.class == Proc ? unless_condition.call(self) : !!self.send(unless_condition)
if !existing_error and validation_if_condition_passes and validation_unless_condition_passes
v.validate(self)
end
end
return false if self.errors.messages[field].present?
end
return true
end
I recall running into this issue quite some time ago, still unclear if validations order can be set and execution chain halted if a validation returns error.
I don't think Rails offers this option. It makes sense; we want to show all of the errors on the record (including those that come after a failing, due to invalid input, validation).
One possible approach is to validate only if the input to validate is present:
def within_required_range?
return unless [attribute1, attribute2, ..].all?(&:present?)
# check the calculations and return true or false here
end
Make it pretty & better structured (single responsibility) with Rails idiomatic validation options:
validates :attribute1, :presence => true
validates :attribute2, :presence => true
# and so on through the attributes
validate :calculations_ok?, if: :attributes_present?
private
def attributes_present?
[attribute1, attribute2, ..].all?(&:present?)
end
def calculations_ok?
errors[:base] << "Not within required range" unless within_required_range?
end
def within_required_range?
# check the calculations and return true or false here
end

Resources