Ruby on Rails チュートリアル

プロダクト開発の0→1を学ぼう

古い過去アーカイブ版を表示しています。史料として残してありますが、既に更新が止まっておりセキュリティ上の問題もあるため、学習で使うときは最新版をご利用ください。

第2版 目次

第9章 ユーザーの更新・表示・削除

この章では、Usersリソース用のRESTアクション (表7.1) のうち、これまで未実装だったeditupdateindexdestroyアクションを追加し、RESTアクションを完成させます。まずはユーザーが自分のプロファイルを自分で更新できるようにします。これにより、第8章で実装した認証用のコードによるセキュリティモデルを適用する機会を自然に提供することもできます。次に、すべてのユーザーを一覧できるようにします (もちろん認証を要求します)。これはサンプルデータとページネーション (pagnation) を導入する動機にもなります。最後に、ユーザーを削除し、データベースから完全に消去する機能を追加します。ユーザーの削除はどのユーザーにも許可できるものではないので、管理ユーザー (admin) の特権クラスを作成し、このユーザーにのみ削除を許可するようにします。

では最初に、いつものようにupdating-usersトピックブランチを作成しましょう。

$ git checkout -b updating-users

9.1ユーザーを更新する

ユーザー情報を編集するパターンは、(第7章)の新規ユーザーの作成と極めて似通っています。新規ユーザー用のビューを出力するnewアクションの代わりに、ユーザーを編集するためのeditアクションを作成すればよいのです。 POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成すればよいのです (コラム 3.3)。最大の違いは、ユーザー登録は誰でも実行できますが、ユーザー情報を更新できるのはそのユーザー自身に限られるということです。つまり、アクセス制御を行なって、認可された (authorized) ユーザーだけが編集と更新を行えるようにすればよいということです。第8章の認証 (authentication) システムを使えば、before_actionを使用してこれを行えます。

9.1.1編集フォーム

まずは編集用のフォームを作成しましょう。モックアップを図9.1に示します1。いつものようにテストから始めます。最初に、Gravatar画像を変更するリンクに注目してください。GravatarのWebサイトを探してみると、http://gravatar.com/emailsに画像の追加と編集を行えるページがありましたので、editページ上のこのURL2へのリンクについて テストを行なうことにします。

edit_user_mockup_bootstrap
図9.1ユーザー編集ページのモックアップ。(拡大)

ユーザー編集フォームのテストも、第7章の演習にあるリスト7.31の新規ユーザーフォームのテストと似ています。いずれも、無効な送信を行った時のエラーメッセージをテストします。変更したフォームをリスト9.1に示します。

リスト9.1 ユーザー編集ページのテスト。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "edit" do
    let(:user) { FactoryGirl.create(:user) }
    before { visit edit_user_path(user) }

    describe "page" do
      it { should have_content("Update your profile") }
      it { should have_title("Edit user") }
      it { should have_link('change', href: 'http://gravatar.com/emails') }
    end

    describe "with invalid information" do
      before { click_button "Save changes" }

      it { should have_content('error') }
    end
  end
end

これに対応するアプリケーションコードは、Usersコントローラのeditアクションの中に書き込みます。ここで注意して頂きたいのは、表7.1ではユーザー編集ページの正しいURLが/users/1/editとなっていることです (ユーザーのidが1の場合)。 ユーザーのidはparams[:id]変数で取り出すことができるのを思い出してください。つまり、リスト9.2のコードを使えばそのユーザーを指定できるということです。

リスト9.2 ユーザーのeditアクション。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def edit
    @user = User.find(params[:id])
  end
  .
  .
  .
end

テストにパスするためには、実際のeditビューをリスト9.3のように変更する必要があります。このコードがリスト7.17と極めて似通っていることに注目してください。重複が多いということは、それらのコードの繰り返しをパーシャルにまとめることができるということです。パーシャルにまとめる作業は演習の課題 (9.6) に回します。

リスト9.3 ユーザーのeditビュー。
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.text_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirm Password" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Save changes", class: "btn btn-large btn-primary" %>
    <% end %>

    <%= gravatar_for @user %>
    <a href="http://gravatar.com/emails">change</a>
  </div>
</div>

上のコードでは、7.3.3で導入したerror_messagesパーシャルを再利用しています。

リスト9.2@userインスタンス変数を使用することで、リスト9.1の編集ページ用テストはパスするはずです。

$ bundle exec rspec spec/requests/user_pages_spec.rb -e "edit page"

対応するページを図9.2に示します。Railsによって名前フィールドやメールアドレスのフィールドに既に値が自動的に入力されていることがわかります。これらの値には@user変数の属性が使用されています。

edit_page_bootstrap
図9.2名前とメールアドレスが入力済みの、初期状態のユーザー編集ページ。(拡大)

図9.2のHTMLソースを見てみると、formタグは期待どおりに表示されています (リスト9.4)。

リスト9.4 リスト9.3で定義された図9.2のHTML。
<form action="/users/1" class="edit_user" id="edit_user_1" method="post">
  <input name="_method" type="hidden" value="patch" />
  .
  .
  .
</form>

以下の入力フィールドに隠し属性があることに注目してください。

<input name="_method" type="hidden" value="patch" />

WebブラウザはネイティブではPATCHリクエスト (表7.1でRESTの慣習として要求されている) を送信できないので、RailsはPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを「偽造」しています3

ここでもう1つ微妙な点を指摘しておきたいと思います。リスト9.3form_for(@user)のコードは、リスト7.17のコードと完全に同じです。だとすると、Railsはどうやって新規ユーザー用のPOSTリクエストとユーザー編集用のPATCHリクエストを区別するのでしょうか。その答えは、Railsは、ユーザーが新規なのか、それともデータベースに存在する既存のユーザーであるかを、Active Recordのnew_record?論理値メソッドを使用して区別できるからです。

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

Railsは、form_for(@user)を使用してフォームを構成すると、@user.new_record?trueのときにはPOSTを、falseのときにはPATCHを使用します。

仕上げに、ユーザー設定のリンクにURLを1つ追加してサイト内を移動できるようにします。このリンクはユーザーがサインインしているかどうかによって異なるので、[Settings] リンクのテストはリスト9.5のように他の認証テストと同じところで行います。(サインインしていないユーザーにはこのリンクが表示されていないことを確認するテストも追加しておくのがよいでしょう。これは演習のために残しておきます (9.6))。

リスト9.5 [Settings] リンクのテストを追加する。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
    .
    .
    .
    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before { sign_in user }

      it { should have_title(user.name) }
      it { should have_link('Profile',     href: user_path(user)) }
      it { should have_link('Settings',    href: edit_user_path(user)) }
      it { should have_link('Sign out',    href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
      .
      .
      .
    end
  end
end

リスト9.5のコードでは、利便性のためにヘルパーを利用してテスト内でサインインを実行しています。このメソッドは、リスト9.6に示したように、サインインページにアクセスして有効な情報を送信します。

リスト9.6 ユーザーがサインインするためのテストヘルパー。
spec/support/utilities.rb
.
.
.
def sign_in(user, options={})
  if options[:no_capybara]
    # Capybaraを使用していない場合にもサインインする。
    remember_token = User.new_remember_token
    cookies[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
  else
    visit signin_path
    fill_in "Email",    with: user.email
    fill_in "Password", with: user.password
    click_button "Sign in"
  end
end

コメント行にも書いてあるとおり、Capybaraを使用していないとフォームへの自動入力が動作しません。このような場合に備えて、ユーザーからno_capyabara: trueオプションを渡せるようにし、デフォルトのサインインメソッドを上書きしてcookiesを直接操作できるようにします。リスト9.46に示しますが、これは、HTTPリクエスト (getpostpatchdelete) を直接使用する場合には必要となります (注意: テスト用のcookiesオブジェクトは実際のcookiesオブジェクトを完全にシミュレートできているわけではありません。特に、リスト8.19cookies.permanentメソッドはテスト内で動かすことはできません)。 ご想像のとおり、sign_inメソッドは今後のテストでも有意義に使えます。そして実際、(9.6) ではコードの重複を除くためにこのメソッドを使用しています。

[Settings] リンクにURLを追加するコードはシンプルです。表7.1の名前付きルートedit_user_pathに、リスト8.22で定義されたcurrent_userという便利なヘルパーメソッドを追加するだけです。

<%= link_to "Settings", edit_user_path(current_user) %>

完全なアプリケーションコードをリスト9.7に示します。

リスト9.7 [Settings] リンクを追加する。
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", '#' %></li>
            <li id="fat-menu" class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Account <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
                <li><%= link_to "Settings", edit_user_path(current_user) %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Sign out", signout_path, method: "delete" %>
                </li>
              </ul>
            </li>
          <% else %>
            <li><%= link_to "Sign in", signin_path %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>

9.1.2編集の失敗

この節では、編集 (の保存) に失敗した場合を扱います。また、リスト9.1のエラーメッセージのテストがパスするようにします。このアプリケーションコードはupdateアクションを作成しますが、これはリスト9.8のように、update_attributes (6.1.5) を使用して、送信されたparamsハッシュに基いてユーザーを更新します。 無効な情報が送信された場合、更新の結果としてfalseが返され、elseに分岐して編集ページを再度レンダリングします。このパターンは以前にも出現したことを覚えているでしょうか。この構造はcreateアクションの最初のバージョン (リスト7.21) と極めて似通っています。

リスト9.8 最初のユーザーupdateアクション。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end
  .
  .
  .
end

update_attributesへの呼び出しでuser_paramsを使用していることに注目してください。7.3.2でも説明したように、ここではStrong Parametersを使用してマスアサインメントの脆弱性を防止しています。

これによって生成されたエラーメッセージ (図9.3) は、まさにテストにパスするために必要なものになっています。以下のテストスイートを実行して確認してみましょう。

$ bundle exec rspec spec/
edit_with_invalid_information_bootstrap
図9.3更新フォームの送信で発生したエラーメッセージ。(拡大)

9.1.3編集の成功

今度は編集フォームが動作するようにしましょう。プロファイル画像の編集は、画像のアップロードをGravatarに任せてあるので、既に動作するようになっています。図9.2の [change] リンクをクリックすれば、図9.4のようにGravatarを編集できます。ではそれ以外の機能の実装にとりかかりましょう。

gravatar_cropper
図9.4Gravatarの画像調整インターフェイス (写真は誰かさん)

updateアクションのテストも、createアクション用のテストとだいたい同じです。Capybaraを使用してフォームのフィールドに有効な情報を入力し、その場合の動作が正しいことを確認するテストをリスト9.9に示します。コードの量がだいぶ増えてきました。第7章のテストを見返して、十分に理解するようにしてください。

リスト9.9 ユーザーupdateアクションのテスト。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "edit" do
    let(:user) { FactoryGirl.create(:user) }
    before do
      sign_in user
      visit edit_user_path(user)
    end
    .
    .
    .
    describe "with valid information" do
      let(:new_name)  { "New Name" }
      let(:new_email) { "new@example.com" }
      before do
        fill_in "Name",             with: new_name
        fill_in "Email",            with: new_email
        fill_in "Password",         with: user.password
        fill_in "Confirm Password", with: user.password
        click_button "Save changes"
      end

      it { should have_title(new_name) }
      it { should have_selector('div.alert.alert-success') }
      it { should have_link('Sign out', href: signout_path) }
      specify { expect(user.reload.name).to  eq new_name }
      specify { expect(user.reload.email).to eq new_email }
    end
  end
end

リスト 9.9では、リスト9.6で使われたsign_inメソッドをbeforeブロック内に追加しています。これにより、"Sign Out" リンクのテストをパスさせることができます。また、サインインしていないユーザがeditアクションを実行できないことも確認しています (詳しくは9.2.1項で説明します)。

リスト9.9で唯一目新しいのはreloadメソッドです。これはテスト内でユーザーの属性を変更するのに使用します。

specify { expect(user.reload.name).to  eq new_name }
specify { expect(user.reload.email).to eq new_email }

これにより、user.reloadを使用してテストデータベースからuser変数に再度読み込みが行われ、ユーザーの新しい名前とメールアドレスが新しい値と一致するかどうかが確認されます。

テストにパスする必要のある、リスト9.9updateアクションは、リスト9.10に示したように、createアクション (リスト8.27) の最終的なフォームとほぼ同じです。 変更すべき点は、以下を

flash[:success] = "Profile updated"
redirect_to @user

リスト9.8のコードに追加するだけで済みます。

リスト9.10 ユーザーのupdateアクション。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
end

上のコードに変更したことにより、編集を行う際には必ずパスワードの再確認が要求されるようになりました (図9.2にある空欄のパスワード確認用フィールドでそのことが示されています)。ユーザーにとっては少々わずらわしいですが、これによって更新をさらにセキュアにすることができます。

この節のコードを使用することで、ユーザー編集ページは動作するはずです。テストスイートをもう一度実行してみると、今度は緑色になるでしょう。

$ bundle exec rspec spec/

9.2認可

第8章で認証 (authentication) システムを構築したことで、認可 (authorization) のためのシステムを実装する準備もできました。認証はサイトのユーザーを識別することであり、認可はそのユーザーが実行可能な操作を管理することです。両者は似ていますが異なる概念です。

9.1のeditアクションとupdateアクションはすでに完全に動作していますが、セキュリティ上の大穴が1つ空いています。 どのユーザーでもあらゆるアクションにアクセスでき、サインインさえしていれば他のユーザーの情報を編集できてしまいます。この節では、ユーザーにサインインを要求し、かつ自分以外のユーザー情報を変更できないようにするセキュリティモデルを構築しましょう。サインインしていないユーザーや、保護されたページにアクセスしようとしているユーザーをサインインページに移動するようにし、そのときにわかりやすいメッセージも表示するようにしましょう。モックアップを図9.5に示します。

signin_page_protected_mockup_bootstrap
図9.5: 保護されたページにアクセスしたときのページのモックアップ。(拡大)

9.2.1ユーザーのサインインを要求する

editアクションとupdateアクションのセキュリティ制限はまったく同じなので、これらを共通のRSpec describeブロックで扱うことにします。 最初にサインインの要求からテストします。最初のテストでは、サインインしていないユーザーがいずれかのアクションにアクセスしようとしたときには、リスト9.11のように単にサインインページに移動することを確認します。

リスト9.11 editアクションとupdateアクションが保護されているかどうかテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }

      describe "in the Users controller" do

        describe "visiting the edit page" do
          before { visit edit_user_path(user) }
          it { should have_title('Sign in') }
        end

        describe "submitting to the update action" do
          before { patch user_path(user) }
          specify { expect(response).to redirect_to(signin_path) }
        end
      end
    end
  end
end

リスト9.11のコードには、Capybaraのvisitメソッドとは異なる、コントローラのアクションへのアクセス手段が導入されています。これは適切なHTTPリクエストを「直接」発行するという方法で、ここではpatchメソッドを使用してPATCHリクエストを発行しています。

describe "submitting to the update action" do
  before { patch user_path(user) }
  specify { expect(response).to redirect_to(signin_path) }
end

このコードでは、PATCHリクエストを 直接/users/1に発行しています。このリクエストはUsersコントローラのupdateアクションにルーティングされます (表7.1)。なぜこんなことをするかというと、ブラウザはupdateアクションを直接表示することができないからです。ブラウザは、編集フォームを送信することで間接的にそのアクションに到達することしかできないので (訳注: updateは純粋に更新処理を行うアクションであって、そこで何かを表示するわけではないので)、Capybaraでは対応できません。そして、editページを表示してもeditアクションの認可テストはできますが、updateアクションの認可テストはできません。こうした事情から、updateアクション自体をテストするにはリクエストを直接発行する以外に方法がありません (patchメソッドがあることからわかるように、Railsのテストではgetpostdeleteメソッドもサポートされています)。

これらのメソッドのいずれかを使用してHTTPリクエストを直接発行すると、低レベルのresponseオブジェクトにアクセスできるようになります。Capybaraのpageオブジェクトと異なり、 responseオブジェクトはサーバーの応答自体のテストに使用できます。 この場合は、サインインページへのリダイレクトによるupdateアクションの応答を確認します。

specify { expect(response).to redirect_to(signin_path) }

認可のアプリケーションコードでは、before_actionを使用して、与えられたアクションが呼び出される前に特定のメソッド向けの操作を行うことができます。(この操作は、かつて "before_filter" と呼ばれていました。しかし、コントローラの特定のアクションの前に操作が実行されることを明示的にするため、Rails のコアチームによって "before_action" に改名されました。) ユーザーにサインインを要求するために、リスト9.12のようにsigned_in_userメソッドを定義してbefore_action :signed_in_userという形式で呼び出します。

リスト9.12 before_actionにsigned_in_userを追加する。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # Before actions

    def signed_in_user
      redirect_to signin_url, notice: "Please sign in." unless signed_in?
    end
end

デフォルトでは、before_actionはコントローラ内のすべてのアクションに適用されるので、ここでは:onlyオプションハッシュを渡すことによって:edit:updateアクションにのみこのフィルタが適用されるように制限をかけています。

リスト9.12では、redirect_toにオプションハッシュを渡すことによって、flash[:notice]の記述を簡素化していることに注目してください。リスト9.12のその箇所をもっと冗長に書くと以下のようになります。

unless signed_in?
  flash[:notice] = "Please sign in."
  redirect_to signin_url
end

(:errorキーでも同じ構成が使えますが、:successキーではそうではありません。)

flashスタイルでは、:notice:success:errorの3つのキーを指定できますが、これらはBootstrap CSSでネイティブにサポートされています。サインアウトしてユーザー編集ページ/users/1/editにアクセスすると、図9.6のように黄色い "notice" ボックスが表示されます。

protected_sign_in_bootstrap
図9.6保護されたページにアクセスした直後のサインインフォーム。(拡大)

今度はテストスイートがパスするはずです。

$ bundle exec rspec spec/

9.2.2正しいユーザーを要求する

当然のことですが、サインインを要求するだけでは十分ではありません。ユーザーが自分自身の情報以外の他ユーザーの情報を編集できないようにする必要もあります。これをテストするには、無効なユーザーとしてサインインしてから、editアクションと updateアクションにアクセスします (リスト9.13)。このとき、今回は Capybara を使わずにテストしているため (no_capybara: true)、getpatch メソッドを使って editupdate アクションに直接アクセスしていることに注意してください。また、ユーザーは他のユーザーのプロファイルを編集しようとすることすらできないようにしないといけません。そのため、そのような不正なアクセスを検出したら、サインインページではなくルートURLにリダイレクトさせなくてはいけません。

リスト9.13 editアクションとupdateアクションで正しいユーザーを要求することをテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do
    .
    .
    .
    describe "as wrong user" do
      let(:user) { FactoryGirl.create(:user) }
      let(:wrong_user) { FactoryGirl.create(:user, email: "wrong@example.com") }
      before { sign_in user, no_capybara: true }

      describe "submitting a GET request to the Users#edit action" do
        before { get edit_user_path(wrong_user) }
        specify { expect(response.body).not_to match(full_title('Edit user')) }
        specify { expect(response).to redirect_to(root_url) }
      end

      describe "submitting a PATCH request to the Users#update action" do
        before { patch user_path(wrong_user) }
        specify { expect(response).to redirect_to(root_path) }
      end
    end
  end
end

なお、ファクトリーでは以下のオプションを使用できます。

FactoryGirl.create(:user, email: "wrong@example.com")

上のコードは、作成するユーザーのメールアドレスをデフォルトと異なるものに変更します。このテストでは、元のユーザーが別のユーザーのeditアクションやupdateアクションにアクセスできないことを確認します。

リスト9.14のアプリケーションコードでは、correct_userメソッドを呼び出すためにbefore_actionをもう1つ追加しています。

リスト9.14 edit/updateページを保護するためのcorrect_user before_action。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # Before actions

    def signed_in_user
      redirect_to signin_url, notice: "Please sign in." unless signed_in?
    end

    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_path) unless current_user?(@user)
    end
end

correct_userフィルタではcurrent_user?論理値 (boolean) メソッドを使用しています。このメソッドはセッションヘルパーで定義します (リスト9.15)。

リスト9.15 current_user?メソッド。
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def current_user?(user)
    user == current_user
  end
  .
  .
  .
end

リスト9.14では、editアクションとupdateアクションも更新されていることに注目してください。リスト9.2の時点では以下のようなコードでした。

def edit
  @user = User.find(params[:id])
end

このコードはupdateアクションでも同様でした。しかし既にcorrect_user before_actionで@userを定義したので、updateアクションとeditアクションからこのコードを削除できました。

以下を実行してテストスイートがパスすることを確認してから先に進むことにしましょう。

$ bundle exec rspec spec/

9.2.3フレンドリーフォワーディング

ここまででWebサイトの認可機能は完成したかのように見えますが、後1つ小さなキズがあります。保護されたページにアクセスしようとすると、問答無用で自分のプロファイルページに移動させられてしまいます。別の言い方をすれば、ログオンしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがサインインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作です。リダイレクト先は、ユーザーが開こうとしていたページにしてあげるのが親切というものです。

このような動作を “フレンドリーフォワーディング” と呼びますが、これをテストするには次のような手順を踏みます。まずユーザーのeditページにアクセスし、その後サインインページにリダイレクトします。それから正しいサインイン情報を入力し、[Sign in] ボタンをクリックします。サインイン後にリダイレクトされるのはユーザーのプロファイルページですが、この場合はもともと "Edit user" ページにアクセスしようとしていたのですから、そのページにリダイレクトするようにします。この手順のテストをリスト9.16に示します。

リスト9.16 フレンドリーフォワーディングのテスト。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }

      describe "when attempting to visit a protected page" do
        before do
          visit edit_user_path(user)
          fill_in "Email",    with: user.email
          fill_in "Password", with: user.password
          click_button "Sign in"
        end

        describe "after signing in" do

          it "should render the desired protected page" do
            expect(page).to have_title('Edit user')
          end
        end
      end
      .
      .
      .
    end
    .
    .
    .
  end
end

いよいよ実装です4。 この動作を、store_locationredirect_back_orの2つのメソッドを使用して実現します。これらのメソッドは、セッションヘルパーで定義します (リスト9.17)。

リスト9.17 フレンドリーフォワーディングを実装したコード。
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def redirect_back_or(default)
    redirect_to(session[:return_to] || default)
    session.delete(:return_to)
  end

  def store_location
    session[:return_to] = request.url
  end
end

ここで、リダイレクト先を保存するメカニズムは、Railsが提供するsession機能を使用しています。これは、8.2.1で説明した、ブラウザを閉じたときに自動的に破棄されるcookies変数のインスタンスと同様のものと考えていただければよいでしょう。また、url (ここではリクエストされたページのURL) の取得にはrequestオブジェクトを使用しています。store_locationメソッドでは、リクエストされたURLを:return_toというキーでsession変数に保存しています。

store_locationメソッドを使用するためには、リスト9.18のようにこのメソッドをsigned_in_user before_actionに追加しておく必要があります。

リスト9.18 store_locationメソッドを、サインインしたユーザーのbefore_actionに追加する。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # Before actions

    def signed_in_user
      unless signed_in?
        store_location
        redirect_to signin_url, notice: "Please sign in."
      end
    end

    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_path) unless current_user?(@user)
    end
end

フォワーディング自体を実装するには、redirect_back_orメソッドを使用します。リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトします。デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクトします (リスト9.19)。redirect_back_orメソッドでは、以下のようにor演算子||を使用します。

session[:return_to] || default

このコードは、値がnilでなければsession[:return_to]を評価し、nilであれば与えられたデフォルトのURLを使用します (このコードのテストは 9.6の演習とします)。

リスト9.19 セッションのcreateアクションでフレンドリーフォワーディングを実装する。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_back_or user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

(第8章の最初の演習が終わっている場合は、必ずリスト9.19の正しいparams ハッシュを使用してください。)

これで、リスト9.16のフレンドリーフォワーディング用結合テストはパスするはずです。成功すれば、基本ユーザー認証機能とページ保護機能の実装は完了です。いつものように、以下を実行してテストスイートが緑色 (成功) になることを確認してから先に進みましょう。

$ bundle exec rspec spec/

9.3すべてのユーザーを表示する

この節では、いよいよ最後から2番目のユーザーアクションであるindexアクションを追加しましょう。このアクションは、すべてのユーザーを一覧表示します。その際、データベースにサンプルデータを追加する方法や、将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネーション (pagination=ページ分割) の方法を学びます。ユーザーの一覧、ページネーション用リンク、移動用の [Users]リンクのモックアップを図9.7に示します59.4では、ユーザーのインデックスページに管理用のインターフェイスを追加し、トラブルを起こしているユーザーなどをそこで削除できるようにします。

user_index_mockup_bootstrap
図9.7ページネーションと [Users] リンクを実装したユーザーのインデックスページのモックアップ。(拡大)

9.3.1ユーザーインデックス

ユーザーのshowページについては、今後も (サインインしているかどうかにかかわらず) サイトを訪れたすべてのユーザーから見えるようにしておきますが、ユーザーindexページはサインインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限します。そこで、users_path (表7.1) にアクセスしたときにindexアクションが保護され、サインインページにリダイレクトされることをテストすることから始めます。他の認可テストと同様、この例はリスト9.20のように認証の結合テストの中に置くことにします。

リスト9.20 indexアクションが保護されていることをテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      .
      .
      .
      describe "in the Users controller" do
        .
        .
        .
        describe "visiting the user index" do
          before { visit users_path }
          it { should have_title('Sign in') }
        end
      end
      .
      .
      .
    end
  end
end

リスト9.21にアプリケーションのコードを示しました。ここでは、signed_in_user before_actionで保護するアクションのリストにindexを追加するだけです。

リスト9.21 indexアクションでユーザーのサインインを要求する。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

  def show
    @user = User.find(params[:id])
  end
  .
  .
  .
end

次の一連のテストでは、サインインしたユーザーから見たインデックスページに、タイトルとコンテンツとサイトのすべてのユーザーが正しく表示されていることを確認します。このメソッドでは、3つのファクトリーユーザー (最初の1人としてサインインします) を作成し、インデックスページに表示されているそれぞれのユーザーにリスト要素 (li) タグが与えられていることを確認します。リスト9.22のように、3人のユーザーはそれぞれ異なる名前にしておき、ユーザーのリストの要素がそれぞれ一意になるように (重複しないように) していることに注意してください。

リスト9.22 ユーザーのインデックスページのテスト。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "index" do
    before do
      sign_in FactoryGirl.create(:user)
      FactoryGirl.create(:user, name: "Bob", email: "bob@example.com")
      FactoryGirl.create(:user, name: "Ben", email: "ben@example.com")
      visit users_path
    end

    it { should have_title('All users') }
    it { should have_content('All users') }

    it "should list each user" do
      User.all.each do |user|
        expect(page).to have_selector('li', text: user.name)
      end
    end
  end
  .
  .
  .
end

デモアプリケーション (リスト2.4) にも同様のアクションがあったことを思い出した人もいると思います。このアプリケーションコードでも、リスト9.23のようにUser.allを使用してすべてのユーザーをデータベースから取り出し、それらを@usersインスタンス変数に割り当ててビューで使用しています。(すべてのユーザーを一気に読みだすのはデータ量が多い場合に問題が生じるのではないかと思われる方、そのとおりです。このキズは9.3.3で修正します。)

リスト9.23 ユーザーのindexアクション。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

実際のインデックスページを作成するには、ユーザーを列挙してユーザーごとにliタグで囲むビューを作成する必要があります。ここではeachメソッドを使用してこれを行います。それぞれの行を非序列リスト (ul)タグで囲いながら、各ユーザーのGravatarと名前を表示します (リスト9.24)。リスト9.24では、7.6の演習のリスト7.30の結果を利用しています。これは、Gravatarヘルパーにデフォルト以外のサイズを指定するオプションを渡します。この演習をまだやっていない場合は、リスト7.30に従ってUsersヘルパーファイルを更新してから先に進んでください。

リスト9.24 ユーザーのindexビュー。
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 52 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

CSS (正確にはSCSSですが) にもちょっぴり手を加えておきましょう (リスト9.25)。

リスト9.25 ユーザーインデックス用のスタイル。
app/assets/stylesheets/custom.css.scss
.
.
.

/* users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-top: 1px solid $grayLighter;
    &:last-child {
      border-bottom: 1px solid $grayLighter;
    }
  }
}

最後に、サイト内移動用のヘッダーにユーザー一覧表示用のリンクを追加します。これにはusers_pathを使用し、表7.1に残っている最後の名前付きルートを割り当てます。リスト9.26のテストとリスト9.27のアプリケーションのコードは、いずれも素直な作りです。

リスト9.26 [Users] リンク用のURL。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
    .
    .
    .
    describe "with valid information" do
      let(:user) { FactoryGirl.create(:user) }
      before { sign_in user }

      it { should have_title(user.name) }
      it { should have_link('Users',       href: users_path) }
      it { should have_link('Profile',     href: user_path(user)) }
      it { should have_link('Settings',    href: edit_user_path(user)) }
      it { should have_link('Sign out',    href: signout_path) }
      it { should_not have_link('Sign in', href: signin_path) }
      .
      .
      .
    end
  end
end
リスト9.27 このURLにユーザー一覧へのリンクを追加する。
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", users_path %></li>
            <li id="fat-menu" class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Account <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
                <li><%= link_to "Settings", edit_user_path(current_user) %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Sign out", signout_path, method: "delete" %>
                </li>
              </ul>
            </li>
          <% else %>
            <li><%= link_to "Sign in", signin_path %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>

以上でユーザーインデックスページは完全に機能するようになりましたので、以下のテストはすべてパスするはずです。

$ bundle exec rspec spec/

ところで、図9.8のようにユーザーが1人のままではいささか寂しすぎます。もう少し何とかしてみましょう。

user_index_only_one_bootstrap
図9.8ユーザーインデックスページ/usersにユーザーが1人しか表示されていない。(拡大)

9.3.2サンプルのユーザー

この節では、一人ぼっちのユーザーに仲間を加えてあげることにします。複数のユーザーが表示されたユーザーインデックスページにするためには、ブラウザでサインアップページを表示してユーザーを手作業で1人ずつ追加するという方法もありますが、せっかくなのでRubyとRakeを使用してユーザーを一気に作成しましょう。

まず、GemfileFaker gemを追加します (リスト9.28)。これは、実際にありそうなユーザー名とメールアドレスを持つサンプルユーザーを自動的に作成するものです。

リスト9.28 GemfileにFakerを追加する。
source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.5'
gem 'bootstrap-sass', '2.3.2.0'
gem 'sprockets', '2.11.0'
gem 'bcrypt-ruby', '3.1.2'
gem 'faker', '1.1.2'
.
.
.

次に、いつものように以下を実行します。

$ bundle install

次に、サンプルユーザーを作成するRakeタスクを追加します。Rakeタスクはlib/tasksディレクトリに置きます。また、Rakeタスクを定義するときには、リスト9.29のように名前空間 (この場合は:db) を使用します (ここは若干高度な内容ですが、今は詳細を理解する必要はありません)。

リスト9.29 データベースにサンプルユーザーを追加するRakeタスク。
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    User.create!(name: "Example User",
                 email: "example@railstutorial.jp",
                 password: "foobar",
                 password_confirmation: "foobar")
    99.times do |n|
      name  = Faker::Name.name
      email = "example-#{n+1}@railstutorial.jp"
      password  = "password"
      User.create!(name: name,
                   email: email,
                   password: password,
                   password_confirmation: password)
    end
  end
end

このコードはdb:populateタスクを定義します。このタスクは、それらしい名前とメールアドレスを持つ99のユーザーを作成し、従来のユーザーと置き換えます。以下の行は

task populate: :environment do

User.create!を実行する前に、RakeタスクがUserモデルなどのローカルのRails環境にアクセスできるようにします。 ここでcreate!は基本的にcreateメソッドと同じものですが、ユーザーが無効な場合にfalseを返すのではなく例外を発生させる (6.1.4) 点が異なります。こうしておくとより多くのメッセージを生成でき、サイレントエラーを回避できるのでデバッグが容易になります。

:db名前空間をリスト9.29のように設定後、以下のようにRakeタスクを呼び出します。

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake db:test:prepare

Rakeタスクを実行後、図9.9のようにアプリケーションのユーザーは100人になりました (最初のいくつかのサンプルアドレスについては、デフォルトのGravatar画像以外の写真を関連付けてみました)。

user_index_all_bootstrap
図9.9ユーザーインデックスページ/usersに100人のサンプルユーザーが表示されている。(拡大)

9.3.3ページネーション

これで、最初のユーザーにも仲間ができました。しかし今度は逆に、1つのページに大量のユーザーが表示されてしまっています。100人でもかなり大きい数であると思いますし、今後は数千ユーザーに増える可能性もあります。これを解決するのがページネーション (pagination) というもので、この場合は、たとえば1つのページに一度に30人だけユーザーを表示するというものです。

Railsには豊富なページネーションメソッドがあります。今回はその中で最もシンプルかつ堅牢なwill_paginateメソッドを使用してみましょう。これを使用するには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方インクルードし、Bootstrapのページネーションスタイルを使用してwill_paginateを構成します。更新したGemfileリスト9.30に示します。

リスト9.30 will_paginateGemfileにインクルードする。
source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.5'
gem 'bootstrap-sass', '2.3.2.0'
gem 'sprockets', '2.11.0'
gem 'bcrypt-ruby', '3.1.2'
gem 'faker', '1.1.2'
gem 'will_paginate', '3.0.4'
gem 'bootstrap-will_paginate', '0.0.9'
.
.
.

次にbundle installを実行します。

$ bundle install

新しいgemが正しく読み込まれるように、Webサーバーを再起動してください。

will_paginate gemは広く使用されていて実績もあるので、徹底的にテストする必要はありません。ここでは簡単なテストを行うことにします。最初に、divタグのCSS class が “pagination”になっていることをテストします。これはwill_paginateによって出力されます。次に、結果の最初のページに正しいユーザーが表示されていることを確認します。そのためにはpaginateメソッドが必要です。このメソッドについてはこの後説明します。

以前と同様、Factory Girlを使用してユーザーをシミュレートすることにしますが、ここで早くも問題が生じます。ユーザーのメールアドレスは一意でないといけませんが、このままだと手作業で30人ものメールアドレスを作成しなければなりません。それに、ユーザー名もすべて異なるものにしておく方がテストの際に便利です。幸い、この問題はFactory Girlのsequencesメソッドを使用して解決できます。最初に作成したファクトリー (リスト7.8) では、ユーザー名とメールアドレスが固定されています。

FactoryGirl.define do
  factory :user do
    name     "Michael Hartl"
    email    "michael@example.com"
    password "foobar"
    password_confirmation "foobar"
  end
end

これに代えて、以下のようにsequenceメソッドを使用して一連の名前とメールアドレスを列挙します。

factory :user do
  sequence(:name)  { |n| "Person #{n}" }
  sequence(:email) { |n| "person_#{n}@example.com"}
  .
  .
  .

sequenceメソッドの引数には、使用したい属性に対応するシンボル (:name など) を使用し、nという変数を持つブロックを1つ置きます。続いてFactoryGirlメソッドを実行します。

FactoryGirl.create(:user)

ブロック変数nは自動的にインクリメントされますので、最初のユーザーは名前が “Person 1” でメールアドレスが “person_1@example.com”、次のユーザーは名前が “Person 2” でメールアドレスが “person_2@example.com”、というように生成されます。完全なコードをリスト9.31に示します。

リスト9.31 Factory Girlでシーケンスを定義する。
spec/factories.rb
FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"
  end
end

ファクトリーのシーケンスという考えを応用して、テスト用に30人のユーザーを作成します。ページネーションを行うにはこれで十分です。

before(:all) { 30.times { FactoryGirl.create(:user) } }
after(:all)  { User.delete_all }

ここでbefore(:all)を使用して、ブロックにあるすべてのテストの前にサンプルユーザーを一括作成するようにしていることに注目してください。これは、ユーザーを30人も作成するとシステムによっては速度が低下することがあり、それを防ぐためのものです。これと対になるafter(:all)を使用して、完了後ユーザーをすべて削除します。

ページネーションのdivの表示テストと正しいユーザーのテストをリスト9.32に示します。リスト9.22User.all配列がUser.paginate(page: 1)に置き換えられていることに注目してください。この後お見せしますが、これはデータベースから最初の1ページを取り出す方法です。また、リスト9.32では、before(:all)との違いを強調するためにbefore(:each)も使用しています。

リスト9.32 ページネーションのテスト。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "index" do
    let(:user) { FactoryGirl.create(:user) }
    before(:each) do
      sign_in user
      visit users_path
    end

    it { should have_title('All users') }
    it { should have_content('All users') }

    describe "pagination" do

      before(:all) { 30.times { FactoryGirl.create(:user) } }
      after(:all)  { User.delete_all }

      it { should have_selector('div.pagination') }

      it "should list each user" do
        User.paginate(page: 1).each do |user|
          expect(page).to have_selector('li', text: user.name)
        end
      end
    end
  end
  .
  .
  .
end

ページネーションが動作するには、ユーザーのページネーションを行うようにRailsに指示するコードをindexビューに追加する必要があります。また、indexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もあります。まずは、ビューに特殊なwill_paginateメソッドを追加しましょう (リスト9.33)。同じコードがリストの上と下に2つありますが、その理由はこの後で説明します。

リスト9.33 ユーザーインデックスのページネーション。
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 52 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

このwill_paginateメソッドは少々不思議なことに、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成しています。実は、リスト9.33のビューはこのままでは動きません。@usersオブジェクトに現在含まれているのはUser.all (リスト9.23) の結果ですが、will_paginateでは、結果が明示的にpaginateメソッドを使っている必要があるからです。

$ rails console
>> User.paginate(page: 1)
  User Load (1.5ms)  SELECT "users".* FROM "users" LIMIT 30 OFFSET 0
   (1.7ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...

paginateは、キーが:pageで値がリクエストページに等しいハッシュ引数を取る必要があることに注意してください。User.paginateは、:pageパラメーターに基いて、データベースからひとかたまりのデータ (デフォルトでは30) を取り出します。従って、1ページ目は1から30のユーザー、2ページ目は31から60のユーザーという具合にデータが取り出されます。ページがnilの場合、 paginateは単に最初のページを返します。

paginateを使用することで、サンプルアプリケーションのユーザーのページネーションを行えるようになります。具体的には、indexアクション内のallpaginateメソッドに置き換えます (リスト9.34)。ここで:pageパラメーターにはparams[:page]が使用されていますが、これはwill_paginateによって自動的に生成されます。

リスト9.34 indexアクションのユーザーをページネーションする。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.paginate(page: params[:page])
  end
  .
  .
  .
end

以上で、ユーザーのインデックスページは図9.10のように動作するはずです (システム環境によっては、ここでRailsを再起動する必要があるかもしれません) 。will_paginateをユーザーリストの上と下の両方に配置してあるので、ページネーションのリンクもページの上と下の両方に表示されています。

user_index_pagination_rails_3_bootstrap
図9.10ユーザーインデックスページ /usersでのページネーション。(拡大)

[2] リンクまたは [Next] リンクをクリックすると、図9.11のように次のページに移動します。

user_index_page_two_rails_3_bootstrap
図9.11ユーザーインデックスの2ページ目 (/users?page=2)。(拡大)

テストもパスするはずです。

$ bundle exec rspec spec/

9.3.4パーシャルのリファクタリング

ユーザーインデックスへのページネーション実装はついに完了しました。でも私は、ここでぜひともある1つの改良を加えてみたいのです。実はRailsにはコンパクトなビューを作成するための素晴らしいツールがいくつもあります。この節ではそれらのツールを使用してインデックスページのリファクタリング (動作を変えずにコードを整理すること) を行うことにします。サンプルアプリケーションのテストは既に完了しているので、Webサイトの機能を損なうことなく安心してリファクタリングに取りかかれます。

リファクタリングの第一歩は、リスト9.33のユーザーのlirender呼び出しに置き換えることです (リスト9.35)。

リスト9.35 インデックスビューで最初のリファクタリングを行う。
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>

ここでは、renderをパーシャルの名前の文字列に対してではなく、Userクラスのuser変数に対して実行していることに注意してください6。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探します。このパーシャルを作成する必要があります (リスト9.36)。

リスト9.36 単一のユーザーを出力するパーシャル。
app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 52 %>
  <%= link_to user.name, user %>
</li>

これは間違いなく大きな進歩です。しかしここで終わらせず、さらに改良してみましょう。今度はrender@users変数に対して直接実行します (リスト9.37)。

リスト9.37 完全にリファクタリングされたユーザーインデックス。
app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <%= render @users %>
</ul>

<%= will_paginate %>

Railsは@usersUserオブジェクトのリストであると推測します。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力します (訳注: each doとendで囲む必要がなくなります)。これにより、リスト9.37のコードは極めてコンパクトになります。これに限らず、リファクタリングを行う場合には、アプリケーションのコードを変更する前と後で必ずテストを実行し、いずれも緑色 (成功) になることを確認するようにしてください。

$ bundle exec rspec spec/

9.4ユーザーを削除する

ユーザーインデックスはとうとう完了しました。残るはdestroyだけです。これを実装することで、RESTに準拠した正統なアプリケーションとなります。この節では、ユーザーを削除するためのリンクを追加します。モックアップを図9.12に示します。また、削除を行うのに必要なdestroyアクションも実装します。しかしその前に、削除を実行できる権限を持つ管理ユーザーのクラスを作成しましょう。

user_index_delete_links_mockup_bootstrap
図9.12削除リンクを追加したユーザーインデックスのモックアップ。(拡大)

9.4.1管理ユーザー

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド (論理値を返す) も使えるようになりますので、これを使用して管理ユーザーの状態をテストできます。この属性に対するテストはリスト9.38のように書くことができます。

リスト9.38 admin属性に対するテスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:authenticate) }
  it { should respond_to(:admin) }

  it { should be_valid }
  it { should_not be_admin }

  describe "with admin attribute set to 'true'" do
    before do
      @user.save!
      @user.toggle!(:admin)
    end

    it { should be_admin }
  end
  .
  .
  .
end

ここではtoggle!メソッドを使用して admin属性の状態をfalseからtrueに反転しています。また、以下の行にも注目してください。

it { should be_admin }

これはユーザーに対してadmin?メソッド (論理値を返す) が使用できる必要があることを (RSpecの論理値慣習に基いて) 示しています。

ここでいつものように、マイグレーションを実行してadmin属性を追加しましょう。コマンドラインで、この属性の型をbooleanと指定します。

$ rails generate migration add_admin_to_users admin:boolean

マイグレーションを実行すると単にadminカラムがusersテーブル (リスト9.39) に追加され、図9.13のデータモデルが生成されます。

リスト9.39 論理値を取るadmin属性をユーザーに追加するマイグレーション。
db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

リスト9.39では、default: falseという引数をadd_columnに追加しています。これは、デフォルトでは管理者になれないということを示すためです (default: false引数を与えない場合、 adminの値はデフォルトでnilになりますが、これはfalseと同じ意味ですので、必ずしもこの引数を与える必要はありません。ただし、このように明示的に引数を与えておけば、コードの意図をRailsと開発者に明確に示すことができます)。

user_model_admin_31
図9.13: 論理値をとるadmin属性が追加されたUserモデル。

最後に、開発用データベースにマイグレーションを行い、テスト用データベースを準備します。

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

Rails consoleで動作を確認すると、期待どおりadmin属性が追加されて論理値をとり、さらに疑問符の付いたadmin?メソッドも利用できるようになっています。

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true

これで、adminテストはパスするはずです。

$ bundle exec rspec spec/models/user_spec.rb

仕上げに、最初のユーザーだけをデフォルトで管理者にするようサンプルデータ生成を更新しましょう (リスト9.40)。

リスト9.40 サンプルデータ生成コードに管理者を1人追加する
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    admin = User.create!(name: "Example User",
                         email: "example@railstutorial.jp",
                         password: "foobar",
                         password_confirmation: "foobar",
                         admin: true)
    .
    .
    .
  end
end

次にデータベースをリセットし、サンプルデータを再度生成します。

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake db:test:prepare

Strong Parameters、再び

リスト9.40では、初期化ハッシュにadmin: trueを設定することでユーザーを管理者にしていることにお気付きになりましたでしょうか。ここでは、荒れ狂うWeb世界にオブジェクトをさらすことの危険性を改めて強調しています。もし、任意のWebリクエストの初期化ハッシュをオブジェクトに渡せるとなると、攻撃者は以下のようなPATCHリクエストを送信してくるかもしれません7

patch /users/17?admin=1

このリクエストは、ユーザー番号17番を管理者に変えてしまいます。ユーザーのこの行為は、少なくとも重大なセキュリティ違反となる可能性がありますし、実際にはそれだけでは済まないでしょう。

このような危険があるからこそ、編集してもよい属性だけを許可するように処理されたパラメータを渡すことが重要になります。7.3.2で説明したとおり、Strong Parametersを使用してこれを行います。具体的には、 以下のようにparamsハッシュに対してrequirepermitを呼び出します。

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

上のコードでは、許可された属性リストにadminが含まれていないことに注目してください。これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できます。この問題は重大であるため、編集可能になってはならない属性に対するテストを作成することをぜひともお勧めします。admin属性のテストについては演習に回します (9.6)。

9.4.2 destroyアクション

Usersリソースの最後の仕上げとして、destroyアクションへのリンクを追加しましょう。まず、ユーザーインデックスページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限します。

削除機能をテストするには、管理者を作成するファクトリーがあると便利です。これを行うには、リスト9.41のように既存のファクトリーに:adminブロックを追加します。

リスト9.41 管理ユーザー向けのファクトリーを追加する。
spec/factories.rb
FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"

    factory :admin do
      admin true
    end
  end
end

リスト9.41のコードによって、FactoryGirl.create(:admin)を使用してテスト内に管理ユーザーを作成することができるようになります。

私たちのセキュリティモデルでは、一般ユーザーにはこの削除リンクを表示しないようにします。

it { should_not have_link('delete') }

逆に管理ユーザーにはこの削除リンクが表示され、このリンクをクリックすることでそのユーザーが管理ユーザーによって削除され、Userカウントが-1だけ変わることが期待されます。

it { should have_link('delete', href: user_path(User.first)) }
it "should be able to delete another user" do
  expect do
    click_link('delete', match: :first)
  end.to change(User, :count).by(-1)
end
it { should_not have_link('delete', href: user_path(admin)) }

上のコードにはmatch: :firstという記述があります。これは、どの削除リンクをクリックするかは問わないことをCapybaraに伝えます。これによりCapybaraは、最初に見つけたリンクを単にクリックするようになります。このテストには、管理者自身を削除するためのリンクが管理者に表示されていないことを確認するテストも含まれていることに注意してください。削除リンクテストのフルセットをリスト9.42に示します。

リスト9.42 削除リンクのテスト。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do

  subject { page }

  describe "index" do

    let(:user) { FactoryGirl.create(:user) }

    before do
      sign_in user
      visit users_path
    end

    it { should have_title('All users') }
    it { should have_content('All users') }

    describe "pagination" do
      .
      .
      .
    end

    describe "delete links" do

      it { should_not have_link('delete') }

      describe "as an admin user" do
        let(:admin) { FactoryGirl.create(:admin) }
        before do
          sign_in admin
          visit users_path
        end

        it { should have_link('delete', href: user_path(User.first)) }
        it "should be able to delete another user" do
          expect do
            click_link('delete', match: :first)
          end.to change(User, :count).by(-1)
        end
        it { should_not have_link('delete', href: user_path(admin)) }
      end
    end
  end
  .
  .
  .
end

上のテストに対応するアプリケーションコードは、現在のユーザーが管理者の場合に [delete] リンクを有効にします (リスト9.43)。ここで、必要なDELETEリクエストを発行するリンクの生成はmethod: :delete引数によって行われている点に注目してください。また、各リンクをif文で囲い、管理者にだけ削除リンクが表示されるようにしています。管理者から見えるページを図9.14に示します。

リスト9.43 ユーザー削除用リンク (管理者にのみ表示される)。
app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 52 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

ブラウザはネイティブではDELETEリクエストを送信できないため、RailsではJavaScriptを使用してこれを偽造します。つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になるということです。JavaScriptをサポートしないブラウザをサポートする必要がある場合は、フォームとPOSTリクエストを使用してDELETEリクエストを偽造することもできます。こちらはJavaScriptがなくても動作します。詳細についてはRailsCastの「JavaScriptを使用しないで削除する(英語)」を参照してください。

index_delete_links_rails_3_bootstrap
図9.14ユーザーインデックス/usersに削除リンクが表示されている。(拡大)

この削除リンクが動作するためには、destroyアクション (表7.1) を追加する必要があります。このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使用して削除し、最後にユーザーインデックスに移動します (リスト9.44)。なお、このとき、:destroysigned_in_user の before_action に追加しています。

リスト9.44 実際に動作するdestroyアクションを追加する。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User destroyed."
    redirect_to users_url
  end
  .
  .
  .
end

destroyアクションでは、findメソッドとdestroyメソッドを1行で書くために2つのメソッドを連結 (chain) している点に注目してください。

User.find(params[:id]).destroy

既に、管理者のみがユーザーを削除できるように構成済みです。削除リンクは管理者にしか表示されません。残念なことに、実はまだ大きなセキュリティホールがあります。ある程度の腕前を持つ攻撃者なら、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができるでしょう。サイトを正しく防衛するには、destroyアクションにもアクセス制御を行う必要があります。そこで、管理者はユーザーを削除できるが一般ユーザーはできないことをテストで確認しましょう。作成したテストをリスト9.45に示します。リスト9.11patchメソッドのときと同様、deleteメソッドを使用してDELETEリクエストを指定のURL (ここでは表7.1で要求されているユーザーパス) に直接発行していることに注目してください。

リスト9.45 destroyアクションの保護のテスト。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do
    .
    .
    .
    describe "as non-admin user" do
      let(:user) { FactoryGirl.create(:user) }
      let(:non_admin) { FactoryGirl.create(:user) }

      before { sign_in non_admin, no_capybara: true }

      describe "submitting a DELETE request to the Users#destroy action" do
        before { delete user_path(user) }
        specify { expect(response).to redirect_to(root_path) }
      end
    end
  end
end

原則に従えば、ここにはまだ小さなセキュリティホールが残っています。管理者がやろうと思えば、DELETEリクエストをコマンドラインで直接発行して自分自身を削除できてしまいます。単なる管理者の自業自得ではないかという考えもあるかと思いますが、こういう事故を未然に防ぐに越したことはありません。この対策については演習の課題に回します (9.6)。

上のテストに対応するアプリケーションコードではbefore_actionを使用していますが、ここでも同じようにdestroyアクションから管理者へのアクセスを制限するのに使用します。変更後のadmin_user before_actionをリスト9.46に示します。

リスト9.46 destroyアクションから管理者へのアクセスを制限するbefore_action。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    def admin_user
      redirect_to(root_path) unless current_user.admin?
    end
end

ここまでくれば、すべてのテストはパスするはずです。そしてUsersリソースとUsersコントローラ、Userモデル、Usersビューも今や完全に動作します。

$ bundle exec rspec spec/

9.5最後に

5.4でUsersコントローラをご紹介して以来、長い道のりをたどってきました。あの頃はユーザー登録すらありませんでしたが、今は登録もサインインもサインアウトもできます。プロファイルの表示も、設定の編集も、すべてのユーザーのインデックスページもあります。一部のユーザーは他のユーザーを削除することすらできるようになりました。

本書の以後の章では、これまでUsersリソース (と関連する認証/認可システム) で学んだ基礎を元に、Twitterのようなマイクロポスト機能をWebサイトに作成します (第10章)。続いて、自分がフォローしているユーザーのステータスフィードを作成します (第11章)。これらの章では、has_manyhas_many throughを使用したデータモデルなど、Railsの最も強力な機能をいくつも紹介します。

次の章に進む前に、すべての変更をmasterブランチにマージしておきましょう。

$ git add .
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users

アプリケーションをデプロイしたり、サンプルデータを本番データとして作成することもできます (本番データベースをリセットするにはpg:resetタスクを使用します)。

$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate

アプリケーションを強制的に再起動するには、Herokuに再度デプロイする必要があります。以下は、Herokuでアプリケーションを強制再起動するためのちょっとしたハックです。

$ touch foo
$ git add foo
$ git commit -m "foo"
$ git push heroku

なお、必要なgemはここまでですべてインストールしたので、今後の章では新たなgemは追加しません。参考までに、最終状態のGemfileリスト9.47に示します。(システム環境に依存する可能性のあるgemはコメントアウトされています。自分の環境で動作するのであれば、それらのgemの行をコメント解除しても構いません。)

リスト9.47 サンプルアプリケーションの最終Gemfile
source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.5'
gem 'bootstrap-sass', '2.3.2.0'
gem 'sprockets', '2.11.0'
gem 'bcrypt-ruby', '3.1.2'
gem 'faker', '1.1.2'
gem 'will_paginate', '3.0.4'
gem 'bootstrap-will_paginate', '0.0.9'

group :development, :test do
  gem 'sqlite3', '1.3.8'
  gem 'rspec-rails', '2.13.1'
  # The following optional lines are part of the advanced setup.
  # gem 'guard', '2.6.1'
  # gem 'guard-rspec', '2.5.0'
  # gem 'spork-rails', '4.0.0'
  # gem 'guard-spork', '1.5.0'
  # gem 'childprocess', '0.3.6'
end

group :test do
  gem 'selenium-webdriver', '2.35.1'
  gem 'capybara', '2.1.0'
  gem 'factory_girl_rails', '4.2.1'
  gem 'cucumber-rails', '1.4.0', :require => false
  gem 'database_cleaner', github: 'bmabey/database_cleaner'

  # Uncomment this line on OS X.
  # gem 'growl', '1.0.3'

  # Uncomment these lines on Linux.
  # gem 'libnotify', '0.8.0'

  # Uncomment these lines on Windows.
  # gem 'rb-notifu', '0.0.4'
  # gem 'win32console', '1.3.2'
   # gem 'wdm', '0.1.0'
end

gem 'sass-rails', '4.0.5'
gem 'uglifier', '2.1.1'
gem 'coffee-rails', '4.0.1'
gem 'jquery-rails', '3.0.4'
gem 'turbolinks', '1.1.1'
gem 'jbuilder', '1.0.2'

group :doc do
  gem 'sdoc', '0.3.20', require: false
end

group :production do
  gem 'pg', '0.15.1'
  gem 'rails_12factor', '0.0.2'
end

9.6演習

  1. Web経由でadmin属性を変更できないことを確認してください。リスト9.48に示したように、PATCHリクエストを updateメソッドに直接発行するテストを作成してください。テストは最初は赤色 (失敗)、次に緑色 (成功) になるようにしてください (ヒント: 最初に、user_paramsの許可リストにadmin追加する必要があります)。
  2. リスト9.3の Gravatarの [change] リンクを改造し、別ウィンドウ (または別タブ) で開くようにしてください。ヒント: Webを検索してみましょう。この目的にうってつけの堅牢なメソッドが見つかるはずです。_blankという文字も一緒に検索してみてください。
  3. 現状の認証テストでは、ユーザーがサインインすると [Profile] や [Settings] などのリンクは表示されることをチェックしています。その逆に、ユーザーがサインインしていないときはこれらのリンクが表示されないことを確認するテストも追加してください。
  4. リスト9.6sign_inテストヘルパーを、なるべく多くの場所で使ってください。
  5. リスト9.49のパーシャルを使用して、new.html.erbビューとedit.html.erbビューをリファクタリングし、コードの重複を取り除いてください。 このとき、リスト9.50のようにフォーム変数fを明示的にローカル変数として渡す必要があることに注意してください。また、それぞれのフォームは完全に同じではないため、テストも更新の必要があります。フォームのわずかな違いを見つけ出し、テストの更新にそれを反映してください。
  6. サインインしたユーザーは、もはやUsersコントローラのnewアクションや createアクションにアクセスする必要はありません。サインインしたユーザーがこれらのアクションをブラウザで開こうとしたら、ルートURLにリダイレクトするようにしてください。
  7. requestオブジェクトについて調べてください。具体的には、Rails API8の一覧に記載されているメソッドのうちいくつかをサイトのレイアウトに挿入することによってこれを調べます (やり方がわからない場合はリスト7.1を参照してください)。
  8. フレンドリーフォワーディングで、最初に与えられたURLにのみ確実に転送されていることを確認するテストを作成してください。続けてサインインを行った後、転送先のURLはデフォルト (ユーザープロファイルページ) に戻る必要もありますので、これもテストで確認してください。リスト9.51がヒントです (と言いつつそこで全部やってしまってますが)。
  9. destroyアクションを改造し、管理者が自分自身を削除できないようにしてください。(最初にテストを作成してからにしてください。)
リスト9.48 admin属性へのアクセスが禁止されていることをテストする。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "edit" do
    .
    .
    .
    describe "forbidden attributes" do
      let(:params) do
        { user: { admin: true, password: user.password,
                  password_confirmation: user.password } }
      end
      before do
        sign_in user, no_capybara: true
        patch user_path(user), params
      end
      specify { expect(user.reload).not_to be_admin }
    end
  end
end
リスト9.49 newフォームとeditフォームのフィールドに使用するパーシャル。
app/views/users/_fields.html.erb
<%= render 'shared/error_messages' %>

<%= f.label :name %>
<%= f.text_field :name %>

<%= f.label :email %>
<%= f.text_field :email %>

<%= f.label :password %>
<%= f.password_field :password %>

<%= f.label :password_confirmation, "Confirm Password" %>
<%= f.password_field :password_confirmation %>
リスト9.50 パーシャルを使用したnewユーザービュー。
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>
      <%= render 'fields', f: f %>
      <%= f.submit "Create my account", class: "btn btn-large btn-primary" %>
    <% end %>
  </div>
</div>
リスト9.51 フレンドリーフォワーディングの後、転送先がデフォルトページに変わることを確認するテスト。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      .
      .
      .
      describe "when attempting to visit a protected page" do
        before do
          visit edit_user_path(user)
          fill_in "Email",    with: user.email
          fill_in "Password", with: user.password
          click_button "Sign in"
        end

        describe "after signing in" do

          it "should render the desired protected page" do
            expect(page).to have_title('Edit user')
          end

          describe "when signing in again" do
            before do
              delete signout_path
              visit signin_path
              fill_in "Email",    with: user.email
              fill_in "Password", with: user.password
              click_button "Sign in"
            end

            it "should render the default (profile) page" do
              expect(page).to have_title(user.name)
            end
          end
        end
      end
    end
    .
    .
    .
  end
end
  1. 画像はhttps://www.flickr.com/photos/sashawolff/4598355045/より引用しました。
  2. Gravatarサイトにアクセスすると、実際にはhttp://ja.gravatar.com/emailsにリダイレクトされます。ここは英語ユーザー向けですが、他の言語を考慮し、enをURLに含めませんでした。
  3. この動作の詳細を気にする必要はありません (悪いことをしているわけでもありません)。この詳細に関心を抱くのはRailsフレームワークそのものの開発者ぐらいであり、Railsでアプリケーションを開発する人にとっては重要ではありません。
  4. この節のコードは、thoughtbotが作成したClearance gemに合わせたものです。
  5. 赤ちゃんの写真はhttps://www.flickr.com/photos/glasgows/338937124/より引用しました。
  6. このuserという名前そのものはまったく重要ではないことに注意してください。たとえばuserをfoobarに置き換え、@users.each do |foobar|と書いてからrender foobarと呼び出しても問題なく動作します。重要なのは、そのオブジェクトそのものではなく、そのオブジェクトが属しているクラス (この場合はUserクラス) の方です。
  7. curlなどのコマンドラインツールを使用すると、PATCHリクエストをこの形式で送信することができます。
  8. http://api.rubyonrails.org/v4.0.0/classes/ActionDispatch/Request.html 
前の章
第9章 ユーザーの更新・表示・削除 Rails 4.0 (第2版)
次の章