How to modify core Redmine classes from a plugin

I’ve been writing Redmine plugins since 2007 and one thing that stumped me was how to add new methods to Redmine‘s core classes and have them working in development. The standard Ruby on Rails way of including a module into the class works great except in development mode. Thanks to Thomas Löber, I found a way to overcome this error by making Ruby on Rails reload specific plugin classes.

Patching the core for fun and profit

I’m going to create a quick plugin that shows an example. Lets say you have a method called moo that you want to add to the Issue class. The standard Ruby on Rails plugin way would be to create a module and call Issue.send(:include).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# lib/my_moo_patch.rb
module MyMooPatch
  def self.included(base)
    base.send(:include, InstanceMethods)
  end
 
  module InstanceMethods
    def moo
      logger.info 'moo'
    end
  end    
end
 
# init.rb
require 'my_moo_patch'
Issue.send(:include, MyMooPatch)

This is a pretty standard case and would be used anytime your plugin needs to relate to the core classes. To illustrate this example and some errors that are caused, I added a call to @issue.moo on the Issue show page. Typically you would use the Redmine Hooks to have Redmine run your plugin’s code.

Running the code

Running in the production environment, everything works fine:

Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 09:57:41) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}
moo
Rendering template within layouts/base
Rendering issues/show.rhtml
Completed in 1037ms (View: 873, DB: 31) | 200 OK [http://localhost/issues/1765]


Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 09:57:44) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}
moo
Rendering template within layouts/base
Rendering issues/show.rhtml
Completed in 257ms (View: 173, DB: 17) | 200 OK [http://localhost/issues/1765]

But in development, the first request will work but the second request will now throw an exception:

# First request
Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 09:59:32) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}
moo
Rendering template within layouts/base
Rendering issues/show.rhtml
Completed in 1285ms (View: 990, DB: 24) | 200 OK [http://localhost/issues/1765]

# Second request
Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 09:59:36) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}

NoMethodError (undefined method `moo' for #<Issue:0xb4ab3d94>):
    /vendor/rails/activerecord/lib/active_record/attribute_methods.rb:260:in `method_missing'
    /app/controllers/issues_controller.rb:98:in `show'
    <<snip>>

Rendering /home/edavis/dev/redmine/redmine-core/vendor/rails/actionpack/lib/action_controller/templates/rescues/layout.erb (internal_server_error)

Debugging the error

The NoMethodError is a standard error thrown by Ruby when a class doesn’t have a method defined. But we defined Issue#moo in our plugin, so why is it not found? Lets take a look at what’s happening in the Ruby debugger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# First request
&gt;&gt; MyMooPatch.object_id
=&gt; -621509448
&gt;&gt; @issue.class.object_id
=&gt; -621657688
&gt;&gt; pp @issue.class.included_modules
[MyMooPatch::InstanceMethods,
 MyMooPatch,
 Redmine::Acts::ActivityProvider::InstanceMethods,
 Redmine::Acts::Event::InstanceMethods,
 Redmine::Acts::Searchable::InstanceMethods,
 Redmine::Acts::Watchable::InstanceMethods,
 Redmine::Acts::Customizable::InstanceMethods,
 Redmine::Acts::Attachable::InstanceMethods,
 Redmine::I18n,
 Kernel]
=&gt; nil
&gt;&gt; @issue.class.included_modules.include?(MyMooPatch)
=&gt; true

Nothing out of the ordinary here. Issue has MyMooPatch included and the request processes successfully.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Second request
&gt;&gt; MyMooPatch.object_id
=&gt; -621509448               # Same id as above
&gt;&gt; @issue.class.object_id
=&gt; -630808728               # Different id from above.
&gt;&gt; pp @issue.class.included_modules
[Redmine::Acts::ActivityProvider::InstanceMethods,
 Redmine::Acts::Event::InstanceMethods,
 Redmine::Acts::Searchable::InstanceMethods,
 Redmine::Acts::Watchable::InstanceMethods,
 Redmine::Acts::Customizable::InstanceMethods,
 Redmine::Acts::Attachable::InstanceMethods,
 Redmine::I18n,
 Kernel]
=&gt; nil
&gt;&gt; @issue.class.included_modules.include?(MyMooPatch)
=&gt; false

But on the second request we notice something wrong. Issue no longer has MyMooPatch included and it also has a different class id from the last request. This means that something created a new Issue class object that is used instead of the original one. In development mode, Ruby on Rails reloads classes after each request so on the second request the Issue class was reloaded but our patch wasn’t applied.

Wrapping our monkey-patch in a callback

Fortunately, Ruby on Rails provides a callback method we can use to add our module back, after a class is reloaded. It’s called Dispatcher.to_prepare.

Add a preparation callback. Preparation callbacks are run before every
request in development mode, and before the first request in production
mode. (From: rails/actionpack/lib/action_controller/dispatcher.rb)

So we will need to change the way out module is included in Issue. Instead of just using the straight send, we need to wrap it in the Dispatcher.to_prepare method:

1
2
3
4
5
6
require 'dispatcher'
require 'my_moo_patch'
 
Dispatcher.to_prepare do
  Issue.send(:include, MyMooPatch)
end

Now if we restart the Ruby on Rails server in development mode, all the requests should be successful.

Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 10:42:58) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}
moo
Rendering template within layouts/base
Rendering issues/show.rhtml
Completed in 1382ms (View: 1056, DB: 26) | 200 OK [http://localhost/issues/1765]


Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 10:43:21) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}
moo
Rendering template within layouts/base
Rendering issues/show.rhtml
Completed in 1266ms (View: 951, DB: 26) | 200 OK [http://localhost/issues/1765]


Processing IssuesController#show (for 127.0.0.1 at 2009-04-13 10:43:25) [GET]
  Parameters: {"action"=>"show", "id"=>"1765", "controller"=>"issues"}
moo
Rendering template within layouts/base
Rendering issues/show.rhtml
Completed in 1210ms (View: 901, DB: 25) | 200 OK [http://localhost/issues/1765]

Success, our Issue is now mooing!

Summary

So if you write a Ruby on Rails or Redmine plugin that needs to patch the core, you should wrap your patches in Dispatcher.to_prepare so that they will work in development mode. I’ll be converting all my plugins to use this pattern soon. If you would like to develop a patch for one yourself, fork one of my plugins on GitHub and sent me a pull request.

Eric

4 thoughts on “How to modify core Redmine classes from a plugin”

  1. Henrique Bastos: Great idea. I know it’s been covered several times in the forums but it would be good to consolidate it to a single place.

  2. Great post!
    Do you have any idea how to access the flash[] from within the patch. If you use the dispatcher the flash[] won’t be loaded by the time the patch is executed.

Comments are closed.