About

This gem provides a simple patch that allows you to set transient (non-db-backed) model attributes from multiparameter attribute groups (like those passed by date/time select form elements) in Rails.

Usage

Add the following to your Gemfile:

gem 'multiparameter_assignable_attr'

and run 'bundle install' (or just 'bundle')

$ bundle

That's it, you can now mark transient attributes as multiparameter-assignable with the following macro:

multiparameter_assignable_attr :attribute_name => AttributeClass

See below for more info and examples.

More Info

Let's say you have a class "Member" and you have added a transient "enrollment_time" attribute to it (perhaps so that you can cleanly place separate date and time inputs in a form for the model). If you have your model definition as follows:

class Member < ActiveRecord::Base
  attr_accessible :name, :email, :enrollment_date, :enrollment_time

  attr_accessor :enrollment_time

  before_save :merge_enrollment_date_and_time

  private

  def merge_enrollment_date_and_time
    if enrollment_time
      self.enrollment_date = enrollment_date.change(
        :hour => enrollment_time.hour,
        :min => enrollment_time.min
      )
    end
  end
end

...and the following form:

<%= form_for @member do |f| %>
  Name:
  <%= f.text_field :name %>
  <br />

  Enrollment Date:
  <%= f.datepicker :enrollment_date %>
  <br />

  Enrollment Time:
  <%= f.time_select :enrollment_time %>
  <br />

  <%= f.submit %>
<% end %>

...and you try to process it via mass assignment:

# app/controllers/members_controller.rb
# ...

def create
  @member = Member.new(params[:member])

  if @save
    redirect_to :action => 'index'
  else
    render :action => 'new'
  end
end

# ...

...you will get an error like:

ActiveRecord::MultiparameterAssignmentErrors
1 error(s) on assignment of multiparameter attributes

This error occurs because the value for the transient "start_time" attribute is being passed in as a so-called "multiparameter attribute" (attribute composed of multiple keys, ex: 'start_time(1i)', 'start_time(2i)', 'start_time(3i)'...). In order to parse the multiparameter attribute into the appropriate object, ActiveRecord must know the target attribute type. It does the lookup in the body of the following method from 'lib/activerecord/attribute_assignment.rb'

def read_value_from_parameter(name, values_hash_from_param)
  klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
  if values_hash_from_param.values.all?{|v|v.nil?}
    nil
  elsif klass == Time
    read_time_parameter_value(name, values_hash_from_param)
  elsif klass == Date
    read_date_parameter_value(name, values_hash_from_param)
  else
    read_other_parameter_value(klass, name, values_hash_from_param)
  end
end

This patch just overrides the default implementation of ActiveRecord.reflect_on_aggregation to return the appropriate class for indicated transient attributes instead of nil (and defer to super for the other attributes). To use it, we would update our class definition to use the multiparameter_assignable_attr macro:

class Member < ActiveRecord::Base
  attr_accessible :name, :email, :enrollment_date, :enrollment_time

  multiparameter_assignable_attr :enrollment_time => DateTime

  before_save :merge_enrollment_date_and_time

  private

  def merge_enrollment_date_and_time
    if enrollment_time
      self.enrollment_date = enrollment_date.change(
        :hour => enrollment_time.hour,
        :min => enrollment_time.min
      )
    end
  end
end

Now when ActiveRecord looks up the type of Member's "enrollment_date" attribute via reflect_on_aggregation, it will get an OpenStruct with a klass attribute set to DateTime and the assignment from the multiparameter attribute will go forward.