Extending Redmine Using Hooks - Redmine Plugin Extension and Development (2014)

Redmine Plugin Extension and Development (2014)

Chapter 2. Extending Redmine Using Hooks

Redmine, at its core, is a project management and issue tracking system. Its developers have invested a lot of time and energy into building an extremely robust solution that rivals even proprietary competitors, but we occasionally find ourselves wishing we could perform a certain task or see a piece of information differently.

Thankfully, Redmine was designed with extensibility in mind. Not only is there a plugin system in place to allow custom functionality to be implemented, but core features can be extended using a system of hooks and callbacks.

In this chapter, we will dive into the various classifications of hooks and how our plugin can leverage them to add new functionality to existing Redmine systems and components.

We will cover the following topics in this chapter:

· An introduction to what a hook is

· What types of hooks exist and where they can be used

· An example view hook implementation

Understanding hooks

A hook is essentially just a listener for which we've registered a callback function. These callback functions expect a single parameter: a hash that provides some context to the function. The contents of the hash depend on what type of hook is being evaluated.

There are four basic categories of hooks available in Redmine:

· View hooks

· Controller hooks

· Model hooks

· Helper hooks

For view and controller hooks, the context hash contains the following fields as well as data specific to the hook being used:

· :project: This is the current project

· :request: This contains the current web request instance

· :controller: This contains the current controller instance

· :hook_caller: This holds the object that called the hook

Note

The full list of available hooks is maintained at http://www.redmine.org/projects/redmine/wiki/Hooks_List.

To quickly build the hook list for the version of Redmine you have installed, run the following commands:

cd /path/to/redmine/app

grep -r call_hook *

By doing this from the app directory, we prune out any results from the hook class definition or any of the test files.

Redmine has many hooks registered throughout the codebase by means of the call_hook method, whose syntax is as follows:

call_hook(hook, context={})

For example, the partial /path/to/redmine/app/views/issues/_form.html.erb contains the following hook declaration:

<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>

View hooks

The primary use of hooks in Redmine is to inject functionality into an existing view.

A view hook is executed while the HTML code of a view is being rendered.

View hooks are likely to be the most frequently used type of hook by plugin authors. Through these hooks, we can add functionality from our plugins to existing Redmine views and partials.

As an example, let's add the ability to associate knowledgebase articles with an issue. We'll implement this in a similar fashion to how issues can be associated with each other.

In order to display this association, we will extend the relevant issue views using view hooks. To accomplish this, the first step is to create a class that extends Redmine::Hook::ViewListener:

module RedmineKnowledgebase

class Hooks < Redmine::Hook::ViewListener

render_on :view_issues_show_description_bottom,

:partial => 'redmine_knowledgebase/hooks/view_issues_show_description_bottom'

end

end

This file will be saved to our plugin's lib folder as /path/to/redmine/plugins/redmine_knowledgebase/lib/hooks.rb.

To include the custom hook in our plugin, the hooks.rb file will simply need to be added to the plugin's init.rb file as a requirement.

The preceding hook implementation is done using the render_on helper method, which facilitates rendering a partial using the context.

In the following sample, we'll accomplish the same result by defining the callback method ourselves and manually configuring the context object:

module RedmineKnowledgebase

class Hooks < Redmine::Hook::ViewListener

def view_issues_show_description_bottom(context = {})

# the controller parameter is part of the current params object

# This will render the partial into a string and return it.

context[:controller].send(:render_to_string, {

:partial => " redmine_knowledgebase/hooks/view_issues_show_description_bottom",

:locals => context

})

# Instead of the above statement, you could return any string generated

# by your code. That string will be included into the view

end

end

end

When this hook is called and a callback has been registered, it will yield raw HTML code that will be inserted in the following issue form details:

View hooks

In our example, we've added an Articles section to the issues of the current project. Note that the actual implementation code for this is not covered as it goes a bit out of the scope of this book.

Controller hooks

Controller hooks allow custom functionality to be injected into an existing process. A normal use-case for this type of hook is to perform some custom validation on the context object provided to the callback.

In /path/to/redmine/app/models/issue.rb, there is a hook registered for controller_issues_edit_before_save. In order to take advantage of this hook, we would have to provide our own callback function. This can be done as follows:

module Knowledgebase

module Hooks

class ControllerIssuesEditBeforeSaveHook < Redmine::Hook::ViewListener

def controller_issues_edit_before_save(context={})

if context[:params] && context[:params][:issue]

if User.current.allowed_to?(:assign_article_to_issue, context[:issue].project)

if context[:params][:issue][:article_id].present?

article = KbArticle.find_by_id(context[:params][:issue][:article_id])

if article.category.project == context[:issue].project

context[:issue].article = article

end

else

context[:issue].article = nil

end

end

end

return ''

end

end

end

end

Once registered, this hook will check to see whether the current user has permission to attach a knowledgebase article to an issue before saving the issue.

Model hooks

These hooks are used even less frequently than controller hooks but are being included here for completeness.

Model extension is better handled through the use of new methods or encapsulation of existing methods by means of the alias_method_chain pattern. For a summary of alias_method_chain see http://stackoverflow.com/a/3697391.

A common use-case for model hooks is the :model_project_copy_before_save hook as this can be used to replicate content from our plugin that belonged to a specific project if that project is copied:

module RedmineKnowledgebase

class Hooks < Redmine::Hook::ViewListener

def model_project_copy_before_save(context = {})

source = context[:source_project]

destination = context[:destination_project]

if source.module_enabled?(:redmine_knowledgebase)

# TODO: clone all categories

# TODO: clone all articles

# TODO: ensure cloned articles refer to cloned categories

end

end

end

end

The actual implementation has been left out in the preceding snippet, but placeholders have been left intact to illustrate what actions we could be taking.

Helper hooks

According to the official Redmine hooks list, there is only a single helper hook currently available (http://www.redmine.org/projects/redmine/wiki/Hooks_List#Helper-hooks). The :helper_issues_show_details_after_setting hook is called when journal details are being rendered in an issue and can be used to override the label and value that is passed to the journal entry.

A sample view hook implementation

We will be glossing over a lot of implementation details as they are out of the scope for this book, but the full code will be available on the GitHub repository at https://github.com/alexbevi/redmine_knowledgebase.

Identifying the callback

We've determined that our plugin will be hooking into the existing issue tracking system in order to allow users to attach knowledgebase articles.

The desired functionality is the same as the Subtasks functionality that already exists, so we will model our hook after that.

Our first step is to determine which hook best suits our needs. In order to add additional functionality to the existing issues#show view, we will choose the :view_issues_show_description_bottom hook as it allows us to insert a partial just below the standard issue details form, as indicated in the following screenshot:

Identifying the callback

With the desired view hook identified, we need to define a listener class and tie that into our plugin initialization code.

Integrating the hook

The necessary code to define our new listener will be placed in lib/redmine_knowledgebase/hooks.rb and will be defined as follows:

module RedmineKnowledgebase

class Hooks < Redmine::Hook::ViewListener

render_on :view_issues_show_description_bottom,

:partial => 'redmine_knowledgebase/hooks/view_issues_show_description_bottom'

end

end

In order to include this new class in our plugin, it just needs to be required in our init.rb file:

require_dependency 'redmine_knowledgebase/hooks'

Note that if we want the contents of our included classes and modules to be reloaded during development or to keep them from potentially overwriting content defined by other plugins, we should encapsulate them in a Rails.configuration.to_prepare block.

See http://guides.rubyonrails.org/configuring.html#configuring-action-dispatch for more information.

Creating the view partial

As referenced in our hooks.rb file, the callback for our hook is actually a view partial.

This partial will be created at app/views/redmine_knowledgebase/hooks/_view_issues_show_description_bottom.html.erb and can be defined as follows:

<% if @project.module_enabled?(:knowledgebase) %>

<div class="contextual">

<% if User.current.allowed_to?(:manage_issue_articles, @project) %>

<%= toggle_link l(:button_add), 'new-article-form', { :focus => 'article_issue_to_id' } %>

<% end %>

</div>

<p><strong><%=l(:label_article_plural)%></strong></p>

<%= form_for @article, {

:as => :article, :remote => true,

:url => issue_articles_path(@issue),

:method => :post,

:html => {:id => 'new-article-form', :style => (@article ? '' : 'display: none;')}

} do |f| %>

<%= render :partial => 'hook/issue_articles/form', :locals => {:f => f} %>

<% end %>

<% end %>

The preceding partial has been stripped down to show the bare minimum, that is, a reduced view with an Add button that reveals a search form on being clicked, as seen in the following screenshot:

Creating the view partial

Please note a couple of things about the preceding code:

· @project.module_enabled?(:knowledgebase) is used to check whether the project module provided by our plugin, as we've defined in the plugin's init.rb file, has been toggled in the project settings. If it is disabled, we just hide everything (the search form and any associated articles).

· User.current.allowed_to?(:manage_issue_articles, @project) references a project module permission we've defined in our init.rb.

Summary

There are a number of different types of hooks available within Redmine, but the odds are that most use-cases we'll encounter will call for view hooks.

In this chapter, we were introduced to the various types of hooks Redmine provides as well as some sample implementations of each. We also implemented a basic view hook, from which we gained a better understanding of the hook implementation and integration process.

In the next chapter, we will cover the permission registration process in detail and will discuss how plugin permissions are administered and enforced.