Project

General

Profile

Plugin Internals » History » Version 21

Bernhard Rohloff, 2019-01-24 14:56
Added note about missing support of ailas_method_chain in Rails 5

1 1 Mischa The Evil
h1. Plugin Internals
2
3
{{>toc}}
4
5
This page will be used as a central place to store information about plugin-development in Redmine.
6
7 2 Mischa The Evil
h2. Require a certain Redmine version
8
9
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
11 14 Harry Garrood
The above can be accomplished by utilizing the @requires_redmine@-method (see issue #2162 for the implementation discussion  and its 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
15
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
17
h3. Controllers (or models)
18
19
# Rails bootstraps and loads all it's framework
20
# Rails starts to load code in the plugins
21
# Rails finds @IssueController@ in MyPlugin and see it defines a @show@ action
22
# Rails loads all the other plugins
23
# Rails then loads the application from _../app_
24
# Rails finds @IssueController@ again and see it also defines a @show@ action
25
# Rails (or rather Ruby) overwrites the @show@ action from the plugin with the one from _../app_
26
# Rails finishes loading and serves up requests
27
28
h3. Views
29
30
View loading is very similar but with one small difference (because of Redmine's patch to Engines)
31
32
# Rails bootstraps and loads all it's framework
33
# Rails starts to load code in the plugins
34
# Rails finds a views directory in _../vendor/plugins/my_plugin/app/views_ and *pre-pends* it to the views path
35
# Rails loads all the other plugins
36
# Rails then loads the application from _../app_
37
# Rails finishes loading and serves up requests
38
# Request comes in, and a view needs to be rendered
39
# Rails looks for a matching template and loads the plugin's template since it was *pre-pended* to the views path
40
# Rails renders the plugins'view
41
42
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
44 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_.
45 1 Mischa The Evil
46
h2. Extending the Redmine Core
47
48
As explained above: you rarely want to override a model/controller. Instead you should either:
49
* add new methods to a model/controller or 
50
* wrap an existing method.
51
52
h3. Adding a new method
53
54
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
56 6 Eric Davis
<pre><code class="ruby">
57
module IssuePatch
58
  def self.included(base) # :nodoc:
59
    base.send(:include, InstanceMethods)
60
  end
61
  
62
  module InstanceMethods
63
    # Wraps the association to get the Deliverable subject.  Needed for the 
64
    # Query and filtering
65
    def deliverable_subject
66
      unless self.deliverable.nil?
67
        return self.deliverable.subject
68
      end
69
    end
70 9 Igor Zubkov
  end
71 6 Eric Davis
end
72
</code></pre>
73
74 1 Mischa The Evil
h3. Wrapping an existing method
75
76 21 Bernhard Rohloff
77
> *Caution!*
78
> The alias_method_chain pattern is deprecated in Rails 5 so this technique is only applicable to Redmine versions below 4.0.0.
79
80 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:
81
82
# Redmine Core calls @UsersHelper#user_settings_tabs@ 
83
# @UsersHelper#user_settings_tabs@ runs (which is actually @UsersHelper#user_settings_tabs_with_rate_tab@)
84
# @UsersHelper#user_settings_tabs_with_rate_tab@ calls the original @UsersHelper#user_settings_tabs@ (renamed to @UsersHelper#user_settings_tabs_without_rate_tab@)
85
# The result then has a new Hash added to it
86
# @UsersHelper#user_settings_tabs_with_rate_tab@ returns the combined result to the Redmine core, which is then rendered
87 6 Eric Davis
88
<pre><code class="ruby">
89
module RateUsersHelperPatch
90
  def self.included(base) # :nodoc:
91
    base.send(:include, InstanceMethods)
92
93
    base.class_eval do
94
      alias_method_chain :user_settings_tabs, :rate_tab
95
    end
96
  end
97
  
98
  module InstanceMethods
99
    # Adds a rates tab to the user administration page
100
    def user_settings_tabs_with_rate_tab
101
      tabs = user_settings_tabs_without_rate_tab
102
      tabs << { :name => 'rates', :partial => 'users/rates', :label => :rate_label_rate_history}
103
      return tabs
104
    end
105
  end
106
end
107
</code></pre>
108 11 Tony Marschall
109 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.
110
111
"@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.
112 17 Denis Savitskiy
113 4 Mischa The Evil
h2. Using Rails callbacks in Redmine plugins
114
115
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.
116
For example see the implementation of this in Eric Davis' "Kanban plugin":
117
# http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/init.rb#L10
118
# http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/lib/redmine_kanban/issue_patch.rb#L13
119
120
This will make sure that @issue.update_kanban_from_issue@ runs every time an issue is saved (new or updated).
121
122
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.
123
124 7 Terence Mill
h2. Hooking in MyPage
125
126
h3. FAQ
127
128 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.
129 7 Terence Mill
130 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.
131 7 Terence Mill
132 3 Mischa The Evil
h2. References
133 1 Mischa The Evil
134 20 Toshi MARUYAMA
* message#5121
135
* message#4283
136
* message#4095