项目

常规

个人资料

操作

插件教程

本教程基于 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_。对于本示例,可以通过调用 Setting.plugin_polls 访问哈希。

传递给设置方法的 :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次修订