Writing plugins compatible with both Redmine 1.x and 2.x - some tips

Added by Vitaly Klimov almost 5 years ago

Writing plugins compatible with both Redmine 1.x and 2.x

This guide is not intended to be complete and fool proof. It simply contains bits and scraps i gathered while adding Redmine 2.x (and Rails 3) compatibility to my plugins. I believe tips i give here will help create cross-version plugins.

Routing

Theory

Most simple part so far. Although routing syntax completely changed in Rails 3, it is straightforward and easy to learn. For complete guide please see http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/ link.

Code sample

Below is sample routing.rb file which is compatible with both Rails' versions.

if Rails::VERSION::MAJOR >= 3
  RedmineApp::Application.routes.draw do
    match 'projects/:project_id/boards/:board_id/manage', :to => 'boards_watchers#manage', :via => [:get, :post]
    match 'projects/:project_id/boards/:board_id/manage_topic', :to => 'boards_watchers#manage_topic', :via => [:get, :post]
  end
else
  ActionController::Routing::Routes.draw do |map|
    map.with_options :controller => 'boards_watchers' do |bw_routes|
      bw_routes.with_options :conditions => {:method => :get} do |bw_views|
        bw_views.connect 'projects/:project_id/boards/:board_id/manage', :action => 'manage'
        bw_views.connect 'projects/:project_id/boards/:board_id/manage_topic', :action => 'manage_topic'
      end
      bw_routes.with_options :conditions => {:method => :post} do |bw_views|
        bw_views.connect 'projects/:project_id/boards/:board_id/manage', :action => 'manage'
        bw_views.connect 'projects/:project_id/boards/:board_id/manage_topic', :action => 'manage_topic'
      end
    end
  end
end

Overriding models, controllers and helpers

Theory

Since patching mechanism is changed in Rails 3, in order to support both Redmine version in the plugin we have to check two different code branches – one should use Dispatcher functionality (Rails 2.x) and another – new to_prepare functionality from Rails 3.

Code

To make things simple and clear, i moved all similar parts of the code to the separate file in lib folder, and left only Rails specific code in the init.rb file. So usual inclusion code would look like following:

init.rb:

# Including dispatcher.rb in case of Rails 2.x
require 'dispatcher' unless Rails::VERSION::MAJOR >= 3

if Rails::VERSION::MAJOR >= 3
  ActionDispatch::Callbacks.to_prepare do
    # use require_dependency if you plan to utilize development mode
    require 'boards_watchers_patches'
  end
else
  Dispatcher.to_prepare BW_AssetHelpers::PLUGIN_NAME do
    # use require_dependency if you plan to utilize development mode
    require 'boards_watchers_patches'
  end
end

boards_watchers_patches.rb:

# use require_dependency if you plan to utilize development mode
require 'application_helper'

module BoardsWatchers
  module Patches
    module ApplicationHelperPatch
      def self.included(base) # :nodoc:
        # sending instance methods to module
        base.send(:include, InstanceMethods)

        base.class_eval do
          unloadable

          # aliasing methods if needed
          alias_method_chain :render_page_hierarchy, :watchers
        end
      end

      # Instance methods are here
      module InstanceMethods
        def render_page_hierarchy_with_watchers(pages, node=nil, options={})
          # our code is here
        end
      end
    end
  end
end

# now we should include this module in ApplicationHelper module
unless ApplicationHelper.included_modules.include? BoardsWatchers::Patches::ApplicationHelperPatch
    ApplicationHelper.send(:include, BoardsWatchers::Patches::ApplicationHelperPatch)
end

Views and form tags

This is the part of common problem, when it is not possible to patch different parts of views files and in order to add/change couple of strings in any .erb file plugin authors have to replace whole file. I filed a ticket on redmine.org (http://www.redmine.org/issues/11104) proposing possible solution to this issue. Meanwhile i would like to ask anyone who interested in cross-version compatibility to take a look at my plugin Plugin views revisions This plugin solves this issue.

Anyway, if we are about to make existing plugin with its own views compatible with Rails 3.x, there are some things that are changed in view syntax and code.

First of all, it is the form_tag and form_for methods, In Rails 2.x we had to use <% form_for %> without equal sign and in Rails 3.x we need to use equal sign: <%= form_for ... %>

Another thing that is related to the views is that in Rails 3.x all strings that displayed by views are escaped by default, so you have to add manually .html_safe method if you want to return actual HTML code in your helpers.

Extending default helpers and controllers

In Redmine versions prior to 2.0.0 all plugins were in vendor/plugins folder and because of that Rails automatically loaded and added all files inside app/controllers, app/helpers, app/models, etc folders.

This approach allowed to extend existing controllers and helpers by simply adding appropriate files and methods to the plugin' folders. For example, if we have to add method get_list_of_projects to SettingsHelper we can simply create file named settings_helper.rb in app/helpers folder of the plugin with the following content:

app/helpers/settings_helper.rb

module SettingsHelper
  def get_list_of_projects
    # do what we need here
  end
end

Redmine 2.x moved plugins to plugins folder in application's root and because of that method described above does not work at all. So we have to use patching and override mechanisms described above to make things work.

Miscellaneous

Links and paths to plugin assets

Sometimes you need to reference some assets from plugin assets folder. If you are using Redmine 1.x it was possible to do it by using extended image_path function with extra parameter (:plugin) to get full asset path, for example:

image_path("../flash/some_flash.swf",:plugin => 'my_plugin')

Unfortunately, Redmine 2.x removed support for this function, so i wrote my own version of function which will return path to the assets. Please put file with this function into any subfolder of the plugin folder, otherwise plugin name retrieval would not work

module MyPluginAssetHelpers
  PLUGIN_NAME = File.expand_path('../../*', __FILE__).match(/.*\/(.*)\/\*$/)[1].to_sym

  def self.plugin_asset_link(asset_name,options={})
    plugin_name=(options[:plugin] ? options[:plugin] : PLUGIN_NAME)
    File.join(Redmine::Utils.relative_url_root,'plugin_assets',plugin_name.to_s,asset_name)
  end
end

Relative url root

If you installed Redmine to the sub-URI you should set Redmine::Utils.relative_url_root to the
value of sub-URI in config/additional_environment.rb:

Redmine::Utils::relative_url_root = "/redmine" 

Unfortunately this method does not work for Redmine 2.x and you should set ActionController root directly:

config.action_controller.relative_url_root = '/redmine'

Conclusion

This is all so far. Please do comment and correct this post if you would like to.

I think it would be great if something based on this post will be added to the plugins related wiki pages

Replies (10)

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by William Roush almost 5 years ago

This should be part of official docs for plugin development, extremely useful!

Thanks for the write up. :)

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Mischa The Evil almost 5 years ago

Stickied and relocated toc to the right.

Reference: #11151.

Thanks for writing this useful information.

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Jiří Křivánek over 4 years ago

Thank You for this great guide. It helped me a lot when porting my plugin from 1.3 to 2.0. But I still have one big problem:

There is NO redmine around my plugin view!

No HTML headers, no styles, no menu...

Just a content of my plugin - which works!

Any idea which little detail of too complex Ruby/Rails/Redmine conglomerat I am missing?

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Jiří Křivánek over 4 years ago

In a different thread someone adviced me: The problem was in the constructor of my controller class.

At the old Redmine, there was no need to call the ancestors constructor from within the overriden one.

I know, most of plugins do not do this but I stuck on it many hours and it should have been written in the porting guide.

Please add it!

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Eric Boudaillier over 4 years ago

Hi,

I am migrating from redmine 1.4 to redmine 2.1.
My redmine sites are in sub URIs. I have never done specific configuration in 1.4, but now, the redmine graphs plugin (the one ported for 2.x - https://github.com/dmp/redmine-graphs-plugin) shows incorrect links.

I have tried to add in additonnal_environement.rb:

config.action_controller.relative_url_root = '/redmine'

but without success. And adding the following line raises an error:

Redmine::Utils::relative_url_root = "/redmine"

Has someone solved this issue?

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Marco Nobler over 4 years ago

First of all, thanks for this, it's very useful.
I'm trying to port a plugin built for 1.3 redmine (rails 2) to redmine 2.1.2.
I have an error running migration (see "redmine_tlcit_migration.out" file attached):

undefined method `validate' for class `Issue'

attached you can also find the "issue_patch.rb".

Could you give me some hint to solve this?
thanks in advance
Marco Nober

redmine_tlcit_migration.out - the migrate ouput (6.41 KB)

issue_patch.rb Magnifier - the source (1.35 KB)

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Marco Nobler over 4 years ago

Hi,

i'm triyng to port a redmine 1.x plugin to redmine 2.1.2.

I'm facing with a problem with patching modules:
I need to patch application helper and some other modules but probably i'm making some mistakes, attached can find the code:

plugins/redmine_tlcit/init.rb
plugins/redmine_tlcit/lib/redmine_tlcit/patches/application_helper_patch.rb

but i have this run-time error, it seems have not loaded the patch definitions:
ActionView::Template::Error (undefined method `prompt_to_remote' for #<#<Class:0x7f7b8d368610>:0x7f7b8d0d5498>):
13: <% end >
14: < unless @project.issue_categories.empty? >
15: <p><= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true >
16: <= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
17: l(:label_issue_category_new),
18: 'category[name]',
19: {:controller => 'issue_categories', :action => 'new', :project_id => @project},
app/views/issues/_form.html.erb:41:in `_app_views_issues__form_html_erb___1076854482_70084312332980'
app/helpers/application_helper.rb:985:in `labelled_fields_for'
app/views/issues/_form.html.erb:1:in `_app_views_issues__form_html_erb___1076854482_70084312332980'
app/views/issues/_edit.html.erb:8:in `_app_views_issues__edit_html_erb__1983334900_70084312901780'
app/helpers/application_helper.rb:978:in `labelled_form_for'
app/views/issues/_edit.html.erb:1:in `_app_views_issues__edit_html_erb__1983334900_70084312901780'
app/controllers/issues_controller.rb:117:in `show'
app/controllers/issues_controller.rb:114:in `show'

please, can you all help me?
Thanks in advance

init.rb Magnifier (1.7 KB)

application_helper_patch.rb Magnifier (688 Bytes)

RE: Writing plugins compatible with both Redmine 1.x and 2.x - some tips - Added by Arun M V almost 4 years ago

Hi,

Currently I have Redmine 2.3.0 installed and I am trying to make the Rate Plugin compatible with it.But I am  facing some issues.Please find the below error log.

ActionView::Template::Error (undefined method `project_options_for_select_with_selected' for #<#<Class:0xb5adc540>:0xb587f748>):
15: <%= # TODO: move to controller once a hook is in place for the Admin panel
16: projects = Project.find(:all, :conditions => { :status => Project::STATUS_ACTIVE})
17:
18: select_tag("rate[project_id]", project_options_for_select_with_selected(projects, @rate.project))
19: %>
20: </td>
21: <td align="right">
app/views/common/_tabs.html.erb:24:in `_app_views_common__tabs_html_erb__225850422__623011288'
app/views/common/_tabs.html.erb:23:in `each'
app/views/common/_tabs.html.erb:23:in `_app_views_common__tabs_html_erb__225850422__623011288'
app/helpers/application_helper.rb:271:in `render_tabs'
app/views/users/edit.html.erb:9:in `_app_views_users_edit_html_erb___547050721__623564338'

Please find the attached files init.rb and rate_users_helper_patch.rb.

Please help.

Thanks,
Arun

init.rb Magnifier (1.61 KB)

rate_users_helper_patch.rb Magnifier (1.53 KB)

(1-10/10)