Reputation: 3660
My app has the user input two times, and I calculate the amount of time between them as a convenience for the user. So the user enters 1:00 AM as the start time and 2:00 AM as the stop time, it will output "1 hour". I have a helper method that does this, using a gem for the heavy lifting. The times are stored as a Time data type in the database in order for that to work.
That all works fine, and now I want to validate that the time the user enters is valid - but also NOT require it. So if the user enters nothing for those times, the record still saves - but if the user enters, say "asdfasdf", I want the validations to get triggered and say that it is an invalid time, rather than just silently not storing the time.
I created my own custom time validation like so:
class TimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
Time.parse(value.to_s)
rescue ArgumentError
record.errors[attribute] << (options[:message] || "is not a valid time")
end
end
Then in my model, I'm doing:
validates :start_time, time: true
validates :stop_time, time: true
This does what I want as far as making sure the time can be correctly parsed, at it works pretty well. The user could enter 14:00, 3:00 PM, etc...and Rails can parse it. If they enter "asdfasdf", or any value that cannot be parsed as a valid time, the user is notified via a form validation error and the record is not saved.
But as that code stands, it will also trigger the validation error if the value is left blank.
So again, I want it to pass validations with a nil value, but fail if there is some value that cannot be parsed.
I tried:
validates :start_time, time: true, allow_blank: true
but then "asdfasdf" will pass. I also tried
Time.parse(value.to_s) unless value.nil?
but again, that breaks the whole thing and allows invalid dates to pass.
Any ideas?
Upvotes: 4
Views: 6583
Reputation: 31
I have to admit first that I am not a Rails/Ruby expert.
I had the very same Problem today and I think i figured it out. The Problem is that Rails performs a type cast. If you enter an invalid parameter (e.g. starttime="asdf"), a dummy Time object will be created. When you validate it, it seems as everything went okay although it clearly didn't. You can access the "real" parameter value with :starttime_before_type_cast
.
I adjusted Yule's code accordingly:
class TimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return true unless value
begin
Time.parse(value.to_s)
rescue ArgumentError
y = attribute.to_s
y.slice! "_before_type_cast"
record.errors[y] << (options[:message] || "invalid time format entered")
end
end
end
The validation in the model should look something like this:
validates :starttime, presence: true
validates :starttime_before_type_cast, time: true
The only snag with this approach is that the field in the form will contain the time 00:00 and not the malformed value after validation. I.e. The user won't see what he entered and what lead to the error message.
An article about Rails Type Casting
Upvotes: 3
Reputation: 2742
If you want to use a Gem, you can use this one.
This validates times and dates. See here some examples:
validates_datetime :occurred_at
validates_date :date_of_birth, :before => lambda { 18.years.ago },
:before_message => "must be at least 18 years old"
validates_datetime :finish_time, :after => :start_time # Method symbol
validates_date :booked_at, :on => :create, :on_or_after => :today # See Restriction Shorthand.
validates_time :booked_at, :between => ['9:00am', '5:00pm'] # On or after 9:00AM and on or before 5:00PM
validates_time :booked_at, :between => '9:00am'..'5:00pm' # The same as previous example
validates_time :booked_at, :between => '9:00am'...'5:00pm' # On or after 9:00AM and strictly before 5:00PM
validates_time :breakfast_time, :on_or_after => '6:00am',
:on_or_after_message => 'must be after opening time',
:before => :lunchtime,
:before_message => 'must be before lunch time'
Upvotes: 2
Reputation: 9764
You're almost there, you just need to explitly say that nil is valid
class TimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return true unless value #nil value is valid
Time.parse(value.to_s)
rescue ArgumentError
record.errors[attribute] << (options[:message] || "is not a valid time")
end
end
Upvotes: 1