Order/precedence of rails validation checks - ruby-on-rails

I'm trying to get my head around the order/precedence with which Rails processes validation checks. Let me give an example.
In my model class I have these validation checks and custom validation methods:
class SupportSession < ApplicationRecord
# Check both dates are actually provided when user submits a form
validates :start_time, presence: true
validates :end_time, presence: true
# Check datetime format with validates_timeliness gem: https://github.com/adzap/validates_timeliness
validates_datetime :start_time
validates_datetime :end_time
# Custom method to ensure duration is within limits
def duration_restrictions
# Next line returns nil if uncommented so clearly no start and end dates data which should have been picked up by the first validation checks
# raise duration_mins.inspect # Returns nil
# Use same gem as above to calculate duration of a SupportSession
duration_mins = TimeDifference.between(start_time, end_time).in_minutes
if duration_mins == 0
errors[:base] << 'A session must last longer than 1 minute'
end
if duration_mins > 180
errors[:base] << 'A session must be shorter than 180 minutes (3 hours)'
end
end
The problem is that Rails doesn't seem to be processing the 'validates presence' or 'validates_datetime' checks first to make sure that the data is there in the first place for me to work with. I just get this error on the line where I calculate duration_mins (because there is no start_time and end_time provided:
undefined method `to_time' for nil:NilClass
Is there a reason for this or have I just run into a bug? Surely the validation checks should make sure that values for start_time and end_time are present, or do I have to manually check the values in all of my custom methods? That's not very DRY.
Thanks for taking a look.

Rails will run all validations in the order specified even if any validation gets failed. Probably you need to validate datetime only if the values are present.
You can do this in two ways,
Check for the presence of the value before validating,
validates_datetime :start_time, if: -> { start_time.present? }
validates_datetime :end_time, if: -> { end_time.present? }
Allows a nil or empty string value to be valid,
validates_datetime :start_time, allow_blank: true
validates_datetime :end_time, allow_blank: true

Simplest way, add this line right after def duration_restrictions
return if ([ start_time, end_time ].find(&:blank?))
Rails always validate custom method first.

Related

How to set the order of validation methods ruby on rails?

I am a Ror beginner. There is a model Lecture, I want to validate the format of start time and end time firstly, and then check if the end time is after start time. It works well when the format is valid but once the format is wrong it comes with: undefined method `<' for nil:NilClass. How to makes start_must_be_before_end_time triggered only when the format is valid? Thanks!
Here is the code:
class Lecture < ActiveRecord::Base
belongs_to :day
belongs_to :speaker
validates :title, :position, presence: true
validates :start_time, format: { with: /([01][0-9]|2[0-3]):([0-5][0-9])/,
message: "Incorrect time format" }
validates :end_time, format: { with: /([01][0-9]|2[0-3]):([0-5][0-9])/,
message: "Incorrect time format" }
validate :start_must_be_before_end_time
private
def start_must_be_before_end_time
errors.add(:end_time, "is before Start time") unless start_time < end_time
end
end
There are no guarantees on the order of validations that are defined by validates methods. But, the order is guaranteed for custom validators that are defined with validate method.
From docs:
You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered.
Alternatively
You can only run your validation method if all other validations pass:
validate :start_must_be_before_end_time, :unless => Proc.new { |obj| obj.times_valid? }
# Then, define `times_valid?` method and check that start_time and end_time are valid
You can go another way:
errors.add(:end_time, "is before Start time") unless start_time.nil? || end_time.nil? || start_time < end_time

Rails distinct validations

How to distinct which validator failed ?
I have multiple validations on the same field:
class User < ActiveRecord::Base
validates :name, presence: true, length: {minimum: 1, maximum: 20 }
validates_uniqueness_of :name
end
When I save the user as user.save - I want to distinct what failed.
if user.__not_valid_name_length__?
# name length wrong
# do smth 1
end
if **user.__not_valid_name_unique__?
# name is not unique
# do smth 2
end
I can access user.errors[:name] and see all error messages for the field.
But I don't want to rely on message text which can change.
Is there any way to know which validator failed?
A feature to return machine-parseable symbols instead of strings was committed to Rails almost a year ago, but it's still not available in the 4-x-stable branch. You can use it if you use the edge version, and it will be available in Rails 5.
Example:
user = User.new
user.valid?
user.errors.details[:name] # returns: [{error: :blank}, {error: :too_short}]
More info:
https://github.com/rails/rails/pull/18322
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb
There are no built-in callbacks for rails validation fails, for rails validation available callbacks are:
before_validation
after_validation
To learn more about callback, please read this call_back
Checking validation fail base on the message is no good approach. It may change in different scenarios i.e internationalization. Do it by defining the method for validation i.e
# check presence_of validation
def is_name_present?
self.name.present?
end
# check uniqueness validation
def is_name_uniqe?
User.where(name: self.name).count == 0
end
Use one which suit you best, I suggest use after_validation
after_validation :post_validatiom
def post_validation
unless is_name_present?
# do_someting
end
unless is_name_uniqe?
# do_something
end
end

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

Rails: validation fails with before_create method on datetime attribute

ApiKey.create!
Throws a validation error: expires_at can't be blank.
class ApiKey < ActiveRecord::Base
before_create :set_expires_at
validates :expires_at, presence: true
private
def set_expires_at
self.expires_at = Time.now.utc + 10.days
end
end
with attribute
t.datetime :expires_at
However, if the validation is removed the before_create method works on create.
Why is this? - This pattern works for other attributes, e.g. access_tokens (string), etc.
I would say because the before_create runs after the validation, maybe you want to replace before_create with before_validation
Note: If you leave the call back like that, it would set the expiry date whenever you run valid? or save or any active record method that fires the validation, You might want to limit this validation to the creation process only
before_validation :set_expires_at, on: :create
This will limit the function call only when the creation is run first time.

Rails validation method comparing two fields?

My model has two fields that i want to compare to each other as part of the validation. I want to be sure end_time is after start_time. I have written a validation method to compare them, but I must be doing something wrong, because the values are always nil. Can someone help?
class LogEntry < ActiveRecord::Base
validates :start_time, :presence => { :message => "must be a valid date/time" }
validates :end_time, :presence => {:message => "must be a valid date/time"}
validate :start_must_be_before_end_time
def start_must_be_before_end_time
errors.add(:start_time, "must be before end time") unless
start_time > end_time
end
end
gets the error
undefined method `>' for nil:NilClass
So, start_time and/or end_time are nil. I thought I was following the many examples I found, but apparently not. What am I missing?
Thanks.
My best guess is you need your method to look like this:
private
def start_must_be_before_end_time
errors.add(:start_time, "must be before end time") unless
start_time < end_time
end
(Also, notice the < rather than > (or change to if and >=)
If this doesn't work then you should also check that start_time and end_time are being defined correctly in the controller as there can be funny things happen if the time is created across more than one form element.
With Rails 7 ComparisonValidator
Rails 7 has added the ComparisonValidator which allows you to use the convenience method validates_comparison_of like so:
class LogEntry < ActiveRecord::Base
validates_comparison_of :start_time, less_than: :end_time
# OR
validates_comparison_of :end_time, greater_than: :start_time
end
Find out more here: https://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html#method-i-validates_comparison_of
You need to check for presence yourself (and skip the validation step if not present).
def start_must_be_before_end_time
return unless start_time and end_time
errors.add(:start_time, "must be before end time") unless start_time < end_time
end
Prints either "must be a valid date/time" OR "start time must be before end time".
Alternative
def start_must_be_before_end_time
valid = start_time && end_time && start_time < end_time
errors.add(:start_time, "must be before end time") unless valid
end
Prints "start time must be a valid date/time" AND "start time must be before end time" if start_time or end_time isn't set.
Personal preference for the first, since it only shows what the user did wrong. The latter is like many websites which just load off 20 lines of error text onto the user just because the programmer thought it would be nice to see every validation result. Bad UX.
Clean and Clear (and under control?)
I find this to be the clearest to read:
In Your Model
# ...
validates_presence_of :start_time, :end_time
validate :end_time_is_after_start_time
# ...
#######
private
#######
def end_time_is_after_start_time
return if end_time.blank? || start_time.blank?
if end_time < start_time
errors.add(:end_time, "cannot be before the start time")
end
end
Ruby on Rails 7.0 supports validates_comparison_of like this
validates :start_time, comparison: { less_than: :end_date }
You can use validates_timeliness gem https://github.com/adzap/validates_timeliness
start_time.to_i < end_time.to_ishould fix it. You are trying to compare datetime but for some reason it can't, so convert them to int before comparing.

Resources