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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With