In
my previous post, I tried to describe in detail all the details of installing Redmine on Linux Ubuntu. In this, I want to talk about the subtleties of writing plug-ins under Redmine, about the main possibilities of changing the functionality of the standard Redmine, about the pitfalls that met my team along the way.
I think this article will be useful to those who are already familiar with the basics of the Ruby on Rails framework and want to start developing plugins for Redmine.
First of all, it is necessary to divide all Redmine plug-ins into two categories:
')
The first one includes those plug-ins that do not actually affect the functionality of the standard Redmine. In fact, these are ordinary Rails applications inside Redmine, there are few difficulties with them, so they are of little interest. There is a
good tutorial on the official Redmine website
that describes in detail how to create a plugin for voting .
Everything is a little more complicated when the plugin has to change the built-in functionality!
Let's start with the team that creates the folder structure for the Redmine plugin. Let our plugin be called Luxury Buttons. Go to the root folder of Redmine, run the command that creates the folder structure:
$cd /usr/share/srv-redmine/redmine-2.3 $rails generate redmine_plugin LuxuryButtons
After executing the command, the luxury_buttons folder should appear in the plugins folder with the following structure:

In the lib folder, you should immediately add a folder that matches the name of the plugin, i.e. folder luxury_buttons (hereinafter the patch
folder ). In this folder, further on, there will be patch files of various Redmine methods.
Why did we call this folder as the plugin was called? This is just a recommendation, the folder can be called differently, but here comes the first pitfall: if in another plugin the name of this folder matches, and the name of the patch file matches, then one of the patch files simply does not apply! Therefore, I recommend calling the
patch folder with the same name as the plug-in. This method minimizes the occurrence of errors!
When a plugin should add something to the view.
Suppose we need to add something to the standard Redmine view. The easiest and most inappropriate way to do this is to rewrite the view inside the plugin. This is usually done by copying a view file from the Redmine core to the corresponding plugin directory and further editing this file. For example, in one of our plugins, we rewrite the view with the form for saving the request.

Why do so badly:
- You doom yourself to constant monitoring of the relevance of your view. If something changes in the new version of Redmine in this view, you will lose this functionality. Monitoring the relevance of a view is quite difficult.
- If another plugin appears that rewrites the same view, either your view or another viewport will be applied. Which view is used depends on the order of the plugins.
Therefore, it is better to use alternative methods.
Hooks
A hook in a view is such a line of code that allows you to embed your content into the view. To find a hook, you just need to search for the substring "hook" in all Redmine files, or you can use
this table .
Hook connection
We try to keep all hook connections in a single file. This file needs to be included in init.rb like this:
require 'luxury_buttons/view_hooks'
The contents of the file itself may be:
module LuxuryButtons module LuxuryButtons class Hooks < Redmine::Hook::ViewListener render_on( :view_issues_form_details_top, :partial => 'lu_buttons/view_issues_form_details_top') render_on( :view_layouts_base_html_head, :partial => 'lu_buttons/page_header') render_on( :view_issues_show_description_bottom, :partial => "lu_buttons/button_bar" ) render_on( :view_issues_history_journal_bottom, :partial => "lu_buttons/journal_detail") end end end
The name of the first module should coincide with the name of the plug-in, the second with the name of the
patch folder .
Inside the class, there are functions that show which hook should be rendered to which template.
If two plugins use the same hook, then the contents from both the first and second plugins will appear in the views. Those. hooks do not rewrite each other.
There are two problems with hooks:
- Hook may not be.
- Sometimes you need to remove something from the view, and the hook allows you to only add.
The only way we found to solve these problems is to use jQuery to modify the content on the page when the view has already been rendered.
The easiest way to do this is to use the view_layouts_base_html_head hook; it allows you to insert content into the page header. We need to insert a link to connect the js-file with the logic of cutting or adding certain DOM elements. In order for this js file not to be loaded on pages where it is not needed, it is better to load its loading into a conditional expression. Those. to cut the file loading by the action and the controller. For example:
<% if controller_name == 'issues' && action_name == 'update' %> <%= javascript_include_tag :luxury_buttons_common, :plugin => :luxury_buttons %> <% end %>
The folder “luxury_buttons_common.js” must be in the folder's assets / javascript folder:
jQuery(document).ready(function(){
Sometimes, more competently, embed the js-file connection string not through the hook “view_layouts_base_html_head”, but through a certain hook that embeds the content on a limited number of pages that we need. For example, if we need to add or cut something on the task page, then you can use the "view_issues_form_details_bottom" hook.
In this case, in order for the file to be connected not into the body of the document, but into the header, you need to use the construct:
<% content_for :header_tags do %> <%= javascript_include_tag :luxury_buttons_common, :plugin => :luxury_buttons %> <% end %>
True, with the method of "content_for" in plug-ins, from version to version
there are difficulties .
How to change the methods of models, controllers and helpers.
Changing (patching) methods is in many ways similar to changing views and has similar problems.
Hooks in controllers and models
In controllers and models there are also hooks. They are connected differently. In init.rb there should be a line that connects a specific hook. For example, a hook that is called before saving a new task:
require 'luxury_buttons/controller_issues_new_before_save_hook'
In the patching directory there should be a file “controller_issues_new_before_save_hook.rb”, for example, with the following contents:
module LuxuryButtons class ControllerIssuesNewBeforeSaveHook < Redmine::Hook::ViewListener def controller_issues_new_before_save(context={}) if context[:params] && context[:params][:issue] if (not context[:params][:issue][:assigned_to_id].nil?) and context[:params][:issue][:assigned_to_id].to_s=='' context[:issue].assigned_to_id = context[:issue].author_id if context[:issue].new_record? and Setting.plugin_luxury_buttons['assign_to_author'] end end '' end end end
The name of the module must match the name of the plug-in, the name of the class with the name of the file.
In this case, we will implement the ability to automatically assign a new task to the author.
Method patching
As in views, the necessary hooks in Redmine are by no means always. And then you need to patch the methods of the model, helper or controller.
First, you need to connect the patch file in init.rb. For example, we need to patch the “read_only_attribute_names” method of the “Issue” model.
Rails.application.config.to_prepare do Issue.send(:include, LuxuryButtons::IssuePatch) end
The
patching folder should contain the file “issue_patch.rb”, of the following content:
module LuxuryButtons module IssuePatch def self.included(base) base.extend(ClassMethods) base.send(:include, InstanceMethods) base.class_eval do alias_method_chain :read_only_attribute_names, :luxury_buttons end end module ClassMethods end module InstanceMethods def read_only_attribute_names_with_luxury_buttons(user) attribute = read_only_attribute_names_without_luxury_buttons(user) if Setting.plugin_luxury_buttons['hidden_fields_into_new_issue_form'] && new_record? hidden_fields = Setting.plugin_luxury_buttons['hidden_fields_into_new_issue_form'] attribute += hidden_fields attribute end attribute end end end end
By construction
alias_method_chain :read_only_attribute_names, :luxury_buttons
we generate two methods, "read_only_attribute_names_with_luxury_buttons" and "read_only_attribute_names_without_luxury_buttons".
The first method will now be called instead of the standard method of the read_only_attribute_names model, the second method is an alias for the standard read_only_attribute_names method.
The combination of the two methods can patch the standard Redmine method. In our example, we first call the standard Redmine method, which returns an array of values, and then add values ​​to this array.
If something changes in the standard Redmine method in the new version, then the chances that our patching will work correctly are much more than if we simply rewrote the standard Redmine method by adding our own logic to it.
Important! There are
some problems with patching User model in Redmine. For correct patching, you need to explicitly include the following files:
require_dependency 'project' require_dependency 'principal' require_dependency 'user'
The article does not contain everything that I would like to say about writing plugins under Redmine. I tried to collect basic methodologies and pitfalls. I hope the article will be useful.