プロジェクト

全般

プロフィール

Plugin Internals の日本語訳です。(結構、意訳しています。よく言えば超訳 ?)

プラグイン インターナル

プラグイン開発関連の情報はこのページに書いていきましょう。
(訳注 : このページは redmine.org のページの翻訳なので、実際にプラグイン開発関連の情報を追加したい場合は プラグイン Tips の方に追加してください)

プラグインが動作する Redmine のバージョンを指定する。

プラグインで Redmine 本体の機能を使ったり、Redmine のページを改造するようなプラグインを作ったりすると、ある特定のバージョンの Redmine でしかプラグインが動作しなくなることがあります。

このような場合、プラグインが動作する Redmine のバージョンを指定する必要が出てきます。 requires_redmine メソッドを使えばこれが実現できます。(この機能は #2162 で提案され、r2042 で実際に実装されました)
このメソッドを使えば簡単、確実にプラグインが動作する Redmine のバージョンを指定することが出来ます。プラグインがロードされる際、 このメソッドを記述していると要求する Redmine のバージョンを満たしていない場合には、未対応のバージョンですとといったメッセージを出力してロードを中止します。

例えば IIda さんの Wiki extensions プラグイン では以下のように使用されています。

Redmine::Plugin.register :redmine_wiki_extensions do
  name 'Redmine Wiki Extensions plugin'
  # :
  version '0.3.5'
  requires_redmine :version_or_higher => '1.1.0'

  # :

Redmine の本体機能のオーバーロード

Rails は MVC構造 になっています。プラグインで Redmine 本体の機能を変えようした時、MVC のうちビューの場合にはコントローラやモデルと違って Redmine 本体のものをプラグインのもので上書きするオーバーロードの方法をとることになります。

コントローラやビューをプラグインで書き変えた場合に Redmine/Rails がどのような動作をとるか説明します。ここでのプラグインの名前は MyPlugin としています。
コントローラだけ説明していますが、モデルの場合も同じような流れになります。

コントローラ(モデル)

  1. Rails の起動の開始
  2. Rails フレームワークをロード
  3. 各プラグインのロード
    1. MyPlugin 内で IssueController を見つけると、その show アクションの定義を見にいきます。
  4. <redmine_folder>/app から Rails アプリケーション(Redmine)をロード
    1. そこで再度 IssueController を見つけ、アプリケーションの show アクションの定義を見にいきます。
    2. ここでプラグインで定義された show アクションはアプリケーションのものに上書きされてしまいます。これは Rails というよりも Ruby の仕様上そうなるようになっています。
  5. Rails の起動が完了して、サーバが立ち上がる

ビュー

ビューの場合もコントローラとほぼ同じようにロードされますが、少し違うところがあります。これは Redmine のパッチ機能のためです。

  1. Rails の起動の開始
  2. Rails フレームワークをロード
  3. 各プラグインのロード
    1. <redmine_folder>/vendor/plugins/my_plugin/app/views 以下にディレクトリを見つけると、それを views のパッチの 先頭に追加 します。
  4. <redmine_folder>/app から Rails アプリケーション(Redmine)をロード
  5. Rails の起動が完了して、サーバが立ち上がる
  6. サーバに要求がきて、ビューの描画が必要になる
  7. Rails は要求されたアクションに合うテンプレートを探した後、プラグインのテンプレートをロード
    これはプラグインのビューがパッチとして 先頭に追加 されていたためです。
  8. Rails はプラグインのビューを表示

なぜ、 MVC のうちビューだけこのようになっているかというと、 モデルやコントローラの場合には Ruby のモジュールのインクルードを使えば、こちらは簡単に機能を拡張することが出来るからです。
プラグインで Redmine 本体の機能を変えたい場合にも Redmine 本体のモデルやコントローラのメソッドなどは上書きするべきではありませし、実際そういった機能の API は Redmine では用意されていません。
しかし、ビューの場合には Redmine の本体の機能を上書きする方法をとることになります。Rails ではビューはモデルやコントローラと比べるとちょっとトリッキーな方法で機能が実現されいて、拡張するよりも書き換える機能の方が使い勝手がいいためです。

Redmine 本体の表示を変えるには <redmine_folder>/app/views 以下のファイルと全く同じ名前のファイルをプラグインのディレクトリに置いておくだけです。そうするとそちらが表示の際に使われるようになります。例えばプロジェクトのインデックスページを書き換えたい場合、 <redmine_folder>/vendor/plugins/my_plugin/app/views/projects/index.rhtml のファイルを作成します。

Redmine の本体機能の拡張

先ほどモデルやコントローラはオーバーロードしないと説明しましたが、まれに書き換えくなることはあります。そのような場合、かわりに次の方法をとります。

  • モデルやコントローラに新しいメソッドを追加する
  • 既存のメソッドをラップする

新しいメソッドの追加

新しいメソッドを追加する方法の分かりやすい例は Eric Davi さんの Budget plugin にあります。
このプラグインではチケットモデルクラスに deliverable_subject というメソッドを追加しています。

module IssuePatch
  def self.included(base) # :nodoc:
    base.send(:include, InstanceMethods)
  end

  module InstanceMethods
    # Wraps the association to get the Deliverable subject.  Needed for the 
    # Query and filtering
    def deliverable_subject
      unless self.deliverable.nil?
        return self.deliverable.subject
      end
    end
  end    
end

既存メソッドのラップ

Eric Davis さんの Rate plugin には既存のメソッドをラップするいい例となる記述があります。
ここでは alias_method_chain を使って UsersHelperuser_settings_tabs メソッドをラップして、 DB にチケットを保存するタイミングで処理を追加しています。
user_settings_tabs が Redmine から呼ばれる時の流れは次のようになります。

  1. Redmine 本体が UsersHelper#user_settings_tabs を呼び出す
  2. user_settings_tabs が実行される (これは実際には user_settings_tabs_with_rate_tab です)
  3. user_settings_tabs_with_rate_tab がもともとの user_settings_tabs を呼び出す。(元のものは user_settings_tabs_without_rate_tab と名前が変更されています)
  4. 元のメソッドの結果にプラグイン用のデータを追加
  5. user_settings_tabs_with_rate_tab は Redmine 本体の結果にプラグイン用の結果を結合したものを返す
module RateUsersHelperPatch
  def self.included(base) # :nodoc:
    base.send(:include, InstanceMethods)

    base.class_eval do
      alias_method_chain :user_settings_tabs, :rate_tab
    end
  end

  module InstanceMethods
    # Adds a rates tab to the user administration page
    def user_settings_tabs_with_rate_tab
      tabs = user_settings_tabs_without_rate_tab
      tabs << { :name => 'rates', :partial => 'users/rates', :label => :rate_label_rate_history}
      return tabs
    end
  end
end

alias_method_chain は小さな拡張用メソッドですが、とても強力です。

Rails のコールバックの使用

チケットの保存や作成など際にプラグインで処理を追加したい場合、すべてのチケットに対して処理を追加したいならば、 Redmine のホック機能 よりも Rails の コールバック を使った方がいいと思います。
その主な理由は新しいチケットを作ったとき、:controller_issues_edit_before_save のホックにメソッドを追加したとしてもこちらは呼び出されないためです。

Rails のコールバックを利用した例は Eric Davis さんの "Kanban plugin" を見てください。

このプラグインでは、新規作成や更新を含めたチケットのすべての保存のタイミングで issue.update_kanban_from_issue が確実に実行されるようになっています。

もし、チケットを新規作成したい場合にだけ処理を追加したい場合には、 before_create ではなく after_save コールバックを使用してください。 after_create コールバックではチケットの保存が成功したかどうかにかかわらず処理が呼び出されますが、 after_save を使うと実際に保存が成功された場合にだけ処理が実行されるようになっています。

マイページへのブロックの追加

マイページのブロックに関して次のような質問がよくあります。

  • マイページで、なぜかブロック選択用のドロップダウンメニューの項目名が翻訳されません。

ドロップダウンメニューの項目用の翻訳メッセージはプラグインのローケルファイルを記述する際、規約でどのエントリ名を使うか決められています。
そのエントリ名はブロック用のプラグインのファイル名と同じでなければなりません。例えばそのブロック用ファイルの名前が次のようなものだったとします。

<myplugin_folder>/app/views/my/blocks/<myblocks_view_file_name>.erb

メニュー項目を翻訳したい場合、プラグインのローケルファイル <myplugin_folder>/confige/locale/en.yml などには次の行を追加する必要があります。

<myblocks_view_file_name>: <ドロップダウンメニュー項目の表示名をここに記述します>

この決まった名前でローケルファイルに定義が記述されていないとメニュー項目は翻訳されていない状態になってしまいます。

参照情報