Plugin Internals » History » Version 3

Mischa The Evil, 2009-07-28 00:40
Added "To Do" section containing an abstract of an IRC-discussion about using hooks and call-backs.

1 1 Mischa The Evil
h1. Plugin Internals
2 1 Mischa The Evil
3 1 Mischa The Evil
{{>toc}}
4 1 Mischa The Evil
5 1 Mischa The Evil
This page will be used as a central place to store information about plugin-development in Redmine.
6 1 Mischa The Evil
7 2 Mischa The Evil
h2. Require a certain Redmine version
8 2 Mischa The Evil
9 2 Mischa The Evil
Sometimes plugins require a specific feature implemented in the Redmine core or the plugin overrides a specific view which requires you to control on which (specific) versions of Redmine the plugin can be installed to assure that the required core is available. Such prevents a lot of issues regarding plugin-compatibility.
10 2 Mischa The Evil
11 2 Mischa The Evil
The above can be accomplished by utilizing the @requires_redmine@-method (see issue #2162 for the implementation dicussion  and it's actual implementation in r2042). Utilisation of the method provides an easy, reliable way to create plugins that require a specific version of Redmine and which are setup to stop Redmine with a message about a non-supported version if the version-requirement is not met.
12 2 Mischa The Evil
13 1 Mischa The Evil
h2. Overriding the Redmine Core
14 1 Mischa The Evil
15 1 Mischa The Evil
You can override views but not controllers or models in Redmine. Here's how Redmine/Rails works if you try to override a controller (or model) and a view for a fictional plugin @MyPlugin@:
16 1 Mischa The Evil
17 1 Mischa The Evil
h3. Controllers (or models)
18 1 Mischa The Evil
19 1 Mischa The Evil
# Rails bootstraps and loads all it's framework
20 1 Mischa The Evil
# Rails starts to load code in the plugins
21 1 Mischa The Evil
# Rails finds @IssueController@ in MyPlugin and see it defines a @show@ action
22 1 Mischa The Evil
# Rails loads all the other plugins
23 1 Mischa The Evil
# Rails then loads the application from _../app_
24 1 Mischa The Evil
# Rails finds @IssueController@ again and see it also defines a @show@ action
25 1 Mischa The Evil
# Rails (or rather Ruby) overwrites the @show@ action from the plugin with the one from _../app_
26 1 Mischa The Evil
# Rails finishes loading and serves up requests
27 1 Mischa The Evil
28 1 Mischa The Evil
h3. Views
29 1 Mischa The Evil
30 1 Mischa The Evil
View loading is very similar but with one small difference (because of Redmine's patch to Engines)
31 1 Mischa The Evil
32 1 Mischa The Evil
# Rails bootstraps and loads all it's framework
33 1 Mischa The Evil
# Rails starts to load code in the plugins
34 1 Mischa The Evil
# Rails finds a views directory in _../vendor/plugins/my_plugin/app/views_ and *pre-pends* it to the views path
35 1 Mischa The Evil
# Rails loads all the other plugins
36 1 Mischa The Evil
# Rails then loads the application from _../app_
37 1 Mischa The Evil
# Rails finishes loading and serves up requests
38 1 Mischa The Evil
# Request comes in, and a view needs to be rendered
39 1 Mischa The Evil
# Rails looks for a matching template and loads the plugin's template since it was *pre-pended* to the views path
40 1 Mischa The Evil
# Rails renders the plugins'view
41 1 Mischa The Evil
42 1 Mischa The Evil
Due to the fact that it is so easy to extend models and controllers the Ruby way (via including modules), Redmine shouldn't (and doesn't) maintain an API for overriding the core's models and/or controllers. Views on the other hand are tricky (because of Rails magic) so an API for overriding them is way more useful (and thus implemented in Redmine).
43 1 Mischa The Evil
44 1 Mischa The Evil
To override an existing Redmine Core view just create a view file named exactly after the one in _../app/views/_ and Redmine will use it. For example to override the project index page add a file to _../vendor/plugins/my_plugin/app/views/projects/index.rhtml_.
45 1 Mischa The Evil
46 1 Mischa The Evil
h2. Extending the Redmine Core
47 1 Mischa The Evil
48 1 Mischa The Evil
As explained above: you rarely want to override a model/controller. Instead you should either:
49 1 Mischa The Evil
* add new methods to a model/controller or 
50 1 Mischa The Evil
* wrap an existing method.
51 1 Mischa The Evil
52 1 Mischa The Evil
h3. Adding a new method
53 1 Mischa The Evil
54 1 Mischa The Evil
A quick example of *adding a new method* can be found on Eric Davis' "Budget plugin":http://github.com/edavis10/redmine-budget-plugin/blob/5076b1c88b57c2068aa92cdf694769dbd22d061a/lib/issue_patch.rb. Here he added a new method to Issue called @deliverable_subject@ and also declared a relationship.
55 1 Mischa The Evil
56 1 Mischa The Evil
h3. Wrapping an existing method
57 1 Mischa The Evil
58 1 Mischa The Evil
A quick example of *wrapping an existing method* can be found on Eric Davis' "Rate plugin":http://github.com/edavis10/redmine_rate/blob/4666ddb10e1061ca3ef362735d0d264676b99024/lib/rate_users_helper_patch.rb. Here he uses the @alias_method_chain@ to hook into the UsersHelper and wrap the @user_settings_tabs@ method. So when the Redmine Core calls @user_settings_tabs@ the codepath looks like:
59 1 Mischa The Evil
60 1 Mischa The Evil
# Redmine Core calls @UsersHelper#user_settings_tabs@ 
61 1 Mischa The Evil
# @UsersHelper#user_settings_tabs@ runs (which is actually @UsersHelper#user_settings_tabs_with_rate_tab@)
62 1 Mischa The Evil
# @UsersHelper#user_settings_tabs_with_rate_tab@ calls the original @UsersHelper#user_settings_tabs@ (renamed to @UsersHelper#user_settings_tabs_without_rate_tab@)
63 1 Mischa The Evil
# The result then has a new Hash added to it
64 1 Mischa The Evil
# @UsersHelper#user_settings_tabs_with_rate_tab@ returns the combined result to the Redmine core, which is then rendered
65 1 Mischa The Evil
66 1 Mischa The Evil
"@alias_method_chain@":http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Module.html#M001188 is a pretty advanced method but it's also really powerful.
67 1 Mischa The Evil
68 1 Mischa The Evil
h2. References
69 1 Mischa The Evil
70 2 Mischa The Evil
* http://www.redmine.org/boards/3/topics/show/5121 (Which version of Redmine I need to use your plugin?)
71 2 Mischa The Evil
* http://www.redmine.org/boards/3/topics/show/4283 (Can a plugin modify the view of the projects page?)
72 2 Mischa The Evil
* http://www.redmine.org/boards/3/topics/show/4095 (Rails Engines and extending the issue model)
73 3 Mischa The Evil
74 3 Mischa The Evil
h2. To Do
75 3 Mischa The Evil
76 3 Mischa The Evil
This is an abstract of an IRC-discussion between @chantra__@ and @edavis10@ about using hooks and call-backs within a plugin. This needs to be rewritten obviously.
77 3 Mischa The Evil
78 3 Mischa The Evil
<pre>
79 3 Mischa The Evil
[16:06:48] <chantra__> hi all, I am playing with hooks....
80 3 Mischa The Evil
[16:06:58] <chantra__> is there a way to detect a new logged issue?
81 3 Mischa The Evil
[16:07:20] <chantra__> the only hooks I find are: http://pastebin.com/m779d73b0
82 3 Mischa The Evil
[16:08:11] <chantra__> and :controller_issues_edit_before_save is not triggered when a new issue is logged
83 3 Mischa The Evil
[16:09:21] <chantra__> any ideas?
84 3 Mischa The Evil
...
85 3 Mischa The Evil
[17:11:45] <+edavis10> chantra__: http://www.redmine.org/wiki/redmine/Hooks
86 3 Mischa The Evil
[17:12:02] <chantra__> edavis10: cheers
87 3 Mischa The Evil
[17:12:10] <chantra__> I found out that there is no hook for new issues
88 3 Mischa The Evil
[17:12:16] <+edavis10> chantra__: if you want to hook into when every issue is saved/created, I'd recommend using Rails callbacks
89 3 Mischa The Evil
[17:12:19] <chantra__> and no way to override issuescontroller
90 3 Mischa The Evil
[17:12:51] <chantra__> so far, only trick is to patch isses_controller.rb and add a hook call when issue is saved Ls
91 3 Mischa The Evil
[17:13:26] <+edavis10> chantra__: http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/init.rb#L10
92 3 Mischa The Evil
[17:13:41] <+edavis10> chantra__: http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/lib/redmine_kanban/issue_patch.rb
93 3 Mischa The Evil
[17:13:52] <+edavis10> Line 13 of the second url
94 3 Mischa The Evil
[17:14:16] <+edavis10> chantra__: that will run `issue.update_kanban_from_issue` every time an issue is saved (new or updated)
95 3 Mischa The Evil
[17:14:19] <chantra__> edavis10: thanks a bunch, I will dig this out
96 3 Mischa The Evil
[17:14:40] <+edavis10> chantra__: here's the Rails documentation on callbacks http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
97 3 Mischa The Evil
[17:14:42] <chantra__> then I can compared created_on and updated_on to see if it is new ;)
98 3 Mischa The Evil
[17:15:02] <chantra__> splendid
99 3 Mischa The Evil
...
100 3 Mischa The Evil
[17:15:07] <+edavis10> chantra__: or you can use the `before_create` callback.  It's only run on new records
101 3 Mischa The Evil
[17:15:20] <chantra__> \o/ :)
102 3 Mischa The Evil
[17:15:32] <+edavis10> (or after_create if you want to make sure the record was saved)
103 3 Mischa The Evil
[17:15:52] <chantra__> thanks, you are saving me loads of time here :)
104 3 Mischa The Evil
[17:16:05] <chantra__> coz I was heading the wrong way :p
105 3 Mischa The Evil
[17:16:21] <+edavis10> you're welcome. My Github account as all of my code (~20+ Redmine plugins)
106 3 Mischa The Evil
[17:16:39] <+edavis10> the kanban plugin is my latest, so it's the best code at the moment
107 3 Mischa The Evil
[17:17:04] <chantra__> yeah, I checked a lot of them already... but being a ruby noob, it took me time to assimilate
108 3 Mischa The Evil
...
109 3 Mischa The Evil
[17:17:36] <+edavis10> chantra__: if they have a lib/ directory, that is where I put my hooks or where I patch Redmine's behavior
110 3 Mischa The Evil
...
111 3 Mischa The Evil
[17:18:29] <chantra__> yeah, found that out, but I was hitting a wall on hooking new issue, so I was going to patch the core redmine.... which is not really nifty when doing a plugin :)
112 3 Mischa The Evil
[17:19:29] <+edavis10> chantra__: some plugins have overrode the IssuesController from a plugin.  It will work but the second the core code changes, the plugin will break
113 3 Mischa The Evil
...
114 3 Mischa The Evil
[17:56:45] <chantra__> edavis10: yet again thanks a bunch, I start to get my stuff working without patching around
115 3 Mischa The Evil
[17:56:47] <chantra__> gtg
116 3 Mischa The Evil
</pre>