Rails validation method comparing two fields? - ruby-on-rails

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.

Related

Order/precedence of rails validation checks

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.

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

Trying to validate a date

I have a form where i'm trying to validate that a field called "birthday" is not blank, and that the date is a valid date format that the Chronic gem can parse. I always get the error message "Birthday is invalid". I've been trying a simple format "10/10/2010".
How can i validate that the birthday field is of a format that chronic can parse?
User.rb model
class User < ActiveRecord::Base
validates :birthday, :presence => true
validate :birthday_is_date
validate :position, :presence => true
# validate the birthday format
def birthday_is_date
errors.add(:birthday ,Chronic.parse(birthday)) # testing to see the value of :birthday
errors.add(:birthday, "is invalid test message") if ((Chronic.parse(:birthday) rescue ArgumentError) == ArgumentError)
end
end
contacts_controller.rb
# POST /contacts/1/edit
# actually updates the users data
def update_user
#userProfile = User.find(params[:id])
respond_to do |format|
if #userProfile.update_attributes(params[:user])
format.html {
flash[:success] = "Information updated successfully"
redirect_to(profile_path)
}
else
format.html {
flash[:error] = resource.errors.full_messages
render :edit
}
end
end
end
Your birthday_is_date validation always adds an error on the first line. It should be written as follows:
def birthday_is_date
errors.add(:birthday, "is invalid") unless Chronic.parse(birthday)
end
try this
install validates_date_time gem
you can pass validation for date, for example
class Person < ActiveRecord::Base
validates_date :date_of_birth
validates_time :time_of_birth
validates_date_time :date_and_time_of_birth
end
Use :allow_nil to allow the value to be blank.
class Person < ActiveRecord::Base
validates_date :date_of_birth, :allow_nil => true
end
Source : https://github.com/smtlaissezfaire/validates_date_time
From chronic.rubyforge.org:
Chronic uses Ruby’s built in Time class for all time storage and
computation. Because of this, only times that the Time class can
handle will be properly parsed. Parsing for times outside of this
range will simply return nil. Support for a wider range of times is
planned for a future release.
Time zones other than the local one are not currently supported.
Support for other time zones is planned for a future release.
From Date validation in Ruby using the Date object:
A simple validate function
One way to test for a valid date is to try to create a Date object. If
the object is created, the date is valid, and if not, the date is
invalid. Here is a function that accepts year, month, and day, then
returns true if the date is valid and false if the date is invalid.
def test_date(yyyy, mm, dd)
begin
#mydate = Date.new(yyyy, mm, dd)
return true
rescue ArgumentError
return false
end
end
From the accepted answer of How do I validate a date in rails?:
If you're looking for a plugin solution, I'd checkout the
validates_timeliness plugin. It works like this (from the github
page):
class Person < ActiveRecord::Base
validates_date :date_of_birth, :on_or_before => lambda { Date.current }
# or
validates :date_of_birth, :timeliness => {:on_or_before => lambda { Date.current }, :type => :date}
end
The list of validation methods available are as follows:
validates_date - validate value as date
validates_time - validate value as time only i.e. '12:20pm'
validates_datetime - validate value as a full date and time
validates - use the :timeliness key and set the type in the hash.

How to validate the date such that it is after today in Rails?

I have an Active Record model that contains attributes: expiry_date. How do I go about validating it such that it is after today(present date at that time)? I am totally new to Rails and ruby and I couldn't find a similar question answering exactly this?
I am using Rails 3.1.3 and ruby 1.8.7
Your question is (almost) exactly answered in the Rails guides.
Here's the example code they give. This class validates that the date is in the past, while your question is how to validate that the date is in the future, but adapting it should be pretty easy:
class Invoice < ActiveRecord::Base
validate :expiration_date_cannot_be_in_the_past
def expiration_date_cannot_be_in_the_past
if expiration_date.present? && expiration_date < Date.today
errors.add(:expiration_date, "can't be in the past")
end
end
end
Here's the code to set up a custom validator:
#app/validators/not_in_past_validator.rb
class NotInPastValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.blank?
record.errors.add attribute, (options[:message] || "can't be blank")
elsif value <= Time.zone.today
record.errors.add attribute,
(options[:message] || "can't be in the past")
end
end
end
And in your model:
validates :signed_date, not_in_past: true
The simplest and working solution is to use the in-built validation from Rails. Just validates it like that:
validates :expiry_date, inclusion: { in: (Date.today..Date.today+5.years) }
I took #dankohn answer, and updated to be I18n ready. I also removed the blank test, because that's not the responsibility of this validator, and can easily be enabled by adding presence: true to the validates call.
The updated class, now named in_future, which I think is nicer than not_in_past
class InFutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, (options[:message] || :in_future)) unless in_future?(value)
end
def in_future?(date)
date.present? && date > Time.zone.today
end
end
Now add the in_future key to your localization file.
For all fields under errors.messages.in_future, e.g. for Dutch:
nl:
errors:
messages:
in_future: 'moet in de toekomst zijn'
Or per field under activerecord.errors.models.MODEL.attributes.FIELD.in_future, e.g. for the end_date in a Vacancy model in Dutch:
nl:
activerecord:
errors:
models:
vacancy:
attributes:
end_date:
in_future: 'moet in de toekomst zijn'
In rails 4+ there are future? and past? methods for DateTime objects, so a simpler answer is
class Invoice < ActiveRecord::Base
validate :expiration_date_cannot_be_in_the_past
def expiration_date_cannot_be_in_the_past
if expiration_date.present? && expiration_date.past?
errors.add(:expiration_date, "can't be in the past")
end
end
end

Rails: how to require at least one field not to be blank

I know I can require a field by adding validates_presence_of :field to the model. However, how do I require at least one field to be mandatory, while not requiring any particular field?
thanks in advance
-- Deb
You can use:
validate :any_present?
def any_present?
if %w(field1 field2 field3).all?{|attr| self[attr].blank?}
errors.add :base, "Error message"
end
end
EDIT: updated from original answer for Rails 3+ as per comment.
But you have to provide field names manually.
You could get all content columns of a model with Model.content_columns.map(&:name), but it will include created_at and updated_at columns too, and that is probably not what you want.
Here's a reusable version:
class AnyPresenceValidator < ActiveModel::Validator
def validate(record)
unless options[:fields].any?{|attr| record[attr].present?}
record.errors.add(:base, :blank)
end
end
end
You can use it in your model with:
validates_with AnyPresenceValidator, fields: %w(field1 field2 field3)
Add a validate method to your model:
def validate
if field1.blank? and field2.blank? and field3.blank? # ...
errors.add_to_base("You must fill in at least one field")
end
end
I believe something like the following may work
class MyModel < ActiveRecord::Base
validate do |my_model|
my_model.my_validation
end
def my_validation
errors.add_to_base("Your error message") if self.blank?
#or self.attributes.blank? - not sure
end
end
Going further with #Votya's correct answer, here is a way to retrieve all columns besides created_at and updated_at (and optionally, any others you want to throw out):
# Get all column names as an array and reject the ones we don't want
Model.content_columns.map(&:name).reject {|i| i =~ /(created|updated)_at/}
For example:
1.9.3p327 :012 > Client.content_columns.map(&:name).reject {|i| i =~ /(created|updated)_at/}
=> ["primary_email", "name"]
If you only have two fields, this will get the job done:
validates :first_name, presence: true, if: :nick_name.blank?
validates :nick_name, presence: true, if: :first_name.blank?
This does not scale up well with more fields, but when you only have two, this is perhaps clearer than a custom validation method.
n.b. If they omit both, the error message will appear more restrictive than you intend. (e.g. First Name is required. Nick Name is required.) ¯\(ツ)/¯

Resources