專案

一般

個人檔案

動作

外掛教學

本教學基於 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) ===========================================

請注意,每個外掛都有自己的一組移轉。

讓我們在控制台中新增一些 Polls,以便我們有一些東西可以使用。控制台是您可以互動地工作和檢查 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

這會建立一個帶有 2 個動作(#index#vote)的控制器 PollsController

編輯 plugins/polls/app/controllers/polls_controller.rb 來實作這 2 個動作。

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 %>

您可以移除 plugins/polls/app/views/polls/vote.html.erb,因為 #vote 動作不執行任何渲染。

新增路由

Redmine 不提供預設的萬用字元路由(':controller/:action/:id')。插件必須在其適當的 config/routes.rb 檔案中宣告所需的路由。因此,請編輯 plugins/polls/config/routes.rb,為這 2 個動作新增 2 個路由。

get 'polls', to: 'polls#index'
post 'post/:id/vote', to: 'polls#vote'

您可以在這裡找到有關 Rails 路由的更多資訊:https://rails-guides.dev.org.tw/routing.html

現在,重新啟動應用程式並將您的瀏覽器指向 https://127.0.0.1:3000/polls
您應該會看到 2 個投票,並且應該可以為它們投票。

國際化

翻譯檔必須儲存在 config/locales 中,例如 plugins/polls/config/locales/

擴展選單

我們的控制器運作良好,但使用者必須知道網址才能看到投票。使用 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 - 選單標題,可以是:
    • 本地化的字串符號
    • 字串
    • 可以將專案作為參數的 Proc
  • :before:after - 指定應插入選單項目的位置(例如 after: :activity
  • :first:last - 如果設定為 true,則該項目將保留在選單的開頭/結尾(例如 last: true
  • :html - 傳遞給 link_to 的 html 選項雜湊,用於渲染選單項目

在我們的範例中,我們在應用程式選單中新增了一個預設為空的項目。
重新啟動應用程式並前往 https://127.0.0.1:3000/projects

現在,您可以透過點擊當使用者不在專案內時出現的「投票」標籤來存取投票。

擴展專案選單

現在,讓我們考慮投票是在專案層級定義的(即使在我們的範例投票模型中並非如此)。因此,我們希望將「投票」標籤新增到專案選單中。
開啟 init.rb 並將剛才新增的行替換為以下 2 行:

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 個動作是公開的。我們稍後會詳細解釋這一點。 重新啟動應用程式並轉到您的一個專案

如果您點選「投票」標籤(位於第三個位置),您應該會注意到專案選單不再顯示。
要顯示專案選單,您必須初始化控制器的實例變數 @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 %>

專案 ID 在 :project_id 參數中可用,因為上述選單項目聲明中使用了 param: :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/,以便透過網路伺服器取得。因此,若要變更插件樣式表或 JavaScript,需要重新啟動應用程式。

連結需要使用引入的類別。因此,請在 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' 選項。

可以使用 javascript_include_tag 輔助函式,以相同的方式在插件視圖中包含 JavaScript。

設定頁面標題

您可以使用 html_title 輔助函式,從視圖內部設定 HTML 標題。
範例

  <% html_title "Polls" %>

使用掛鉤

視圖中的掛鉤

Redmine 視圖中的掛鉤可讓您將自訂內容插入常規 Redmine 視圖中。例如,查看 source: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。

控制器中的掛鉤

TODO

使您的插件可配置

在 Redmine 中註冊的每個插件都會顯示在管理/插件頁面上。設定控制器提供了對基本配置機制的支援。透過在插件的 init.rb 檔案中將「settings」方法添加到插件註冊區塊,即可啟用此功能。

Redmine::Plugin.register :polls do
  [ ... ]

  settings default: {'empty' => true}, partial: 'settings/poll_settings'
end

添加此方法將完成兩件事。首先,它會在管理/插件列表中為插件的說明區塊添加一個「配置」連結。點擊此連結將載入一個通用的插件配置範本視圖,該視圖將依次渲染 :partial 所引用的部分視圖。呼叫 settings 方法還將在插件的設定模組中添加支援。設定模型將根據插件名稱儲存和檢索序列化雜湊。可以使用設定方法名稱以 plugin_<插件名稱> 的形式存取此雜湊。對於此範例,可以透過呼叫 Setting.plugin_polls 來存取雜湊。

傳遞給 settings 方法的 :partial 雜湊鍵所引用的視圖將作為插件配置視圖中的部分載入。基本頁面佈局受插件配置視圖的限制:會宣告一個表單並產生提交按鈕。部分內容會被拉入表單內部表格 div 中的視圖中。插件的配置設定將會顯示,並且可以透過標準 HTML 表單元素進行修改。

注意:如果兩個插件的設定檔名有部分相同,則第一個插件會覆蓋第二個插件的設定頁面。因此,請確保為您的設定檔名提供唯一的名稱。

當頁面提交時,settings_controller 將採用由 'settings' 引用的參數雜湊,並以序列化格式將其直接儲存在 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 雜湊。表單助手將嘗試使用不存在的模型存取器方法來存取屬性。例如,對 @settings.notification_default 的呼叫將會失敗。此表單設定的值將作為 Setting.plugin_polls['notification_default'] 存取。

最後,settings 方法呼叫中的 :default 是用於註冊一個值,如果設定表中沒有為此插件儲存任何內容,則將從 Setting.plugin_polls 呼叫中返回該值。

測試您的插件

plugins/polls/test/test_helper.rb

這是我的測試助手檔案的內容

require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')

範例測試

適用於 Redmine 4.x 的 plugins/polls/test/functional/polls_controller_test.rb 內容

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

適用於 Redmine 3.x 的 plugins/polls/test/functional/polls_controller_test.rb 內容

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 Murata8 個月 前更新 · 119 個修訂版本