插件教程¶
本教程基于 Redmine 4.x 和 3.x。
您可以查看本教程的先前版本。
Redmine 1.x
Redmine 2.x
它假设您熟悉 Ruby on Rails 框架。
- 目录
- 插件教程
创建新插件¶
您可能需要设置 RAILS_ENV 变量才能使用以下命令
$ export RAILS_ENV="production"
在 Windows 上
$ set RAILS_ENV=production
可以使用 Redmine 插件生成器创建新插件。
此生成器的语法为
bundle exec rails generate redmine_plugin <plugin_name>
因此,打开命令提示符,将 "cd" 切换到您的 redmine 目录,然后执行以下命令
$ bundle exec rails generate redmine_plugin Polls create plugins/polls/app create plugins/polls/app/controllers create plugins/polls/app/helpers create plugins/polls/app/models create plugins/polls/app/views create plugins/polls/db/migrate create plugins/polls/lib/tasks create plugins/polls/assets/images create plugins/polls/assets/javascripts create plugins/polls/assets/stylesheets create plugins/polls/config/locales create plugins/polls/test create plugins/polls/test/fixtures create plugins/polls/test/unit create plugins/polls/test/functional create plugins/polls/test/integration create plugins/polls/test/system create plugins/polls/README.rdoc create plugins/polls/init.rb create plugins/polls/config/routes.rb create plugins/polls/config/locales/en.yml create plugins/polls/test/test_helper.rb
插件结构在 plugins/polls
中创建。编辑 plugins/polls/init.rb
以调整插件信息(名称、作者、描述和版本)
Redmine::Plugin.register :polls do
name 'Polls plugin'
author 'Author name'
description 'This is a plugin for Redmine'
version '0.0.1'
url 'http://example.com/path/to/plugin'
author_url 'http://example.com/about'
end
然后重新启动应用程序并将浏览器指向 https://127.0.0.1:3000/admin/plugins。
登录后,您应该在插件列表中看到您的新插件
注意:对插件中的 init.rb
文件的任何更改都需要重新启动应用程序,因为它不会在每次请求时重新加载。
生成模型¶
目前插件不存储任何内容。让我们为插件创建一个简单的 Poll 模型。语法是
bundle exec rails generate redmine_plugin_model <plugin_name> <model_name> [field[:type][:index] field[:type][:index] ...]
所以,转到命令提示符并运行
$ bundle exec rails generate redmine_plugin_model polls poll question:string yes:integer no:integer create plugins/polls/app/models/poll.rb create plugins/polls/test/unit/poll_test.rb create plugins/polls/db/migrate/xxxxxxxxxxxx_create_polls.rb
这将在 plugins/polls/db/migrate
中创建 Poll 模型及其对应的迁移文件 xxxxxxxxxxxx_create_polls.rb
class CreatePolls < ActiveRecord::Migration[5.2]
def change
create_table :polls do |t|
t.string :question
t.integer :yes, default: 0
t.integer :no, default: 0
end
end
end
注意:对于 Redmine 3.x class CreatePolls < ActiveRecord::Migration[5.2]
是 class CreatePolls < ActiveRecord::Migration
。
您可以根据需要调整迁移文件(例如,默认值...),然后使用以下命令迁移数据库
$ bundle exec rake redmine:plugins:migrate Migrating polls (Polls plugin)... == CreatePolls: migrating ==================================================== -- create_table(:polls) -> 0.0410s == CreatePolls: migrated (0.0420s) ===========================================
注意,每个插件都有自己的迁移集。
让我们在控制台中添加一些 Poll,以便我们有所操作。控制台是您可以交互式工作并检查 Redmine 环境的地方,并且非常适合玩耍。但现在我们只需要创建两个 Poll 对象
bundle exec rails console >> Poll.create(question: "Can you see this poll") >> Poll.create(question: "And can you see this other poll") >> exit
编辑您插件目录中的 plugins/polls/app/models/poll.rb
以添加一个 #vote 方法,该方法将从我们的控制器调用
class Poll < ActiveRecord::Base
def vote(answer)
increment(answer == 'yes' ? :yes : :no)
end
end
生成控制器¶
目前,此插件没有任何功能。那么我们来为我们的插件创建一个控制器。
我们可以使用插件控制器生成器来完成这个任务。语法是:
bundle exec rails generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]
因此,回到命令提示符并运行:
$ bundle exec rails generate redmine_plugin_controller Polls polls index vote create plugins/polls/app/controllers/polls_controller.rb create plugins/polls/app/helpers/polls_helper.rb create plugins/polls/test/functional/polls_controller_test.rb create plugins/polls/app/views/polls/index.html.erb create plugins/polls/app/views/polls/vote.html.erb
创建了一个控制器 PollsController
,包含2个操作(#index
和 #vote
)。
编辑 plugins/polls/app/controllers/polls_controller.rb
来实现这两个操作。
class PollsController < ApplicationController
def index
@polls = Poll.all
end
def vote
poll = Poll.find(params[:id])
poll.vote(params[:answer])
if poll.save
flash[:notice] = 'Vote saved.'
end
redirect_to polls_path(project_id: params[:project_id])
end
end
然后编辑 plugins/polls/app/views/polls/index.html.erb
,该文件将显示现有的投票。
<h2>Polls</h2>
<% @polls.each do |poll| %>
<p>
<%= poll.question %>?
<%= link_to 'Yes', { action: 'vote', id: poll[:id], answer: 'yes', project_id: @project }, method: :post %> <%= poll.yes %> /
<%= link_to 'No', { action: 'vote', id: poll[:id], answer: 'no', project_id: @project }, method: :post %> <%= poll.no %>
</p>
<% end %>
由于 #vote
动作没有执行渲染,所以您可以删除 plugins/polls/app/views/polls/vote.html.erb
。
添加路由¶
Redmine 不提供默认通配符路由(':controller/:action/:id'
)。插件必须在它们适当的 config/routes.rb
文件中声明所需的路由。因此,编辑 plugins/polls/config/routes.rb
以添加两个操作的2个路由。
get 'polls', to: 'polls#index'
post 'post/:id/vote', to: 'polls#vote'
有关 Rails 路由的更多信息,请参阅此处:https://guides.rubyonrails.net.cn/routing.html。
现在,重新启动应用程序,并将您的浏览器指向 https://127.0.0.1:3000/polls。
您应该能看到两个投票,并且应该能够为他们投票。
国际化¶
翻译文件必须存储在 config/locales 中,例如 plugins/polls/config/locales/
。
扩展菜单¶
我们的控制器运行良好,但用户必须知道 URL 才能查看投票。使用 Redmine 插件 API,您可以扩展标准菜单。
那么让我们向应用程序菜单添加一个新项目。
扩展应用程序菜单¶
编辑插件目录根目录下的 plugins/polls/init.rb
,在插件注册块的末尾添加以下行:
Redmine::Plugin.register :redmine_polls do
[...]
menu :application_menu, :polls, { controller: 'polls', action: 'index' }, caption: 'Polls'
end
语法是:
menu(menu_name, item_name, url, options={})
您可以扩展以下五个菜单:
:top_menu
- 左上角的顶部菜单:account_menu
- 包含登录/注销链接的右上角顶部菜单:application_menu
- 用户不在项目内时显示的主要菜单:project_menu
- 用户在项目内时显示的主要菜单:admin_menu
- 在管理页面上显示的菜单(只能在设置之后、插件之前插入)
可用选项有:
:param
- 用来表示项目 ID 的参数键(默认为:id
):if
- 在渲染项目之前调用的 Proc,只有当它返回 true 时,项目才显示:caption
- 菜单标题可以是:- 一个本地化字符串 Symbol
- 一个 String
- 一个可以接受项目作为参数的 Proc
:before
、:after
- 指定菜单项应插入的位置(例如,after: :activity
):first
、:last
- 如果设置为 true,则项目将保持在菜单的起始/结束位置(例如,last: true
):html
- 传递给link_to
的 HTML 选项的哈希,用于渲染菜单项
在我们的示例中,我们已经向应用程序菜单添加了一个空的项目。
重新启动应用程序并转到 https://127.0.0.1:3000/projects。
现在,您可以通过单击用户不在项目内时出现的“投票”选项卡来访问投票。
扩展项目菜单¶
现在,让我们假设投票是在项目级别定义的(即使在我们的示例投票模型中不是这样)。因此,我们希望将“投票”选项卡添加到项目菜单中。
打开 init.rb
并用以下两行替换刚刚添加的行:
Redmine::Plugin.register :redmine_polls do
[...]
permission :polls, { polls: [:index, :vote] }, public: true
menu :project_menu, :polls, { controller: 'polls', action: 'index' }, caption: 'Polls', after: :activity, param: :project_id
end
第二行将我们的“投票”选项卡添加到项目菜单中,紧挨着“活动”选项卡之后。第一行是必需的,它声明了来自PollsController
的2个操作是公开的。我们稍后将以更详细的方式解释这一点。重新启动应用程序,然后转到您的项目之一
如果您点击“投票”选项卡(第3个位置),您应该会注意到项目菜单不再显示。
要使项目菜单可见,您必须初始化控制器的实例变量@project
。
编辑您的PollsController以实现这一点
def index
@project = Project.find(params[:project_id])
@polls = Poll.all # @project.polls
end
def vote
poll = Poll.find(params[:id])
poll.vote(params[:answer])
if poll.save
flash[:notice] = 'Vote saved.'
end
redirect_to :action => 'index', project_id: params[:project_id]
end
编辑您的routes.rb以实现这一点
post 'projects/:project_id/post/:id/vote', to: 'polls#vote', as: 'vote_poll'
编辑您的views/polls/index.heml.erb以实现这一点
<h2>Polls</h2>
<% @polls.each do |poll| %>
<p>
<%= poll.question %>?
<%= link_to 'Yes', { action: 'vote', id: poll.id, answer: 'yes', project_id: @project.id }, method: :post %> (<%= poll.yes %>) /
<%= link_to 'No', { action: 'vote', id: poll.id, answer: 'no', project_id: @project.id }, method: :post %> (<%= poll.no %>)
</p>
<% end %>
由于在菜单项声明中选择了param: :project_id
选项,因此项目ID在:project_id
参数中可用。
现在,当查看投票时,您应该会看到项目菜单
从菜单中删除项目¶
要删除菜单中的项目,您可以使用delete_menu_item
,如下例所示
Redmine::Plugin.register :redmine_polls do
[...]
delete_menu_item :top_menu, :my_page
delete_menu_item :top_menu, :help
delete_menu_item :project_menu, :overview
delete_menu_item :project_menu, :activity
delete_menu_item :project_menu, :news
end
添加新权限¶
到目前为止,任何人都可以为投票投票。让我们通过更改权限声明来使其更具可配置性。
我们将声明2个基于项目的权限,一个用于查看投票,另一个用于投票。这些权限不再是公开的(移除了public: true
选项)。
编辑plugins/polls/init.rb
,用这两行替换之前的权限声明
permission :view_polls, polls: :index
permission :vote_polls, polls: :vote
重新启动应用程序,然后转到https://127.0.0.1:3000/roles/permissions
现在,您可以为现有的角色分配这些权限。
当然,需要在PollsController中添加一些代码,以便操作根据当前用户的权限实际上受到保护。为此,我们只需追加:authorize
过滤器,并确保在调用此过滤器之前,Herve Harster实例变量被适当地设置。
下面是#index
操作的样子
class PollsController < ApplicationController
before_action :find_project, :authorize, only: [:index, :vote]
[...]
def index
@polls = Poll.all # @project.polls
end
[...]
private
def find_project
# @project variable must be set before calling the authorize filter
@project = Project.find(params[:project_id])
end
end
在#vote
操作之前检索当前项目可以使用类似的方式完成。
之后,查看和投票将仅限于管理员用户或具有项目适当角色的用户。
如果您想以多语言方式显示权限的符号,则需要向语言文件中添加必要的文本标签。
只需在plugins/polls/config/locales
中创建一个*.yml文件(例如,en.yml
),并填充如下标签
"en":
permission_view_polls: View Polls
permission_vote_polls: Vote Polls
在此示例中,创建的文件称为en.yml
,但所有其他受支持的语言文件也是可能的。
如上例所示,标签由权限符号:view_polls
和:vote_polls
组成,前面添加了额外的permission_
。
重新启动您的应用程序,并指向权限部分。
创建项目模块¶
到目前为止,投票功能已添加到所有项目中。但是,您可能只想为某些项目启用投票。
因此,让我们创建一个“投票”项目模块。这是通过在#project_module
调用中包裹权限声明来完成的。
编辑init.rb
并更改权限声明
project_module :polls do
permission :view_polls, polls: :index
permission :vote_polls, polls: :vote
end
重新启动应用程序,然后转到您的项目设置之一。
点击“模块”选项卡。您应该在模块列表末尾看到“投票”模块(默认禁用)
您现在可以按项目级别启用/禁用投票。
改进插件视图¶
添加样式表¶
让我们从向插件视图中添加样式表开始。
在plugins/polls/assets/stylesheets
目录中创建一个名为voting.css
的文件
a.vote { font-size: 120%; }
a.vote.yes { color: green; }
a.vote.no { color: red; }
当启动应用程序时,插件资源会自动复制到 public/plugin_assets/polls/
以通过您的 web 服务器提供。因此,对插件样式表或javascripts的任何更改都需要重启应用程序。
引入的类需要由链接使用。因此,更改文件 plugins/polls/app/views/polls/index.html.erb
中的链接声明为
<%= link_to 'Yes', {action: 'vote', id: poll[:id], answer: 'yes', project_id: @project }, method: :post, class: 'vote yes' %> (<%= poll.yes %>)
<%= link_to 'No', {action: 'vote', id: poll[:id], answer: 'no', project_id: @project }, method: :post, class: 'vote no' %> (<%= poll.no %>)
然后,将以下行追加到 index.html.erb
文件末尾,以便 Redmine 通过页面头部包含您的样式表
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'voting', plugin: 'polls' %>
<% end %>
请注意,在调用 stylesheet_link_tag
辅助函数时需要 plugin: 'polls'
选项。
JavaScripts 可以使用相同的 javascript_include_tag
辅助函数包含在插件视图中。
设置页面标题¶
您可以使用 html_title
辅助函数从您的视图中设置 HTML 标题。
示例
<% html_title "Polls" %>
使用钩子¶
视图中的钩子¶
Redmine 视图中的钩子允许您向常规 Redmine 视图中插入自定义内容。例如,查看 源代码:tags/2.0.0/app/views/projects/show.html.erb#L52 显示存在 2 个钩子:一个名为 :view_projects_show_left
用于向左侧添加内容,另一个名为 :view_projects_show_right
用于向右侧添加内容。
要在视图中使用一个或多个钩子,您需要创建一个继承自 Redmine::Hook::ViewListener
的类并实现您想要使用的钩子的方法名称。要向项目概览中添加一些内容,请向您的插件添加一个类,并在您的 init.rb
中要求它,然后实现名称与钩子名称匹配的方法。
为我们的插件创建一个文件 plugins/polls/lib/polls_hook_listener.rb
,内容如下
class PollsHookListener < Redmine::Hook::ViewListener
def view_projects_show_left(context = {})
return content_tag("p", "Custom content added to the left")
end
def view_projects_show_right(context = {})
return content_tag("p", "Custom content added to the right")
end
end
将此行添加到 plugins/polls/init.rb
require_dependency File.expand_path('../lib/polls_hook_listener', __FILE__)
重启 Redmine 并查看项目的概览标签。您应该在概览的左侧和右侧看到字符串。
您还可以使用 render_on
辅助函数渲染一个部分。在我们的插件中,您必须在 plugins/polls/lib/polls_hook_listener.rb
中替换刚刚创建的内容,如下所示
class PollsHookListener < Redmine::Hook::ViewListener
render_on :view_projects_show_left, partial: "polls/project_overview"
end
通过创建文件 app/views/polls/_project_overview.html.erb
将部分添加到您的插件中。其内容(使用一些文本,如 '钩子消息!')将被追加到项目概览的左侧。别忘了重启 Redmine。
控制器中的钩子¶
待办事项
使您的插件可配置¶
注册到 Redmine 的每个插件都会在管理员/插件页面显示。通过 Settings 控制器提供对基本配置机制的支持。此功能通过将 "settings" 方法添加到插件注册块中的插件 init.rb 文件中启用。
Redmine::Plugin.register :polls do
[ ... ]
settings default: {'empty' => true}, partial: 'settings/poll_settings'
end
这将完成两件事。首先,它将为管理员/插件列表中的插件描述块添加一个 "配置" 链接。通过此链接将加载一个通用的插件配置模板视图,然后它会渲染由 :partial 引用的部分视图。调用设置方法还将为 Setting 模块添加对插件的支持。Setting 模型将根据插件名称存储和检索序列化的哈希。此哈希通过 Setting 方法名称访问,格式为 plugin_
传递给设置方法的 :partial 哈希键引用的视图将作为插件配置视图的一部分加载。基本页面布局受插件配置视图的限制:声明一个表单并生成提交按钮。部分被拉入表单内的 table div 中。将显示插件的配置设置,并且可以通过标准 HTML 表单元素进行修改。
注意:如果两个插件具有相同的设置部分名称,则第一个将覆盖第二个的设置页面。因此,请确保您的设置部分名称是唯一的。
当页面提交时,settings_controller将根据'reseting'引用的参数hash,以序列化格式直接存储在Setting.plugin_polls中。每次生成页面时,Setting.plugin_polls的当前值将被分配给局部变量settings。
创建一个名为plugins/polls/app/views/settings/_poll_settings.erb
的文件,并填充以下内容
<table>
<tbody>
<tr>
<th>Notification Default Address</th>
<td>
<input type="text" id="settings_notification_default"
value="<%= settings['notification_default'] %>"
name="settings[notification_default]" >
</td>
</tr>
</tbody>
</table>
在上面的示例中,配置表单不是使用Rails表单辅助程序创建的。这是因为没有@settings模型,只有setting hash。表单辅助程序将尝试使用模型访问器方法访问属性,但这些方法不存在。例如,调用@settings.notification_default将失败。此表单设置的值可以通过Setting.plugin_polls['notification_default']访问。
最后,在settings方法调用中的:default是为了注册一个值,如果此插件在settings表中没有任何存储,则Setting.plugin_polls调用将返回该值。
测试您的插件¶
plugins/polls/test/test_helper.rb¶
以下是我的测试辅助文件的内容
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
示例测试¶
plugins/polls/test/functional/polls_controller_test.rb
的内容,适用于Redmine 4.x
require File.expand_path('../../test_helper', __FILE__)
class PollsControllerTest < ActionController::TestCase
fixtures :projects
def test_index
get :index, params: { project_id: 1 }
assert_response :success
assert_template 'index'
end
end
plugins/polls/test/functional/polls_controller_test.rb
的内容,适用于Redmine 3.x
require File.expand_path('../../test_helper', __FILE__)
class PollsControllerTest < ActionController::TestCase
fixtures :projects
def test_index
get :index, project_id: 1
assert_response :success
assert_template 'index'
end
end
运行测试¶
如有必要,初始化测试数据库
$ RAILS_ENV=test bundle exec rake db:drop db:create db:migrate redmine:plugins:migrate redmine:load_default_data
执行polls_controller_test.rb
$ RAILS_ENV=test bundle exec rake test TEST=plugins/polls/test/functional/polls_controller_test.rb
使用权限测试¶
如果您的插件需要项目成员资格,请将以下内容添加到功能测试的开始处
def test_index
@request.session[:user_id] = 2
...
end
如果您的插件需要特定的权限,您可以将该权限添加到用户角色中(在 fixtures 中查找适合用户的角色)
def test_index
Role.find(1).add_permission! :my_permission
...
end
您可以如此启用/禁用特定模块
def test_index
Project.find(1).enabled_module_names = [:mymodule]
...
end
参考文件层次结构¶
以下是本教程和钩子中提到的所有文件和目录的简单列表。这有助于确保使用标准路径,并且对于新手来说,了解文件应该放在哪里也很有用。
plugins/PLUGIN/README.rdoc plugins/PLUGIN/init.rb plugins/PLUGIN/app/ plugins/PLUGIN/app/controllers/ plugins/PLUGIN/app/controllers/CONTROLLER_controller.rb plugins/PLUGIN/app/helpers/ plugins/PLUGIN/app/helpers/CONTROLLER_helper.rb plugins/PLUGIN/app/models/ plugins/PLUGIN/app/models/MODEL.rb plugins/PLUGIN/app/views/ plugins/PLUGIN/app/views/CONTROLLER/ plugins/PLUGIN/app/views/CONTROLLER/_PARTIAL.html.erb plugins/PLUGIN/app/views/CONTROLLER/CONTROLLER-ACTION.html.erb plugins/PLUGIN/app/views/hooks/ plugins/PLUGIN/app/views/hooks/_HOOK.html.erb plugins/PLUGIN/app/views/settings/ plugins/PLUGIN/app/views/settings/_MODEL_settings.html.erb plugins/PLUGIN/assets/ plugins/PLUGIN/assets/images/ plugins/PLUGIN/assets/javascripts/ plugins/PLUGIN/assets/stylesheets/ plugins/PLUGIN/assets/stylesheets/voting.css plugins/PLUGIN/config/ plugins/PLUGIN/config/locales/ plugins/PLUGIN/config/locales/en.yml plugins/PLUGIN/config/routes.rb plugins/PLUGIN/db/ plugins/PLUGIN/db/migrate/ plugins/PLUGIN/db/migrate/001_create_MODELs.rb plugins/PLUGIN/lib/ plugins/PLUGIN/lib/PLUGIN_hook_listener.rb plugins/PLUGIN/lib/PLUGIN/ plugins/PLUGIN/lib/PLUGIN/hooks.rb plugins/PLUGIN/lib/PLUGIN/MODEL_patch.rb plugins/PLUGIN/lib/tasks/ plugins/PLUGIN/test/ plugins/PLUGIN/test/test_helper.rb plugins/PLUGIN/test/functional/ plugins/PLUGIN/test/functional/CONTROLLER_controller_test.rb plugins/PLUGIN/test/unit/ plugins/PLUGIN/test/unit/MODEL_test.rb
由 Tomofumi Murata 更新 8个月前 · 119次修订