Writing Redmine 2.x plugins tutorial

This is a simple tutorial for creating a redmine plugin. My example is based on “Latest Issues” plugin, which you can find on github.

First make sure your plugin will be set up for correct environment

$ export RAILS_ENV="production" 

Now that the environment was set, lets run a plugin creation script. For the purposes of this tutorial, I’m going to be building latest_issues plugin

$ ruby script/rails generate redmine_plugin LatestIssues 

This should generate output as per below:

$ ruby script/rails generate redmine_plugin LatestIssues 

create plugins/latest_issues/app 
create plugins/latest_issues/app/controllers 
create plugins/latest_issues/app/helpers 
create plugins/latest_issues/app/models 
create plugins/latest_issues/app/views 
create plugins/latest_issues/db/migrate 
create plugins/latest_issues/lib/tasks 
create plugins/latest_issues/assets/images 
create plugins/latest_issues/assets/javascripts 
create plugins/latest_issues/assets/stylesheets 
create plugins/latest_issues/config/locales 
create plugins/latest_issues/test 
create plugins/latest_issues/README.rdoc 
create plugins/latest_issues/init.rb 
create plugins/latest_issues/config/routes.rb 
create plugins/latest_issues/config/locales/en.yml 
create plugins/latest_issues/test/test\_helper.rb 

Let’s set up the basic plugin information in plugins/latest_issues/init.rb file:

Redmine::LatestIssues.register
    :latest_issues do name 'Latest Issues' 
    author 'John Smith' 
    description 'Displays latest issues lodged with Redmine'
    version '0.0.1' 
end 

Creating the model

Users should be able to configure the plugin, so let’s create a model that will allow us to store the default setup and any changes that the administrator made to the plugin:

$ ruby script/rails generate redmine_plugin_model latest_issues latest_issues_setup max_count:integer side:string 

you will notice quite a few new files have been created. I want to specify the default constants that the script will start with. When no other values were set the script will display 5 latest issues, on the left hand side of the Redmine home page. Edit the app/models/latest_issues_setup.rb and set up those constants:

class LatestIssuesSetup < ActiveRecord::Base
    unloadable
    attr_accessible :max_count, :side
    DEFAULT_SIDE = 'left'
    DEFAULT_COUNT = 5
end

The model is set up, so let’s create the database using rake. It’s the same command you will run if you make any changes to your db setup, or if you’re installing new plugin. Adding RAILS_ENV ensures you will be using the correct environment.

$ rake redmine:plugins:migrate --trace RAILS_ENV=production 

Creating the controller

Now that the model is set up, let’s create the controller that will allow for saving of the setup for our plugin. Let’s call it LiSetup. The command below will create our controller, with one action – index

$ ruby script/rails generate redmine_plugin_controller latest_issues li_setup index 

This will automatically create both controller and the views for it. Let’s start with the index action. It simply needs to load the current Latest Issues Setup model.

$ vim app/controllers/li_setup_controller.rb 



class LiSetupController < ApplicationController  
    unloadable  
    def index  
        setup = LatestIssuesSetup.find\_by\_id(1)  
        if setup == nil   
            setup = LatestIssuesSetup.create(:max\_count => LatestIssuesSetup::DEFAULT\_COUNT, :side => LatestIssuesSetup::DEFAULT_SIDE)   
        end   
        @setup = setup  
    end   
end  

We’re only just going to have one record for the setup, and we will update it each time the change happens, so I’m hardcoding the ID as 1, if the record doesn’t exist, I’m creating it based on the default values declared in the model. @setup assigns the variable to the view.

Now for our view, let’s display a form

$ vim app/views/li_setup/index.html.erb 

<h2>Latest Issues Configuration</h2>
<%= form_tag("/latest-issues/change") do %> 
    <dl>
        <label>Number of issues on page:</label> 
        <input type="text" maxsize="2" name="count" value="<%= @setup.max_count %>"/> 
    </dl><dl> 
        <label>Which side do you want the plugin on?</label>
        <select name="side">
            <option value="left" <% if @setup.side == 'left' %>selected<% end %>>left</option>
            <option value="right" <% if @setup.side == 'right' %>selected<% end %>>right</option>
        </select>
    </dl>
    <dl>
        <input type="submit" value="Save"/>
    </dl>
<% end %> 

Please notice the form_tag(). Redmine checks each form for authentication tokens, to prevent malicious attacks. Posting a form that was not created using form_tag will return errors.

Another thing worth noticing is the url to which we will be posting our values /latest-issues/change. This will be a POST only action, that we can now create in LiSetup Controller

def change 
    setup = LatestIssuesSetup.find_by_id(1) 
    setup.max_count = params[:count] 
    setup.side = params[:side] 
    if setup.save 
        flash[:notice] = 'Latest Issues setup saved.' 
    end 
    redirect_to "/latest-issues"
end 

As you can see this action will update the LatestIssuesSetup record, and redirect user back to the main page, while displaying a success message.

The last thing to do, to make our controllers work is adding the correct routes

$ vim config/routes.rb 

post 'latest-issues/change', :to => 'li_setup#change' 
get 'latest-issues', :to => 'li_setup#index' 

And adding the correct menu path in our init.rb, in the register section

$ vim init.rb

Redmine::Plugin.register :latest_issues do 
    (...) 
    permission :li_setup, { :li_setup => [:index, :change] }, :public => true 
    menu :admin_menu, :latest_issues, {:controller => 'li_setup', :action => 'index'}, :caption => 'Latest Issues'
end 

If you restart your Redmine, you should be able to see the link to Latest Issues setup page, in the Administration menu. Other menu options available, are listed below:

top_menu – the top left menu

account_menu – the top right menu with sign in/sign out links

application_menu – the main menu displayed when the user is not inside a project

project_menu – the main menu displayed when the user is inside a project

admin_menu – the menu displayed on the Administration page (can only insert after Settings, before Plugins)

Creating the helper

The most important part of the plugin will be the view helper displayed on the home page. If you view redmine/app/views/welcome/index.html.erb you will see two lines that call the hooks.

<%= call_hook(:view_welcome_index_left, :projects => @projects) %>
(...)
<%= call_hook(:view_welcome_index_right, :projects => @projects) %> 

This means that you can display your helper either in left (view_welcome_index_left), or right (view_welcome_index_right) column. Please note that the hook names are different in each template.

Now, let’s create our hook

$ vim plugins/latest_posts/lib/latest_posts/view_hook_listener.rb 

The helper will consist of 4 functions.

Let’s start with loading of the setup login. We will have to include the model for it, and load the basic configuration in load_setup function

module LatestIssues 
    class ViewHookListener < Redmine::Hook::ViewListener 
        def load_setup()
            require 'plugins/latest_issues/app/models/latest_issues_setup.rb' 
            setup = LatestIssuesSetup.find_by_id(1) 
            if setup == nil 
                count = LatestIssuesSetup::DEFAULT_COUNT 
                side = LatestIssuesSetup::DEFAULT_SIDE 
            else 
                count = setup.max_count 
                side = setup.side 
            end 
            {:count => count, :side => side} 
        end 
    end 
end 

Next function, load_issues, will take care of displaying the issues in the view. It will iterate through the fetched issues, and display their subject, date created and whether they’re assigned to anyone.

Note I have tried using render_on to move the display out of the helper and into the actual HTML template, but it would seem to “cache” both issues and my setup values (count + side). The values would stay the same until I would restart Redmine. I assume it’s something to do with how the templates are compiled, but don’t really understand it, if anyone does, please shed some light on it.

def load_issues(count) 
    html = '<div class="box" id="statuses"><br />' 
    html += '<h3 class="icon22 icon22-users">Latest Issues</h3><ul>' 
    issues = Issue.find(:all, :limit => count, :order => "created_on DESC") 
    issues.each do |issue|
        html += <<EOHTML</p> <li>
         #{link_to h(truncate(issue.subject, :length => 60)), 
            :controller => 'issues', :action => 'show', :id => issue }
         (#{format_time(issue.created_on)}) 
        #{ issue.assigned_to ? 'assigned to ' + link_to_user(issue.assigned_to) : " not assigned" } 
        </li> <p> EOHTML 
    end 
    html += '</ul> </div> <p>' 
    return html 
end 

Finally view_welcome_index_left and view_welcome_index_right will deal with display for the home page on either of the sides.

def view_welcome_index_left(context={})
    setup = load_setup() 
    if setup[:side] == "left" 
        load_issues(setup[:count])
    end 
end 

def view_welcome_index_right(context={}) 
    setup = load_setup() 
    if setup[:side] == "right" 
        load_issues(setup[:count]) 
    end 
end 

Summary

If you would like to download the whole project get it on github https://github.com/kgogolek/latest-issues-redmine-plugin.

If you found a bug, or if you have a feature you would like to add, please log it at the projects issue tracker here: http://redmine.gogolek.co.uk/projects/redmine-latest-issues