From time to time, we override ActiveRecord attribute methods to add some additional behavior. Yesterday, I was working on some code to require a user to approve a new email address before the address is updated. Unfortunately, this broke our form handling. Let’s take a look at what happened and how we can fix it.
First, let’s take a look at the code in question. I have a
User model with an
To implement this, I added a
pending_email attribute on the user mode. I then use this method in all of my forms for users. By default, the
pending_email attribute is blank. When a user views their profile edit form, I want them to see their existing email address. It should be easy to make the
pending_email attribute return either itself if it is set, or the
def pending_email read_attribute(:pending_email) || email end
That does exactly what we want. Calling
pending_email gives us the email address by default. Unfortunately, things break down when we try to use this method in a form. When we do something like:
<% form_for @user do |f| %> <%=f.text_field :pending_email %> <% end %>
pending email never displays the existing email address. To understand what was happening, I had to dig in to the Rails source. I started by looking at the
text_field method in form_helper.rb. That pointed me to the
InstanceTag class. That finally led me to the following code:
class << self def value_before_type_cast(object, method_name) unless object.nil? object.respond_to?(method_name + "_before_type_cast") ? object.send(method_name + "_before_type_cast") : object.send(method_name) end end end
That code looks at the object in our form, and sees whether it has a
pending_email_before_type_cast method. If it does, it calls it to get the value. If it doesn’t, it calls
pending_email. By default, ActiveRecord creates the
_before_type_cast methods so that code can access the textual value of a field before it is turned into a ruby object. For example, calling
created_at_before_type_cast will give you the textual representation of a date inside the database. There are good reasons for this behavior, but they don’t really matter to us. All we need to know is that our pending email method doesn’t every get called.
So how do we fix this? We’re going to exploit the trick that the
value_before_type_cast method checks to see if our object responds to a method before it calles the _before_type_cast method. We can easily override respond_to? to make it lie about the existence of our method. Here’s the code that I used:
# fake out the helpers to use the method and not the raw attribute def respond_to?(*args) if args.first.to_s == "pending_email_before_type_cast" false else super end end
That’s all it takes! Our code checks to see if the caller is asking about our
pending_email method. If they are, it lies and says the _before_type_cast method doesn’t exist. For anything else, we fall back to the default behavior. Now, our users see their existing email address when they view their profile edit form.
You can see why we want to clean up this code a little. It’s not obvious what that code is doing, and it’s really easy to get wrong. (I spent 20 minutes debugging because I spelled type_cast wrong. I don’t want to have to ever do that again)
Expect to see more frequent posting here. My book, Developing Facebook Platform Applications with Rails is almost done and I will have a little bit more time to write.