Decorating ActiveRecord Accessor Methods

Recently, I was faced with a quandary. I needed to impart some of our ActiveRecord accessor methods with special functionality, but I also needed to maintain the default behavior for those fields. This was done to support a new input type which we added to the venerable CalendarDateSelect plugin which allows a user to enter a date, time, and meridian, all in separate fields.

calendar_date_select_fields

Though I could parse each of the fields in the controller and pass the resulting time to the model, this is messy, error-prone, and unnecessary.

What I really want to be able to do is something like this:

>> CalendarEvent.new(
:start_time => {:date => '6/23/2009', :time => '12:30', :meridian => 'pm'})
=> #<CalendarEvent start_time: "2009-06-23 19:30:00">

My first thought was to simply override the accessor methods like so:

CalendarEvent < ActiveRecord::Base
...
def start_time=(new_time)
self[:start_time] = parse_time_from_hash(new_time)
end
end

However, as I alluded to before, there’s a problem with this approach. Rails already does some magic behind the scenes to ensure that time values stored to your ActiveRecord objects are automatically cast to and from UTC. The code above will completely overwrite these default accessors, and time zone casting will cease to function for those fields. What I really want to do is decorate these accessor methods, so that I can add to the existing functionality without completely circumventing it.

One way we might think about accomplishing this would be to redefine our accessor method and add behavior to it using alias_method_chain.

def start_time_with_hash_parsing=(new_time)
new_time = parse_time_from_hash(new_time)
start_time_without_hash_parsing = new_time
end
alias_method_chain :start_time=, :hash_parsing

Try that, though, and you’ll get this fun little error

`alias_method':NameError: undefined method `start_time=' for class `CalendarEvent'

It doesn’t work because there is no original start_time= method to alias; ActiveRecord creates those methods only when the class is loaded, because it has to read the columns from the database at runtime. So, how do you decorate methods which haven’t even been created yet? Well, it turns out you have to decorate ActiveRecord::Base.define_attribute_methods, which is responsible for creating accessor methods for each model.

There are a couple of ways you can go about doing this, depending on your goals. I wanted to be able to use these enhanced date/time inputs with any model in our application and trigger it from an initializer, so my approach uses alias_method_chain to modify ActiveRecord::Base directly.

module TimeAttributeExtensions
module ClassMethods

def define_attribute_methods_with_time_parsing
if define_attribute_methods_without_time_parsing

columns_hash.each do |name, column|
if [:datetime, :timestamp].include?(column.type)
unless method_defined?(:"#{name}_without_time_parsing=")

define_method("#{name}_with_time_parsing=") do |time|
send(:"#{name}_without_time_parsing=", parse_time_from_hash(time))
end

alias_method_chain :"#{name}=", :time_parsing
end
end
end

return true
end
end
end

module InstanceMethods
def parse_time_from_hash(time)
# Do time parsing here
end
end

def self.included(receiver)
receiver.send(:include, InstanceMethods)
receiver.extend ClassMethods

unless receiver.respond_to?(:define_attribute_methods_without_time_parsing)
class << receiver
alias_method_chain :define_attribute_methods, :time_parsing
end
end
end
end

Ok, so what exactly does that code do? First, we call the original version of define_attribute_methods. If that returns true, then we know that the attribute accessor methods have been generated. We loop over each column in the model, checking to see whether it stores either a datetime or a timestamp. If so, this is an accessor we’re interested in decorating. We do a quick check to make sure we haven’t already decorated the accessor (which can bite us in development mode), then we redefine the accessor with time parsing, and use the magic of alias_method_chain to alias our original method but still maintain a reference to it.

At the bottom, we use the Module#included callback to add the methods contained in our module, and we once again use alias_method_chain to redefine our version of define_attribute_methods.

Then, in the initializer, you would write something like

ActiveRecord::Base.send(:include, TimeAttributeExtensions)

At this point, any time-based field in any of your models will be able to accept a hash as a value, and properly parse it.

This works well, but one thing I don’t like about this solution is its rampant use of alias_method_chain. There has been some debate in the Rails community over its validity as a design choice, though there really are no other options if you need to modify all subclasses of ActiveRecord::Base (as you would to support a plugin like CalendarDateSelect).

Another Approach

If modifying every subclass isn’t a design requirement, you can clean up the code significantly:

module TimeAttributeExtensions
module ClassMethods

def define_attribute_methods
if super
columns_hash.each do |name, column|
if [:datetime, :timestamp].include?(column.type)
define_method("#{name}=") do |time|
super parse_time_from_hash(time)
end
end
end

return true
end
end
end

module InstanceMethods
def parse_time_from_hash(time)
# Do time parsing here
end
end

def self.included(receiver)
receiver.send(:include, InstanceMethods)
receiver.extend ClassMethods
end
end

Notice here we’re just defining define_attribute_methods and calling super when we want access to the previous implementation. The same goes for decorating the actual accessor methods.

The downside of this approach is that you have to explicitly include the module in your ActiveRecord models:

CalendarEvent < ActiveRecord::Base
include TimeAttributeExtensions
...

As with any decision, the approach you take will depend on your requirements, and tradeoffs you’re willing to make between explicitness and ease of use.

Delivery Agent Creates Secure Collaboration Portal

Delivery Agent chooses Onehub to Create a Secure Collaboration Portal and Reduce IT Administration

Delivery Agent, the leader in shopping-enabled entertainment, chooses Onehub to collaborate with high-profile media company partners as well as create a company-wide intranet

delivery_agent_logo_280x33

Situation

Delivery Agent, Inc., is the leader in shopping-enabled entertainment for television shows, movies, magazines, sports, and music videos. Delivery Agent created the market for shopping-enabled entertainment by redefining how products seen on or related to entertainment content are catalogued, sold and measured online.

Delivery Agent partners with high-profile media companies, such as Discovery, A&E, NBC, ABC, CBS, FOX NBC, MTV, UFC, and Universal just to name a few.

They need to share different types of graphical and audio-video media files, spreadsheets, and presentation documents with theses partners.

delivery_agent_screensplash

Prior to finding Onehub, Delivery Agent was using a combination of systems and services. Including their own hosted FTP, FTP hosted by their partners, other systems that required software installation on Delivery Agent systems and finally email. Delivery Agent knew that the FTP server had to be replaced and also wanted to consolidate the variety of systems and methods used to share files in order to better manage costs, training time, administration, and support issues. “We had been waiting for an easy to use application like Onehub that would combine collaboration functions (similar to those in popular social networking sites), security that would enable us to protect assets and access, and robust file sharing that was reliable and would accommodate the size and variety of files we work with,” said Glen Hendler, Delivery Agent Customer Support Technician.

Solution
BENEFITS
  • Cost Effective
  • Reduced IT Administration
  • Intranet and Extranet Solution in One

Delivery Agent explored many possible solutions and found Onehub to be the most cost effective solution to meet their needs. Onehub had all the features the Delivery Agent team needed for their extranet and company intranet — and with unlimited user accounts and generous files size and bandwidth restrictions, their search was over.

Implementation and deployment of the Onehub service was effortless, especially compared to solutions that require new client software. With Onehub, Delivery Agent already enjoys savings in time spent on internal IT support and administration. But soon (if not already) the ability for employees who work with outside contacts to create a customized and secure portal to collaborate will outweigh the IT benefits.

“When I see the number of hubs created by staff, and files shared with partners, without IT requests for assistance, I consider that positive feedback. I fully expect a number of our partners will purchase their own Onehub accounts to share files with their other contacts.”

Delivery Agent also needed a Human Resources intranet site for facilitating employee access to benefit information and company news. Instead of purchasing a separate hardware/software solution and taking the time to install, configure and deploy, it was a simple matter instead to create a hub called Human Resources and populate it with information. “Now we have created several other Intranet portals, including one for company/corporate services, one for the IT Helpdesk and several for other organizational groups,” says Hendler.