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.