require'active_model/forbidden_attributes_protection'moduleActiveRecordmoduleAttributeAssignmentextendActiveSupport::ConcernincludeActiveModel::ForbiddenAttributesProtection# Allows you to set all the attributes by passing in a hash of attributes with# keys matching the attribute names (which again matches the column names).## If the passed hash responds to <tt>permitted?</tt> method and the return value# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt># exception is raised.defassign_attributes(new_attributes)if!new_attributes.respond_to?(:stringify_keys)raiseArgumentError,"When assigning attributes, you must pass a hash as an argument."endreturnifnew_attributes.blank?attributes=new_attributes.stringify_keysmulti_parameter_attributes=[]nested_parameter_attributes=[]attributes=sanitize_for_mass_assignment(attributes)attributes.eachdo|k,v|ifk.include?("(")multi_parameter_attributes<<[k,v]elsifv.is_a?(Hash)nested_parameter_attributes<<[k,v]else_assign_attribute(k,v)endendassign_nested_parameter_attributes(nested_parameter_attributes)unlessnested_parameter_attributes.empty?assign_multiparameter_attributes(multi_parameter_attributes)unlessmulti_parameter_attributes.empty?endaliasattributes=assign_attributesprivatedef_assign_attribute(k,v)public_send("#{k}=",v)rescueNoMethodErrorifrespond_to?("#{k}=")raiseelseraiseUnknownAttributeError.new(self,k)endend# Assign any deferred nested attributes after the base attributes have been set.defassign_nested_parameter_attributes(pairs)pairs.each{|k,v|_assign_attribute(k,v)}end# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done# by calling new on the column type or aggregation type (through composed_of) object with these parameters.# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and# f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.defassign_multiparameter_attributes(pairs)execute_callstack_for_multiparameter_attributes(extract_callstack_for_multiparameter_attributes(pairs))enddefexecute_callstack_for_multiparameter_attributes(callstack)errors=[]callstack.eachdo|name,values_with_empty_parameters|beginsend("#{name}=",MultiparameterAttribute.new(self,name,values_with_empty_parameters).read_value)rescue=>exerrors<<AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})",ex,name)endendunlesserrors.empty?error_descriptions=errors.map{|ex|ex.message}.join(",")raiseMultiparameterAssignmentErrors.new(errors),"#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"endenddefextract_callstack_for_multiparameter_attributes(pairs)attributes={}pairs.eachdo|(multiparameter_name,value)|attribute_name=multiparameter_name.split("(").firstattributes[attribute_name]||={}parameter_value=value.empty??nil:type_cast_attribute_value(multiparameter_name,value)attributes[attribute_name][find_parameter_position(multiparameter_name)]||=parameter_valueendattributesenddeftype_cast_attribute_value(multiparameter_name,value)multiparameter_name=~/\([0-9]*([if])\)/?value.send("to_"+$1):valueenddeffind_parameter_position(multiparameter_name)multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_iendclassMultiparameterAttribute#:nodoc:attr_reader:object,:name,:values,:columndefinitialize(object,name,values)@object=object@name=name@values=valuesenddefread_valuereturnifvalues.values.compact.empty?@column=object.class.reflect_on_aggregation(name.to_sym)||object.column_for_attribute(name)klass=column.klassifklass==Timeread_timeelsifklass==Dateread_dateelseread_other(klass)endendprivatedefinstantiate_time_object(set_values)ifobject.class.send(:create_time_zone_conversion_attribute?,name,column)Time.zone.local(*set_values)elseTime.send(object.class.default_timezone,*set_values)endenddefread_time# If column is a :time (and not :date or :timestamp) there is no need to validate if# there are year/month/day fieldsifcolumn.type==:time# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil{1=>1970,2=>1,3=>1}.eachdo|key,value|values[key]||=valueendelse# else column is a timestamp, so if Date bits were not provided, errorvalidate_required_parameters!([1,2,3])# If Date bits were provided but blank, then return nilreturnifblank_date_parameter?endmax_position=extract_max_param(6)set_values=values.values_at(*(1..max_position))# If Time bits are not there, then default to 0(3..5).each{|i|set_values[i]=set_values[i].presence||0}instantiate_time_object(set_values)enddefread_datereturnifblank_date_parameter?set_values=values.values_at(1,2,3)beginDate.new(*set_values)rescueArgumentError# if Date.new raises an exception on an invalid dateinstantiate_time_object(set_values).to_date# we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid datesendenddefread_other(klass)max_position=extract_max_parampositions=(1..max_position)validate_required_parameters!(positions)set_values=values.values_at(*positions)klass.new(*set_values)end# Checks whether some blank date parameter exists. Note that this is different# than the validate_required_parameters! method, since it just checks for blank# positions instead of missing ones, and does not raise in case one blank position# exists. The caller is responsible to handle the case of this returning true.defblank_date_parameter?(1..3).any?{|position|values[position].blank?}end# If some position is not provided, it errors out a missing parameter exception.defvalidate_required_parameters!(positions)ifmissing_parameter=positions.detect{|position|!values.key?(position)}raiseArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")endenddefextract_max_param(upper_cap=100)[values.keys.max,upper_cap].minendendendend