at posts/single.html

Rails 2.0 で map.resource のネスト

Rails 2.0 で簡単なサンプルアプリを作っている。 作りたいのはよくある ToDo リストなんだけど、「複数のユーザで使える」ことが前提。 これが思ったより苦戦した。

まず、普通に scaffold で作ったときの URL はこうなる。

http://example.com/todos/  ← すべての ToDo リスト
http://example.com/todos/new ← 新しい ToDo を作成
http://example.com/todos/1/ ← id:1の ToDo を表示
http://example.com/todos/1/edit ← id:1の ToDo を編集

でもこれじゃ、ユーザごとに分かれていない。 かといって、

http://example.com/todos/?user_id=1

とするのも格好悪い。 以下のような URL にしたいんだよね。

http://example.com/users/1/todos/ ← ユーザ1のすべての ToDo リスト
http://example.com/users/1/todos/new ← ユーザ1の新しい ToDo を作成
http://example.com/users/1/todos/3 ← ユーザ1のid:3の ToDo を表示
http://example.com/users/1/todos/3/edit ← ユーザ1のid:3の ToDo を編集

RESTful 本を見ていたら、 routes.rb で path_prefix を使う方法が書かれていた(P.189, 7.3.11 実装: route.rb ファイル)。 時期的にたぶん Rails 1.2 かな。

user_base = base + '/users/:username'
map.resources :bookmarks, :path_prefix => user_base

それでやってみたんだけど、どうも view 周りでエラーがでる。

Showing todos/index.html.erb where line #16 raised:
todo_url failed to generate from {:controller=>"todos", :user_id=>#<Todo id: 4, user_id: 1, body: "テスト", status: "テスト", created_at: "2008-01-04 16:37:36", updated_at: "2008-01-04 16:38:38">, :action=>"show"}, expected: {:controller=>"todos", :action=>"show"}

Extracted source (around line #16):
16:     <td><%= link_to 'Show', todo %></td>

エラーが出ている箇所で、以下のように user_id と id を指定すれば、一応動くようになるけど、これは面倒。

<td><%= link_to 'Show', todo_path(:user_id => todo.user_id, :id => todo.id) %></td>

map.resource でのネスト

Module: ActionController::Resources を見てお勉強。 そうすると、 routes.rb を以下のように書けることが分かった。

map.resources :users do |user|
  user.resources :todos
end

これで、 /users/1/todos/ という書き方ができる。 でも、やっぱり view でエラーが起きる。 URL を生成するヘルパーである todo_path を user_todo_path に変えないとダメみたい。

RESTful 本を参考にしつつ、以下のようにした。

app/controllers/todos_controller.rb

フィルタで @user にオブジェクトを格納しておく。

  before_filter :must_user_present

  def must_user_present
    @user = User.find(params[:user_id])
  end

create では @todo.save の代わりに @user.todos << @todo を使う。 @todo.save のままだとリクエストパラメータを詐称して違う user_id に差し替えることができちゃうから。 それから、生成後のリダイレクト先で user_todo_url を使うように書き換え。 これは destroy でも同じ。

  def create
    @todo = Todo.new(params[:todo])

    respond_to do |format|
      if @user.todos << @todo
        flash[:notice] = 'Todo was successfully created.'
        format.html { redirect_to(user_todo_url(@user, @todo)) }
        format.xml  { render :xml => @todo, :status => :created, :location => user_todo_url(@user, @todo) }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @todo.errors, :status => :unprocessable_entity }
      end
    end
  end

app/views/todo/*.erb

  • link_to で以下のヘルパを使うように書き換え
    • index: user_todos_path(@user)
    • show: user_todo_path(@user, @todo)
    • edit: edit_user_todo_path(@user, @todo)
  • form_for で、以下のように url 属性を追加する
form_for(@todo, :url => user_todo_path(@user, @todo))

これで動くようになった。 scaffold でここまで genarate してくれると楽だなぁ。 もっと正しい使い方があるのかも?

関連する日記