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.rbat the top level, which introduces the plugin to Redmine
assetsdirectory at the plugin’s top level
- everything else (most notably the
configdirectories) follows Rails conventions.
- you may add a
Gemfileto 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.
init.rbclean and simple. Define a single setup method for your plugin, and do all your initialization and patching of Redmine core methods there. Use
require_dependencyto load the relevant file, and call your
setupmethod in a Rails
to_preparehook. 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
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
Next, create that file as
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
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 environment variable will run the tests of all installed
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 level
lib/issue_patch.rbis 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
requireanything. 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 use
requireto load parts of my Redmine plugin. By the way, this rule holds true for any Rails app. Everything you manually
requirehas the potential to break things in development mode.
- Do not call
unloadableanywhere. 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
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
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).
- 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.