スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
Prev.    Category    Next 

Redmine のルーティングと RESTful なプラグインの書き方

Redmine では 1.4 以降からプラグインでもルーティングの記述は必須となりました。
どうせ書くなら、なるべく Redmine のやり方に合わせておいた方がいいでしょう。 そこで、 Redmine のルーティングについて調べてみました。
それを基に Redmine のルーティングに合わせたプラグインの記述方法についてもまとめています。

また、 Redmine は RESTful になっているので、 Redmine のやり方を見るのは、 Rails で RESTful なルーティングの書き方の参考になると思います。

なお、調べた対象は Redmine の 2.2.3, 2.3.0 です。

Rails 3 のルーティングの基本

まず、最初にルーティングについて、簡単に説明しておきます。
詳しい説明は以下のサイト等を見てください。 ルーティングは (ルート)/config/routes.rb に記述します。
(ルート)は 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 削除
/meetings や /meetings/:id は同じパスなのに、呼ばれるアクションが違います。
パスが同じでも GET や POST などのメソッドの違いにより処理が変わるのが RESTful なところです。
メソッドで切り替えるので、アクション名も一部書く必要がなります。

また、 new と create という意味の似たアクションがあります。
new が作成フォームのページを表示するアクションで、 create はフォームの中身をパラメーターとして受け取り、実際に作成するアクションです。
edit と update も同じような関係です。

ルーティングの確認

現在のルーティングの設定は次のコマンドで確認することができます。
$ rake routes
meetings 関連のパスを抜粋すると次のようになっています。
     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 ではプロジェクトの識別名がないところです。
チケットはプロジェクトに属するものなので、プロジェクトの特定は必須です。 しかし、アドレスとしては渡していません。
プロジェクトは次の手順で特定しています。
  1. チケット番号からチケットのデータを取得
  2. チケットデータのプロジェクトの ID からプロジェクトを特定
これはおそらく #123 のような記述からリンクを張るのを簡単にするためでしょう。


チケット以外の例も挙げておきます。
微妙な違いはありますが、同じような方針になっています。

ニュース(news):
機能 アドレス(リンク) アドレス(表示)
一覧(index) /projects/(project識別名)/news 同左
作成(new) /projects/(project 識別名)/news/new /projects/(project 識別名)/news
[一覧で作成]
詳細表示(show) /news/(ID) 同左
編集(edit) /news/(ID)/edit /projects/(project 識別名)/news
[一覧で編集]
文書(documents):
機能 アドレス(リンク) アドレス(表示)
一覧(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
  end
Rails 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 %>
関連記事
スポンサーサイト
Prev.    Category    Next 

Facebook コメント


コメント

コメントの投稿

Font & Icon
非公開コメント

このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
28位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
4位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

07月 | 2017年08月 | 09月
- - 1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31 - -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。