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
Hi,
I have followed your instructions and the plugin works, but I get an error 500 when I try to access the admin page /related-issues
By the way, it looks like your redmine instalation is broken:
http://redmine.gogolek.co.uk/
Thanks, regards
I’ve been struggling with redmine plugins for a bit, but this post was very helpful. Cheers for that. I’m writing my own plugin at the moment.