Rails Date Validation – Step by Step

Update 5/31/2008: Use ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS

I have invested some time to get Rails date validation to work (whoo hoo!).

Without further ado, here are the step by step instructions:
1. Download Rails Date Kit from my site. Note that I got the original kit from http://www.methods.co.nz/rails_date_kit/rails_date_kit.html.
Extract the files in rails_date_kit_1.2.0.tar.gz to <your application>/vendor/plugins/rails_date_kit.
Rails Date Kit

2. Get the Validates Date Time plugin by running the following command in your application directory:

script/plugin install http://svn.viney.net.nz/things/rails/plugins/validates_date_time

You should have a new directory in your <your application>/vendor/plugins/validates_date_time
Validates Date Time-1

3. Put the necessary files in the right places.

Copy vendor/plugins/rails_date_kit/calendar.js to public/javascripts
Copy vendor/plugins/rails_date_kit/calendar.css to public/stylesheets
Copy vendor/plugins/rails_date_kit/calendar_prev.png to public/images
Copy vendor/plugins/rails_date_kit/calendar_next.png to public/images
Copy vendor/plugins/rails_date_kit/date_helper.rb to app/helpers


4. Include the css and javascript files for the calendar in your page header (e.g. view/layout/tasks.html.erb):

<%= stylesheet_link_tag ‘calendar.css’ %>
<%= javascript_include_tag ‘calendar’ %>

My application uses application.rhtml, thus I just added the lines there.

5. Create a new file called date_formats.rb in config/initializers. All you need to add is:

ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(:default_date => ‘%m/%d/%Y’)

Note the capital Y in ‘%m/%d/%Y’. Obviously, you can add more date formats as needed.
Update: Adjust the date format accordingly to your locale. For example, use ‘%d/%m/%Y’ for displaying date/month/year.

6. Use the following code to add the date field in your input form (e.g. view/tasks/new.html.erb):

<%= date_field :task, :due_date, :format => ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS[:default_date], :value => @task.due_date %>

You can also wrap the whole ‘ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS[:default_date]‘ into a function in app/helpers/application_helper.rb.

6. Tell your model to use en-us date format by adding the following line:

ValidatesDateTime.us_date_format = true #use mm/dd/yyyy format

Update: Set ValidatesDateTime.us_date_format to ‘false’ if your date format is not month/date/year.

7. Validate away! :)
Here’s an example of my model validation:

Class Task < ActiveRecord::Base
.
.
validates_date :due_date, :allow_nil => true
.
.
End


Enjoy! :)

Post any questions in the comment.

17 Responses to “Rails Date Validation – Step by Step”

  1. John McAuley Says:

    Hi Handy,

    Thanks for the great tutorial. It is really very usefull, however I am getting one error on your implementation. Whne I place ValidatesDateTime.us_date_format = true #use mm/dd/yyyy format in my model I receive the following error:

    uninitialized constant Visit::ValidatesDateTime

    Have you any ideas, I’m pulling out my hair!

    Thanks again.

    j

  2. John McAuley Says:

    Hi Handy,

    I installed the plugin as a above, instead of downloading it, and the error has disappeared but there are only nulls being writted to the database. Any ideas?

    j

  3. John McAuley Says:

    Handy,

    I got all nulls untill I did the following:

    ValidatesDateTime.us_date_format = false #use mm/dd/yyyy format

    Hey Presto, it worked.

    Laters,
    j

  4. Handy Says:

    John,

    This post is used mainly for en-us locale. If you use a different locale, you’re correct that ValidatesDateTime.us_date_format = false.

    Also, don’t forget to change step 5:
    ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(:default_date => ‘%m/%d/%Y’)

    The default date format for display should be changed as well.

    Let me know if you have other question.

    Handy

  5. Cheri A Says:

    Ok, the ruby code did not go through on my previous post.
    I want to embed the calendar in a Rails form using
    “form_for @posting do |f|”?

    Thanks,
    Cheri

  6. Handy Says:

    Cheri,

    Interesting idea… I’ve never thought about doing the embedded calendar since I think the form looks more clean for my project. Different projects have different requirements, correct? :)

    The date field is calling calendar_open from public/javascripts/calendar.js.
    To show the calendar, you may try to call calendar_open with the same parameters on a hidden date field. When the user clicks on the date, the calendar should update the hidden field, which can be submitted when the user accepts the form.

    Let me know if this works and I’ll be happy to put a link to your investigation.

    Handy

  7. Colin Stevens Says:

    Hi Handy,

    One problem i’ve come across is that it seems impossible to change a date to nil from a valid date once it is set – any ideas ?

  8. Handy Says:

    Colin,

    I can still delete a valid date from my form.

    A couple things to check:
    - The field value should be empty when the user saves the form. Can you verify that the controller correctly get an empty string from the field?
    - Are you sure the date is not protected field?

    Handy

  9. Doc Pneumo Says:

    Handi,

    Using Rails 2.2.0 Other versions were not tried.

    Validates_date_time has some behaviors that are problematic. It does not appear possible to allow validation of a date_time field to distinguish between a nil entry and an entry that Rails rejects by substituting a nil for that value. For example setting :allow_nil => true causes both “record.field = nil” and “record.field = ‘a funny date’” to pass validation. :allow_nil => false ,the default, rejects both entries. However, a field for which an optional entry must be validated is not handled by validates_date_time.

    I think the problem is that if time zone conversion is in effect, Rails checks in ActiveRecord::AttributeMethods for a valid date_time before storing the raw_value in @attributes. “#{attr_name}_before_type_cast” is a misnomer. It returns @attributes[attr_name]. But date or time fields stored in @attributes have already been parsed into an appropriate format or replaced with nil. This prevents validates_date_time from doing its own validation and issuing an appropriate error message. If the DB is set up to allow nil for that field then no complaint will ever be issued. A near miss date would be accepted and stored as nil without warning! The only validations that can be done are that a date is sane from the validation perspective or that a nil will be stored in the database on save or update. Validation cannot check for an unsupplied date or time versus an invalid string.

    You can see the behavior of Rails by making the following change in the validates_date_time code:

    def validates_date(*args)

    validates_each(attr_names, options) do |record, attr_name, value|
    raw_value = record.send(”#{attr_name}_before_type_cast”)

    rv = raw_value.nil ? ‘nil’ : raw_value
    puts “Attr Name: #{attr_name}, Value: #{value}, Raw Value: #{rv}”
    # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    if (!raw_value.blank? and !value) || (raw_value.blank? and !allow_nil)

    end

    Run some unit tests against a model with validates_date_time properly installed and with the following in place:

    validates_date :start, :allow_nil => true

    Validating a record with start set to ‘a funny date’ will pass. This will appear in the test run:

    Attr Name: start, Value: a funny date, Raw Value: nil

    Inspection of the record after the assignment will show:

    … start: nil, …

    Assuming that time zone conversion is desirable, I do not know how to get around this except by changing the Rails behavior, itself. For example one could add a @raw_date_time hash to the model and store the unparsed value for the attribute as @raw_date_time[attr_name]. Then validate_date_time could access @raw_date_time with a getter for the raw_value rather than using “#{attr_name}_before_type_cast”. the relevant code is in attribute_methods.rb of the Rails code:

    def define_write_method_for_time_zone_conversion(attr_name)
    method_body = <<-EOV
    def #{attr_name}=(time)
    @raw_date_time[#{attr_name}] = time #<<<<< Added code.
    # @raw_date_time must have been defined
    # for the instance, of course.
    unless time.acts_like?(:time)
    time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
    end
    time = time.in_time_zone rescue nil if time
    write_attribute(:#{attr_name}, time)
    end
    EOV
    evaluate_attribute_method attr_name, method_body, “#{attr_name}=”
    end

    Any thoughts on this? It seems date & time validation is still a problem! I would love to find a good solution or to be proven wrong.

    BTW. The methods for validates_date, validates_time, and validates_date_time share a lot of code. A good place to DRY the code base. ;)

    Doc Pneumo

  10. Doc Pneumo Says:

    Handy,

    After submitting my last post, I found the validates_timeliness plugin at git://github.com/adzap/validates_timeliness.git. This appears to answer my concerns and more. I’ve skimmed the code but haven’t used it yet. Will swap it in for validates_date_time in a project I’m working on. Will report any issues I find.

    Doc Pneumo

  11. Doc Pneumo Says:

    validates_timeliness seems to work well on the first pass.

    At the moment I’m developing in a Windows environment. Loading the plugin had lots of problems. However, doing ‘gem install validates_timeliness’ worked fine. The current version is 1.1.5 .
    Remember to put this in config/environment: config.gem “validates_timeliness”

    Doc Pneumo

  12. Flo Says:

    Nice work ! and nice doc !
    I’ve followed the step-by-step doc and everything went fine !

  13. Ahad L. Amdani Says:

    Handy,

    I’ve followed the tutorial step-by-step, and wrapped

    ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(:default_date => ‘%m/%d/%Y’)

    into a function into the application helper file:

    module ApplicationHelper

    def calendar_format(date_field)
    ValidatesDateTime.us_date_format = true #use mm/dd/yyyy format
    ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS[date_field]
    end

    end

    However, trying to use the following in ../views/attorneys/edit.html.erb causes an error:
    calendar_format(@attorney.current_retroactive_date) %>

    The error I receive is:

    undefined method `date_field’ for #

    I don’t understand it, since I have copied the included date_helper.rb file to ../app/helpers/date_helper.rb and it clearly defines the date_field:

    module DateHelper

    def date_field(object_name, method, options={})
    format = options.delete(:format) ||
    ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS[:default] ||
    ‘%d %b %Y’
    if options[:value].is_a?(Date)
    options[:value] = options[:value].strftime(format)
    end
    months = Date::MONTHNAMES[1..12].collect { |m| “‘#{m}’” }
    months = ‘[' + months.join(',') + ']‘
    days = Date::DAYNAMES.collect { |d| “‘#{d}’” }
    days = ‘[' + days.join(',') + ']‘
    options = {:onfocus => “this.select();calendar_open(this,{format:’#{format}’,images_dir:’/images’,month_names:#{months},day_names:#{days}})”,
    :o nclick => “event.cancelBubble=true;this.select();calendar_open(this,{format:’#{format}’,images_dir:’/images’,month_names:#{months},day_names:#{days}})”,
    }.merge(options);
    text_field object_name, method, options
    end
    end

    Do you have any idea why this may be the case? I have restarted my server, as I thought that may be the issue, but no such luck. I am running:

    ruby 1.8.6 (2008-08-11 patchlevel 287) [universal-darwin9.0]
    Rails 2.3.2

    Thanks for any assistance in advance,
    Ahad.

  14. Ahad L. Amdani Says:

    the part of the error left out is due to formatting is:

    undefined method `date_field’ for #

  15. Ahad L. Amdani Says:

    ActionView::Helpers::FormBuilder:0×2425584 with angle brackets around it (happened again)

  16. Ahad L. Amdani Says:

    Hah, I resolved it by understanding that I am very new to this material and made a rookie mistake. Just needed to take the “f.” portion out. Thanks for this great tutorial!

  17. Handy Says:

    I’m glad that my tutorial works well for you. :)

Leave a Reply