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:
- User commits a form with a missing date_select value
- Rails throws an exception
- 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:
- User commits a form with a missing date_select value
- multipart_error_on_attribute gets set to the name of the attribute that contains the invalid date
- In the controller, when save is called, the validations fail, and an error is added to the instance
- 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)