This code supersedes the code in this post.

Given a date_select, a user might (intentionally or otherwise) neglect to enter a value for one of the selects. Rather than add an error to the instance or default the missing date attributes to the current date, Rails will throw an exception. This is problematic, as it leads to the following sequence of events:

  1. User commits a form with a missing date_select value
  2. Rails throws an exception
  3. User sees a 500

The following code patches Activerecord::Base – adding a validation and messing with #execute_callstack_for_multiparameter_attributes. The end result is that on submitting an invalid date, an error is added to the instance when validations are run:

  1. User commits a form with a missing date_select value
  2. multipart_error_on_attribute gets set to the name of the attribute that contains the invalid date
  3. In the controller, when save is called, the validations fail, and an error is added to the instance
  4. Typically, the user will then see the form rendered and an error message.

This will only work for a narrow set of problems – this code isn’t really fit for stuffing back into Rails, so use with caution.

 

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)