What is the best way to validate a date in Ruby on Rails? I need to make sure the "birthday" is a date, is less than 125 years ago and is not in the future (today is ok).
I have tried three methods:
1) date_validator gem
I used the following code (after installing the gem):
validates :birthday,
date: {
after: Proc.new {Time.now - 125.years}, message: :after,
before: Proc.new {Time.now}, message: :cannot_be_in_the_future
}
This worked except that I could set the date to the number 12 and pass validation.
2) Checking if the date is in a range of dates, in a custom validation method, like so:
from = 125.years.ago.to_date
to = Time.now.to_date
unless (from..to).include? birthday
errors.add(:birthday, :custom_error_msg)
end
This worked well and passed all my tests, but the drawback is that you only get one error message. I would have liked separate error messages for the case when the date is in the future, when it is too long ago and when the input is not a date.
3) Multiple checks in a custom validation method, like so:
begin
birthday.to_date
rescue
errors.add(:birthday, "must be a date")
else
if birthday > Time.now
errors.add(:birtday, "cannot be in the future")
elsif birthday < Time.now - 125.years
errors.add(:birthday, "cannot be over 125 years ago")
end
end
This also passes all my test, and I get different messages as explained above.
So I am wondering, is this the best method? Can it be improved at all (except that the error message text needs work)?
Thanks
For this simple validation, I think following ruby code is enough!
Please check :
validate :is_valid_dob?
private
def is_valid_dob?
if((birthday.is_a?(Date) rescue ArgumentError) == ArgumentError)
errors.add(:birthday, 'Sorry, Invalid Date of Birth Entered.')
end
end
Just use gem 'validates_timeliness'
In your case, using this gem
validates_date :birthday, on_or_after: lambda { 125.years.ago }
You can easily use validates_each method
Just put there 2 validations:
1) For birthday in the past
validates_each :birthday do |record, attr, value|
record.errors.add(attr, 'must be in the past') if value >= Time.now.to_date
end
2) For birthday not more than 150 years ago
validates_each :birthday do |record, attr, value|
record.errors.add(attr, 'must be less than 150 years in the past')
if value <= (Time.now.to_date - 125.years)
end
Related
Is there a way to limit the number of lines / text rows in a type text attribute in Rails?
I know that I can limit the number of characters like so:
validates :message, :length => { :maximum => 100 }
But how about the number of lines?
Thanks for any help.
You could create a custom validator to implement the required logic. Something along the following idea should do the trick:
class LinesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
lines = value.split("\n")
if lines.size > options[:maximum]
record.errors[attribute] << "too many lines"
end
if lines.any? { |l| l.size > options[:length]}
record.errors[attribute] << "line longer than allowed length"
end
end
end
And the usage could be as follows:
validates :message, lines: { maximum: 5, length: 10}
For more information regarding validations and custom validators, you can read the rails docs
Writing a custom validation to do this is pretty easy. You use validate (not validates) to do this:
validate :not_too_many_lines
private
def not_too_many_lines
if self.message.split("\n").length > 10
self.errors.add :message, "has too many lines"
end
end
Under the hood this runs before valid?, which checks to see whether errors has anything in it. If you then ran <record>.errors.full_messages after trying to save an invalid record you'd see "Message has too many lines". If you just want it to say "too many lines" you can use self.errors.add :base, "too many lines"
I can't comment yet.
So to add top the answer, split('\n') is not always safe.
Better just use String#lines
Here is the example with the lines method instead.
validate :not_too_many_lines
private
def not_too_many_lines
if self.message.lines.count > 10
errors.add :message, "has too many lines"
end
end
I need to make a method that renders a date to a table that is one year past the creation date. I've tried the line as listed in the title, but that didn't work. I have a table right now that lists "date joined" next to it I'd like it to say "date expired". which will be one year from the date joined.
Example:
class Subscriber < ActiveRecord::Base
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true
validates :phone_number, presence: true
def date_joined
created_at.strftime("%-m/%-d/%-y")
end
def expiration_date
created_at.1.year.from_now
end
end
How should I format that expiration_date method. The date_joined works fine.
You should add 1.year to the created_at time object:
def expiration_date
created_at + 1.year
end
Formatted:
def expiration_date
(created_at + 1.year).strftime("%-m/%-d/%-y")
end
rails console:
=> some_object.created_at
=> Wed, 12 Apr 2016 17:37:12 UTC +00:00
=> some_object.created_at + 1.year
=> Wed, 12 Apr 2017 17:37:12 UTC +00:00
=> (some_object.created_at + 1.year).strftime("%-m/%-d/%-y")
=> "4/12/17"
Remember that created_at isn't populated until the model is saved, your calculations won't work until then. This could be a problem.
The date_joined method you have shouldn't exist. Any formatting concerns should be the responsibility of your view, so push that logic in there like this:
<%= model.created_at.strftime("%-m/%-d/%-y") %>
You can even define a format for your dates using the locales as demonstrated in this question where you can add this to config/locales/en.yml:
en:
date:
formats:
default: "%-m/%-d/%-y"
time:
formats:
default: "%-m/%-d/%-y %H:%M"
Then you can use this in your view as the default without any special handling required:
<%= model.created_at %>
That will format all times the way you want instead of you having to go out of your way to define special formatter methods for each model and then remember to call them.
When it comes to computing dates in the future you can do math on dates:
def expiration_date
(self.created_at || DateTime.now) + 1.year
end
That will work even if the model hasn't been saved.
I feel like you're actually asking the wrong question here. Rather than doing this feature in Rails, you're asking a Ruby question which is "how do I work with interaction between Ruby datetime objects." I suggest you take at Ruby/Rails datetime objects and see how they work first.
That said, I'm pretty sure someone else is gonna post the answer you want to see.
I am trying to get a date from a user and send it inside an email as plain text in the following format: "07/30/2015".
In order to do that, if the output I am getting is a string, I could just do:
Date.parse("2015-07-30").strftime("%m/%d/%Y")
The problem is, I am getting a FixNum.
The issues are many:
If I try to convert to a string to parse it with Date.parse, it becomes "2001".
If I apply the code I just wrote, Date.parse... it will throw 'invalid date'.
For instance:
(2016-02-13).to_s #=> "2001"
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
Date.parse("2001").strftime("%m/%d/%Y") #=> invalid date
So if I can convert 2015-07-30 into "2015-07-30", it would work:
Date.parse("2015-07-30").strftime("%m/%d/%Y") #=> "07/30/2015"
Then I tried using date_select instead of date_field, but now the message arrives with those fields empty.
Any suggestions?
Here is my form:
= form_for #contact do |f|
= f.text_field :product_name
= f.date_field :purchase_date
= f.submit
Here is my code:
<%= message.subject %>
<% #resource.mail_form_attributes.each do |attribute, value|
if attribute == "mail_subject"
next
end
%>
<%= "#{#resource.class.human_attribute_name(attribute)}: #{Date.parse(value).class == Date ? Date.parse(value).strftime("%m/%d/%Y") : value}" %>
<% end %>
My controller:
class ContactsController < ApplicationController
before_action :send_email, except: [:create]
def create
#contact = Contact.new(params[:contact])
#contact.request = request
if #contact.deliver
#thank = "Thank you for your message!"
#message = "We have received your inquiry and we'll be in touch shortly."
else
#error = "Cannot send message. Please, try again."
end
end
def contact_page
end
def product_complaint
#the_subject = "Product Complaint Form"
end
private
def send_email
#contact = Contact.new
end
end
My model:
class Contact < MailForm::Base
# all forms
attribute :mail_subject
attribute :first_name, validate: true
attribute :last_name, validate: true
# product-complaint
attribute :best_by, validate: true, allow_blank: true # date
attribute :bag_code, validate: true, allow_blank: true
attribute :purchase_date, validate: true, allow_blank: true # date
attribute :bag_opened, validate: true, allow_blank: true # date
attribute :problem_noticed, validate: true, allow_blank: true # date
# all forms
attribute :message, validate: true
attribute :nickname, captcha: true
def headers
{
content_type: "text/plain",
subject: %(#{mail_subject}),
to: "xxxxx#xxxxxxx.com",
# from: %("#{first_name.capitalize} #{last_name.capitalize}" <#{email.downcase}>)
from: "xxx#xxxxx.com"
}
end
end
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
youre getting this error because you dont have quotes around the value. i.e. its not a string, its a number that is having subtraction applied to it. this is being interpreted as
2016 - 2
2014 - 13
2001.to_date
it needs to be ('2016-02-13').to_date
if youre unable to get it as a string, can you post how you're getting it from the user to begin with? (a date field ought to be sending you a string to your controller, not a series of numbers)
You're not understanding something about receiving values from forms: You can NOT receive an integer, a fixnum or anything else other than strings. So, you can't have received 2016-02-13. Instead you got "2016-02-13" or "2016", "02" or "2" and "13" depending on the form. If you're running under Rails, then it got the strings, and through its meta-data understands you want an integer (which really should probably be defined as a string), and it converts it to an integer for you.
Either way, when you write:
(2016-02-13).to_s
(2016-02-13).to_date
you're propagating that misunderstanding into your testing. This is how it MUST be written because you need to be working with strings:
require 'active_support/core_ext/string/conversions'
("2016-02-13").to_s # => "2016-02-13"
("2016-02-13").to_date # => #<Date: 2016-02-13 ((2457432j,0s,0n),+0s,2299161j)>
You can create dates without them being strings though: Ruby's Date initializer allows us to pass the year, month and day value and receive a new Date object:
year, month, day = 2001, 1, 2
date = Date.new(year, month, day) # => #<Date: 2001-01-02 ((2451912j,0s,0n),+0s,2299161j)>
date.year # => 2001
date.month # => 1
date.day # => 2
Moving on...
Parsing dates in Ruby quickly demonstrates it's not a U.S.-centric language. Americans suppose all dates of 01/01/2001 are in "MM/DD/YYYY" but that's a poor assumption because much of the rest of the world uses "DD/MM/YYYY". Not knowing that means that code written under that assumption is doing the wrong thing. Consider this:
require 'date'
date = Date.parse('01/02/2001')
date.month # => 2
date.day # => 1
Obviously something "wrong" is happening, at least for 'mericans. This is very apparent with:
date = Date.parse('01/31/2001')
# ~> -:3:in `parse': invalid date (ArgumentError)
This occurs because there is no month "31". In the previous example of '01/02/2001', that misunderstanding means the programmer thinks it should be "January 2" but the code thinks it's "February 1", and work with that. That can cause major havoc in an enterprise system, or anything dealing with financial calculations, product scheduling, shipping or anything else that works with dates.
Because the code is assuming DD/MM/YYYY format for that sort of string, the sensible things to do are:
KNOW what format your users are going to send dates in. Don't assume, ever. ASK them and make your code capable of dealing with alternates, or tell them what they MUST use and vet out their data prior to actually committing it to your system. Or, provide a GUI that forces them to pick their dates from popups and never allows them to enter it by hand.
Force the date parser to use explicit formats of dates so it can always do the right thing:
Date.strptime('01/31/2001', '%m/%d/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
Date.strptime('31/01/2001', '%d/%m/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
The last point is the crux of writing code: We're telling the language what to do, not subjecting ourselves, and our employers, to code that's guessing. Give code half a chance and it'll do the wrong thing, so you control it. That's why programming is hard.
I'm a bit new to Rails and Rspec and as such I'm not sure how to test that date time validations are correct in my model.
I've made a model Event that has start and end times and there's a few imporant conditions on these such as a start time cannot be in the past and the end time must be after the start time.
To ensure these validations I'm using ValidatesTimeliness https://github.com/adzap/validates_timeliness
My model is as follows:
class Event < ActiveRecord::Base
...
validates_datetime :start_date,
:after => :now,
:after_message => "Event cannot start in the past"
validates_datetime :end_date,
:after => :start_date,
:after_message => "End time cannot be before start time"
end
In my RSpec test I have:
describe Event do
let(:event) { FactoryGirl.build :event }
subject { event }
context "when start_date is before the current time" do
it {should_not allow_value(1.day.ago).
for(:start_date)}
end
context "when end_date is before or on start date" do
it {should_not allow_value(event.start_date - 1.day).
for(:end_date)}
it {should_not allow_value(event.start_date).
for(:end_date)}
end
context "when the end_date is after the start_date" do
it {should allow_value(event.start_date + 1.day).
for(:end_date)}
end
end
However this doesn't really test that my start date had to be before the exact date time.
For example if I'd accidentally used :today instead of :now in my model, these tests would also pass.
I read online that there used to be an RSpec matcher called validate_date (http://www.railslodge.com/plugins/1160-validates-timeliness) which would be exactly what I'm looking for but as far as I can tell it's been removed.
My question is how can I improve my tests, do I need to add tests that try the smallest amount of time (i.e. a ms) to ensure the pass/fail accordingly or is there a better way to do it?
Thanks in advance!
You could work with valid? and errors.messages:
Build anEvent that passes validation except for your start_date and end_date
Set the start_date and end_date in the right order, and assert that event.valid? is true
Set the start_date and end_date in the wrong order, and assert that it is not valid? and that event.errors.messages includes the right validation errors. (Note, you have to call event.valid? before checking event.errors.messages, otherwise they will be empty)
Example for valid? and errors.messages:
user = User.new
user.errors.messages #=> {} # no messages, since validations never ran
user.valid? # => false
user.errors.messages #=> {:email=>["can't be blank"]}
user.email = "foo#bar.com"
user.valid? #=> true
user.errors.messages #=> {}
Try this
validates_date :end_time, :after => [:start_time, Proc.new {1.day.from_now_to_date}]
validates_date :start_time, :after => Time.now
In my form I would like users to type a date in DD/MM/YYYY format. I have validation that checks that.
The problem is that in the database it is stored in YYYY-MM-DD format, so if try just to update the is_money_paid field:
job = Job.find(params[:id])
job.update_attributes(:is_money_paid => ...)
the validation fails saying that job's date is in wrong format (YYYY-MM-DD rather than DD/MM/YYYY).
What would be an appropriate way to solve this ?
Here is the relevant code:
class Job < ActiveRecord::Base
validate :job_date_validator
end
DATE_REGEX = /\A\d{2}\/\d{2}\/\d{4}\z/
def job_date_validator
if job_date_before_type_cast.blank?
errors.add(:job_date, "^Date must be selected")
elsif job_date_before_type_cast !~ DATE_REGEX
errors.add(:job_date, "^Date must be in DD/MM/YYYY format")
end
end
is_money_paid is a boolean field.
I would change the validator to say:
validate_presence_of :job_date, :message => "must be selected"
validate :job_date_validator, :if => :job_date_changed?
Or something along those lines.
You can take out the .blank? check too.
Format it in before_validation
http://guides.rubyonrails.org/active_record_validations_callbacks.html#updating-an-object
I would suggest you to see gem validates_timeliness which is rails 3 compatible.