MultiParameter Exception Hack
14 Jul 2010
When using date_select or datetiime_select form helpers in rails, the valies of the individual time fields are sent separately, and reassembled back into a date within ActiveRecord::Base. Unfortunately, if the user enters an invalid date, then this process fails with a ActiveRecord::MultiparameterAssignmentErrors exception.
The exception occurs before we actually get anywhere near our application code, so it’s by no means a trivial problem, so it’s unfortunately necessary to monkey patch the offending function. The following code represents a substantial hack – there are probably better ways of taking care of this problem, but this is the best I could come up with in the short space of time I had available.
The solution I came to involved reimplementing the execute_callstack_for_multiparameter_attributes function in ActiveRecord::Base, and removing the exception throw. An instance variable (multipart_error_on_attribute) is then used to store the invalid attributes which is checked in a custom validation. If multipart_error_on_attribute contains any number of attribute symbols, then the validation fails, and an error is added to the object.
module MultiparameterHack
def self.included klass
klass.class_eval do
@multipart_error_on_attribute = nil
def validate
errors.add_to_base "#{@multipart_error_on_attribute.humanize} is invalid" unless @multipart_error_on_attribute.nil?
end
undef :execute_callstack_for_multiparameter_attributes
def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
# in order to allow a date to be set without a year, we must keep the empty values.
# Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
values = values_with_empty_parameters.reject(&:nil?)
if values.empty?
send(name + "=", nil)
else
value = if Time == klass
instantiate_time_object(name, values)
elsif Date == klass
begin
values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
Date.new(*values)
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
else
klass.new(*values)
end
send(name + "=", value)
end
rescue => ex
errors << ActiveRecord::AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
@multipart_error_on_attribute = name
end
end
end
end
end
end
ActiveRecord::Base.send(:include, MultiparameterHack)