9

I have a date field in a model backed form in my Rails App:

<%= f.date_select :birthday,
  {:start_year => Time.now.year,
  :end_year => 1900,
  :use_short_month => true,
  :order => [:month, :day, :year],
  :prompt => {:month => 'Month', :day => 'Day', :year => 'Year'}},
  {:class => 'year',
  :id => 'user_birthday'}
%>

It is being validated in the model code using:

validates_presence_of :birthday, :message => 'is a required field'

Unfortunately, if the user enters a partial value such as just the year, the form still submits without an error. Instead a funky date value gets written to the db. How do I make all three fields be mandatory?

I'd like to write a custom validation for this, but I don't know how to properly access the indvidual pieces of the birthday element. How can I do this?

Thanks! Moe

Mohit Jain
  • 43,139
  • 57
  • 169
  • 274
Moe
  • 641
  • 1
  • 7
  • 16
  • I notice you don't have `:include_blank => true`. Is it even possible to leave one blank? What date do you get if you just set the year to 2000? Also, :start_year should be earlier than :end_year. – mckeed Jul 21 '10 at 01:02
  • 1
    Hi mckeed, `:prompt => {:month => 'Month', :day => 'Day', :year => 'Year'}` inserts blank default values for month, day and year. `:end_year` is less than `:start_year` because I wanted the values ordered desc. – Moe Jul 21 '10 at 03:33

4 Answers4

2

I think you would have to create the validation in the controller itself.

The date parts are being passed to birthday(1i), birthday(2i) and birthday(3i). The problem here is that they are assigned immediately when passing the attributes and thus before any validations occur.

You could also overwrite the attributes= method to create your own validation there, but I would not suggest you to do that.

Keep in mind that if you do validations, it might be good to validate against any incorrect date as well. (for instance 31st of February, which when passed will yield 2nd of March and not an error).

I think the main issue here is that ActiveRecord is actually replacing the empty values with 1 before creating the Date, which also means that if the visitor pass only the year, the date will be created on the 1st of January that year. I guess that is an expected behaviour to allow use of only one of year/month/day select and still create a useful date.

Jimmy Stenke
  • 11,140
  • 2
  • 25
  • 20
1

Related to this post, this is the best solution I've found. However I should add :day, :month, :year as attr_accessible, thing I don't understand why.. (because of validation? please let me know..)

User.rb

MONTHS = ["January", 1], ["February", 2], ...
DAYS = ["01", 1], ["02", 2], ["03", 3], ...
START_YEAR = Time.now.year - 100
END_YEAR = Time.now.year
YEAR_RANGE = START_YEAR..END_YEAR

attr_accessible :day, :month, :year
attr_accessor :day, :month, :year

before_save :prepare_birthday

validate :validate_birthday

private

def prepare_birthday
  begin
    unless year.blank? # in order to avoid Year like 0000
      self.birthday = Date.new(self.year.to_i, self.month.to_i, self.day.to_i)
    end
      rescue ArgumentError
    false
  end
end

def validate_birthday
  errors.add(:birthday, "Birthday is invalid") unless prepare_birthday
end

user registration form

<%= f.select :month, options_for_select(User::MONTHS), :include_blank => "Month" %>
<%= f.select :day, options_for_select(User::DAYS), :include_blank => "Day" %>
<%= f.select :year, options_for_select(User::YEAR_RANGE), :include_blank =>"Year" %>
Community
  • 1
  • 1
benoitr
  • 6,025
  • 7
  • 42
  • 67
  • 1
    The reason you add it to `attr_accessible` is to allow the attributes to be mass-assigned. Otherwise they would be stripped out when doing a `User.new(params[:user])` or `@user.update_attributes(params[:user])`. However, I thought that that was not necessary unless you had it already specified (that is, when it is not specified at all, all attributes in the hash is sent to the model, but perhaps that don't apply to virtual attributes) – Jimmy Stenke Mar 22 '11 at 02:31
0

You could override the validate_on_create method, like the following:

class MyModel < ActiveRecord::Base
  def validate_on_create
    Date.parse(birthday)
  rescue
    errors.add_to_base("Wrong date format")
  end
end
Daniel Vandersluis
  • 91,582
  • 23
  • 169
  • 153
sespindola
  • 156
  • 2
  • Date.parse won't work here since the date value isn't invalid. it's just funky. if you leave the year blank, you'll get something like "0007-01-01". – Moe Jul 21 '10 at 00:52
  • You're right. Instead of parse, you should use civil. Like this: Date.civil(birthday[:year].to_i, birthday[:month].to_i, birthday[day].to_i) That should throw an ArgumentError if any field is blank. – sespindola Jul 21 '10 at 01:05
  • Unfortunately, this didn't work either. It throws an error even if I enter all fields properly. I attempted to write a custom validation method in the model class, but I can't access the field value using `birthday[:month]` or even with `.to_i` appended. Any ideas on how to do this? – Moe Jul 21 '10 at 03:35
0

After following Benoitr's suggestions I came up with something similar using virtual attributes. On the View side there are 3 separate select's (year,mon,day) inside of a 'fields_for'. The data is submitted to the controller's mass assignment (no modifications in controller, see asciicasts #16) and then passed to a getter/setter (i.e. virtual attribute) in the model. I'm using Rails 3.0.3, and simpleForm for the view code.

In the View:

<%= f.label "Design Date", :class=>"float_left" %>
<%= f.input :design_month, :label => false, :collection => 1..12 %>
<%= f.input :design_day,   :label => false, :collection => 1..31 %>
<%= f.input :design_year,  :label => false, :collection => 1900..2020 %>

In the Model:

validate :design_date_validator 

def design_year
  design_date.year
end
def design_month
  design_date.month
end
def design_day
  design_date.day
end
def design_year=(year)
  if year.to_s.blank?
    @design_date_errors = true  
  else      
    self.design_date = Date.new(year.to_i,design_date.month,design_date.day)
  end
end  
def design_month=(month)
  if month.to_s.blank?
     @design_date_errors = true  
   else      
   self.design_date = Date.new(design_date.year,month.to_i,design_date.day)
  end
end  
def design_day=(day)
  if day.to_s.blank?
    @design_date_errors = true  
  else      
   self.design_date = Date.new(design_date.year,design_date.month,day.to_i)
  end
end  
#validator
def design_date_validator
  if @design_date_errors
    errors.add(:base, "Design Date Is invalid")
  end
end  

'design_date_attr' is the virtual attribute which sets the value of design_date in the database. The getter passes back an hash similar to what gets submitted in the form. The setter checks for blanks and creates a new date object and sets it and also sets the error variable. The custom validator :design_date_validator checks for the error instance variable and sets the errors variable. I used ':base' because the variable name was not human readable and using base removes that value from the error string.

A few things to refactor might be the error checking instance variable, but it seems to work at least. If anyone knows a better way to update the Date objects I'd love to hear it.

James
  • 1,841
  • 1
  • 18
  • 23