Plugin Internals » History » Revision 23
« Previous |
Revision 23/24
(diff)
| Next »
Jakob Fix, 2022-09-16 09:35
Correct typo
Plugin Internals¶
- Table of contents
- Plugin Internals
This page will be used as a central place to store information about plugin-development in Redmine.
Overriding the Redmine Core¶
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
:
Controllers (or models)¶
- Rails bootstraps and loads all its framework
- Rails starts to load code in the plugins
- Rails finds
IssueController
in MyPlugin and see it defines ashow
action - Rails loads all the other plugins
- Rails then loads the application from ../app
- Rails finds
IssueController
again and see it also defines ashow
action - Rails (or rather Ruby) overwrites the
show
action from the plugin with the one from ../app - Rails finishes loading and serves up requests
Views¶
View loading is very similar but with one small difference (because of Redmine's patch to Engines)
- Rails bootstraps and loads all it's framework
- Rails starts to load code in the plugins
- Rails finds a views directory in ../vendor/plugins/my_plugin/app/views and pre-pends it to the views path
- Rails loads all the other plugins
- Rails then loads the application from ../app
- Rails finishes loading and serves up requests
- Request comes in, and a view needs to be rendered
- Rails looks for a matching template and loads the plugin's template since it was pre-pended to the views path
- Rails renders the plugins'view
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).
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.
Extending the Redmine Core¶
As explained above: you rarely want to override a model/controller. Instead you should either:- add new methods to a model/controller or
- wrap an existing method.
Adding a new method¶
A quick example of adding a new method can be found on Eric Davis' Budget plugin. Here he added a new method to Issue called deliverable_subject
and also declared a relationship.
module IssuePatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
end
module InstanceMethods
# Wraps the association to get the Deliverable subject. Needed for the
# Query and filtering
def deliverable_subject
unless self.deliverable.nil?
return self.deliverable.subject
end
end
end
end
Wrapping an existing method¶
Caution!
The alias_method_chain pattern is deprecated in Rails 5 so this technique is only applicable to Redmine versions below 4.0.0.
A quick example of wrapping an existing method can be found on Eric Davis' Rate plugin. 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:
- Redmine Core calls
UsersHelper#user_settings_tabs
UsersHelper#user_settings_tabs
runs (which is actuallyUsersHelper#user_settings_tabs_with_rate_tab
)UsersHelper#user_settings_tabs_with_rate_tab
calls the originalUsersHelper#user_settings_tabs
(renamed toUsersHelper#user_settings_tabs_without_rate_tab
)- The result then has a new Hash added to it
UsersHelper#user_settings_tabs_with_rate_tab
returns the combined result to the Redmine core, which is then rendered
module RateUsersHelperPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
alias_method_chain :user_settings_tabs, :rate_tab
end
end
module InstanceMethods
# Adds a rates tab to the user administration page
def user_settings_tabs_with_rate_tab
tabs = user_settings_tabs_without_rate_tab
tabs << { :name => 'rates', :partial => 'users/rates', :label => :rate_label_rate_history}
return tabs
end
end
end
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.
alias_method_chain
is a pretty advanced method but it's also really powerful.
Using Rails callbacks in Redmine plugins¶
When you want to hook into all issues which are saved/created for example, you can better use Rails callbacks instead of Redmine hooks. Main reason for this is that the:controller_issues_edit_before_save
-hook is not triggered when a new issue is created.For example see the implementation of this in Eric Davis' "Kanban plugin":
- http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/init.rb#L10
- http://github.com/edavis10/redmine_kanban/blob/000cf175795c18033caa43082c4e4d0a9f989623/lib/redmine_kanban/issue_patch.rb#L13
This will make sure that issue.update_kanban_from_issue
runs every time an issue is saved (new or updated).
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.
Hooking in MyPage¶
FAQ¶
- 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.
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.
References¶
Updated by Jakob Fix almost 2 years ago · 23 revisions