So recently, I had a Rails app that raised the following exception:
ActiveRecord::MultiparameterAssignmentErrors
. This was raised in
ActiveRecord::Base#initialize. It was caused by a model that had a date
attribute. I was using ActionView::Helpers::DateHelper#dateselect to get the
value for a Job
model’s `expireson` attribute. Apparently a user had
selected an invalid date, e.g. June 31, 2007.
ActiveRecord::Base#initialize
basically executes the following code:
Date.new 2007, 6, 31
Now, Ruby’s Date
class will raise an ArgumentError
which Rails puts in an
ActiveRecord::AttributeAssignmentError
object that is available via an array
from ActiveRecord::MultiparameterAssignmentErrors#errors
.
So I thought, How can I handle this nicely?
How about overriding the setter in my model?
class Job < ActiveRecord::Base
def expires_on=(expires_on_date) # expires_on_date is a Ruby Date object already
end
end
Nope. Rails is going to pass a Ruby Date
object to Job#expires_on=
. The
ArgumentError exception would have already occurred.
How about overriding ActiveRecord::Base#initialize
on Job
?
class Job < ActiveRecord::Base
def initialize(attributes)
unless valid_date?(attributes['expires_on(1i)'],
attributes['expires_on(2i)'],
attributes['expires_on(3i)'])
errors.add :expires_on, 'is invalid'
attributes.delete 'expires_on(1i)'
attributes.delete 'expires_on(2i)'
attributes.delete 'expires_on(3i)'
end
super
end
def valid_date?(year, month, date)
Date.new year, month, date
rescue ArgumentError
nil
end
end
Nope. Too bad adding errors to ActiveRecord::Base#errors
outside any of the
validation callbacks, i.e. #validate
, #validate_on_create
,
#validate_on_update
doesn’t prevent the object from being saved. In other
words, ActiveRecord::Base#errors
is cleared before calling #validate
. I’m
glad of this behavior, because the code above sucks anyway. Overriding
ActiveRecord::Base#initialize
is crazy.
What if we just set expires_on
to nil? Then if we don’t
#validate_presence_of :expires_on
, the object will be saved with a null
expires_on
date, even though the user thought they just entered an invalid
date.
So if I can’t put it in the model nicely, I’ll have to rescue
ActiveRecord::MultiparameterAssignmentErrors
in the action in the controller,
like so:
class JobsController < ApplicationController
def create
@job = Job.new params[:job]
# save the job
rescue ActiveRecord::MultiparameterAssignmentErrors
@job.errors.add :expires_on, 'is invalid'
render :action => :new
end
end
Nope. Since ActiveRecord::MultiparameterAssignmentErrors
is raised in
ActiveRecord::Base#initialize
, the job object is never created. Therefore,
the job instance variable is nil and this code will raise a NoMethodError
because NilClass
doesn’t understand #errors.
How about:
class JobsController < ApplicationController
def create
@job = Job.new params[:job]
# save the job
rescue ActiveRecord::MultiparameterAssignmentErrors
flash[:notice] = 'Expiration date is invalid'
render :action => :new
end
end
Nope. Since a job object is never created, when rendering JobsController#new
you’ll get a bunch of errors because the job instance variable is nil.
So we ended up replacing the year, month and day dropdowns created by
ActionView::Helpers::DateHelper#date_select
with a Javascript calendar.