You're viewing all posts tagged with rails

overriding rails validations metaprogramatically

I had a need to modify the validations for a model in a Rails 2.3.5 app from a plugin.  I did not want to directly modify the source code; in my case, I was writing my code as a plugin for an existing Rails application so I could upgrade the base code without worrying about losing my changes, or dealing with the hassle of reapplying my patches.  There are several really great Rails frameworks out there that are applications in their own right, such as Redmine and Spree, and writing my customizations as plugins or extensions to them is simply a more flexible solution than modifying the applications directly.

I was modifying a User class, extending the login length to 255 characters. We were using emails as the login, and where I work they can get pretty long because they are usually a concatenation of one’s first and last names. And some of my co-workers have obscenely long names. Directly modifying User would be trivial — you would just increase the value associated with the maximum attribute for the validates_length_of validation we are changing:

validates_length_of :login, :maximum => 255

But, as I mentioned, I did not want to change the original code. So, we need to get at the list of validation callbacks. Luckily, that’s easy: there’s a @validate_callbacks instance method available to us. So, we just need to iterate over it and find the callback that we don’t want.

Each entry in @validate_callbacks is an instance of ActiveSupport::Callbacks::Callback. This class contains an accessor called method which returns the Proc that is executed to run the validation. Unfortunately, we want to get at the values being sent to that proc. We know that the validates_length_of method takes in an attrs parameter, so we just eval that against the callback method’s binding to find the list of attributes that this proc will validate.

Because we know we only passed one attribute in (instead of a chain of them), we just check the first value, and double check that the maximum option was set to 30 (which is the original value). This also allows us to easily filter out the other validations on the login field that we aren’t interested in, such as validates_format_of.  The finished product looks something like this:

require_dependency 'user'
require 'dispatcher'
module UserPatch
def self.included(base)
  base.class_eval do
    @validate_callbacks.reject! { |c| true if Proc === c.method &&
      eval("attrs", c.method.binding).first == :login && c.options[:maximum] == 30 rescue false }
    validates_length_of :login, :maximum => 255 # new value
end end Dispatcher.to_prepare do unless User.included_modules.include?(UserPatch) User.send(:include, UserPatch) end end

building a redmine plugin

I was experimenting with the Redmine, a popular Ruby on Rails open source project management web application.  It’s pretty full featured: you can set up multiple projects, each with its own issue tracking, wikis, document stores, and calendars, all within the same instance of Redmine.  But it didn’t have everything I wanted.

One problem that I quickly realized I wouldn’t be able to live with is that emails sent from a Redmine instance all use the same emission address.  For me, this was a showstopper, since I had a requirement that each project would be tied to its own email address, which our users email for tech support.  So I set out to figure out how to customize Redmine.  While it would be trivial to simply modify Redmine’s source code directly, I wanted to be able to upgrade the original system, while preserving my customizations.  The ideal way to do that is to create a plugin.

To start a Redmine plugin project is simple - there is a Rails generator for that:

ruby script/generate redmine_plugin <plugin_name>

In my case, I called it “project_email”.  This creates your standard folder hierarchy for a Rails app: controllers, helpers, models, views, db, as well as a lib folder.  The most important file is init.rb, which is invoked when the plugin is loaded.  This contains some information that Redmine needs.

require 'redmine'
require 'mailer_patch'
require 'project_patch'

Redmine::Plugin.register :redmine_redmine_project_email do
name 'Redmine Project Email plugin'
author 'Lawrence McAlpin'
description 'Adds a per-project email emission address'
version '0.0.1'
url 'http://github.com/lmcalpin/redmine_project_email'
author_url 'http://www.lmcalpin.com/'
end

Like any Rails plugin, we can add our own tables and fields.  We simply create a new migration that looks like this:

class AddMailFromToProject < ActiveRecord::Migration
def self.up
add_column :projects, :mail_from, :string
end

def self.down
remove_column :projects, :mail_from
end
end

… and run rake db:migrate:plugins to load it up! Now, the “project” model will automagically have a new property called “mail_from.”

At this point, I need to override some of Redmine’s controllers and models.  The problem is: anything loaded by the plugin will be overwritten by the base application.  That is not quite what we want.  Luckily, Ruby makes it incredibly easy to tame those classes: through metaprogramming.

We set up a few modules with our patches and force the class to include it. 

require_dependency 'mail_handler'

module MailerPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
alias_method_chain :issue_add, :project_emission_email
# ... override the rest of the methods as well
end
end
module InstanceMethods
def issue_add_with_project_emission_email(issue)
from_project issue
issue_add_without_project_emission_email issue
end
def from_project(container)
unless container.nil? || container.project.nil? || container.project.mail_from.nil? || container.project.mail_from.empty?
from container.project.mail_from
end
end
end
end

This code is straightforward: we override the issue_add method.   alias_method_chain takes in two parameters, the first being a symbol representing the method we are overriding, as well as a suffix.  In our case, we use “project_emission_email” as the suffix, so the alias_method_chain call will rename the original issue_add method to “issue_add_without_project_emission_email” and rename the “issue_add_with_project_emission_email” method that we define to “issue_add”.  Any existing code that calls issue_add will end up calling our “issue_add_with_project_emission_email” method.

We simply override the original mailer to set the from address to the value set in the new “mail_from” field we added to our project.  If no customized “mail_from” is set for a project, the default emission email will be used.

But now we have a problem: Redmine ignores any attributes not specifically marked as “safe.”  So we need to modify the project model to add a call to

safe_attributes 'mail_from'

No problem! Monkey patching to the rescue!

module ProjectPatch
def self.included(base) # :nodoc:
base.class_eval do
unloadable
safe_attributes 'mail_from'
end
end
end

Unfortunately, in development mode, our model appears to be reloaded upon every request!  And, you know what?  The same thing happens to our mailer!  Oh nos.  But hey, no problem, we’ll just patch the Rails dispatcher to reapply the patch every time:

Dispatcher.to_prepare do
unless Project.included_modules.include?(ProjectPatch)
Project.send(:include, ProjectPatch)
end
unless Mailer.included_modules.include?(MailerPatch)
Mailer.send(:include, MailerPatch)
end
end

Now we’re almost done!  We just need to modify the view.  The easiest way would be to simply add our own customized _form.rhtml in the app/views/projects folder.  Unlike controllers and models, the views in our plugin take precedence, so our _form.rhtml will be loaded instead of the one included in Redmine.

Redmine provides a hook that lets you add new fields, without overriding the entire view file.  This would be the better approach (since a future version of Redmine may have other UI changes that we want) but it’s late, and this beer isn’t going to drink itself, so we’ll just stop here.  We’ll learn about plugin hooks another time.

devise 1.3.3, rails 3 #fail

I’m starting a small Rails project for a webapp that I hope to have ready by January.  While working on building up the application’s foundation, I ran into a small  compatibility issue with Devise and Rails 3.

Devise 1.3.3 and Rails 3.0, on my Windows development machine at least, seems to have some kind of issue where the helpers don’t become available.  authenticate_user!, user_signed_in?, current_user, and user_session were unavailable to controllers or views.  Backing out to Devise 1.3.2 did the trick, everything works fine now.