Ruby on Rails チュートリアル
-
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
|
||
第3版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
第9章 ユーザーの更新・表示・削除
この章では、Usersリソース用のRESTアクション (表7.1) のうち、これまで未実装だったedit
、update
、index
、destroy
アクションを追加し、RESTアクションを完成させます。まずはユーザーが自分のプロファイルを自分で更新できるようにします。ここで早速第8章で実装した認証用のコードを使いますが、これは認可モデルについて説明する自然なキッカケになります。次に、すべてのユーザーを一覧できるようにします (もちろん認証を要求します)。これはサンプルデータとページネーション (pagination) を導入する動機にもなります。最後に、ユーザーを削除し、データベースから完全に消去する機能を追加します。ユーザーの削除はどのユーザーにも許可できるものではないので、管理ユーザーという特権クラスを作成し、このユーザーにのみ削除を許可するようにします。
9.1 ユーザーを更新する
ユーザー情報を編集するパターンは、(第7章)の新規ユーザーの作成と極めて似通っています。新規ユーザー用のビューを出力するnew
アクションの代わりに、ユーザーを編集するためのedit
アクションを作成すればよいのです。 POSTリクエストに応答するcreate
の代わりに、PATCHリクエストに応答するupdate
アクションを作成すればよいのです (コラム3.3)。最大の違いは、ユーザー登録は誰でも実行できますが、ユーザー情報を更新できるのはそのユーザー自身に限られるということです。第8章の認証 (authentication) システムを使えば、before_actionを使用してこれを行えます。
では最初に、いつものようにupdating-users
トピックブランチを作成しましょう。
$ git checkout master
$ git checkout -b updating-users
9.1.1 編集フォーム
まずは編集フォームから始めます。モックアップは図9.1のとおりです1。
図9.1のモックアップを動くページにするためには、Usersコントローラにedit
アクションを追加して、それに対応するeditビューを実装する必要があります。edit
アクションの実装から始めますが、ここではデータベースから適切なユーザーデータを読み込む必要があります。ここで注意して頂きたいのは、表7.1ではユーザー編集ページの正しいURLが/users/1/editとなっていることです (ユーザーのidが1の場合)。ユーザーのidはparams[:id]
変数で取り出すことができるのを思い出してください。つまり、リスト9.1のコードを使えばそのユーザーを指定できるということです。
edit
アクション app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
def edit
@user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
ユーザー編集ページに対応するビュー を、リスト9.2に示します (このファイルは手動で作成する必要があります)。このコードがリスト7.13と極めて似通っていることに注目してください。重複が多いということは、それらのコードの繰り返しをパーシャルにまとめることができるということです。パーシャルにまとめる作業は演習の課題 (9.6) に回します。
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
上のコードでは、7.3.3で導入したerror_messages
パーシャルを再利用しています。ところで、Gravatarへのリンクでtarget="_blank"
が使われていますが、これを使うとリンク先を新しいタブ (またはウィンドウ) で開くようになるので、別のWebサイトへリンクするときなどに便利です。
リスト9.1の@user
インスタンス変数使うと、編集ページがうまく描画されるようになります (図9.2)。図9.2の"Name"や"Email"の部分を見ると、Railsによって名前やメールアドレスのフィールドに値が自動的に入力されていることがわかります。これらの値は、@user
変数の属性情報から引き出されています。
図9.2のHTMLソースを見てみると、少しだけ違う箇所もありますが、おおよそformタグは期待どおりに表示されています (リスト9.3)。。
<form accept-charset="UTF-8" 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リクエストを「偽造」しています2。
ここでもう1つ微妙な点を指摘しておきたいと思います。リスト9.2のform_for(@user)
のコードは、リスト7.13のコードと完全に同じです。だとすると、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を使用します。
仕上げに、ナビゲーションバーにあるユーザー設定へのリンクを更新します。表7.1で示したedit_user_path
という名前付きルートと、 リスト8.36で定義したcurrent_user
というヘルパーメソッドを使うと、実装が簡単です。
<%= link_to "Settings", edit_user_path(current_user) %>
完全なアプリケーションコードをリスト9.4に示します。
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li 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 "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
9.1.2 編集の失敗
本項では、7.3のユーザー登録に失敗したときと似た方法で、編集に失敗した場合について扱っていきます。まずはupdate
アクションの作成から進めますが、これはリスト9.5にあるように、update_attributes
(6.1.5) を使って送信されたparams
ハッシュに基いてユーザーを更新します。無効な情報が送信された場合、更新の結果としてfalse
が返され、else
に分岐して編集ページをレンダリングします。このパターンは以前にも出現したことを覚えているでしょうか。この構造はcreate
アクションの最初のバージョン (リスト7.16) と極めて似通っています。
update
アクションの初期実装 app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
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
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
update_attributes
への呼び出しでuser_params
を使用していることに注目してください。7.3.2でも説明したように、ここではStrong Parametersを使用してマスアサインメントの脆弱性を防止しています。
Userモデルのバリデーションとエラーメッセージのパーシャルが既にあるので (リスト9.2)、無効な情報を送信すると役立つエラーメッセージが表示されるようになっています (図9.3)。
9.1.3 編集失敗時のテスト
9.1.2では編集フォームの失敗時を実装しました。次に、コラム3.3で説明したテストのガイドラインに従って、エラーを検知するための統合テストを書いていきましょう。まずはいつものように、統合テストを生成するところから始めます。
$ rails generate integration_test users_edit
invoke test_unit
create test/integration/users_edit_test.rb
最初は編集失敗時の簡単なテストを追加します (リスト9.6)。リスト9.6のテストでは、まず編集ページにアクセスし、editビューが描画されるかどうかをチェックしています。その後、無効な情報を送信してみて、editビューが再描画されるかどうかをチェックします。ここで、PATCHリクエストを送るためにpatch
メソッドを使っていることに注目してください。これはget
やpost
、delete
メソッドと同じように、HTTPリクエストを送信するためのメソッドです。
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
patch user_path(@user), user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" }
assert_template 'users/edit'
end
end
この時点では、テストは成功しているはずです。
$ bundle exec rake test
9.1.4 TDDで編集を成功させる
今度は編集フォームが動作するようにしましょう。プロファイル画像の編集は、画像のアップロードをGravatarに任せてあるので、既に動作するようになっています。図9.2の [change] リンクをクリックすれば、図9.4のようにGravatarを編集できます。ではそれ以外の機能の実装にとりかかりましょう。
そろそろ、より快適にテストをするためには、アプリケーション用のコードを「実装する前に」統合テストを書いた方が便利だと気付いた読者もいるかもしれません。実際、そういったテストのことは「受け入れテスト (Acceptance Tests)」として呼ばれていて、ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決めるテストとして知られています。実際に体験してもらうために、今回はテスト駆動開発を使ってユーザーの編集機能を実装してみましょう。
まずは、リスト9.6のテストを参考にして、ユーザー情報を更新する正しい振る舞いをテストで定義します (今回は有効な情報を送信するように修正します)。次に、flashメッセージが空でないかどうかと、プロフィールページにリダイレクトされるかどうかをチェックします。また、データベース内のユーザー情報が正しく変更されたかどうかも検証します。変更の結果をリスト9.8に示します。このとき、リスト9.8のパスワードとパスワード確認が空であることに注目してください。ユーザー名やメールアドレスを編集するときに毎回パスワードを入力するのは不便なので、(パスワードを変更する必要が無いときは) パスワードを入力せずに更新できると便利です。また、6.1.5で紹介した@user.reload
を使って、データベースから最新のユーザー情報を読み込み直して、正しく更新されたかどうかを確認している点にも注目してください。(こういった正しい振る舞いというのは一般に忘れがちですが、受け入れテスト (もしくは一般的なテスト駆動開発) では先にテストを書くので、効果的なユーザー体験について考えるようになります。)
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), user: { name: name,
email: email,
password: "",
password_confirmation: "" }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
テストにパスする必要のある、リスト9.8のupdate
アクションは、リスト9.9に示したように、create
アクション (リスト8.22) の最終的なフォームとほぼ同じです。
update
アクション RED 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.9のキャプションにも書いておきましたが、このテストはまだREDのままのはずです。というのも、パスワードの長さに対するバリデーション (リスト6.39) があるので、パスワードやパスワード確認の欄を空にしておくとこれに引っかかってしまうからです (リスト9.8)。テストがGREENになるためには、パスワードのバリデーションに対して、空だったときの例外処理を加える必要があります。こういったときに便利なallow_nil: true
というオプションがあるので、これを validates
に追加します (リスト9.10)。
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 }
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
.
.
.
end
リスト9.10によって、新規ユーザー登録時に空のパスワードが有効になってしまうのかと心配になるかもしれませんが、安心してください。6.3.3で説明したように、has_secure_password
では (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワードが新規ユーザー登録時に有効になることはありません。(それだけでなく、実は空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordのバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグがあったのですが、この問題も解決できてしまいます。)
このコードを追加したことにより、ユーザー編集ページが動くようになります (図9.5)。すべてのテストを走らせてみて、成功したかどうか確かめてみてください。
$ bundle exec rake test
9.2 認可
ウェブアプリケーションの文脈では、認証 (authentication) はサイトのユーザーを識別することであり、認可 (authorization) はそのユーザーが実行可能な操作を管理することです。第8章で認証システムを構築したことで、認可のためのシステムを実装する準備もできました。
9.1のeditアクションとupdateアクションはすでに完全に動作していますが、セキュリティ上の大穴が1つ空いています。 どのユーザーでもあらゆるアクションにアクセスでき、ログインさえしていれば他のユーザーの情報を編集できてしまいます。この節では、ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないようにするセキュリティモデルを構築しましょう。
9.2.1では、ログインしていないユーザーが保護されたページにアクセスしようとした際のケースについて対処していきます。こういったケースはアプリケーションを使っていると普通に起こることなので、ログインページに転送して、そのときに分かりやすいメッセージも表示するようにしましょう。モックアップを図9.6に示します。一方で、許可されていないページに対してアクセスするログイン済みのユーザーがいたら (たとえば他人のユーザー編集ページにアクセスしようとしたら)、ルートURLにリダイレクトさせるようにします (9.2.2)。
9.2.1 ユーザーにログインを要求する
図9.6のように転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使います。beforeフィルターは、before_action
メソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みです3。今回はユーザーにログインを要求するために、リスト9.12のようにlogged_in_user
メソッドを定義してbefore_action :logged_in_user
という形式で使います
logged_in_user
を追加する RED app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeフィルター
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:only
オプションハッシュを渡すことによって:edit
と:update
アクションにのみこのフィルタが適用されるように制限をかけています。
beforeフィルターを使って実装した結果 (リスト9.12) は、一度ログアウトしてユーザー編集ページ (/users/1/edit) にアクセスしてみることで確認できます (図9.7)。
リスト9.12のキャプションに記したように、今の段階ではテストは失敗します。
$ bundle exec rake test
原因は、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったためです。
このため、editアクションやupdateアクションをテストする前にログインしておく必要があります。解決策は簡単で、 8.4.6で開発したlog_in_as
ヘルパー (リスト8.50) を使うことです。修正した結果をリスト9.14に示します。
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
end
(リスト9.14のsetup
メソッド内でログイン処理をまとめてしまうことも可能です。しかし、9.2.3で片方のテストをログインする前に編集ページにアクセスするように変更したいので、ここでまとめてしまっても結局は元に戻すことになってしまいます。)
今度はテストスイートがパスするはずです。
$ bundle exec rake test
これでテストスイートがパスするようになりましたが、実はbeforeフィルターの実装はまだ終わっておりません。セキュリティモデルに関する実装を取り外してもテストがGREENになってしまうかどうか、実際にコメントアウトして確かめてみましょう (リスト9.16)。なんと悪いことに、すべてのテストが成功してしまいました。beforeフィルターをコメントアウトして巨大なセキュリティーホールが作られたら、テストスイートでそれを検出できるべきです。つまり、リスト9.16のコードはREDにならなければいけないのです。テストを書いて、この問題に対処しましょう。
class UsersController < ApplicationController
# before_action :logged_in_user, only: [:edit, :update]
.
.
.
end
beforeフィルターは基本的にアクションごとに適用していくので、Usersコントローラのテストもアクションごとに書いていきます。具体的には、正しい種類のHTTPリクエストを使ってedit
アクションとupdate
アクションをそれぞれ実行させてみて、flashにメッセージが代入されたかどうか、ログイン画面にリダイレクトされたかどうかを確認してみましょう。表7.1から、適切なリクエストはそれぞれGETとPATCHであることがわかります。したがって、テスト内ではget
メソッドとpatch
メソッドを使います。修正した結果をリスト9.17に示します。
edit
とupdate
アクションの保護に対するテストする RED test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
end
test "should get new" do
get :new
assert_response :success
end
test "should redirect edit when not logged in" do
get :edit, id: @user
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_not flash.empty?
assert_redirected_to login_url
end
end
ここで、get
もpatch
も次のように
get :edit, id: @user
といった形で引数が渡されていることに注目してください。
patch :update, id: @user, user: { name: @user.name, email: @user.email }
上のコードでは、Railsの慣習によってid: @user
という引数が自動的に@user.id
に変換されています (これはコントローラでリダイレクトしたときと同様です)。2つ目のケースでは、ルーティングで正しく処理されるようにuser
というハッシュも渡しています。(実は第2章のToyアプリケーションのUsersコントローラではテストも生成されていて、中を見ると上と同じコードになっています。)
この時点では、(beforeフィルターが無効のままなので) テストスイートはREDになるはずです。beforeフィルターのコメントアウトを元に戻して、GREENになるかどうか確かめてみましょう (リスト9.18)。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.
.
end
コメントアウトしていた箇所を元に戻すと、テストが成功するようになるはずです。
$ bundle exec rake test
これらのテストを実装したことによって、うっかり誰でも編集できてしまうバグがあっても、すぐに検知できるようになりました。
9.2.2 正しいユーザーを要求する
当然のことですが、ログインを要求するだけでは十分ではありません。ユーザーが自分の情報だけを編集できるようにする必要があります。9.2.1では、深刻なセキュリティ上の欠陥を見逃してしまうテストを見てきました。そこで本項では、セキュリティモデルが正しく実装されている確信を持つために、テスト駆動開発で進めていきます。したがって、Usersコントローラのテスト (リスト9.17) を補完するように、テストを追加するところから始めていきます。
まずはユーザーの情報が互いに編集できないことを確認するために、サンプルユーザーをもう一人追加します。ユーザー用のfixtureファイルに2人目のユーザーを追加してみましょう (リスト9.20)。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
次に、 リスト8.50で定義したlog_in_as
メソッドを使って、edit
アクションとupdate
アクションをテストします (リスト9.21)。このとき、既にログイン済みのユーザーを対象としているため、ログインページではなくルートURLにリダイレクトしている点に注意してください。
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
test "should get new" do
get :new
assert_response :success
end
test "should redirect edit when not logged in" do
get :edit, id: @user
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user)
get :edit, id: @user
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert flash.empty?
assert_redirected_to root_url
end
end
別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_user
というメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにします (リスト9.22)。beforeフィルターのcorrect_user
で@user
変数を定義しているため、リスト9.22ではedit
とupdate
の各アクションから、@user
への代入文を削除している点にも注意してください。
correct_user
) を使って編集と更新を保護する GREEN app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_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フィルター
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless @user == current_user
end
end
今度はテストスイートが成功するはずです。
$ bundle exec rake test
最後に、リファクタリングではありますが、一般的な慣習に倣ってcurrent_user?
という論理値を返すメソッドを実装します。correct_user
の中で使えるようにしたいので、Sessionsヘルパーの中にこのメソッドを追加します (リスト9.24)。このメソッドを使うと今までの
unless @user == current_user
といった部分が、次のように (少し) 分かりやすいコードになります。
unless current_user?(@user)
current_user?
メソッド app/helpers/sessions_helper.rb
module SessionsHelper
# 与えられたユーザーをログイン
def log_in(user)
session[:user_id] = user.id
end
# 永続セッションとしてユーザーを記憶する
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# 与えられたユーザーがログイン済みユーザーであればtrueを返す
def current_user?(user)
user == current_user
end
.
.
.
end
先ほどのメソッドを使って比較演算していた行を置き換えると、リスト9.25になります。
correct_user
の実装 GREEN app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_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フィルター
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
9.2.3 フレンドリーフォワーディング
ここまででWebサイトの認可機能は完成したかのように見えますが、後1つ小さなキズがあります。保護されたページにアクセスしようとすると、問答無用で自分のプロファイルページに移動させられてしまいます。別の言い方をすれば、ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作です。リダイレクト先は、ユーザーが開こうとしていたページにしてあげるのが親切というものです。
実際のコードは少し複雑ですが、フレンドリーフォワーディングのテストは非常にシンプルに書くことができます。ログインした後に編集ページへのアクセスする、という順序を逆にしてあげるだけです (リスト9.14)。リスト9.26が示すように、実際のテストはまず編集ページにアクセスし、ログインした後に、(デフォルトのプロフィールページではなく) 編集ページにリダイレクトされているかどうかをチェックするといったテストです。(なお、リダイレクトによってedit用のテンプレートが描画されなくなったので、リスト9.26では該当するテストを削除しています)
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_path(@user)
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), user: { name: name,
email: email,
password: "",
password_confirmation: "" }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
失敗するテストが書けたので、ようやくフレンドリーフォワーディングを実装する準備ができました4。ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があります。この動作をstore_location
とredirect_back_or
の2つのメソッドを使用して実現してみましょう。なお、これらのメソッドはSessionsヘルパーで定義しています (リスト9.27)。
module SessionsHelper
.
.
.
# 記憶したURL (もしくはデフォルト値) にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.url if request.get?
end
end
転送先のURLを保存する仕組みは、8.2.1でユーザーをログインさせたときと同じで、session
変数を使います。また、リクエスト先のURLを取得するために、リスト9.27ではrequest
オブジェクトも使っています (request.url
でリクエスト先が取得できます)。
リスト9.27のstore_location
メソッドでは、 リクエストが送られたURLをsession
変数の:forwarding_url
キーに格納しています。ただし、GET
リクエストが送られたときだけ格納するようにしておきます。これによって、たとえばログインしていないユーザーがフォームを使って送信した場合、転送先のURLを保存させないようにできます。これは稀なケースですが起こり得ます。たとえばユーザがセッション用のcookieを手動で削除してフォームから送信するケースなどです。こういったケースに対処しておかないと、POST
や PATCH
、DELETE
リクエストを期待しているURLに対して (リダイレクトを通して) GET
リクエストが送られてしまい、場合によってはエラーが発生します。このため、if request.get?
という条件文を使ってこのケースの対策しています5。
先ほど定義したstore_location
メソッドを使って、早速beforeフィルターのlogged_in_user
を修正してみます (リスト9.28)。
store_location
を追加する app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_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フィルター
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
フォワーディング自体を実装するには、redirect_back_or
メソッドを使用します。リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトします。デフォルトのURLは、Sessionコントローラのcreate
アクションに追加し、サインイン成功後にリダイレクトします (リスト9.29)。redirect_back_or
メソッドでは、次のようにor演算子||
を使用します。
session[:forwarding_url] || default
このコードは、値がnil
でなければsession[:forwarding_url]
を評価し、nilであれば与えられたデフォルトのURLを使用しますリスト9.27では、session.delete(:forwarding_url) という式を通して転送用のURLを削除している点に注意してください。これをやっておかないと、次回ログインしたときに保護されたページに転送されてしまい、ブラウザを閉じるまでこれが繰り返されてしまいます (このコードのテストは9.6の演習とします)。ちなみに、最初にredirect文を実行しても、セッションが削除される点を覚えておくとよいでしょう。実は、明示的にreturn
文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しません。したがって、redirect文の後にあるコードでも、そのコードは実行されるのです。
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])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
これで、リスト9.26のフレンドリーフォワーディング用統合テストはパスするはずです。成功すれば、基本ユーザー認証機能とページ保護機能の実装は完了です。いつものように、以下を実行してテストスイートが 緑色 (成功) になることを確認してから先に進みましょう。
$ bundle exec rake test
9.3 すべてのユーザーを表示する
この節では、いよいよ最後から2番目のユーザーアクションであるindex
アクションを追加しましょう。このアクションは、すべてのユーザーを一覧表示します。その際、データベースにサンプルデータを追加する方法や、将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネート (paginate=ページ分割) の方法を学びます。ユーザーの一覧、ページネーション用リンク、移動用の [Users] リンクのモックアップを図9.8に示します6。
9.3.1 ユーザーインデックス
ユーザーの一覧ページを実装するために、まずはセキュリティモデルについて考えてみましょう。ユーザーのshow
ページについては、今後も (ログインしているかどうかに関わらず) サイトを訪れたすべてのユーザーから見えるようにしておきますが、ユーザーのindex
ページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限します7。
index
ページを不正なアクセスから守るために、まずはindex
アクションが正しくリダイレクトするか検証するテストを書いてみます (リスト9.31)。
index
アクションのリダイレクトをテストする RED test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
test "should redirect index when not logged in" do
get :index
assert_redirected_to login_url
end
.
.
.
end
次に、beforeフィルターのlogged_in_user
にindex
アクションを追加して、このアクションを保護します (リスト9.32)。
index
アクションにはログインを要求する GREEN app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_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
今度はすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装します。Toyアプリケーションにも同じindexアクションがあったことを思い出してください (リスト2.5)。そのときと同様に、User.all
を使ってデータベース上の全ユーザーを取得し、ビューで使用可能な@users
というインスタンス変数に代入させます (リスト9.33)。(すべてのユーザーを一気に読み出すとデータ量が多い場合に問題が生じるのではないかと思われた方、そのとおりです。このキズは9.3.3で修正します。)
index
アクション app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.all
end
.
.
.
end
実際のインデックスページを作成するには、ユーザーを列挙してユーザーごとにli
タグで囲むビューを作成する必要があります。ここではeach
メソッドを使用してこれを行います。それぞれの行をリストタグul
で囲いながら、各ユーザーのGravatarと名前を表示します (リスト9.34)。
訳注: 7.7の1つ目の演習 (リスト7.31) でgravatarメソッドを拡張していない場合、以下のリストがうまく動きません。まだの方は当該リストのコードを先に反映させておいてください。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
リスト9.34では、7.7の演習のリスト7.31の結果を利用しています。これは、Gravatarヘルパーにデフォルト以外のサイズを指定するオプションを渡します。この演習をまだやっていない場合は、リスト7.31に従ってUsersヘルパーファイルを更新してから先に進んでください。
CSS (正確にはSCSSですが) にもちょっぴり手を加えておきましょう (リスト9.35)。
.
.
.
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
最後に、サイト内移動用のヘッダーにユーザー一覧表示用のリンクを追加します。これにはusers_path
を使用し、表7.1に残っている最後の名前付きルートを割り当てます。変更の結果をリスト9.36に示します。
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li 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 "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
これでユーザーのインデックスは完全に動くようになり、テストも全てパスするようになります。
$ bundle exec rake test
ところで、図9.9のようにユーザーが1人のままではいささか寂しすぎます。もう少し何とかしてみましょう。
9.3.2 サンプルのユーザー
この節では、一人ぼっちのユーザーに仲間を加えてあげることにします。複数のユーザーが表示されたユーザーインデックスページにするためには、ブラウザでサインアップページを表示してユーザーを手作業で1人ずつ追加するという方法もありますが、せっかくなのでRubyとRakeを使用してユーザーを一気に作成しましょう。
まず、GemfileにFaker
gemを追加します (リスト9.38)。これは、実際にありそうなユーザー名とメールアドレスを持つサンプルユーザーを自動的に作成するものです。
Gemfile
にFakerを追加する
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
.
.
.
次に、いつものように以下を実行します。
$ bundle install
では、サンプルユーザーを生成するRakeタスクを追加してみましょう。Railsではdb/seeds.rb
というファイルを標準として使います。結果をリスト9.39に示します。(リスト9.39のコードは少し応用的です。詳細が完全に理解できなくても問題ありません)
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
リスト9.39のコードでは、Example Userという名前とメールアドレスを持つ1人のユーザと、それらしい名前とメールアドレスを持つ99人のユーザーを作成します。create!
は基本的にcreate
メソッドと同じものですが、ユーザーが無効な場合にfalse
を返すのではなく例外を発生させる (6.1.4) 点が異なります。こうしておくと見過ごしやすいエラーを回避できるので、デバッグが容易になります。
それでは、データベースをリセットして、リスト9.39のRakeタスクを実行 (db:seed
) してみましょう8。
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
データベース上にデータを追加するのは遅くなりがちで、システムによっては数分かかることもあり得ます。
db:seed
でRakeタスクを実行し終わると、サンプルアプリケーションのユーザーが100人になっています。図9.10が示すように、 最初のいくつかのメールアドレスについては、デフォルトのGravatar画像以外の写真を関連付けてみました。(システム環境によっては、ここでRailsを再起動させる必要があるかもしれません。)
9.3.3 ページネーション
これで、最初のユーザーにも仲間ができました。しかし今度は逆に、1つのページに大量のユーザーが表示されてしまっています。100人でもかなり大きい数であると思いますし、今後は数千ユーザーに増える可能性もあります。これを解決するのがページネーション (pagination) というもので、この場合は、たとえば1つのページに一度に30人だけユーザーを表示するというものです。
Railsには豊富なページネーションメソッドがあります。今回はその中で最もシンプルかつ堅牢なwill_paginateメソッドを使用してみましょう。これを使用するには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方インクルードし、Bootstrapのページネーションスタイルを使用してwill_paginateを構成します。更新したGemfile
をリスト9.40に示します。
Gemfile
にwill_paginateを追加する
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'will_paginate', '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
.
.
.
次にbundle install
を実行します。
$ bundle install
新しいgemが正しく読み込まれるように、Webサーバーを再起動してください。
ページネーションが動作するには、ユーザーのページネーションを行うようにRailsに指示するコードをindexビューに追加する必要があります。また、index
アクションにあるUser.all
を、ページネーションを理解できるオブジェクトに置き換える必要もあります。まずは、ビューに特殊なwill_paginate
メソッドを追加しましょう (リスト9.41)。同じコードがリストの上と下に2つありますが、その理由はこの後で説明します。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
このwill_paginate
メソッドは少々不思議なことに、users
ビューのコードの中から@users
オブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成しています。ただし、リスト9.41のビューはこのままでは動きません。というのも、現在の@users
変数にはUser.all
の結果が含まれていますが (リスト9.33)、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のユーザーといった具合にデータが取り出されます。ちなみにpage
がnil
の場合、 paginate
は単に最初のページを返します。
paginate
を使用することで、サンプルアプリケーションのユーザーのページネーションを行えるようになります。具体的には、index
アクション内のall
をpaginate
メソッドに置き換えます (リスト9.42)。ここで:page
パラメーターにはparams[:page]
が使用されていますが、これはwill_paginate
によって自動的に生成されます。
index
アクションでUsersをページネートする app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.paginate(page: params[:page])
end
.
.
.
end
以上で、ユーザー一覧ページは図9.11のように動作するはずです (システム環境によっては、ここでRailsを再起動する必要があるかもしれません)。will_paginate
をユーザーリストの上と下の両方に配置してあるので、ページネーションのリンクもページの上と下の両方に表示されています。
[2] リンクまたは [Next] リンクをクリックすると、図9.12のように次のページに移動します。
9.3.4 ユーザーインデックスのテスト
これでユーザー一覧ページが動くようになったので、9.3.3のページネーションに対する簡単なテストも書いておきましょう。今回のテストでは、ログイン、indexページにアクセス、最初のページにユーザーがいることを確認、ページネーションのリンクがあることを確認、といった順でテストしていきます。最後の2つのステップでは、テスト用のデータベースに31人以上のユーザーがいる必要があります。
リスト9.20で2人目のユーザーをfixtureに追加しましたが、今回はもっと多くのユーザーを作成する必要があります。手動で追加するのは面倒そうですね。幸運には、ユーザー用fixtureファイルのpassword_digest
属性で使ったように、fixtureでは埋め込みRubyをサポートしています。これを利用してさらに30人のユーザーを追加してみましょう (リスト9.43)。なお、今後必要になるので、リスト9.43では2人の名前付きユーザーも一緒に追加しています。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
リスト9.43のfixtureファイルができたので、indexページに対するテストを書いてみます。まずは、いつものように統合テストを生成します。
$ rails generate integration_test users_index
invoke test_unit
create test/integration/users_index_test.rb
今回のテストでは、pagination
クラスを持ったdiv
タグをチェックして、最初のページにユーザーがいることを確認します。結果はリスト9.44のようになります。
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end
このテストは成功するはずです。
$ bundle exec rake test
9.3.5 パーシャルのリファクタリング
ユーザー一覧ページにページネーションを実装することができましたが、私はここで1つの改良を加えてみたいのです。実はRailsにはコンパクトなビューを作成するための素晴らしいツールがいくつもあります。この節ではそれらのツールを使用して一覧ページのリファクタリング (動作を変えずにコードを整理すること) を行うことにします。サンプルアプリケーションのテストは既に完了しているので、Webサイトの機能を損なうことなく安心してリファクタリングに取りかかれます。
リファクタリングの第一歩は、リスト9.41のユーザーのli
をrender
呼び出しに置き換えることです (リスト9.46)。
<% 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
変数に対して実行していることに注意してください9。この場合、Railsは自動的に_user.html.erb
という名前のパーシャルを探します。このパーシャルを作成する必要があります (リスト9.47)。
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
これは間違いなく大きな進歩です。しかしここで終わらせず、さらに改良してみましょう。今度はrender
を@users
変数に対して直接実行します (リスト9.48)。
<% 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.48のコードは極めてコンパクトになります。
これに限らず、リファクタリングを行う場合には、アプリケーションのコードを変更する前と後で必ずテストを実行し、いずれも成功になることを確認するようにしてください。
$ bundle exec rake test
9.4 ユーザーを削除する
ユーザーの一覧ページはついに完了しました。残るはdestroy
だけです。これを実装することで、RESTに準拠した正統なアプリケーションとなります。この節では、ユーザーを削除するためのリンクを追加します。モックアップを図9.13に示します。また、削除を行うのに必要なdestroy
アクションも実装します。しかしその前に、削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成しましょう。
9.4.1 管理ユーザー
特権を持つ管理ユーザーを識別するために、論理値をとるadmin
属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?
メソッド (論理値を返す) も使えるようになりますので、これを使用して管理ユーザーの状態をテストできます。変更後のデータモデルは図9.14のようになります。
ここでいつものように、マイグレーションを実行してadmin
属性を追加しましょう。コマンドラインで、この属性の型をboolean
と指定します。
$ rails generate migration add_admin_to_users admin:boolean
マイグレーションを実行するとadmin
カラムがusers
テーブル (リスト9.50) に追加されます。リスト9.50では、default: false
という引数をadd_column
に追加しています。これは、デフォルトでは管理者になれないということを示すためです (default: false
引数を与えない場合、 admin
の値はデフォルトでnil
になりますが、これはfalse
と同じ意味ですので、必ずしもこの引数を与える必要はありません。ただし、このように明示的に引数を与えておけば、コードの意図をRailsと開発者に明確に示すことができます)。
admin
属性をUserに追加するマイグレーション db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: false
end
end
後はいつものようにマイグレーションを実行します。
$ bundle exec rake db:migrate
Rails consoleで動作を確認すると、期待どおりadmin
属性が追加されて論理値をとり、さらに疑問符の付いたadmin?
メソッドも利用できるようになっています。
$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false
>> user.toggle!(:admin)
=> true
>> user.admin?
=> true
ここではtoggle!
メソッドを使用して admin
属性の状態をfalse
からtrue
に反転しています。
仕上げに、最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新しましょう (リスト9.51)。
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
次に、データベースをリセットして、サンプルデータを再度生成します。
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
Strong Parameters、再び
リスト9.51では、初期化ハッシュにadmin: true
を設定することでユーザーを管理者にしていることにお気付きになりましたでしょうか。ここでは、荒れ狂うWeb世界にオブジェクトをさらすことの危険性を改めて強調しています。もし、任意のWebリクエストの初期化ハッシュをオブジェクトに渡せるとなると、攻撃者は以下のようなPATCHリクエストを送信してくるかもしれません10。
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
アクションへのリンクを追加しましょう。まず、ユーザーインデックスページの各ユーザーに削除用のリンクを追加し、続いて管理ユーザーへのアクセスを制限します。これによって、現在のユーザーが管理者のときに限り [delete]
リンクが表示されるようになります (リスト9.52)。
<li>
<%= gravatar_for user, size: 50 %>
<%= 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
リクエストを発行するリンクの生成はmethod: :delete引数によって行われている点に注目してください。 また、各リンクをif
文で囲い、管理者にだけ削除リンクが表示されるようにしています。管理者から見えるページを図9.15に示します。
ブラウザはネイティブではDELETEリクエストを送信できないため、RailsではJavaScriptを使用してこれを偽造します。つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になるということです。JavaScriptをサポートしないブラウザをサポートする必要がある場合は、フォームとPOSTリクエストを使用してDELETEリクエストを偽造することもできます。こちらはJavaScriptがなくても動作します11。
この削除リンクが動作するためには、destroy
アクション (表7.1) を追加する必要があります。このアクションでは、該当するユーザーを見つけてActive Recordのdestroy
メソッドを使用して削除し、最後にユーザーインデックスに移動します (リスト9.53)。ユーザーを削除するためにはログインしていなくてはならないので、リスト9.53では:destroy
アクションもlogged_in_user
フィルターに追加しています。
destroy
アクションを追加する app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
.
.
.
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
.
.
.
end
destroy
アクションでは、find
メソッドとdestroy
メソッドを1行で書くために2つのメソッドを連結 (chain) している点に注目してください。
User.find(params[:id]).destroy
結果として、管理者だけがユーザーを削除できるようになります (より具体的には、削除リンクが見えているユーザーのみ削除できる)。しかし、実はまだ大きなセキュリティホールがあります。ある程度の腕前を持つ攻撃者なら、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができるでしょう。サイトを正しく防衛するには、destroy
アクションにもアクセス制御を行う必要があります。これを実装してようやく、管理者だけがユーザーを削除できるようにします。
9.2.1と9.2.2と同じように、今回はbeforeフィルターを使ってdestroy
アクションへのアクセスを制御します。実装するadmin_user
フィルターをリスト9.54に示します。
destroy
アクションを管理者だけに限定する app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_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_url) unless current_user.admin?
end
end
9.4.3 ユーザー削除のテスト
ユーザー削除と同じくらい重要なことは、その振る舞いが期待されたかどうかを確かめる良いテストを書くことです。そこで、まずはユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にしてみます (リスト9.55)。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
9.2.1で経験してきたように、Usersコントローラをテストするために、アクション単位でアクセス制御をテストします。リスト8.28のログアウトのテストと同様に、削除
をテストするために、DELETEリクエストを発行してdestroy
アクションを直接動作させます。このとき2つのケースをチェックします。1つは、ログインしていないユーザーであれば、ログイン画面にリダイレクトされることです。もう1つは、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされることです。変更の結果をリスト9.56に示します。
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect destroy when not logged in" do
assert_no_difference 'User.count' do
delete :destroy, id: @user
end
assert_redirected_to login_url
end
test "should redirect destroy when logged in as a non-admin" do
log_in_as(@other_user)
assert_no_difference 'User.count' do
delete :destroy, id: @user
end
assert_redirected_to root_url
end
end
このとき、リスト9.56ではassert_no_difference
メソッド (リスト7.21) を使って、ユーザー数が変化しないことを確認している点に注目してください。
リスト9.56のテストでは、管理者ではないユーザーの振る舞いについて検証していますが、管理者ユーザーの振る舞いと一緒に確認できるとよさそうです。そこで、管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して、リスト9.44のテストに今回のテストを追加していくことにします。これにより、後ほど追加する管理者の振る舞いについても簡単にテストが書けそうです。さて、今回のテストで唯一の手の込んだ箇所は、管理者が削除リンクをクリックしたときに、ユーザーが削除されたことを確認する部分です。今回は次のようなテストでこれを実現しました。
assert_difference 'User.count', -1 do
delete user_path(@other_user)
end
リスト7.26ではassert_difference
メソッドを使ってユーザーが作成されたことを確認しましたが、今回は同じメソッドを使ってユーザーが削除されたことを確認しています。具体的には、DELETE
リクエストを適切なURLに向けて発行し、User.count
を使ってユーザー数が\( -1 \)減ったかどうかを確認しています。
したがって、管理者や一般ユーザーのテスト、そしてページネーションや削除リンクのテストをすべてまとめると、リスト9.57のようになります。
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@admin = users(:michael)
@non_admin = users(:archer)
end
test "index as admin including pagination and delete links" do
log_in_as(@admin)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
first_page_of_users = User.paginate(page: 1)
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
unless user == @admin
assert_select 'a[href=?]', user_path(user), text: 'delete'
end
end
assert_difference 'User.count', -1 do
delete user_path(@non_admin)
end
end
test "index as non-admin" do
log_in_as(@non_admin)
get users_path
assert_select 'a', text: 'delete', count: 0
end
end
リスト9.57では各ユーザーの削除リンクをテストするときに、ユーザーが管理者であればスキップしている点にも注目してください (これはリスト9.52により、管理者であれば削除リンクが表示されないからです)。
これで、削除に関するコードに対して、よくテストできている状態になりました。テストスイートを走らせると成功するはずです。
$ bundle exec rake test
9.5 最後に
5.4でUsersコントローラをご紹介して以来、長い道のりをたどってきました。あの頃はユーザー登録すらありませんでしたが、今は登録もログインもログアウトもできます。プロフィールの表示も、設定の編集も、すべてのユーザーの一覧画面もあります。さらに、一部のユーザーは他のユーザーを削除することすらできるようになりました。
この時点で、サンプルアプリケーションはWebサイトとしての十分な基盤 (ユーザーを認証したり認可したり) が整ったといえるでしょう。第10章では、さらに2つの改善を加えます。メールアドレスを使ってアカウントを有効化する機能と (すなわち本当に有効なメールアドレスか検証するプロセスと)、ユーザーがパスワードを忘れてしまったときのためのパスワードリセット機能です。
次の章に進む前に、すべての変更をmasterブランチにマージしておきましょう。
$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git checkout master
$ git merge updating-users
$ git push
アプリケーションを本番展開したり、サンプルデータを本番データとして作成することもできます (本番データベースをリセットするにはpg:reset
タスクを使用します)。
$ bundle exec rake test
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:seed
$ heroku restart
もちろん、実際のWebサイトではサンプルデータを生成したくないという人もいるかと思いますが、これには理由があります (図9.16)。それは、図9.16が示すように、サンプルユーザーの表示順序が変化してしまい、図9.11にあるようなローカル環境での表示順序と異なってしまうことです。これは現時点ではまだデフォルトの表示順序が指定されていないことが原因です。 結果として、データベースの内容に応じて表示順序が異なってしまいます。それだけのことかと思われるかもしれませんが、これは今後マイクロポストを実装するときに問題となります。なお、この問題については11.1.4で解決していきます。
9.5.1 本章のまとめ
- ユーザーは、編集フォームからPATCHリクエストを
update
アクションに対して送信し、情報を更新する - Strong Parametersを使うことで、安全にWeb上から更新させることができる
- beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出すことができる
- beforeフィルターを使って、認可 (アクセス制御) を実現した
- 認可に対するテストでは、特定のHTTPリクエストを直接送信する低級なテストと、ブラウザの操作をシミュレーションする高級なテスト (統合テスト) の2つを利用した
- フレンドリーフォワーディングとは、ログイン成功時に元々行きたかったページに転送させる機能である
- ユーザー一覧ページでは、すべてのユーザーをページ毎に分割して表示する
-
rake db:seed
コマンドは、db/seeds.rb
にあるサンプルデータをデータベースに流し込む -
render @users
を実行すると、自動的に_user.html.erb
パーシャルを参照し、各ユーザーをコレクションとして表示する - 論理属性
admin
を追加すると、自動的にuser.admin?
メソッドが使えるようになる - 管理者が削除リンクをクリックすると、DELETEリクエストが
destroy
アクションに向けて送信され、該当するユーザーが削除される - fixtureファイル内で埋め込みRubyを使うと、多量のテストユーザーを作成することができる
9.6 演習
注: 『演習の解答マニュアル (英語)』にはRuby on Railsチュートリアルのすべての演習の解答が掲載されており、www.railstutorial.orgで原著を購入いただいた方には無料で配布しています (訳注: 解答は英語です)。
なお、演習とチュートリアル本編の食い違いを避ける方法については、演習用のトピックブランチに追加したメモ (3.6) を参考にしてください。
- フレンドリーフォワーディングで、最初に与えられたURLにのみ確実に転送されていることを確認するテストを作成してください。続けてログインを行った後、転送先のURLはデフォルト (プロフィール画面) に戻る必要もありますので、これもテストで確認してください。ヒント: リスト9.26に
session[:forwarding_url]
の正しい値を確認するテストを追加してください。 - レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント:
log_in_as
ヘルパーを使ってリスト5.25にテストを追加してみましょう。 - Web経由で
admin
属性を変更できないことを確認してください。リスト9.59に示したように、PATCHリクエストをupdate
メソッドに直接発行するテストを作成してください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadmin
をuser_params
メソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストはREDになるはずです。 -
リスト9.60のパーシャルを使用して、
new.html.erb
ビューとedit.html.erb
ビューをリファクタリングし、コードの重複を取り除いてください。このとき、リスト9.61のようにフォーム変数f
を明示的にローカル変数として渡す必要があることに注意してください。また、provide関数を使うと、パーシャル化したnewフォームやeditフォームの重複をさらに取り除くことも可能です (Jose Carlos Montero Gómezの指摘に感謝します)。
admin
属性の変更が禁止されていることをテストする test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch :update, id: @user, user: { name: @user.name, email: @user.email }
assert_redirected_to root_url
end
test "should not allow the admin attribute to be edited via the web" do
log_in_as(@other_user)
assert_not @other_user.admin?
patch :update, id: @other_user, user: { password: FILL_IN,
password_confirmation: FILL_IN,
admin: FILL_IN }
assert_not @other_user.FILL_IN.admin?
end
.
.
.
end
new
フォームとedit
フォームをパーシャル化する app/views/users/_form.html.erb
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: @user %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>
<% provide(:title, 'Sign up') %>
<% provide(:button_text, 'Create my account') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
</div>
</div>
<% provide(:title, "Edit user") %>
<% provide(:button_text, "Save changes") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render "form" %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">Change</a>
</div>
</div>
</div>
- 画像はhttp://www.flickr.com/photos/sashawolff/4598355045/からの引用です。↑
- この動作の詳細を気にする必要はありません (悪いことをしているわけでもありません)。この詳細に関心を抱くのはRailsフレームワークそのものの開発者ぐらいであり、Railsでアプリケーションを開発する人にとっては重要ではありません。↑
- beforeフィルター (before filters) はこれまで
before_filter
と呼ばれていましたが、「アクションの直前で実行される」という点を強調するためにRailsコアチームによって名前が変更されました。↑ - このセクションのコードでは、thoughtbot社が提供するClearance gemを適用しています。↑
- Yoel Adlerの指摘によって、この問題と解決策が見つかりました。感謝いたします。↑
- 子供の写真はhttp://www.flickr.com/photos/glasgows/338937124/からの引用です。↑
- ちなみにこれはTwitterの認可モデルと同じです。↑
- 原理的には、
rake db:reset
コマンド1つでこれら2つのタスクを実行することがもできますが、最新のRailsだとうまく動かないのでこのようにしています。↑ - この
user
という名前そのものはまったく重要ではないことに注意してください。たとえばuserをfoobarに置き換え、@users.each do |foobar|
と書いてからrender foobar
と呼び出しても問題なく動作します。重要なのは、そのオブジェクトそのものではなく、そのオブジェクトが属しているクラス (この場合はUser
クラス) の方です。↑ - curlなどのコマンドラインツールを使用すると、PATCHリクエストをこの形式で送信することができます。↑
- 詳しくはRailsCastの “JavaScriptを使わない削除” (英語) を観てください。↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!