Ruby on Rails チュートリアル
-
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
|
||
第2版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
第9章 ユーザーの更新・表示・削除
この章では、Usersリソース用のRESTアクション (表7.1) のうち、これまで未実装だったedit
、update
、index
、destroy
アクションを追加し、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へのリンクについて テストを行なうことにします。
ユーザー編集フォームのテストも、第7章の演習にあるリスト7.31の新規ユーザーフォームのテストと似ています。いずれも、無効な送信を行った時のエラーメッセージをテストします。変更したフォームをリスト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のコードを使えばそのユーザーを指定できるということです。
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) に回します。
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
変数の属性が使用されています。
図9.2のHTMLソースを見てみると、formタグは期待どおりに表示されています (リスト9.4)。
<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.3のform_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))。
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に示したように、サインインページにアクセスして有効な情報を送信します。
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リクエスト (get
、post
、patch
、delete
) を直接使用する場合には必要となります (注意: テスト用のcookies
オブジェクトは実際のcookiesオブジェクトを完全にシミュレートできているわけではありません。特に、リスト8.19のcookies.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に示します。
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) と極めて似通っています。
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/
9.1.3編集の成功
今度は編集フォームが動作するようにしましょう。プロファイル画像の編集は、画像のアップロードをGravatarに任せてあるので、既に動作するようになっています。図9.2の [change] リンクをクリックすれば、図9.4のようにGravatarを編集できます。ではそれ以外の機能の実装にとりかかりましょう。
update
アクションのテストも、create
アクション用のテストとだいたい同じです。Capybaraを使用してフォームのフィールドに有効な情報を入力し、その場合の動作が正しいことを確認するテストをリスト9.9に示します。コードの量がだいぶ増えてきました。第7章のテストを見返して、十分に理解するようにしてください。
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.9のupdate
アクションは、リスト9.10に示したように、create
アクション (リスト8.27) の最終的なフォームとほぼ同じです。 変更すべき点は、以下を
flash[:success] = "Profile updated"
redirect_to @user
リスト9.8のコードに追加するだけで済みます。
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に示します。
9.2.1ユーザーのサインインを要求する
edit
アクションとupdate
アクションのセキュリティ制限はまったく同じなので、これらを共通のRSpec describe
ブロックで扱うことにします。 最初にサインインの要求からテストします。最初のテストでは、サインインしていないユーザーがいずれかのアクションにアクセスしようとしたときには、リスト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のテストではget
、post
、delete
メソッドもサポートされています)。
これらのメソッドのいずれかを使用して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
という形式で呼び出します。
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" ボックスが表示されます。
今度はテストスイートがパスするはずです。
$ bundle exec rspec spec/
9.2.2正しいユーザーを要求する
当然のことですが、サインインを要求するだけでは十分ではありません。ユーザーが自分自身の情報以外の他ユーザーの情報を編集できないようにする必要もあります。これをテストするには、無効なユーザーとしてサインインしてから、edit
アクションと update
アクションにアクセスします (リスト9.13)。このとき、今回は Capybara を使わずにテストしているため (no_capybara: true
)、get
や patch
メソッドを使って edit
や update
アクションに直接アクセスしていることに注意してください。また、ユーザーは他のユーザーのプロファイルを編集しようとすることすらできないようにしないといけません。そのため、そのような不正なアクセスを検出したら、サインインページではなくルートURLにリダイレクトさせなくてはいけません。
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つ追加しています。
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)。
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に示します。
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_location
とredirect_back_or
の2つのメソッドを使用して実現します。これらのメソッドは、セッションヘルパーで定義します (リスト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に追加しておく必要があります。
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の演習とします)。
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に示します5。9.4では、ユーザーのインデックスページに管理用のインターフェイスを追加し、トラブルを起こしているユーザーなどをそこで削除できるようにします。
9.3.1ユーザーインデックス
ユーザーのshow
ページについては、今後も (サインインしているかどうかにかかわらず) サイトを訪れたすべてのユーザーから見えるようにしておきますが、ユーザーindex
ページはサインインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限します。そこで、users_path
(表7.1) にアクセスしたときにindex
アクションが保護され、サインインページにリダイレクトされることをテストすることから始めます。他の認可テストと同様、この例はリスト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
を追加するだけです。
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人のユーザーはそれぞれ異なる名前にしておき、ユーザーのリストの要素がそれぞれ一意になるように (重複しないように) していることに注意してください。
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で修正します。)
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ヘルパーファイルを更新してから先に進んでください。
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)。
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のアプリケーションのコードは、いずれも素直な作りです。
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
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人のままではいささか寂しすぎます。もう少し何とかしてみましょう。
9.3.2サンプルのユーザー
この節では、一人ぼっちのユーザーに仲間を加えてあげることにします。複数のユーザーが表示されたユーザーインデックスページにするためには、ブラウザでサインアップページを表示してユーザーを手作業で1人ずつ追加するという方法もありますが、せっかくなのでRubyとRakeを使用してユーザーを一気に作成しましょう。
まず、Gemfile
にFaker gemを追加します (リスト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
) を使用します (ここは若干高度な内容ですが、今は詳細を理解する必要はありません)。
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画像以外の写真を関連付けてみました)。
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に示します。
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'
.
.
.
次に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に示します。
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.22のUser.all
配列がUser.paginate(page: 1)
に置き換えられていることに注目してください。この後お見せしますが、これはデータベースから最初の1ページを取り出す方法です。また、リスト9.32では、before(:all)
との違いを強調するためにbefore(:each)
も使用しています。
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つありますが、その理由はこの後で説明します。
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
アクション内のall
をpaginate
メソッドに置き換えます (リスト9.34)。ここで:page
パラメーターにはparams[:page]
が使用されていますが、これはwill_paginate
によって自動的に生成されます。
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
をユーザーリストの上と下の両方に配置してあるので、ページネーションのリンクもページの上と下の両方に表示されています。
[2] リンクまたは [Next] リンクをクリックすると、図9.11のように次のページに移動します。
テストもパスするはずです。
$ bundle exec rspec spec/
9.3.4パーシャルのリファクタリング
ユーザーインデックスへのページネーション実装はついに完了しました。でも私は、ここでぜひともある1つの改良を加えてみたいのです。実はRailsにはコンパクトなビューを作成するための素晴らしいツールがいくつもあります。この節ではそれらのツールを使用してインデックスページのリファクタリング (動作を変えずにコードを整理すること) を行うことにします。サンプルアプリケーションのテストは既に完了しているので、Webサイトの機能を損なうことなく安心してリファクタリングに取りかかれます。
リファクタリングの第一歩は、リスト9.33のユーザーのli
をrender
呼び出しに置き換えることです (リスト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)。
app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 52 %>
<%= link_to user.name, user %>
</li>
これは間違いなく大きな進歩です。しかしここで終わらせず、さらに改良してみましょう。今度はrender
を@users
変数に対して直接実行します (リスト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は@users
をUser
オブジェクトのリストであると推測します。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erb
パーシャルで出力します (訳注: each doとendで囲む必要がなくなります)。これにより、リスト9.37のコードは極めてコンパクトになります。これに限らず、リファクタリングを行う場合には、アプリケーションのコードを変更する前と後で必ずテストを実行し、いずれも緑色 (成功) になることを確認するようにしてください。
$ bundle exec rspec spec/
9.4ユーザーを削除する
ユーザーインデックスはとうとう完了しました。残るはdestroy
だけです。これを実装することで、RESTに準拠した正統なアプリケーションとなります。この節では、ユーザーを削除するためのリンクを追加します。モックアップを図9.12に示します。また、削除を行うのに必要なdestroy
アクションも実装します。しかしその前に、削除を実行できる権限を持つ管理ユーザーのクラスを作成しましょう。
9.4.1管理ユーザー
特権を持つ管理ユーザーを識別するために、論理値をとるadmin
属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?
メソッド (論理値を返す) も使えるようになりますので、これを使用して管理ユーザーの状態をテストできます。この属性に対するテストはリスト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のデータモデルが生成されます。
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と開発者に明確に示すことができます)。
最後に、開発用データベースにマイグレーションを行い、テスト用データベースを準備します。
$ 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)。
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
ハッシュに対してrequire
とpermit
を呼び出します。
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
ブロックを追加します。
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に示します。
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に示します。
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を使用しないで削除する(英語)」を参照してください。
この削除リンクが動作するためには、destroy
アクション (表7.1) を追加する必要があります。このアクションでは、該当するユーザーを見つけてActive Recordのdestroy
メソッドを使用して削除し、最後にユーザーインデックスに移動します (リスト9.44)。なお、このとき、:destroy
も signed_in_user
の before_action に追加しています。
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.11のpatch
メソッドのときと同様、delete
メソッドを使用してDELETEリクエストを指定のURL (ここでは表7.1で要求されているユーザーパス) に直接発行していることに注目してください。
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に示します。
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_many
やhas_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の行をコメント解除しても構いません。)
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演習
- Web経由で
admin
属性を変更できないことを確認してください。リスト9.48に示したように、PATCHリクエストをupdate
メソッドに直接発行するテストを作成してください。テストは最初は赤色 (失敗)、次に緑色 (成功) になるようにしてください (ヒント: 最初に、user_params
の許可リストにadmin
を追加する必要があります)。 - リスト9.3の Gravatarの [change] リンクを改造し、別ウィンドウ (または別タブ) で開くようにしてください。ヒント: Webを検索してみましょう。この目的にうってつけの堅牢なメソッドが見つかるはずです。
_blank
という文字も一緒に検索してみてください。 - 現状の認証テストでは、ユーザーがサインインすると [Profile] や [Settings] などのリンクは表示されることをチェックしています。その逆に、ユーザーがサインインしていないときはこれらのリンクが表示されないことを確認するテストも追加してください。
- リスト9.6の
sign_in
テストヘルパーを、なるべく多くの場所で使ってください。 - リスト9.49のパーシャルを使用して、
new.html.erb
ビューとedit.html.erb
ビューをリファクタリングし、コードの重複を取り除いてください。 このとき、リスト9.50のようにフォーム変数f
を明示的にローカル変数として渡す必要があることに注意してください。また、それぞれのフォームは完全に同じではないため、テストも更新の必要があります。フォームのわずかな違いを見つけ出し、テストの更新にそれを反映してください。 - サインインしたユーザーは、もはやUsersコントローラの
new
アクションやcreate
アクションにアクセスする必要はありません。サインインしたユーザーがこれらのアクションをブラウザで開こうとしたら、ルートURLにリダイレクトするようにしてください。 request
オブジェクトについて調べてください。具体的には、Rails API8の一覧に記載されているメソッドのうちいくつかをサイトのレイアウトに挿入することによってこれを調べます (やり方がわからない場合はリスト7.1を参照してください)。- フレンドリーフォワーディングで、最初に与えられたURLにのみ確実に転送されていることを確認するテストを作成してください。続けてサインインを行った後、転送先のURLはデフォルト (ユーザープロファイルページ) に戻る必要もありますので、これもテストで確認してください。リスト9.51がヒントです (と言いつつそこで全部やってしまってますが)。
destroy
アクションを改造し、管理者が自分自身を削除できないようにしてください。(最初にテストを作成してからにしてください。)
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
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 %>
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>
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
- 画像はhttps://www.flickr.com/photos/sashawolff/4598355045/より引用しました。↑
- Gravatarサイトにアクセスすると、実際にはhttp://ja.gravatar.com/emailsにリダイレクトされます。ここは英語ユーザー向けですが、他の言語を考慮し、enをURLに含めませんでした。↑
- この動作の詳細を気にする必要はありません (悪いことをしているわけでもありません)。この詳細に関心を抱くのはRailsフレームワークそのものの開発者ぐらいであり、Railsでアプリケーションを開発する人にとっては重要ではありません。↑
- この節のコードは、thoughtbotが作成したClearance gemに合わせたものです。↑
- 赤ちゃんの写真はhttps://www.flickr.com/photos/glasgows/338937124/より引用しました。↑
- この
user
という名前そのものはまったく重要ではないことに注意してください。たとえばuserをfoobarに置き換え、@users.each do |foobar|
と書いてからrender foobar
と呼び出しても問題なく動作します。重要なのは、そのオブジェクトそのものではなく、そのオブジェクトが属しているクラス (この場合はUser
クラス) の方です。↑ - curlなどのコマンドラインツールを使用すると、PATCHリクエストをこの形式で送信することができます。↑
- http://api.rubyonrails.org/v4.0.0/classes/ActionDispatch/Request.html ↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!