sou's blog

落ち着いた華やかさがあり、上品に明るく陽気なさまを表す。

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

利用する場合

  • とりあえず、Windowsでしか機能しない。
  • バッチファイルのRAILS_ROOTやRAILS_ENVは独自に直さないといけない。
  • DBなどは使用していないので、githubから落としたファイルコピペでOK(ディレクトリ名はredmine_reminder_ticket)
  • 起動したら設定ページでトラッカーIDとカスタムフィールドIDをひもづける。
  • schtasksコマンド実行時に「ru」と「rp」が必要かもしれない。
  • チケットの一括更新などには対応していない。