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)