Extending Redmine view templates in your plugins (new way)

Added by Anton Argirov over 8 years ago

As you know, there are only two ways of extending Redmine views in plug-ins: use view hooks and rewrites. I want to introduce a new way of doing this.
For a start, I'll remind you about what Redmine offers "out of the box":

View hooks

View hooks are entry points, which you can use to insert your content on a page. For an example look into app/views/issues/index.html.erb:

...
<% html_title(!params[:query_id] ? l(:label_issue_plural) : @query.name) %>
<%= call_hook(:view_issues_index_top, { :issues => @issues, :project => @project, :query => @query }) %>
<%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
            :method => :get, :id => 'query_form') do %>
...

What does call_hook(:view_issues_index_top, {...})? It iterates through hook listeners and calls ones, which were registered with the name :view_issues_index_top. Such way each plug-in can start listen to this hook and render own content on the top of the issues index page. There is an example how to define own hook listener:

class MyPluginHook < Redmine::Hook::ViewListener
  render_on :view_issues_index_top, :partial => 'my_plugin/view_issues_index_top'
end
Which will render my_plugin/view_issues_index_top partial in a place on the issues page where call_hook() was called.
This is a simple and clear way of extending Redmine views. But there are some limitations:
  • Number of view hooks in Redmine is relatively small. You may want to extend (for your plug-in needs) wiki/index.html.erb, news/index.html.erb, boards/show.html.erb or files/index.html.erb views of Redmine, for example, to insert own contextual actions. But you can't. Because Redmine has no any hooks on these pages.
  • You can not alter the content of an original view, only insert the content in specific places on a page. There are some cases when you need to do this in your plug-in, but you can't. Of course, you can use hook and insert some javascript оn a page, that will alter the page on the client-side, but it also not possible, when the page has no hooks at all. It can also lead to flickers when you try to change the page dynamically because browsers are not able to do such things instantly.

View rewrites

Redmine developers offered another way of extending views that doesn't use hooks. You can replace (rewrite) an original Redmine view with the same named one in your plug-in. For example, create the file with the following path:

/plugins/my_redmine_plugin/app/views/issues/index.html.erb

And you will see that it replaces Redmine's one:

/app/views/issues/index.html.erb
This is achieved by so called "view path prepending". How does it work? Rails has a special storage containing an ordered array of paths (view paths). It is used to search a path in which the requested view template is located. When Rails starts, it has only one path in this storage: /app/views. Then Redmine prepends it by all plugins' view paths (/plugins/my_redmine_plugin/app/views). If Rails has been requested to render a template (e.g. issues/index.hmtl.erb), it scans through this array joining each path one by one with the template file name to find аn existing file. And it stops when the first suitable file is found. That's why we can replace (rewrite) original Redmine view templates.
Although we can now freely change any page content, it turns out that there are several problems:
  • You can not render the original (rewritten) page from your template. So you can not render /app/views/issues/index.html.erb from /plugins/my_redmine_plugin/app/views/issues/index.html.erb. This forces you to copy all content from one template to another and then change it.
  • What if the original template's content will change in the next Redmine version?
  • What if someone else will need to replace the same template?

These are the reasons for which I try to completely avoid usage of rewrites.

Extending views (new way)

What if we had a way to render a template that we had rewritten? For example:

# /plugins/my_redmine_plugin/app/views/issues/index.html.erb

Some content before
<%= render :parent, {:you_can_pass => :locals} %>
Some content after
render :parent - is a render helper extension for Rails. It renders a template that is located after the current template on the view path, in our case it's /app/views/issues/index.html.erb. Now we can insert some content before or after the original content. We can also store a rendered content of the original template in a variable and even change it. Moreover, if you have several plugins that extend the same template with render :parent, changes will apply consequently in order in which plugins are loaded.

I did made a special gem, called render_parent, that provides this functionality for both Rails 2 & 3. So you can make your own plugins compatible with Redmine 1.x and 2.x versions. To use it just create Gemfile or add this string to existing Gemfile:

gem 'render_parent', '>= 0.0.4'

First time I made use of it in my Anonymous Watchers plug-in. I extended watchers/new, watchers/watchers, issues/index, files/index, documents/index, news/index templates, added some additional fields and contextual actions. You can download the plugin and find out how it's made.

Changing the original template's content

As I mentioned, you can store the content rendered by render :parent in a variable and then change it later:

<% content = render :parent %>
# change stored content...
<%= content %>

The best way to manipulate the stored content is Nokogiri, library that allows to parse and change HTML content. Add it to your Gemfile:

gem 'nokogiri', '>= 1.5.5'

Now you can do some magic "on-the-fly":

<% html = Nokogiri::HTML.fragment(render(:parent)) %>
<% html.at_css("div.contextual") << link_to(l(:label_something), something_url, :class => "icon icon-fav")
<%= raw html %>

This inserts a new link into the context menu of the original page (it should of course contain <div class="contextual"> somewhere on a page)
We can also insert ERB:

<% html = Nokogiri::HTML.fragment(render(:parent)) %>
<% html.at_css("p.buttons").before(capture do %>
  <p><%= text_field_tag 'some_name' %></p>
<% end) %>
<%= raw html %>

This inserts a new text field before paragraph with buttons. Of course if there is no <p class="buttons"> element on the page, at_css("p.buttons") will return nil and this code will crash, so beware.

Please read the Nokogiri documentation if you have any questions about how to manipulate HTML code.

Limitations

render_parent and nokogiri gems offer a flexible way to extend original Redmine view templates, but these are also some issues with this method:
  • Nokogiri is heavy. Each time we request a page, Nokogiri will parse a HTML fragment and if we have several plugins that redefine the same page with render :parent and Nokogiri, we'll parse the page several times. Nokogiri is the fast library, but for the bulky pages this method may be quite slow. So consider redefine only partials and other small templates.
  • You can not access inner variables of the original page. For example, if the original page has a form (<%= labelled_form_for @some do |f| %> and you want to insert a new form element, you can not simply access variable f and use form helpers like f.text_field. You should use low-level (text_field_tag) helpers.

Another possible method to overcome limitations

Mentioned limitations may be overcomed by using deface gem. This gem allows to modify view templates before it rendered. It is even more flexible and also is fast as plain ERB. But it is only for Rails 3 and it still needs some work to make it compatible and easy to use with Redmine. In future maybe I will consider using this gem in my plugins.

About

If you are interested, I started the development of Redmine with extended functionality for our company needs over year ago, and now I've already published several plugins and have some new plugins to release in my plans. So visit http://redmine.academ.org, maybe you'll find something useful for yourself and your Redmine.

Replies (2)

RE: Extending Redmine view templates in your plugins (new way) - Added by Paulo Neves over 7 years ago

Really cool summary. In the end i got the sensation that all these twists and turns would be solved by making more hooks available. Why is that not done? Are there penalties?

Best Regards
Paulo Neves

RE: Extending Redmine view templates in your plugins (new way) - Added by Ben Cochran over 7 years ago

Where are these plugins mentioned in the "About" section published? http://redmine.academ.org has a login prompt and no way to register.

(1-2/2)