第10回: 完了したタスクの一覧

2010/05/16

前回は、タスクを完了する finish アクションを実装しました。

今回は、完了したタスクだけを表示する done アクションと、完了したタスクを未完了に戻す unfinish アクションを作ります。

ルーティングの修正

エディタで config/routes.rb を開いて、次のように修正してください。

Nchak::Application.routes.draw do
  resources :tasks, :only => [ :index, :create ] do
    put :finish, :on => :member
    put :unfinish, :on => :member
    get :done, :on => :collection
  end
end

前回の記事で書いたように :on オプションに :collection を指定すると、レコードの集合を対象とするアクションへの経路(route)が定義されます。

経路の数が多くなってきたので、rake routes コマンドでちょっと確認してみましょう。

$ rake routes
(in /home/kuroda/work/nchak)
  finish_task PUT  /tasks/:id/finish(.:format)   {:controller=>"tasks", :action=>"finish"}
unfinish_task PUT  /tasks/:id/unfinish(.:format) {:controller=>"tasks", :action=>"unfinish"}
   done_tasks GET  /tasks/done(.:format)         {:controller=>"tasks", :action=>"done"}
              GET  /tasks(.:format)              {:controller=>"tasks", :action=>"index"}
        tasks POST /tasks(.:format)              {:controller=>"tasks", :action=>"create"}

done アクションの実装

done アクションのコードは、index アクションとほとんど同じです。app/controllers/tasks_controller.rb を次のように修正してください。

class TasksController < ApplicationController
  def index
    @task = Task.new
    @tasks = Task.all(:conditions => { :done => false }, :order => "due_date")
  end

  def done
    @task = Task.new
    @tasks = Task.all(:conditions => { :done => true }, :order => "due_date")
    render :action => 'index'
  end

  (省略)
end

実は、Rails 3.0 で導入された where メソッドや order メソッドを利用すると、もっと簡潔に記述できます。

class TasksController < ApplicationController
  def index
    @task = Task.new
    @tasks = Task.where(:done => false).order("due_date")
  end

  def done
    @task = Task.new
    @tasks = Task.where(:done => true).order("due_date")
    render :action => 'index'
  end

  (省略)
end

Pratic Naik 氏のブログ記事によれば、all メソッドに :conditions オプションや :order オプションを渡す方法は、Rails 3.1 で廃止予定(DEPRECATED)になり、Rails 3.2 で廃止されるそうです。

scope

コントローラからモデルにコードを移すことによって、さらに index/done アクションを単純化しましょう。

まず、app/model/task.rb を次のように修正します。

class Task < ActiveRecord::Base
  scope :done, where(:done => true).order("due_date")
  scope :undone, where(:done => false).order("due_date")
end

scope メソッドは、Rails 2.x までは named_scope という名前でした。

すると、tasks コントローラはこう書き換えられます。

class TasksController < ApplicationController
  def index
    @task = Task.new
    @tasks = Task.undone
  end

  def done
    @task = Task.new
    @tasks = Task.done
    render :action => 'index'
  end

  (省略)
end

コントローラの記述は、できるかぎり単純化したいものです。

ビューの修正

app/views/tasks/index.html.erb を修正してください。

(省略)

<%= navigation_links %>

<table class="tasks">
  <col class="name" />
  <col class="due_date" />
  <col class="commands" />
  <%= render @tasks %>
</table>

タスクテーブルの直前にカスタムヘルパーメソッド navigation_links を追加しています。

app/helpers/tasks_helper.rb を開いて、navigation_links を実装します。

module TasksHelper
  def navigation_links
    items = []
    items << link_or_text('未完了タスク', :tasks)
    items << link_or_text('完了したタスク', [ :done, :tasks ])
    content_tag(:ul, :class => 'navigation') { items.join.html_safe }
  end

  private
  def link_or_text(text, resource)
    html_class = current_page?(resource) ? 'selected' : nil
    content_tag(:li, :class => html_class) do
      link_to_unless_current(text, resource)
    end
  end
end

navigation_links メソッドの中身はそれほど複雑ではありませんが、この連載で説明していないメソッドをたくさん使っているので、Rails 初心者の方はチンプンカンプンかもしれません。細かく説明し始めると長くなってしまうので、ここは「こんなものか」と納得していただくとして、次に進みます(スミマセン)。

(訂正) 読者の方からの指摘を受けて、ヘルパーメソッドnavigation_linksの4行目のブロック内部を items.join から items.join.html_safe に修正しました。html_safeは、文字列に「安全である」という印を付ける(正確に言うと、ActiveSupport::SafeBufferのインスタンスに変える)メソッドです。Rails 3.0.0の時は修正前の書き方でも正常に動いたのですが、Rails 3.0.3では意図しないエスケープが行われて、画面表示が乱れてしまいます。(2010/01/06)

次に public/stylesheets/navigation.css を作成します。

ul.navigation {
  width: 560px;
  margin: 15px auto 5px;
  list-style:none;
  padding:0;
}

ul.navigation li {
  display: inline;
  margin-right: 1px;
  background-color: #666;
  border: solid 1px #ccc;
  padding: 5px;
}

ul.navigation li.selected {
  background-color: #eee;
}

ul.navigation li a {
  color: #fff;
  text-decoration: none;
}

ブラウザで動作確認

まだ未完成ですが、ブラウザで動作を確認しましょう。

画面キャプチャ1

「完了したタスク」をクリックすると…

画面キャプチャ2

「Task 0」だけが表示されます。

unfinish アクション

完了したタスクを元に戻す unfinish アクションを作りましょう。

app/controllers/tasks_controller.rb を次のように修正してください。

class TasksController < ApplicationController
  (省略)

  def finish
    @task = Task.find(params[:id])
    @task.update_attribute(:done, true)
    redirect_to :back
  end

  def unfinish
    @task = Task.find(params[:id])
    @task.update_attribute(:done, false)
    redirect_to :back
  end
end

これは簡単ですね。

次に、app/views/tasks/_task.html.erb を修正します。

<tr>
  <td><%= task.name %></td>
  <td><%= task.due_date %></td>
  <td><%= finish_or_unfinish_link(task) %></td>
</tr>

最後に、app/helpers/tasks_helper.rb を開いて、finish_or_unfinish_link を実装します。

module TasksHelper
  (省略)

  def finish_or_unfinish_link(task)
    if task.done?
      link_to('戻す', [ :unfinish, task ], :method => :put)
    else
      link_to('完了', [ :finish, task ], :method => :put)
    end
  end

  private
  (省略)
end

訂正: この記事の発表時には、finish_or_unfinish_link メソッドにおいて :method => :put の記述がなく、正しく動作しませんでした。訂正いたします。(2010/05/20)

ブラウザに戻ってページの再読込をすると、リンク文字列が「戻す」に変わります。

画面キャプチャ3

「戻す」をクリックすると…

画面キャプチャ4

そして、「未完了タスク」をクリックすると…

画面キャプチャ5

イイ感じです。