Plugin Internals » History » Version 22

Mischa The Evil, 2020-08-26 07:17
Moved section on 'requiring a certain Redmine version' to [[Plugin_FAQ]].

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 1 Mischa The Evil
h2. Overriding the Redmine Core
8 1 Mischa The Evil
9 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@:
10 1 Mischa The Evil
11 1 Mischa The Evil
h3. Controllers (or models)
12 1 Mischa The Evil
13 1 Mischa The Evil
# Rails bootstraps and loads all it's framework
14 1 Mischa The Evil
# Rails starts to load code in the plugins
15 1 Mischa The Evil
# Rails finds @IssueController@ in MyPlugin and see it defines a @show@ action
16 1 Mischa The Evil
# Rails loads all the other plugins
17 1 Mischa The Evil
# Rails then loads the application from _../app_
18 1 Mischa The Evil
# Rails finds @IssueController@ again and see it also defines a @show@ action
19 1 Mischa The Evil
# Rails (or rather Ruby) overwrites the @show@ action from the plugin with the one from _../app_
20 1 Mischa The Evil
# Rails finishes loading and serves up requests
21 1 Mischa The Evil
22 1 Mischa The Evil
h3. Views
23 1 Mischa The Evil
24 1 Mischa The Evil
View loading is very similar but with one small difference (because of Redmine's patch to Engines)
25 1 Mischa The Evil
26 1 Mischa The Evil
# Rails bootstraps and loads all it's framework
27 1 Mischa The Evil
# Rails starts to load code in the plugins
28 1 Mischa The Evil
# Rails finds a views directory in _../vendor/plugins/my_plugin/app/views_ and *pre-pends* it to the views path
29 1 Mischa The Evil
# Rails loads all the other plugins
30 1 Mischa The Evil
# Rails then loads the application from _../app_
31 1 Mischa The Evil
# Rails finishes loading and serves up requests
32 1 Mischa The Evil
# Request comes in, and a view needs to be rendered
33 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
34 1 Mischa The Evil
# Rails renders the plugins'view
35 1 Mischa The Evil
36 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).
37 1 Mischa The Evil
38 8 Igor Zubkov
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.html.erb_.
39 1 Mischa The Evil
40 1 Mischa The Evil
h2. Extending the Redmine Core
41 1 Mischa The Evil
42 1 Mischa The Evil
As explained above: you rarely want to override a model/controller. Instead you should either:
43 1 Mischa The Evil
* add new methods to a model/controller or 
44 1 Mischa The Evil
* wrap an existing method.
45 1 Mischa The Evil
46 1 Mischa The Evil
h3. Adding a new method
47 1 Mischa The Evil
48 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.
49 1 Mischa The Evil
50 6 Eric Davis
<pre><code class="ruby">
51 6 Eric Davis
module IssuePatch
52 6 Eric Davis
  def self.included(base) # :nodoc:
53 6 Eric Davis
    base.send(:include, InstanceMethods)
54 6 Eric Davis
  end
55 6 Eric Davis
  
56 6 Eric Davis
  module InstanceMethods
57 6 Eric Davis
    # Wraps the association to get the Deliverable subject.  Needed for the 
58 6 Eric Davis
    # Query and filtering
59 6 Eric Davis
    def deliverable_subject
60 6 Eric Davis
      unless self.deliverable.nil?
61 6 Eric Davis
        return self.deliverable.subject
62 6 Eric Davis
      end
63 6 Eric Davis
    end
64 9 Igor Zubkov
  end
65 6 Eric Davis
end
66 6 Eric Davis
</code></pre>
67 6 Eric Davis
68 1 Mischa The Evil
h3. Wrapping an existing method
69 1 Mischa The Evil
70 21 Bernhard Rohloff
71 21 Bernhard Rohloff
> *Caution!*
72 21 Bernhard Rohloff
> The alias_method_chain pattern is deprecated in Rails 5 so this technique is only applicable to Redmine versions below 4.0.0.
73 21 Bernhard Rohloff
74 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:
75 1 Mischa The Evil
76 1 Mischa The Evil
# Redmine Core calls @UsersHelper#user_settings_tabs@ 
77 1 Mischa The Evil
# @UsersHelper#user_settings_tabs@ runs (which is actually @UsersHelper#user_settings_tabs_with_rate_tab@)
78 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@)
79 1 Mischa The Evil
# The result then has a new Hash added to it
80 1 Mischa The Evil
# @UsersHelper#user_settings_tabs_with_rate_tab@ returns the combined result to the Redmine core, which is then rendered
81 6 Eric Davis
82 6 Eric Davis
<pre><code class="ruby">
83 6 Eric Davis
module RateUsersHelperPatch
84 6 Eric Davis
  def self.included(base) # :nodoc:
85 6 Eric Davis
    base.send(:include, InstanceMethods)
86 6 Eric Davis
87 6 Eric Davis
    base.class_eval do
88 6 Eric Davis
      alias_method_chain :user_settings_tabs, :rate_tab
89 6 Eric Davis
    end
90 6 Eric Davis
  end
91 6 Eric Davis
  
92 6 Eric Davis
  module InstanceMethods
93 6 Eric Davis
    # Adds a rates tab to the user administration page
94 6 Eric Davis
    def user_settings_tabs_with_rate_tab
95 6 Eric Davis
      tabs = user_settings_tabs_without_rate_tab
96 6 Eric Davis
      tabs << { :name => 'rates', :partial => 'users/rates', :label => :rate_label_rate_history}
97 6 Eric Davis
      return tabs
98 6 Eric Davis
    end
99 6 Eric Davis
  end
100 6 Eric Davis
end
101 6 Eric Davis
</code></pre>
102 11 Tony Marschall
103 18 Paulo Neves
It is important to note that this kind of wrapping can only be done once per method. In the case of multiple plugins using this trick, then only the last evaluation of the @alias_method_chain@ would be valid and all the previous ones would be ignored.
104 18 Paulo Neves
105 18 Paulo Neves
"@alias_method_chain@":http://apidock.com/rails/v3.2.13/Module/alias_method_chain is a pretty advanced method but it's also really powerful.
106 17 Denis Savitskiy
107 4 Mischa The Evil
h2. Using Rails callbacks in Redmine plugins
108 4 Mischa The Evil
109 4 Mischa The Evil
When you want to hook into all issues which are saved/created for example, you can better use "Rails callbacks":http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html instead of Redmine [[Hooks|hooks]]. Main reason for this is that the @:controller_issues_edit_before_save@-hook is not triggered when a new issue is created.
110 4 Mischa The Evil
For example see the implementation of this in Eric Davis' "Kanban plugin":
111 4 Mischa The Evil
# http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/init.rb#L10
112 4 Mischa The Evil
# http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/lib/redmine_kanban/issue_patch.rb#L13
113 4 Mischa The Evil
114 4 Mischa The Evil
This will make sure that @issue.update_kanban_from_issue@ runs every time an issue is saved (new or updated).
115 4 Mischa The Evil
116 4 Mischa The Evil
If you want to hook into new issues only you can use the @before_create@ callback instead of the @after_save@ callback. If you want to make sure that the issue indeed is saved successfully before your code is executed you could better use the @after_create@-callback.
117 4 Mischa The Evil
118 7 Terence Mill
h2. Hooking in MyPage
119 7 Terence Mill
120 7 Terence Mill
h3. FAQ
121 7 Terence Mill
122 16 Tobias Fischer
* Why is the drop-down selection for my blocks not localized? The Name of the entry in the drop-dwon box is per convention made of the entry in the locale file of the plugin. This entry must have the same name as the "my site" block filename, e.g. redmine/vendor/plugins/<myplugin_folder>/app/views/my/blocks/<myblocks_view_file_name>.erb. So you need to add a line "<myblocks_view_file_name>: <put here translation for the drop down item in my blocks configuration>" in your locale, e.g redmine/vendor/plugins/<myplugin_folder>/config/locale/en.yml.
123 7 Terence Mill
124 13 Mischa The Evil
If this string is not defined in locale file, alyways the filename <myblocks_view_file_name> without extension is made for label in drop-down.
125 7 Terence Mill
126 3 Mischa The Evil
h2. References
127 1 Mischa The Evil
128 20 Toshi MARUYAMA
* message#4283
129 20 Toshi MARUYAMA
* message#4095