Note that there are some explanatory texts on larger screens.

plurals
  1. POCorrect way to handle multiparameter attributes corresponding to virtual attributes
    primarykey
    data
    text
    <p>I have a Rails app with a model containing a <code>birthdate</code> attribute. This corresponds to a column in my database defined using the ActiveRecord <code>date</code> type. With this I am able to use the <code>date_select</code> form helper method to render this as a three-select input in my view. The form parameters corresponding to this field are then serialized back to the controller as <code>birthdate(1i)</code>, <code>birthdate(2i)</code> and <code>birthdate(3i)</code>. Consequently, I can use the standard <code>update_attributes</code> method within my controller on my model to update all fields on my model.</p> <p>I'm now experimenting with encrypting this field using the <a href="https://github.com/shuber/attr_encrypted" rel="nofollow"><code>attr_encrypted</code></a> gem. While the gem supports marshalling (this is nice), there is no longer a real column of name <code>birthdate</code> of type <code>date</code> - instead, <code>attr_encrypted</code> exposes the value as a <em>virtual</em> attribute <code>birthdate</code> backed by a real <code>encrypted_birthdate</code> column. This means that <code>update_attributes</code> is unable to perform the previous multiparameter attribute assignment to populate and save this column. Instead, I get a <code>MultiparameterAssignmentErrors</code> error resulting from the call to the internal <code>column_for_attribute</code> method returning <code>nil</code> for this column (from somewhere within <code>execute_callstack_for_multiparameter_attributes</code>).</p> <p>I'm currently working around this as follows:</p> <p>My model in <code>app/models/person.rb</code>:</p> <pre><code>class Person &lt; ActiveRecord::Base attr_encrypted :birthdate end </code></pre> <p>My controller in <code>app/controllers/people_controller.rb</code>:</p> <pre><code>class PeopleController &lt; ApplicationController def update # This is the bit I would like to avoid having to do. params[:person] = munge_params(params[:person]) respond_to do |format| if @person.update_attributes(params[:person]) format.html { redirect_to @person, notice: 'Person was successfully updated.' } format.json { head :no_content } else format.html { render action: "edit" } format.json { render json: @person.errors, status: :unprocessable_entity } end end end private def munge_params(params) # This separates the "birthdate" parameters from the other parameters in the request. birthdate_params, munged_params = extract_multiparameter_attribute(params, :birthdate) # Now we place a scalar "birthdate" where the multiparameter attribute used to be. munged_params['birthdate'] = Date.new( birthdate_params[1], birthdate_params[2], birthdate_params[3] ) munged_params end def extract_multiparameter_attribute(params, name) # This is sample code for demonstration purposes only and currently # only handles the integer "i" type. regex = /^#{Regexp.quote(name.to_s)}\((\d+)i)\)$/ attribute_params, other_params = params.segment { |k, v| k =~ regex } attribute_params2 = Hash[attribute_params.collect do |key, value| key =~ regex or raise RuntimeError.new("Invalid key \"#{key}\"") index = Integer($1) [index, Integer(value)] end] [attribute_params2, other_params] end def segment(hash, &amp;discriminator) hash.to_a.partition(&amp;discriminator).map do |a| a.each_with_object(Hash.new) { |e, h| h[e.first] = e.last } end end end </code></pre> <p>And my view <code>app/views/people/_form.html.erb</code>:</p> <pre><code>&lt;%= form_for @person do |f| %&gt; &lt;%= f.label :birthdate %&gt; &lt;%= f.date_select :birthdate %&gt; &lt;% f.submit %&gt; &lt;% end %&gt; </code></pre> <p>What's the proper way to handle this type of attribute without having to introduce ad hoc munging of the <code>params</code> array like this?</p> <p>Update: Looks like <a href="https://rails.lighthouseapp.com/projects/8994/tickets/2675-support-for-multiparameter-attribute-assignment-on-virtual-attribute-writers" rel="nofollow">this</a> might refer to a related problem. And <a href="https://rails.lighthouseapp.com/projects/8994/tickets/2675-support-for-multiparameter-attribute-assignment-on-virtual-attribute-writers" rel="nofollow">this</a> too.</p> <p>Another update:</p> <p>Here is my current solution, based on Chris Heald's answer. This code should be added to the <code>Person</code> model class:</p> <pre><code>class EncryptedAttributeClassWrapper attr_reader :klass def initialize(klass); @klass = klass; end end # TODO: Modify attr_encrypted to take a :class option in order # to populate this hash. ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS = { :birthdate =&gt; EncryptedAttributeClassWrapper.new(Date) } def column_for_attribute(attribute) attribute_sym = attribute.to_sym if encrypted = self.class.encrypted_attributes[attribute_sym] column_info = ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS[attribute_sym] column_info ||= super encrypted[:attribute] column_info else super end end </code></pre> <p>This solution works as is, but would be even better if <code>attr_encrypted</code> were to take a <code>:class</code> option that would construct the <code>ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS</code> hash dynamically. I'm going to look at ways I can extend/monkeypatch <code>attr_encrypted</code> to do this. Gist available here: <a href="https://gist.github.com/rcook/5992293" rel="nofollow">https://gist.github.com/rcook/5992293</a>.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload