Redmine’s project settings consist of a series of tabs for things like general project information, project members, issue categories and so on. If your plugin has any project specific configuration of any kind, it might be a good thing to add an entry there for it, as well. But how? Let’s see how the row of tabs is rendered:
This method resides in the
ProjectsHelper module and as such is automatically available in any views rendered by
ProjectsController. At first sight, not very extension-friendly, especially when compared to the other menus in Redmine which make it very easy to add new entries through the
Old and busted: alias_method_chain
Many plugins extend that method using the
alias_method_chain mechanism like this:
That example stems from the issue templates plugin and was slightly shortened for clarity.
So why is this bad?
alias_method_chain calls once could were all over the place in the Rails code base, but nowadays it is frowned upon. Deprecated in Rails 4, removed from Rails 5 (which Redmine 4 will be using), this is definitely not the way to go. Of course you could ship your own implementation of
alias_method_chain or simply do the aliasing by hand, but luckily there’s a better way. But first, another wrong approach that looks good at first:
Instead of using aliasing, we can simply override the
Looks nice and makes use of
prepend which is awesome and modern, right? But in this case it’s not a good choice.
Why? Because modifying
ProjectsHelper (or any other rails helper for that matter) may or may not lead to the desired result, depending on the fact what code has already been loaded and what not.
If you manage to change the helper module early enough (to be precise: before the corresponding controller is loaded), your changed method will make it into the views, but if you are to late, your modification will still be successful but views will use the ‘original’ version of the method.
Since your plugin will probably have to co-exist with a bunch of other plugins, and you never know what these do (i.e. it’s not so uncommon to extend
ProjectsController - once another plugin does that, and it’s loaded before yours, it’s game over for your helper patch), you cannot rely on this.
The fact that
prepend is ineffective when the module has been included already but method aliasing still works has to do with how Ruby internally handles these things - let’s just say
prepend creates a new version of the module under the old name, while method aliasing just switches around handles to methods in the existing module.
So, to come to the point of this article, what should you do to add your plugin’s project settings tab?
Declare a new helper
It’s that simple. Just declare a new helper and tell the controller to make use of it: ~~~~ruby module SomePlugin module ProjectSettingsTabs def self.apply ProjectsController.send :helper, SomePlugin::ProjectSettingsTabs end
end end ~~~~
As you can see, the implementation of the new
project_settings_tabs method is identical to the previous approach, including the call to
By explicitly referring to the
ProjectsController class we make sure it, and by extension
ProjectsHelper, is loaded.
Since we do not have control what code other plugins load, our safest bet is to load whatever concerns us to create a known state and go on from there.
Depending on code not being loaded on the other hand is never going to work reliably.
The call to super in this case works because the view context, at the time our helper is included, already holds the implementation of
project_settings_tabs from Redmine’s
super simply refers to the ‘old’, already existing method which we are replacing, even if it comes from another module.