restful_authenticationでユーザー別のTodoリストを管理する
前回からの続き。現状のTodoでは、ログインしたユーザーは共通のTodoを利用する仕様になってしまっている。これをユーザー別のTodoに変更したい。restful_authenticationは、そのユーザーが認証された状態かどうかをチェックして、そのユーザーが誰であるかをcurrent_userで確認できるようにしてくれている。あとはTodoをユーザー別に管理できるように対応すれば良さそう。
ユーザー別の管理
- まず思いついたのが、モデルにhas_manyとbelongs_toを設定すること。
# ---------- app/models/user.rb ---------- require 'digest/sha1' class User < ActiveRecord::Base has_many :todos ...(中略)...
# ---------- app/models/todo.rb ---------- class Todo < ActiveRecord::Base belongs_to :user end
- todosテーブルには、関連先を保持するフィールドuser_idも必要になる。
# ---------- app/models/20080726081256_create_todos.rb ---------- class CreateTodos < ActiveRecord::Migration def self.up create_table :todos do |t| t.string :body t.date :due t.boolean :done t.integer :user_id t.timestamps end end ...(中略)...
- マイグレーションのリセットを実行して、設定をデータベースに反映させた。(リセットしたので保存されているデータはすべて破棄される)
$ rake db:migrate:reset
- ルート設定もhas_manyで設定した。
# ---------- config/routes.rb ---------- ActionController::Routing::Routes.draw do |map| map.resources :users, :new=>{:input_email_address=>:get, :deactivate=>:put}, :has_many=>:todos map.resource :session #map.resources :todos map.activate '/activate/:activation_code', :controller => 'users', :action => 'activate' ...(中略)...
- これでtodoに関するルートは以下のように変更された。
$ rake routes ...(中略)... user_todos GET /users/:user_id/todos {:controller=>"todos", :action=>"index"} formatted_user_todos GET /users/:user_id/todos.:format {:controller=>"todos", :action=>"index"} POST /users/:user_id/todos {:controller=>"todos", :action=>"create"} POST /users/:user_id/todos.:format {:controller=>"todos", :action=>"create"} new_user_todo GET /users/:user_id/todos/new {:controller=>"todos", :action=>"new"} formatted_new_user_todo GET /users/:user_id/todos/new.:format {:controller=>"todos", :action=>"new"} edit_user_todo GET /users/:user_id/todos/:id/edit {:controller=>"todos", :action=>"edit"} formatted_edit_user_todo GET /users/:user_id/todos/:id/edit.:format {:controller=>"todos", :action=>"edit"} user_todo GET /users/:user_id/todos/:id {:controller=>"todos", :action=>"show"} formatted_user_todo GET /users/:user_id/todos/:id.:format {:controller=>"todos", :action=>"show"} PUT /users/:user_id/todos/:id {:controller=>"todos", :action=>"update"} PUT /users/:user_id/todos/:id.:format {:controller=>"todos", :action=>"update"} DELETE /users/:user_id/todos/:id {:controller=>"todos", :action=>"destroy"} DELETE /users/:user_id/todos/:id.:format {:controller=>"todos", :action=>"destroy"} ...(中略)...
- 上記ルートに合わせて、コントローラーとビューを修正した。ひたすら、機械的に。
# ---------- app/controllers/todos_controller.rb ---------- class TodosController < ApplicationController before_filter :login_required # GET /todos # GET /todos.xml def index @todos = current_user.todos respond_to do |format| format.html # index.html.erb format.xml { render :xml => @todos } end end # GET /todos/1 # GET /todos/1.xml def show @todo = current_user.todos.find(params[:id]) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @todo } end end # GET /todos/new # GET /todos/new.xml def new @todo = Todo.new respond_to do |format| format.html # new.html.erb format.xml { render :xml => @todo } end end # GET /todos/1/edit def edit @todo = current_user.todos.find(params[:id]) end # POST /todos # POST /todos.xml def create @todo = Todo.new(params[:todo]) respond_to do |format| if current_user.todos << @todo flash[:notice] = 'Todo was successfully created.' format.html { redirect_to([current_user, @todo]) } format.xml { render :xml => @todo, :status => :created, :location => [current_user, @todo] } else format.html { render :action => "new" } format.xml { render :xml => @todo.errors, :status => :unprocessable_entity } end end end # PUT /todos/1 # PUT /todos/1.xml def update @todo = current_user.todos.find(params[:id]) respond_to do |format| if @todo.update_attributes(params[:todo].merge(:user_id=>current_user.id)) flash[:notice] = 'Todo was successfully updated.' format.html { redirect_to([current_user, @todo]) } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @todo.errors, :status => :unprocessable_entity } end end end # DELETE /todos/1 # DELETE /todos/1.xml def destroy @todo = current_user.todos.find(params[:id]) @todo.destroy respond_to do |format| format.html { redirect_to(user_todos_url) } format.xml { head :ok } end end end
<%# ---------- app/views/welcome/index.html.erb ---------- %> <p><%= current_user.login %> さん、Todoへようこそ!</p> <%= link_to 'Todoリストへ', user_todos_path(current_user) %>
<%# ---------- app/views/todos/index.html.erb ---------- %> <h1>Listing todos</h1> <table> <tr> <th>Body</th> <th>Due</th> <th>Done</th> </tr> <% for todo in @todos %> <tr> <td><%=h todo.body %></td> <td><%=h todo.due %></td> <td><%=h todo.done %></td> <td><%= link_to 'Show', [current_user, todo] %></td> <td><%= link_to 'Edit', edit_user_todo_path(current_user, todo) %></td> <td><%= link_to 'Destroy', [current_user, todo], :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %> </table> <br /> <%= link_to 'New todo', new_user_todo_path %>
<%# ---------- app/views/todos/new.html.erb ---------- %> <h1>New todo</h1> <% form_for([current_user, @todo]) do |f| %> <%= render :partial=>f %> <p> <%= f.submit "Create" %> </p> <% end %> <%= link_to 'Back', user_todos_path %>
<%# ---------- app/views/todos/edit.html.erb ---------- %> <h1>Editing todo</h1> <% form_for([current_user, @todo]) do |f| %> <%= render :partial=>f %> <p> <%= f.submit "Update" %> </p> <% end %> <%= link_to 'Show', [current_user, @todo] %> | <%= link_to 'Back', user_todos_path %>
<%# ---------- app/views/todos/show.html.erb ---------- %> <p> <b>Body:</b> <%=h @todo.body %> </p> <p> <b>Due:</b> <%=h @todo.due %> </p> <p> <b>Done:</b> <%=h @todo.done %> </p> <%= link_to 'Edit', edit_user_todo_path(current_user, @todo) %> | <%= link_to 'Back', user_todos_path %>
動作確認
以上で、ひとまず修正完了。ユーザー登録をして、Todoにアクセスしてみる。(zariganiさんと、yamadaさんの二人を登録して試してみた。)
- zariganiさんでログインして、Todoを一つ登録した。
- 今度はyamadaさんでログインして、Todoのindexページを開いても、zariganiさんのTodoは見えない。
問題発生
一見、うまく動いているように見えるけど、ユーザーは何をするか分からない。ありそうなこととして...
- zariganiさんがログインして、yamadaさんのTodoへアクセスしようとする可能性もある。
- あり得ないuser_idを入力する可能性もある。
現状では上記のような場合でも、URLは無視して、常にログインしているユーザーのTodoに限定して表示する。これではユーザーに誤解を与える可能性がある。
- 例えばyamadaさんがログインしている場合、以下のURLを入力してもyamadaさんのTodoのみが表示される結果となる。
http://localhost:3000/users/100/todos
- しかし、yamadaさんは、まるでuser_idが100のユーザーのTodoを表示していると錯覚してしまうかもしれない。(URLと表示される結果が連動していない...。)
警告する
ログインしているユーザーと、URLのuser_idが違っている場合は、user_todos_path(current_user)にリダイレクトして、警告するようにしてみた。
- restful_authenticationのbefore_filter :login_requiredは、ログインを要求するだけで、ユーザーが誰であるかのチェックはない。
- user_checkedメソッドを追加して、ログインしているユーザーと、URLのアクセス先のユーザーが同じかどうか確認するようにしてみた。
# ---------- app/controllers/todos_controller.rb ---------- class TodosController < ApplicationController before_filter :login_required before_filter :user_checked ...(中略)... private def user_checked unless params[:user_id].to_i == current_user.id redirect_to user_todos_path(current_user) flash[:alert] = "access to '#{request.path}' failed" end end end
- yamadaさんでログインしてhttp://localhost:3000/users/100/todosにアクセスすると以下のようになる。
これで、ユーザー別のTodoになった!