Redmineのplugin開発
今回、Redmineのプラグインを開発してみてHookやPlugin用の設定ページの作成や独自rakeコマンド、Mail送信機能拡張など様々な機能を触ってみて思ったよりも便利に開発が出来るんだということがわかった。
できたものは超オレオレプログラムのごり押し機能だけど、勉強になったしとりあえず動くしまぁいいやと思った。
要望
チケットの開始時間単位のリマインダー機能。
Redmineには、リマインダー機能は既に存在するがチケット何時から開始とかいう場合に1時間前にリマインダーを送りたいとかができない。
ソース
GitHubに登録しています。
環境
- Redmine1.2
- Rails2.3.11
機能
まず、カスタムフィールドで開始時間を定義する。
ここを参考に「開始時間」のカスタムフィールドを定義しておく。
そして、それに関連するトラッカーを決める。
例えば「イベント」トラッカーにカスタムフィールド「開始時間」をひも付けておく。
そして、定義したトラッカーでチケットを作成した時に、OSのタスク機能を利用して開始時間の○時間前にメール送信用のタスクを登録する。
チケットが完了や却下など、完了を示すステータスで更新されたら登録しておいたタスクを削除する。
かなりごり押しのテクニックなのであまり参考になるようなところはないかもしれない。
実装
本体を傷つけないように、プラグインとして開発する。
まず、プラグインのスケルトンを作成する。
$ cd RAILS_ROOT $ ruby script/generate redmine_plugin ReminderTicket
これで雛形が作成されるので、個別に実装していく。
まずは、Modelを作成する。
既存のMailerモデルを継承して今回用にRemindMailerクラスとしてカスタマイズする。
self.reminder_ticket
このメソッドはrakeコマンドより呼び出される。
引数はチケットID(issue.id)を受け取るようにしておく。
reminder_time
このメソッドがメール送信時に呼び出されるメソッド。
呼び出し元はself.reminder_ticketでdeliver_をつけて呼び出している。
deliver_を頭に付けるとメールが送信されるみたいで、送信先(TOやCC)や件名、本文などを定義する。
render_multipartが呼び出されたところで、Viewに渡される。
app/models/remind_mailer.rb
class RemindMailer < Mailer def self.reminder_ticket(options={}) issue = Issue.find(options[:issue_id]) deliver_reminder_time(issue.assigned_to, issue) if issue.assigned_to && issue.assigned_to.active? end def reminder_time(user, issue) set_language_if_valid user.language recipients issue.recipients cc(issue.watcher_recipients - @recipients) subject l(:mail_subject_reminder_ticket, :start_time => issue.custom_field_values[Setting.plugin_redmine_reminder_ticket['target_custome_field_value_id'].to_i], :count => Setting.plugin_redmine_reminder_ticket['diff_time'].to_i / 3600.0) body :issue => issue, :start_time => issue.custom_field_values[Setting.plugin_redmine_reminder_ticket['target_custome_field_value_id'].to_i], :count => Setting.plugin_redmine_reminder_ticket['diff_time'].to_i / 3600.0, :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue.id) render_multipart('reminder', body) end end
次にメール送信時のViewと設定用のViewを作成する。
app/views/remind_mailerディレクトリに以下の2つのテンプレートを作成する。(このディレクトリ名はRailsの規約通りModel名と結びつけておく)
メール送信設定でテキスト形式とHTML形式が設定できると思うが、それら2つに対応させておく。
ファイル名はModelのrender_multipartの第一引数で定義した名前.text.html.rhtml | text.plain.rhtml としておく。
View内で使用できるインスタンス変数(@付きの変数)はModel内のbodyでシンボル定義した値となる。
app/views/remind_mailer/reminder.text.html.rhtml
<p><%= l(:mail_body_reminder_ticket, :start_time => @start_time, :count => @count) %></p> <ul> <li><%=h @issue.project %> - <%= "#{@issue.tracker} ##{@issue.id}"%>: <%=h @issue.subject %></li> </ul> <p><%= link_to l(:label_issue), @issue_url %></p>
app/veiws/remind_mailer/remainder.text.plain.rhtml
<%= l(:mail_body_reminder_ticket, :start_time => @start_time, :count => @count) %>: * <%= "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}" %> <%= @issue_url %>
2012.03.06追記
※Redmine1.3以降はテンプレート名が変更されているので注意!!
reminder.text.html.rhtml -> reminder.html.erb
reminder.text.plain.rhtml -> reminder.text.erb
設定用Viewを作成する。
プラグイン内で簡潔する設定内容を編集できる頁を作成する。
これは、Redmineの管理メニュー > プラグインでプラグインの一覧から個別に設定画面へ飛ぶことのできるものである。
app/views/settings/_redmine_reminder_ticket_settings.rhtml
<p> <table> <tr><td> Target Tracker ID </td><td> <%= text_field_tag('settings[target_tracker_id]', @settings['target_tracker_id'])%> </td></tr> <tr><td> Target Custome Field Value ID </td><td> <%= text_field_tag('settings[target_custome_field_value_id]', @settings['target_custome_field_value_id'])%> </td></tr> <tr><td> Diff Time </td><td> <%= text_field_tag('settings[diff_time]', @settings['diff_time'])%> </td></tr> <tr><td> SC </td><td> <%= text_field_tag('settings[sc]', @settings['sc'])%> </td></tr> <tr><td> RU </td><td> <%= text_field_tag('settings[ru]', @settings['ru'])%> </td></tr> <tr><td> RP </td><td> <%= text_field_tag('settings[rp]', @settings['rp'])%> </td></tr> </table> </p>
設定できる内容は、以下の通りとした。
target_tracker_id
- 対象となるトラッカーのID
target_custome_field_value_id
- 対象となる開始時間を定義したカスタムフィールドのID
diff_time
- 何時間前に送られるか
sc
- schtasksのオプション
ru
- schtasksのオプション
rp
- schtasksのオプション
一応多言語対応用にja.ymlを作成しておく。
日本語しか対応させていないけど。
ja: mail_subject_reminder_ticket: "本日%{start_time}から開始されるチケットが%{count}時間後に到来します" mail_body_reminder_ticket: "本日%{start_time}から開始されるチケットが%{count}時間後に到来します"
チケット作成時にタスクを登録して、そのタスクから呼び出されるバッチファイルを作成する。
どこに作成したらいいのかわからない&環境依存なのでもっといい方法があるはず。
rakeコマンド呼び出し用のバッチファイル(Windows専用)。
lib/bat/reminder_ticket.bat
cd C:\redmine rake redmine:send_reminder_ticket issue_id=%1 RAILS_ENV=production
次は要となる部分。
既存のチケット登録、更新処理の後に実行させたい機能をHookというのを使って実現する。
これは、既存コントローラで既に埋め込まれてるHookスクリプトを奪い取って(?)実行してしまう。という機能。
本体に影響を与えずに機能追加したいという場合に有効。
今回は、更新時(edit_after_save)と新規登録時(new_after_save)のHookを利用して
更新時に、完了ステータスなら(schtasksコマンドで)タスク削除
それ以外ならタスク削除&作成で更新処理
新規登録時に、タスク作成処理を実現した。
lib/reminder_ticket/hooks/controller_issues_edit_after_save_hook.rb
module ReminderTicket module Hooks class ControllerIssuesEditAfterSaveHook < Redmine::Hook::ViewListener def controller_issues_edit_after_save(context={}) begin issue = context[:issue] if issue.tracker_id == Setting.plugin_redmine_reminder_ticket['target_tracker_id'].to_i if issue.closing? or issue.closed? Schtasks.delete_from_issue(issue) else Schtasks.delete_from_issue(issue) Schtasks.create_from_issue(issue) end end rescue false end end end end end
lib/reminder_ticket/hooks/controller_issues_new_after_save_hook.rb
module ReminderTicket module Hooks class ControllerIssuesNewAfterSaveHooks < Redmine::Hook::ViewListener def controller_issues_new_after_save(context={}) begin issue = context[:issue] if issue.tracker_id == Setting.plugin_redmine_reminder_ticket['target_tracker_id'].to_i Schtasks.create_from_issue(issue) end rescue false end end end end end
rakeタスクファイルを作成する。
こいつがメール送信をするrakeコマンドを提供する。
バッチファイルで定義した以下のコマンドが打てるようになる。
引数はENV['引数名']で受け取れる。
Modelで定義したRemindMailerに投げてメールが送信される。
rake redmine:send_reminder_ticket issue_id=[issue_id] RAILS_ENV=production
lib/tasks/reminder_ticket.rake
namespace :redmine do task :send_reminder_ticket => :environment do options = {} options[:issue_id] = ENV['issue_id'].to_i if ENV['issue_id'] RemindMailer.reminder_ticket(options) end end
Windowsのタスク機能を提供する「schtasks」を呼び出すモジュールを定義する。
Hook時に呼び出されている。
lib/schtasks.rb
module Schtasks def self.create_from_issue(issue) options = {} options[:tn] = issue.id options[:tr] = "#{RAILS_ROOT}/vendor/plugins/redmine_reminder_ticket/lib/bat/reminder_ticket.bat #{issue.id}" options[:sc] = Setting.plugin_redmine_reminder_ticket['sc'] options[:sd] = issue.start_date.strftime("%Y/%m/%d") options[:st] = (Time.parse("#{options[:sd]} #{issue.custom_field_values[Setting.plugin_redmine_reminder_ticket['target_custome_field_value_id'].to_i]}") - Setting.plugin_redmine_reminder_ticket['diff_time'].to_i).strftime("%H:%M") options[:ru] = Setting.plugin_redmine_reminder_ticket['ru'] options[:rp] = Setting.plugin_redmine_reminder_ticket['rp'] create(options) end def self.create(options) return unless options system "schtasks /create /tn \"#{options[:tn]}\" /tr \"#{options[:tr]}\" /sc \"#{options[:sc]}\" /sd \"#{options[:sd]}\" /st \"#{options[:st]}\" /ru \"#{options[:ru]}\" /rp \"#{options[:rp]}\"" end def self.delete_from_issue(issue) options = {} options[:tn] = issue.id delete(options) end def self.delete(options) return unless options system "schtasks /delete /f /tn #{options[:tn]}" end end
大事なinit.rbファイル。
settingsで定義した:defaultが設定内容となる。
:partialは設定用Viewを指す。
settingsは@settingsとして設定用View内でインスタンス変数としてアクセスできる。
ただし、そのほかのスコープでは、Setting.plugin_redmine_reminder_ticket['設定項目名']でアクセスする必要がある。
命名規則は、Setting.plugin_[プラグイン名]となる。
設定用のModelとかは作成する必要がなく、init.rbでsettingsとして定義した内容が設定内容として保持されるというところに感動した。便利。
最初は個別に設定内容を定数として別ファイルに書こうかと思っていたがその必要がなくなった。
init.rb
require 'redmine' require 'time' require_dependency 'schtasks' require_dependency 'reminder_ticket/hooks/controller_issues_new_after_save_hook' require_dependency 'reminder_ticket/hooks/controller_issues_edit_after_save_hook' Redmine::Plugin.register :redmine_reminder_ticket do name 'Redmine Reminder Ticket plugin' author 'Souichi Saitou' description 'This is a plugin for Redmine' version '0.0.1' url '' author_url 'http://souichi.heroku.com' settings :default => {'target_tracker_id' => 4, 'target_custome_field_value_id' => 0, 'diff_time' => 3600, 'sc' => 'ONCE', 'ru' => nil, 'rp' => nil}, :partial => 'settings/redmine_reminder_ticket_settings' end