Redmine How to write plugins

    In my last post I tried to describe in sufficient detail all the details of installing Redmine on Linux Ubuntu. In this, I want to talk about the intricacies of writing plugins for Redmine, about the main possibilities for changing the functionality of the standard Redmine, about the pitfalls that my team met 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 worth splitting all Redmine plugins into two categories:

    The first includes those plugins 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. The official site of Redmine hasnice tutorial detailing how to create a voting plugin .

    Everything is a little more complicated when the plugin needs 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. Let's 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 the folder that matches the name of the plugin, i.e. luxury_buttons folder (hereinafter referred to as the patch folder ). In this folder, in the future, there will be patch files of various Redmine methods.

    Why did we name this folder the same name as the plugin? This is just a recommendation, the folder can be called differently, but here the first pitfall arises: if in another plugin the name of this folder matches and the name of the patch file matches, then one of the patch files just won’t apply! Therefore, I recommend that you name the patch folder with the same name as the plugin. This method minimizes the occurrence of errors!

    When a plugin needs to add something in a view.

    Let's say we need to add something to the standard Redmine view. The easiest and most incorrect way to do this is to rewrite the view inside the plugin. This is usually done by copying the view file from the Redmine kernel to the appropriate directory of the plugin and then editing this file. For example, in one of our plugins, we rewrite the view with the request saving form.

    Why to do so badly:

    • You doom yourself to constantly monitor the relevance of your view. If something changes in the new version of Redmine in this view, then you will lose this functionality. Tracking the relevance of a view is pretty tricky.
    • If another plugin appears that rewrites the same view, then either your view or the view of another plugin will apply. Which view to apply depends on the order of the plugins.

    Therefore, it is better to use alternative methods.


    A hook in a view is a line of code that allows you to embed your content in a view. To find a hook, you just need to search for the “hook” substring in all Redmine files, or you can use this plate .

    Hook hook

    We try to store all view hook connections in a single file. This file must be included in init.rb like this:

    require 'luxury_buttons/view_hooks'

    The contents of the file itself can be like this:

    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")

    The name of the first module must match the name of the plugin, the second - with the name of the patch folder .

    Inside the class are functions that show in which hook which template to render.

    If two plugins will use the same hook, then the contents of both the first and second plugins will appear in the view. Those. hooks do not rewrite each other.

    There are two problems with hooks:
    • A hook may not be.
    • Sometimes you need to remove something from the view, and the hook allows you to add only.

    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 rendered.

    For this, the easiest way 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 a js file with the logic of cutting or adding certain DOM elements. So that this js-file is not loaded on pages on which it is not needed, it is better to load its load into a conditional expression. Those. cut off file download by action and controller. For instance:

      <% if controller_name == 'issues' && action_name == 'update' %>
        <%= javascript_include_tag :luxury_buttons_common, :plugin => :luxury_buttons %>
      <% end %>

    The plugin's assets / javascript folder should contain the file “luxury_buttons_common.js”:

    //логика вырезания или добавления элементов на страницу

    Sometimes, it’s more competent to embed the js-file connection string not through the “view_layouts_base_html_head” hook, but through a specific hook that embeds 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 we can use the “view_issues_form_details_bottom” hook.

    In this case, so that the file is not connected to the document body, but to the header, you need to use the construction:

    <% content_for :header_tags do %>
      <%= javascript_include_tag :luxury_buttons_common, :plugin => :luxury_buttons %>
    <% end %>

    True, with the “content_for” method in plugins, difficulties arise from version to version .

    How to change the methods of models, controllers and helpers.

    Changing (patching) methods is in many ways similar to changing views and brings similar problems.

    Hooks in controllers and models

    There are hooks in controllers and models too. They connect 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'

    The patch directory should contain the 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']

    The name of the module must match the name of the plugin, the name of the class with the name of the file.

    In this case, we realize the possibility of automatically assigning a new task to the author.

    Patch methods

    Like in views, the necessary hooks in Redmine are not always there. And then you need to patch the methods of the model, helper or controller.

    First, you need to include 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)

    In the patch folder should be the file "issue_patch.rb", approximately the following contents:

    module LuxuryButtons
      module IssuePatch
        def self.included(base)
          base.send(:include, InstanceMethods)  
          base.class_eval do  
            alias_method_chain :read_only_attribute_names, :luxury_buttons
        module ClassMethods   
        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


    alias_method_chain :read_only_attribute_names, :luxury_buttons

    we spawn 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 read_only_attribute_names model method, the second method is an alias for the standard read_only_attribute_names method.

    By combining the two methods, you 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 the values ​​to this array.

    If something changes in the standard Redmine method in the new version, then the chances that our patch will work correctly are much greater than if we simply rewrote the standard Redmine method and add our own logic to it.

    Important! Redmine observedsome problems with patching the User model . For correct patching, you must 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 for Redmine. I tried to collect the basic methodologies and pitfalls. Hope the article will be helpful.

    Also popular now: