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 >> MyMooPatch.object_id => -621509448 >> @issue.class.object_id => -621657688 >> 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] => nil >> @issue.class.included_modules.include?(MyMooPatch) => 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 >> MyMooPatch.object_id => -621509448 # Same id as above >> @issue.class.object_id => -630808728 # Different id from above. >> 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] => nil >> @issue.class.included_modules.include?(MyMooPatch) => 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
Very nice post!
As a suggestion, maybe you could write something about some “right way” to replace Redmine forms when hooks aren’t enough?
[]’s!
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.
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.
I’m not following. If you’re patching a controller, you can just use the flash method like normal inside an action.