I have a Rails app with a model containing a birthdate
attribute. This corresponds to a column in my database defined using the ActiveRecord date
type. With this I am able to use the date_select
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 birthdate(1i)
, birthdate(2i)
and birthdate(3i)
. Consequently, I can use the standard update_attributes
method within my controller on my model to update all fields on my model.
I'm now experimenting with encrypting this field using the attr_encrypted
gem. While the gem supports marshalling (this is nice), there is no longer a real column of name birthdate
of type date
- instead, attr_encrypted
exposes the value as a virtual attribute birthdate
backed by a real encrypted_birthdate
column. This means that update_attributes
is unable to perform the previous multiparameter attribute assignment to populate and save this column. Instead, I get a MultiparameterAssignmentErrors
error resulting from the call to the internal column_for_attribute
method returning nil
for this column (from somewhere within execute_callstack_for_multiparameter_attributes
).
I'm currently working around this as follows:
My model in app/models/person.rb
:
class Person < ActiveRecord::Base
attr_encrypted :birthdate
end
My controller in app/controllers/people_controller.rb
:
class PeopleController < 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, &discriminator)
hash.to_a.partition(&discriminator).map do |a|
a.each_with_object(Hash.new) { |e, h| h[e.first] = e.last }
end
end
end
And my view app/views/people/_form.html.erb
:
<%= form_for @person do |f| %>
<%= f.label :birthdate %>
<%= f.date_select :birthdate %>
<% f.submit %>
<% end %>
What's the proper way to handle this type of attribute without having to introduce ad hoc munging of the params
array like this?
Update: Looks like this might refer to a related problem. And this too.
Another update:
Here is my current solution, based on Chris Heald's answer. This code should be added to the Person
model class:
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 => 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
This solution works as is, but would be even better if attr_encrypted
were to take a :class
option that would construct the ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS
hash dynamically. I'm going to look at ways I can extend/monkeypatch attr_encrypted
to do this. Gist available here: https://gist.github.com/rcook/5992293.