How to Create a Redmine Plugin
Redmine already can do a lot, but often a certain behaviour or feature is desirable in one person’s use case, but not in others. In order to keep Redmine usable for as many people as possible, such corner case features have a hard time making it into the core Redmine code base. And that’s not a problem at all, thanks to Redmine’s powerful plugin system.
Here I’ll walk you through the creation of a small but fully functional (and fully tested!) Redmine plugin.
The Redmine Issue Done Ratio Plugin
There is and has been a lot of discussion around how the ‘percent done’ ratio on issues should be set. Stock Redmine allows you to chose between setting the ratio either manually, or having it set automatically according to the issue’s status. Unfortunately both options are mutually exclusive, so if you want ‘New’ issues set to 0, ‘Resolved’ and ‘Closed’ set to 100 but manage everything in between manually you are out of luck.
There is at least one plugin out there that implements the automatic setting of the field to 100% for issue statuses that are marked as ‘closed’, but in my opinion that’s not enough - I would like to have my Resolved issues auto-set to 100%, but not closed yet.
So instead of setting any closed issues to 100%, lets make this a bit more flexible. Basically we will implement what is asked for in Redmine issue #6975.
Anatomy of a Redmine Plugin
Redmine plugins reside in the plugins
subdirectory of any Redmine installation.
The Redmine plugin system was built at the time of Rails 2, and Redmine plugins still look a lot like Rails plugins back in those days.
The important things to remember are:
- there must be a file called
init.rb
at the top level, which introduces the plugin to Redmine - plugin assets (stylesheets, images, javascript) go into the
assets
directory at the plugin’s top level - everything else (most notably the
app
,lib
andconfig
directories) follows Rails conventions. - you may add a
Gemfile
to specify any gems your plugin depends on.
Here’s the first version of our plugin, doing nothing else but registering itself with Redmine. Have a look at init.rb, it should pretty much explain itself. Two things I’d like to point out:
- Always declare which version of Redmine you require. Without any further testing, this should naturally be the version you develop with. Later you will learn how to leverage Travis CI to test your plugin on various versions of Redmine.
- Keep
init.rb
clean and simple. Define a single setup method for your plugin, and do all your initialization and patching of Redmine core methods there. Userequire_dependency
to load the relevant file, and call yoursetup
method in a Railsto_prepare
hook. This ensures your code gets loaded (and re-loaded) when necessary without breaking Rails’ auto loading. It is a good idea to name this entry point after your plugin, and put in a file that’s named accordingly as well.
You can put this version of the plugin into plugins/redmine_percent_done
,
restart Redmine and you should see the plugin listed with all the meta data
from init.rb in Administration / Plugins. Yay!
Global Plugin Settings
If you already have some other Redmine plugins installed, you might see a Configure link behind the plugin version number. If your plugin requires any global settings this is the place to put them. Indeed we need a place for the administrator to configure which issue statuses should have an automatically set % Done ratio assigned, so let’s implement this!
In your init.rb, inside the Redmine::Plugin.register
block, add
settings partial: 'settings/redmine_percent_done', default: {}
Next, create that file as app/views/settings/_redmine_percent_done.html.erb
:
<% for status in IssueStatus.all do %>
<p>
<label for="settings_status-<%= status.id %>"><%= status.name %></label>
<%= select_tag "settings[status-#{status.id}]",
options_from_collection_for_select(
[[l(:label_no_change_option), '']] +
(0..10).to_a.collect{|r| ["#{r*10} %", r*10] },
:last, :first, @settings["status-#{status.id}"]
) %>
<% end %>
We simply iterate over all issue statuses, showing a select box of possible % values for each. There’s also a no change option which is the default, telling the plugin to not touch the % Done value in this case. I chose to use an already existing Redmine label for this option. This has multiple benefits over inventing our own:
- Redmine already has translations for a lot of languages which are also constantly extended and refined, often by native speakers all around the world. By using already existing i18n keys your plugin’s users automatically will benefit from that.
- It helps to keep the wording consistent across the Redmine instance, wich is important for a good user experience.
So, before you create any own i18n keys for your plugin, check if there is already something in Redmine core you could use.
Create the file or grab the second revision of the plugin, and try out the already fully functional settings form.
Redmine’s plugin system takes care of saving our plugin settings, that’s why we didn’t have to write any controller code for this. If you’re interested, the relevant code is in SettingsController#plugin
.
Testing Redmine Plugins
Redmine plugins are usually tested in the context of a working Redmine setup. In theory it might be possible to mock out all the surrounding Redmine APIs for testing a Redmine plugin in isolation, but in my opinion it’s just not worth the hassle. Let’s stick to the more pragmatic testing inside Redmine instead.
The added benefit of running tests this way is that your tests might help to discover conflicts with any other installed plugins early. Especially if users of your plugin run the tests in their environment.
Redmine plugin tests are invoked using a special rake task:
NAME=redmine_percent_done bin/rake redmine:plugins:test
Omitting the NAME
environment variable will run the tests of all installed
plugins.
The third revision of our plugin adds a basic test_helper
file and a test
for the already existing settings form. There is no need to check wether the
settings are saved properly - that is part of Redmine and already tested there.
I also added a (for now failing) test for what we actually want to implement - automatically changing % Done when an issue’s status changes.
Patching Redmine Core Classes
It’s about time to implement the core functionality of our plugin and make those tests pass.
For that, we add a simple before_save
hook to the core Issue
class. I won’t
go through the actual implementation here since there really isn’t much to
explain. Instead I want to point you to a few things worth considering if
you extend or otherwise change things in Redmine’s core classes:
- Keep all your patches (and any other code in
lib/
) in your plugin’s namespace, and name files and modules clearly and consistently. This is not optional. Having a top levelIssuePatch
module inlib/issue_patch.rb
is asking for trouble. Don’t do that. It might sound obvious but I wouldn’t stress this so much if things like this wouldn’t exist. - Do not
require
anything. Instead let the Rails auto loader do it’s thing and help it by naming your files according to the modules they declare. Against contrary belief it is possible to develop Redmine plugins without breaking automatic code reloading in development mode. Repeat after me: I will never userequire
to load parts of my Redmine plugin. By the way, this rule holds true for any Rails app. Everything you manuallyrequire
has the potential to break things in development mode. - Do not call
unloadable
anywhere. It is still part of some documentation and used in a bunch of plugins, but it is absolutely unnecessary nowadays.
If you look at the IssuePatch module in the next revision of the plugin, you will notice that I used prepend
to add the module with the hook method to the Issue
model.
In this case it has no benefit over doing an include
, it’s just my small contribution to spreading the word about this awesome feature of Ruby. So, prepend
is a
thing
and by using it you will never need to use alias_method_chain
again. Instead, just call super
as you would if you were inheriting from Issue
, for example.
Did I already say this is awesome?
Wrapping it up
Looks like our quick and easy Redmine plugin is finished and we can set issues to ‘resolved but not closed’ and get the done ratio set to 100% automatically.
If you write your own plugin and intend to make it public, don’t forget to add a README, and be clear about the license. This topic is worth an article on its own, I try to make it short:
- Redmine is licensed under the GPL, therefore any code interfacing with Redmine’s code, by definition, is also GPL-licensed. You cannot do anything to prevent anybody from sharing your plugin’s Ruby code (except of course keeping it secret in the first place - there is no obligation to share).
- In theory you can pick a different license for standalone Javascript code as well as for images or stylesheets that are part of your plugin (and not derived from any part of Redmine).
- Please spare the community the hassle of figuring out what they can and can not do with the plugin, and license everything under the same license as Redmine (or any later version of the GPL, which Redmine explicitly allows).
With that out of the way, go ahead and add it to the Redmine plugin directory.
That’s it for now, check out the finished plugin on Github!
PS: You might also be interested in Testing a Redmine Plugin With Travis CI.