SyntaxHighlighter

ラベル Redmine の投稿を表示しています。 すべての投稿を表示
ラベル Redmine の投稿を表示しています。 すべての投稿を表示

2012年6月3日日曜日

Add Subversion Links の Redmine 2.0 対応

Add Subversion Links プラグインを Redmine 2.0 対応したので、そのときのメモです。

Add Subversion Links プラグイン

Add Subversion Links プラグインは、Redmine のプラグインです。

このプラグインを入れておくと、 Redmine のチケット画面やリポジトリ画面に、 Subversion リポジトリ本体へのリンクが勝手に追加されます。

詳しくは GitHub の説明ページを見てください。

Redmine 2.0 対応

Redmine 2.0 では、ベースとなる Rails のバージョンが 3.2 に上がりました。
その結果、多くのプラグインの互換性がなくなったので、 Redmine 2.0 にバージョンアップしていない人も多いと思います。

Add Subversion Links の場合も、動かなくなっていました。

一つめの原因は、 Rails 3 で導入された html_safe に関するものでした。
Rails 3 では、「エスケープされていない String」と「エスケープ済みの SafeBuffer」を区別して扱うことで、エスケープ漏れや、多重のエスケープを回避しています。

Add Subversion Links では、 Subversion リポジトリへのリンクを生成する部分で、一部、エスケープしていない文字が入っており、結果として正しい表示になっていませんでした。
(エスケープしていない文字は、単なる空白だったので、最初は何が起こったかよく分かりませんでした。)

もう一つは、 image_path メソッドが削除されたことでした。

原因はよく調べていませんが、プラグインが持つ image assets の場所を返すメソッド image_path がなくなっていました。

Add Subversion Links では、一部、 JavaScript で動的にリンクを生成している箇所があり、そのリンク画像生成のために image_path を使っていたので、エラーになっていました。
結局、こちらは image_tag というタグ自体を生成するメソッドは残っていたので、こちらを使うように処理を変更することで対応しました。

Add Subversion Links プラグインの仕組み

Redmine 2.0 対応とは関係ないですが、このプラグインの仕組みを少しだけ書いておきます。

Redmine プラグインの作り方は、他にたくさんの優れたページがあるので、ここでは書きません。

ここでは、 Add Subversion Links プラグインを作る上での話について少し書きます。

メソッドの上書き

プラグインを入れると、下記のような Subversion のアイコンが現れ、リポジトリ本体へのリンクが追加されます。

example

これは、 application.js 内の link_to_revision メソッドを上書きして、アイコンを追加してやるだけで簡単に実現できました。

JavaScript による書き換え

面倒だったのは、下記の Name 列のような、個々のファイルへのリンクでした。

example2

こちらの方は、 link_to_revision のような、専用のメソッドがあるわけではないので、プラグインからメソッドを上書きして機能を変更するということが、簡単にはできなさそうでした。

というわけで、方針を変えて、 JavaScript で load 時に動的に追加してやることにしました。

しかし、この Name 列の内容は、 Ajax で動的に追加されるものであるため、 load 時にリンクを作るだけでは、後から Ajax で追加された要素に対してリンクを生成することができませんでした。

そこで、面倒だったのですが、 Ajax で Name 列の要素が生成されたときに呼ばれるコールバック関数 (prototype.js の Ajax.Updater の onSuccess 属性に渡す関数) をフックしてやり、そのフック関数の中で Subversion リポジトリ本体へのリンクを生成してやりました。

コードとしては以下のようになっており、 onSuccess で呼ばれる scmEntryLoaded を Rails の alias_method_chain のように上書きしてやることで、リンクを生成する処理を追加しています。
また、 Ajax.Updater による要素の追加は、 onSuccess の直後に行われるので、実際にリンクを追加する addRepositoryLinkInRepositoryPage は、 setTimeout を使って遅らせています。

これで、めでたく Redmine 2.0 に対応できました。
JavaScript の部分以外はテストも書いたので、少しは (面倒であまり楽しくない) メンテナンスも楽になるはずです。

2011年12月12日月曜日

Redmine Plugin で ApplicationHelper を拡張する際の注意

Redmine Plugin で ApplicationHelper を拡張する際の注意

Windows PC の調子が悪いので、NPAPI プラグインの作り方は少し延期です。

今回は、Redmine 1.3.0もリリースされたので、 Redmine Plugin で ApplicationHelper を拡張する際に悩んだ問題について書きます。

Redmine プラグインで、既存のクラスやモジュールを拡張する

Redmine プラグインを作成し、 ApplicationHelper などの既存のクラスやモジュールを拡張する場合、基本的には以下のようにします。

  1. プラグインのlib以下にredmine_sample_plugin_application_helper_patch.rbのようなファイルを作成し、以下のように記述します。
  2. init.rb内部で、redmine_sample_plugin_application_helper_patch.rbをrequireします。

上に書いたように、一般的にはApplicationHelperを再定義せずに、追加したり置換したりしたいメソッドを別モジュールにまとめた上で、これをインクルードさせることが多いです。

View のフック関数からの呼び出し

Redmine のプラグインへのフレームワークとして、View の決められた場所に、プラグインのコードを埋め込む、という仕組みがあります。

ここでは説明しませんが、知りたい方はRedmine自体に手を入れずに見た目を変更する方法などをご覧ください。

いずれにせよ、Redmine::Hook::ViewListenerを継承したクラスを作成し、その中にあらかじめ定められた名前のフックメソッドを定義することで、View に任意のコードを追加できます。

View のフック関数から、追加したメソッドの呼び出し

上のSamplePluginApplicationHelperPatchのように定義した場合、新規に定義したメソッド (上の例ではsample_instance_method) は、View のフック関数から呼ぶことができません。

ApplicationHelper 内のメソッドは、すべてのコントローラーや View から呼べるはずなのですが、呼ぶことができないのです。

呼び出せない理由

ApplicationHelperは、Redmine::Hook::ViewListener内で適切にincludeされています。
また、上で定義したサンプルでは、確かにSamplePluginApplicationHelperPatch::InstanceMethodがApplicationHelperにインクルードされています。
そのため、間接的にSamplePluginApplicationHelperPatch::InstanceMethodはRedmine::Hook::ViewListenerからインクルードされているように思えます。

しかし、実際にはそうなりません。

Redmine::Hook::ViewListenerがApplicationHelperをインクルードするのは、プラグインの各ファイルがロードされるよりも早いタイミングで行われます。
そのため、Redmine::Hook::ViewListenerがApplicationHelperをインクルードする頃には、まだSamplePluginApplicationHelperPatch::InstanceMethodはApplicationHelperにインクルードされていません。

Ruby では、includeしたタイミングで継承関係が決定します。

例を挙げると、以下のようになります。

上の例で、B.ancestorsにCが含まれてもよさそうですが、含まれません。

これは奇妙なように思いますが、 Ruby の仕様であり、どうしようもありません。

余談ですが、 Ruby 作者のまつもとさんも、これは奇妙に思っているらしく、Matzにっきの Traits の項目に、今後mixを追加しようと思っている旨が書かれています。

そのため、もともとの Redmine の ViewListener に関しても同様に、Redmine::Hook::ViewListenerがApplicationHelperをインクルードするタイミングでは、まだSamplePluginApplicationHelperPatch::InstanceMethodがApplicationHelperにインクルードされていないので、結果としてRedmine::Hook::ViewListenerの ancestors にはSamplePluginApplicationHelperPatch::InstanceMethodが含まれないことになります。

回避策

これは、例えば以下のようにすれば回避することができます。

このようにすることで、sample_instance_method自体はApplicationHelperに直接追加されるので、Redmine::Hook::ViewListenerからでも呼び出すことが可能になります。

もちろん、base.class_eval doの内部で、直接sample_instance_methodを定義しても呼び出すことができますが、このあたりはコードの見易さの問題で、好みの分かれるところだろうと思います。

2011年11月15日火曜日

requireとrequire_dependencyとRuby 1.8.7とRuby 1.9.2

requireとrequire_dependencyと Ruby 1.8.7 と Ruby 1.9.2

先日、Redmineの Version 1.2.2 もリリースされたので、プラグインを書くときに気になった、requireとrequire_dependencyの違いおよびRuby のバージョンによるrequireの挙動の違いについて書こうと思います。

とは言っても、よく書かれている「カレントディレクトリがロードパスに含まれなくなった」ということではありません。

なお、これは Rails 2.3.11, 3.1.0 を見て書いています。また、Redmine は Version 1.2.2 を見ながら書いています。

requireとrequire_dependency

Rails のコードを見たことがある人は分かると思いますが、Rails のコードではrequireの代わりにrequire_dependencyが使われていることに気付くと思います。

それぞれ簡単にまとめると以下のようになります。

require
Ruby の組込み関数。正確には、Kernelモジュールのメソッド。
引数で渡された .rb ファイルなどを読み込む。ただし、既に読み込み済みのファイルは読み込まない。
読み込まれたモジュールは$", $LOADED_FEATURESに格納されて管理される。
require_dependency
Rails が定義した独自のメソッド。activesupport/lib/active_support/dependencies.rbで定義されている。
引数で渡された .rb ファイルなどを読み込む。ただし、既に読み込み済みのファイルは読み込まない。development モードで実行している場合はloadを使って読み込まれる。production モードで実行している場合は、requireを使って読み込まれる。
これは、development モードで実行している場合は、動的にファイルを何度も読み込む必要があるため。

Redmine プラグインで注意すること

結論から書くと、Redmine プラグインの中では、Redmine のモデル、ビュー、コントローラー、ヘルパーを読み込むには、require_dependencyを使うべしということです。

ここで、requireを使うと失敗します。

上の説明で書いたように、production モードで実行するならどちらでも結局requireが呼ばれるので変わらない、と思うかもしれませんが、そうではありません。

以下で順に説明します。

Ruby 1.8.7 と Ruby 1.9.2 でのrequireの挙動の違い

まず、requireの挙動をもう少し詳しく見てみます。

Ruby 1.8.7 でのrequireの挙動

main.rbから、required.rbを、下記のようにrequireする場合を考えます。

ここでmain.rbを実行すると、出力は下記のようになり、二回目のrequireではrequired.rbがロードされていないことが分かります。

これは当然の挙動だと思います。

次に、以下のように、相対パスや絶対パスを指定してrequired.rbを読み込んでみます。

出力は以下のようになります。

このように、予想に反して (かどうかは分かりませんが) 三回とも読み込まれています。

Ruby 1.9.2 でのrequireの挙動

一方で、同じコードを Ruby 1.9.2 で実行してみます。

ただし、Ruby 1.9.2 では、カレントディレクトリがロードパスに含まれなくなっているので、先頭で追加してやった状態で実行してみます。

出力は以下のようになります。

今回は、一度しか読み込まれませんでした。

このように、Ruby のバージョンによってrequireの挙動は微妙に異なります。

Ruby 自体のソースを見れば分かりますが、「既に読み込まれたか」を表すリストである$", $LOADED_FEATURESに記憶する方法が、Ruby 1.8.7 と Ruby 1.9.2 では異なるようです。

Ruby 1.8.7 では指定されたファイルをそのままの形で$LOADED_FEATURESに覚えておきます。 そのため、require("required")やrequire("./required")など、指定方法が異なると、複数回ロードされることとなります。

一方で、Ruby 1.9.2 では、常に絶対パスに直して覚えるようになっています。 その結果、実際に同じファイルであれば、指定方法が異なっても複数回ロードされることはありません。

Redmine プラグインでrequire_dependencyを使わないと正しく動作しない理由

以上を踏まえて、なぜ production モードであっても Redmine プラグインでrequire_dependencyを使わないとうまく動作しないのか、について書きます。

Redmine は、現在も Ruby 1.8.7 で動作しています。

そのため、あるファイルをrequireする際に、絶対パスで指定した箇所とファイル名だけを指定した箇所がある場合、そのファイルは二重にロードされることになります。

一方で、require_dependencyでは、コードを追うと分かりますが、最終的に引数のファイル名を絶対パスに展開してからrequireもしくはloadします。

ここで、以下のように書いてしまった場合を考えてみます。

この例では、ApplicationHelperモジュールのformat_activity_titleメソッドを上書きしています。

しかし、このファイルとは別のファイルでrequire_dependency("application_helper")と書かれていると、そちらではapplication_helperが絶対パスに展開されてから、requireに渡されることになります。

そのため、ファイル名だけを指定してrequire("application_helper")としたときとは別のファイルとして扱われてしまい、別途、application_helper.rbがロードされることになります。

その結果、せっかく上書きしたメソッドが再度上書きされてしまい、変更が反映されなかったように見えてしまいます。

というわけで、素直にrequire_dependencyを使うようにするのがよいと思います。

2011年11月7日月曜日

Redmine プラグイン その2

Redmine プラグイン その2

昨日の続きです。

Redmine プラグインの雛形の作成

Redmine のプラグインを作成するには、以下のようなコマンドを実行します。 Redmine 1.2.1 では、まだ Rails 2.3.X 系統なので、ruby script/generateでプラグインを作成します。

このコマンドにより、vendor/pluginsディレクトリの下にredmine_sample_pluginというディレクトリが作られ、プラグインの雛形が生成されます。

プラグインの仕組み

今回は、簡単のためにデータベースへの保存などは行わず、表示方法をカスタマイズしたり、機能を追加したりする場合について書きます。

init.rbからrequireする

この雛形に含まれるファイルで、最重要なものはinit.rbです。

このファイルは、Redmine が起動する際に自動で読み込まれます。

また、読み込まれる順序は、Redmine の Core ファイルが読み込まれた後です。

前回のオープンクラスの部分で書きましたが、この順序で読み込まれることが保証されているので、init.rbの中で Redmine 関係のクラス (例えばIssueクラス) を再定義することにより、自由にメソッドを追加、削除、上書きすることができます。

例えば、以下のように書いたファイルをlib以下に置き、init.rbからrequireすると、Issueクラスにopen?メソッドを追加することができます。 (ただし、後で書くように、普通はもう少し整理された定型の書き方があります。)

このように書いたファイルをinit.rbからrequireすることで、新たにIssueクラスに新しいメソッドを追加することができます。

重要なポイントは、Issueクラスという既存のクラスへの変更が、既存のファイルであるapp/models/issue.rbをまったく変更することなく可能となっている点です。

これは、Issueクラスのメソッドの全体が分かりにくいという大きな欠点もできますが、プラグインを配布する際に、特定のディレクトリにファイルを展開するだけでよいという、モジュールの可搬性という点において大きな利点を生むことになります。

実際のファイルは、以下のようにして、インスタンスメソッドやクラスメソッドを分離して記述することが多いです。

app以下で上書きする

これとは別に、appディレクトリ以下に直接ファイルを置くことによって、既定の動作を上書きして変更することもできます。

例えば、app/views/issues/_edit.rhtmlの内容を少し変更したものをvendor/plugins/redmine_sample_plugin/app/views/issues/_edit.rhtmlに置くことにより、もともとの_edit.rhtmlではなく、redmine_sample_plugin以下に置かれた_edit.rhtmlを使わせることができます。

これは、Rails がファイルを検索する順序として、各プラグインのapp以下を探し、見つからなければルートディレクトリ直下のappディレクトリを探すとなっているためです。

そのため、同名のファイルをプラグインのapp以下に同じディレクトリ構成で置いてやることにより、自由にデフォルトのファイルを置き換えることができます。

簡単に表示やデフォルトの挙動をカスタマイズする程度であれば、このあたりを理解するだけで比較的自由に変更することができると思います。

一方で、あまりに自由度が高いので、これをやりすぎると、全体の見通しが悪くなったり、他のプラグインとの関係で思わぬ副作用が出たりするかもしれません。

Rails は、そもそも組込みのクラスにもいろいろなメソッドを (勝手に) 追加しています。こういう部分は、きちんとした設計のもとに行えば、非常に使い勝手が上がりますが、下手に追加すると、混乱を招くだけです。

Rails では、きちんとした設計のもとに行うことで、利便性を実現しています。

それと同じように、プラグインに対してもユーザーが自由にカスタマイズできるようにしておき、「自己責任でプログラマが自由にしてね」という余地をあえて残すような設計にしているのかもしれません。

2011年11月6日日曜日

Redmine プラグイン

Redmine プラグイン

以前に Redmine プラグインの練習として作ってみたWiki GChart Formula Pluginの紹介をかねて、Redmine プラグインについて書きます。

オープンクラス

Redmine, Rails に限った話ではありませんが、Ruby のクラスは、オープンです。

Redmine や Rails のプラグインは、このオープンクラスの仕組みをうまく使っているので、今回はその説明を書くことにします。

「クラスがオープンである」というのは、例えば、一度クラスを定義した後からでも、自由にメソッドの追加や上書きなどができるということです。

例として、以下のような Ruby のプログラムを考えます。Testクラスが二回定義されていることに注意してください。

このスクリプトを実行すると以下のように出力されます。

old
new
ok

この例では、(1) で定義したメソッドprintが (4) で再定義され上書きされます。また、(5) で新しいメソッドprint2が新規に追加されています。

C# の部分クラス定義との違い

これは、C# の部分クラス定義の機能を使って、複数ファイルでクラスの各メソッドを定義できることと似た部分があります。

しかし、Ruby ではクラスの定義自体も通常のメソッド呼び出しなどと同じように実行時に行われる点が大きく違います。

C# などでは、定義自体が分割されていようがなかろうが、クラスの定義自体はコンパイル時に解釈され、実行時にクラスのメソッドが途中で動的に追加されたり置き換わったりすることはありません。

そのため、C# でもともと提供されている既存のクラスに対してメソッドを追加するようなことはできません。 また、(1), (4) のように同名のメソッドを定義することもできません。 というか、後から定義した方で上書きできたとしても、実行時にはその片方の定義しか使われないので、意味がありません。

一方で、Ruby ではそれらが可能です。

そのため、上に書いたコードは以下のように動きます。

  • (2) の段階では、まだ (4) の再定義がされていないので、(1) で定義された通りoldが出力されます。
  • (3) の段階では、まだ (5) の定義がされていないので、print2などというメソッドは存在せず、エラーになります。
  • (6) の段階では、(4) の再定義の後なので、newが出力されます。(1) で定義されていた、古いprintメソッドを呼ぶ方法はありません。
  • (7) の段階では、(5) の定義の後なので、okが出力されます。

このように、クラス定義も実行時に行われ、かつクラスがオープンであるために、後から既存クラスの挙動を変更することが非常に柔軟に行えます。

これらの挙動を利用することで、Redmine や Rails では、プラグイン側から Redmine や Rails のクラスを上書きすることにより、自由なカスタマイズをすることができます。

続く。