GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

ruby on fails

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#date_select to get the value for a Job model’s expires_on 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, #validateoncreate, #validateonupdate 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 expireson to nil? Then if we don’t #validatepresenceof :expireson, 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.