Redmine 1.4.0 もうすぐリリース。大量のプラグインが動作しない可能性あり
もうすぐ Redmine の 1.4.0 がリリースしそうです。 ロードマップ をみると、インストールの説明の変更のチケットだけを残して、 期限が 4/1 になっているので、時期はおそらく 4/1 でしょう。
1.4.0 では待ち望んでた Ruby 1.9 への対応が行われています。
残念ながら、 Rails 3 への対応は 2.0.0 に持ち越しのようです。
ただ、システム管理者やプラグイン開発者にとってはあまり喜んでばかりもいられません。
というのも、
1.4.0 では今までのプラグインが大量に動作しなくなる
可能性があるためです。
また、インストールの説明が更新されたら、そこでちゃんと説明されるかも知れませんが、
インストールにもハマりやすいポイントが出来ています。
こちらについて詳しくは以前の記事を見てください。
routes の問題
おそらく多くのプラグインでは Internal Error が発生して動作しなくなります。このときのログは、以下のようなエラーになっています。
No route matches ....動作しなくなる原因は Redmine のルートの設定が変わってしまったためです。 (リビジョン 8162)
以前、 trunk を試しているユーザーの方々から報告をいただいて私も知りました。
ルート設定の変更は 1.3 では見送られたっぽいですし、
こんな大量のプラグインを動かなくする変更を本当にいれるのかなと思っていたのですが、
どうやら本当に入れるみたいです。
Redmine のブランチの仕組みをよく分かっておらず、今の開発版(trunk)がそうというだけで、本当にそうなるかどうかはリリースされるまではっきりしたことは言えませんが。
対応法
ルート設定の変更への対応はプラグインに config/routes.rb のファイルを追加します。基本的には以下のような内容のファイルを作れば、一応の動作はします。
ActionController::Routing::Routes.draw do |map| map.connect 'foos/:action', :controller => 'foos' # : endfoos がコントロール名で、 ブロックの中に使っているコントロールの分だけ書きます。
ただ、これだけで動作するものもあれば、しないものもあります。 私の作成している用語集プラグインもこれだけだとすべての機能がちゃんと動作するわけではありません。
動かない場合の原因はプラグインごとに異なると思うので、プラグインごとにちゃんと対応する必要があります。
私のプラグインの対応状況
私が公開しているプラグインの対応状況です。- TestLink Link プラグイン
- まだ試していませんが、これは wiki マクロの拡張だけで routes に関係がないタイプなので、対応しなくてもそのまま動作すると思います。
- Redmine インフォメーション プラグイン
- こちらは前項の方法で対応済みです。
- 用語集プラグイン
- まだ対応できていません。 4/1 までに対応するつもりではいます。
Redmine 1.4.0 リリース
ルーティングの問題
ルーティングの設定が変わったため、多くのプラグインが動作しなくなる可能性があると前に書いていました。 リリースされたもので確認したところ、 ルーティングの設定の変更は加えられていて、やっぱり、大量のプラグインが動作しなくなるのでははいかと思われます。この対応方法は前の記事でも書いたのですが、もう少し簡単な記述ができるらしいです。 プラグインで Internal Error になり、 エラーログが以下のようなメッセージだった場合がルーティングの問題が発生している時です。
No route matches ....この時、以下の内容で <plugin_dir>/config/routes.rb を作成します。 routes.rb すでにある場合は、ブロック内の行を追加します。
ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' end
ただし、これだけで動くようになるプラグインは結構幸運な方で、 プラグインによっては、あちこち直す必要があります。
ちなみに用語集プラグインは、一応動作するようにはなったのですが、 もう少し、ルーティングの設定を直した後、テストしてからリリースしようかなと思ってます。
インストール
もう一つ、 Redmine 1.4.0 だとインストールするとき、 rmagick のパッケージがインストールできなくてハマりそうという記事を書きました。こちらは redmine.org のインストールの説明で、ちゃんと rmagick なしでインストールする方法の説明がされていました。
Redmine のルーティングと RESTful なプラグインの書き方
Redmine では 1.4 以降からプラグインでもルーティングの記述は必須となりました。
どうせ書くなら、なるべく Redmine のやり方に合わせておいた方がいいでしょう。
そこで、 Redmine のルーティングについて調べてみました。
それを基に Redmine のルーティングに合わせたプラグインの記述方法についてもまとめています。
なお、調べた対象は Redmine の 2.2.3, 2.3.0 です。
Rails 3 のルーティングの基本
まず、最初にルーティングについて、簡単に説明しておきます。詳しい説明は以下のサイト等を見てください。
- ルーティングの設定 - Ruby on Rails入門
- Rails3 routes.rb まとめ | akkunchoi@github
- Ruby on Rails Guides: Rails Routing from the Outside In
(ルート)は Redmine 自身のものはインストールディレクトリー、プラグインの場合は各プラグインのディレクトリーです。
まず、簡単な例として routes.rb に次の記述があったとします。
resources 'meetings'
実際、この記述は Redmine の extra/sample_plugin にあります。ただし、 ルーティングのみで、アクションの実装がないので動きません。
Rails では基本的にページを表示するには コントローラー名とアクション名をアドレスとして指定し、 アクションを実行します。
meetings コントローラーの新規作成(new)アクションを呼び出すアドレスは次のようになります。
(サーバー名)/meetings/new
要素の ID などのパラメーターをアドレスに含めることもできます。
例えば 1234 の ID を持つ meeting 要素の編集では次のアドレスになります。
(サーバー名)/meetings/edit/1234呼び出されたコントローラーのアクションでは ID の値(1234) は
params[:id]
に格納されています。
この他にも resources の記述だけで RESTful な 7 つのルーティングができます。
HTTP メソッド(verbs) | パス | アクション | 説明 |
---|---|---|---|
GET | /meetings | index | 一覧表示 |
GET | /meetings/new | new | 新規作成(フォーム) |
POST | /meetings | create | 新規作成 |
GET | /meetings/:id | show | 詳細表示 |
GET | /meetings/:id/edit | edit | 編集(フォーム) |
PUT | /meetings/:id | update | 更新 |
DELETE | /meetings/:id | destroy | 削除 |
パスが同じでも GET や POST などのメソッドの違いにより処理が変わるのが RESTful なところです。
メソッドで切り替えるので、アクション名も一部書く必要がなります。
また、 new と create という意味の似たアクションがあります。
new が作成フォームのページを表示するアクションで、 create はフォームの中身をパラメーターとして受け取り、実際に作成するアクションです。
edit と update も同じような関係です。
ルーティングの確認
現在のルーティングの設定は次のコマンドで確認することができます。$ rake routesmeetings 関連のパスを抜粋すると次のようになっています。
meetings GET /meetings(.:format) meetings#index POST /meetings(.:format) meetings#create new_meeting GET /meetings/new(.:format) meetings#new edit_meeting GET /meetings/:id/edit(.:format) meetings#edit meeting GET /meetings/:id(.:format) meetings#show PUT /meetings/:id(.:format) meetings#update DELETE /meetings/:id(.:format) meetings#destroy先頭列はパスなどを簡単に書くための helper メソッドです。
通常、 link_to などには以下のような Hash を渡します。
{:controller=>'meetings', :action=> :edit, :id =>123}これを helper メソッドで簡単に書くことができます。
edit_meeting_path(123) # /meetings/123/edit
また、_path の部分を _url にすると URL を返します。
edit_meeting_url(123) # http://(サーバーアドレス)/meetings/123/edit
Redmine のアドレス
Redmine の中身を見て行く前に、 表示上のアドレスについて見ていくことにします。チケット(issues):
機能 | アドレス(リンク) | アドレス(表示) |
---|---|---|
一覧(index) | /projects/(project識別名)/issues | 同左 |
作成(new) | /projects/(project 識別名)/issues/new | 同左 |
詳細表示(show) | /issues/(チケット番号) | 同左 |
更新(edit) | /issues/(チケット番号)/edit | /issues/(チケット番号) [詳細表示で編集] |
更新の場合は実際には編集用のページには行かず、詳細表示ページで編集します。
注目すべき点は show や edit ではプロジェクトの識別名がないところです。
チケットはプロジェクトに属するものなので、プロジェクトの特定は必須です。 しかし、アドレスとしては渡していません。
プロジェクトは次の手順で特定しています。
- チケット番号からチケットのデータを取得
- チケットデータのプロジェクトの ID からプロジェクトを特定
チケット以外の例も挙げておきます。
微妙な違いはありますが、同じような方針になっています。
ニュース(news):
機能 | アドレス(リンク) | アドレス(表示) |
---|---|---|
一覧(index) | /projects/(project識別名)/news | 同左 |
作成(new) | /projects/(project 識別名)/news/new | /projects/(project 識別名)/news [一覧で作成] |
詳細表示(show) | /news/(ID) | 同左 |
編集(edit) | /news/(ID)/edit | /projects/(project 識別名)/news [一覧で編集] |
機能 | アドレス(リンク) | アドレス(表示) |
---|---|---|
一覧(index) | /projects/(project識別名)/documents | 同左 |
作成(new) | /projects/(project識別名)/documents/new | /projects/(project識別名)/documents [一覧で作成] |
詳細表示(show) | /documents/(ID) | 同左 |
編集(edit) | /documents/(ID)/edit | 同左 |
routes.rb
Redmine の routes.rb の記述を見ていきましょう。チケットだと機能が多くてわかりづらいので、 ニュースや文書にします。
ニュース(news) - プロジェクト指定なし
プロジェクトの指定をしないアクションは詳細表示などです。 これらのアクションのルーティング定義を抜粋してみます。RedmineApp::Application.routes.draw do resources :news, :only => [:index, :show, :edit, :update, :destroy] end
:only
でアクションを限定しているところを除けば、
基本的な resources
による指定です。パスを確認すると次のようになります。
news_index GET /news(.:format) news#index edit_news GET /news/:id/edit(.:format) news#edit news GET /news/:id(.:format) news#show PUT /news/:id(.:format) news#update DELETE /news/:id(.:format) news#destroy一覧(index) アクションもプロジェクト指定していません。
これは news のデータから特定するため、指定していないのではありません。 Redmine 全体の news の一覧で、プロジェクト指定が不要なためです。
このページを表示するリンクは用意されていませんが、 アドレス(http://XXXX/news)を直接指定すると表示することができます。 おそらく REST API 用かなんかで用意されているのではないかと思います。
ニュース(news) - プロジェクト指定あり
以下は、プロジェクト指定が必要なアクションの routes.rb の記述とパスの出力です。RedmineApp::Application.routes.draw do resources :projects do resources :news, :except => [:show, :edit, :update, :destroy] end end
project_news_index GET /projects/:project_id/news(.:format) news#index POST /projects/:project_id/news(.:format) news#create new_project_news GET /projects/:project_id/news/new(.:format) news#newプロジェクトの
resources
のブロック内に news の resources を記述しています。また、先ほどの
:only
とは逆に :except
で対象外のアクションを除外しています。
文書(documents)
文書の方も抜粋してみます。RedmineApp::Application.routes.draw do resources :projects do resources :documents, :except => [:show, :edit, :update, :destroy] end resources :documents, :only => [:show, :edit, :update, :destroy] do post 'add_attachment', :on => :member end end
project_documents GET /projects/:project_id/documents(.:format) documents#index POST /projects/:project_id/documents(.:format) documents#create new_project_document GET /projects/:project_id/documents/new(.:format) documents#new add_attachment_document POST /documents/:id/add_attachment(.:format) documents#add_attachment edit_document GET /documents/:id/edit(.:format) documents#edit document GET /documents/:id(.:format) documents#show PUT /documents/:id(.:format) documents#update DELETE /documents/:id(.:format) documents#destroyほとんどニュースと同じです。
ただ、 基本的なアクション以外を追加するため、
resources
をブロックとして、 add_attachment
の定義が記述されています。
View でのパスの指定
ルーティングのパスが実際に指定されているところもいくつか見てみます。ここからは documents のみを対象とします。
リンク
以下は一覧表示(index)のページでの新規作成(new)へのリンクです。app/views/documents/index.html.erb:
<div class="contextual"> <%= link_to l(:label_document_new), new_project_document_path(@project), :class => 'icon icon-add', :onclick => 'showAndScrollTo("add-document", "document_title"); return false;' if User.current.allowed_to?(:add_documents, @project) %> </div>リンクの URL 指定は new アクションのパスになっています。
おそらくこれは JavaScript 未対応ブラウザー用です。 実際は JavaScript が呼ばれて、ページ内に新規作成用のフォームが表示されれます。
一覧から詳細表示(show)へのリンクも、プロジェクトを指定しない例として挙げてみます。
ただし、こちらは部分レンダリングを使っているので、記述は _document.html.erb にあります。
app/views/documents/_document.html.erb:
<h4><%= link_to h(document.title), document_path(document) %></h4>
削除の場合は次のような記述です。
app/views/documents/show.html.erb:
<%= delete_link document_path(@document) %>パスは show などと同じです。 メソッドを delete にすることによって、アクションを destroy にします。
delete_link
はRedmine で用意されている helper です。 その内部でメソッドを指定しています。app/helpers/application_helper.rb :
def delete_link(url, options={}) options = { :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del' }.merge(options) link_to l(:button_delete), url, options end
フォーム
今度は新規作成(new)のフォームの記述を見ていきます。一覧表示(index)ページ内での作成フォームはちょっと複雑なので、 new のページにします。
app/views/documents/new.html.erb:
<%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <p><%= submit_tag l(:button_create) %></p> <% end %>URL の指定は index が対象になっているように見えます。
しかし、先ほどのパスの出力を見てもらうと index と create は同じパスです。
project_documents GET /projects/:project_id/documents(.:format) documents#index POST /projects/:project_id/documents(.:format) documents#create違いは HTML メソッドの部分で、 index が GET で create は POST です。
フォームの場合、メソッドを省略すると POST になります。 そのため create のアクションの方が実行されます。
編集(edit)のページのフォームも見てみます。
app/views/documents/edit.html.erb:
<%= labelled_form_for @document do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <p><%= submit_tag l(:button_save) %></p> <% end %>URL に update が指定されていると思いきや何も指定されていません。
Rails 2 の時は URL 指定の省略時にはカレントと同じアクションになりました。 そこで edit アクションの処理内で、 編集用のページの表示かデータの更新かをHTTP メソッドで切り替えていました。
Rails 3 では @document を指定して、 url を省略していると
document_path(@document)
がパスとして使われます。create と同様に show と update のパスは同じです。 フォームからは POST メソッドであり、 update アクションが呼ばれます。
update は POST ではなく PUT ですが、 その辺りの変換も Rails がやってくれます。
document GET /documents/:id(.:format) documents#show PUT /documents/:id(.:format) documents#updateちなみに
multipart
はファイルを添付するときに必要なオプションです。文書では、 新規作成ページでファイルの添付ができますが、 編集ページではできません。 後から追加する場合は"添付ファイルの追加"(add_attachment) で行います。
Contoroller の記述
Contoroller 側ではどうやってプロジェクト情報を取っているか についても見てみます。以下は DocumentsController の先頭部分です。
app/controllers/documents_controller.rb :
class DocumentsController < ApplicationController default_search_scope :documents model_object Document before_filter :find_project_by_project_id, :only => [:index, :new, :create] before_filter :find_model_object, :except => [:index, :new, :create] before_filter :find_project_from_association, :except => [:index, :new, :create]
before_filter
で呼び出されるメソッドを表にまとめました。
メソッド | 適用対象 | 機能 |
---|---|---|
find_project_by_project_id | プロジェクト識別名を取るアクション(index, new, create) | params[:project_id] でプロジェクトの検索を行い、 @project に格納 |
find_model_object | プロジェクト識別名を取るアクション以外 | params[:id] でモデル(Document)の検索を行い、 @document に格納 |
find_project_from_association | プロジェクト識別名を取るアクション以外 | @document.project でプロジェクトの検索を行い、 @project に格納 |
それぞれのメソッドは次のような定義です。
app/controllers/application_controller.rb:
# Find project of id params[:project_id] def find_project_by_project_id @project = Project.find(params[:project_id]) rescue ActiveRecord::RecordNotFound render_404 end # Finds and sets @project based on @object.project def find_project_from_association render_404 unless @object.present? @project = @object.project end def find_model_object model = self.class.model_object if model @object = model.find(params[:id]) self.instance_variable_set('@' + controller_name.singularize, @object) if @object end rescue ActiveRecord::RecordNotFound render_404 endまた、 edit と update の定義は次のようになっています。
def edit end def update @document.safe_attributes = params[:document] if request.put? and @document.save flash[:notice] = l(:notice_successful_update) redirect_to document_path(@document) else render :action => 'edit' end endRails 2 では edit で編集、データの更新の両方を担当していました。 Rails 3 対応で、 edit は編集用のページの表示、 update がデータの更新と処理が分けられています。(edit は何もせずにページを表示しているだけですが)
まとめとプラグインのルーティングの書き方
まとめとして Redmine に合わせた RESTful なプラグインの書き方を説明します。コントローラー名は meetings としています。
ルーティング
resources を使って記述。一覧(index)、新規作成(new, create)のプロジェクトの指定を必要とするアクションは入れ子。
config/routes.rb :
resources :meetings, :only => [:show, :edit, :update, :destroy] resources :projects do resources :meetings, :except => [:show, :edit, :update, :destroy] end
Controller の記述
ルーティングに必要な 7 つのアクションを定義。before_filter で Redmine のメソッドを使い、 @project 、 @meeting にプロジェクト、モデルのオブジェクトをそれぞれ格納。
app/controllers/meetings_controller.rb :
class MeetingsController < ApplicationController default_search_scope :meetings model_object Meeting before_filter :find_project_by_project_id, :only => [:index, :new, :create] before_filter :find_model_object, :except => [:index, :new, :create] before_filter :find_project_from_association, :except => [:index, :new, :create] before_filter :authorize def index # モデルの一覧の取得 end def show end def new end def create # モデルデータの追加 end def edit end def update # モデルデータの更新 end def destroy # モデルデータの削除 end end
View でのパスの記述
各ページへのリンクを作成する場合はパス用メソッド使用して link_to などにパスを渡す。(削除は link_to ではなく delete_link)
機能 | アクション | パス用メソッド |
---|---|---|
一覧表示 | index | project_meetings_path(@project) |
詳細表示 | show | meeting_path(@meeting) |
編集 | edit | edit_meeting_path(@meeting) |
新規作成 | new | new_project_meeting_path(@project) |
削除 | destroy | meeting_path(@meeting) |
作成、 編集用のフォームでは URL 先が create 、 update となるように指定。
app/views/new.html.erb : (パスは index と同じ)
<%= labelled_form_for @meeting, :url => project_meetings_path(@project) do |f| %> : <% end %>app/views/edit.html.erb : (URL を省略)
<%= labelled_form_for @meeting do |f| %> : <% end %>