3
votes

When using Rails date_select with :prompt => true I see some very strange behavior when submitting the form without all fields selected. Eg.

Submitting the form with January selected but the day and year fields left at the default prompt results in January 1st 0001 getting passed to the model validation. If validation fails and the form is rendered again January is still selected (correctly) but the day is set to 1 (incorrectly). If the form is submitted with just the year selected, both month and day get set to 1.

This is very strange behavior - can anyone give me a workaround?

1
A code sample might help us to help you.eggdrop

1 Answers

3
votes

The problem has to do with multiparameter assignment. Basically you want to store three values into one attribute (ie. written_at). The date_select sends this as { 'written_at(1)' => '2009', 'written_at(2)' => '5', 'written_at(3)' => '27' } to the controller. Active record packs these three values into a string and initializes a new Date object with it.

The problem starts with the fact that Date raises an exception when you try to instantiate it with an invalid date, Date.new(2009, 0, 1) for instance. Rails catches that error and instantiates a Time object instead. The Time class with timezone support in Rails has all kinds of magic to make it not raise with invalid data. This makes your day turn to 1.

During this process active record looses the original value hash because it packed the written_at stuff into an array and tried to create a Date or Time object out of it. This is why the form can't access it anymore using the written_at_before_time_cast method.

The workaround would be to add six methods to your model: written_at_year, written_at_year=, and written_at_year_before_type_cast (for year, month and day). A before_validation filter can reconstruct the date and write it to written_at.

class Event < ActiveRecord::Base
  before_validation :reconstruct_written_at

  def written_at_year=(year)
    @written_at_year_before_type_cast = year
  end

  def written_at_year
    written_at_year_before_type_cast || written_at.year
  end

  def written_at_year_before_type_cast
    @written_at_year_before_type_cast
  end

  private

  def reconstruct_written_at
    written_at = Date.new(written_at_year, written_at_month, written_at_day)
  rescue ArgumentError
    written_at = nil
  end
end