はじめに
これまで未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる。
10.1 ユーザーを更新する
PATCHリクエストに応答するupdateアクションを作成する。
10.1.1 編集フォーム
まず、Usersコントローラにeditアクションを追加して、それに対応するeditビューを実装する。
ユーザー編集ページのURLは/users/1/edit。ユーザーidはparams[:id]
変数で取り出すことができる。
def edit
@user = User.find(params[:id])
end
アクションを作成したら、ビューを作成する。
editビューは見た目はapp/views/users/new.html.erb
と似ているが、HTMLソースに少し違いがある。
<form accept-charset="UTF-8" action="/users/1" class="edit_user"
id="edit_user_1" method="post">
<input name="_method" type="hidden" value="patch" />
# 注目すべきは1つ上のコード↑
.
.
.
</form>
<input name="_method" type="hidden" value="patch" />
URL/users/1
にPATCH
でリクエストをするとユーザーを、Railsが既存のユーザーである(すでにDBに存在する)ということを区別し、更新をしてくれる。
演習 1
先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。
.
.
.
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
</div>
</div>
演習 2
リスト 10.5のパーシャルを使って、new.html.erbビュー(リスト 10.6)とedit.html.erbビュー(リスト 10.7)をリファクタリングしてみましょう(コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3 。
リストに沿ってリファクタリングする。
10.1.2 編集の失敗
update
アクションを作成する。
def update
@user = User.find(params[:id])
# DBからparams[:id]でuserを検索し、@userに代入
if @user.update(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
# falseの場合はeditビューに再レンダリング
end
end
演習 1
編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
確認のみなので省略
10.1.3 編集失敗時のテスト
エラーを検知するための統合テストを実装する。
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'
# editビューがレンダリングされるかどうか検証
patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
# 無効な情報を送信
assert_template 'users/edit'
# editビューが再レンダリングされるか検証
# updateアクションがfalseのときは「render 'edit'」が実行されるため
end
end
演習 1
リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。
.
.
.
test "unsuccessful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
assert_template 'users/edit'
assert_select "div.alert", "The form contains 4 errors."
end
10.1.4 TDDで編集を成功させる
編集の成功に対するテストを実装する。
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit" do
# 編集が成功するときのテスト
get edit_user_path(@user)
# 編集ページにアクセス
assert_template 'users/edit'
# editビューがレンダリングされるか検証
name = "Foo Bar"
# nameを更新
email = "foo@bar.com"
# emailを更新
patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } }
# 有効な情報を送信
assert_not flash.empty?
# flashメッセージが空かどうか
assert_redirected_to @user
# プロフィールページにリダイレクト
@user.reload
# リロードする
assert_equal name, @user.name
# nameと@user.nameが同じかどうか
assert_equal email, @user.email
#emailと@user.emailが同じかどうか
end
end
パスワードが空でも更新できるようにしているが、バリデーションがかかっているため、まだエラーになる。
class User < ApplicationRecord
.
.
.
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
**.
.
.**
end
has_secure_password
がオブジェクト生成時に存在性を検証するため、新規ユーザー登録時に空のパスワードが有効になることは無い。
演習 1
実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。
確認のみなので省略。
演習 2
もしGravatarと紐付いていない適当なメールアドレス(foobar@example.comなど)に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。
初期設定のアイコン?が表示される。
10.2 認可
ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する。
10.2.1 ユーザーにログインを要求する
Usersコントローラの中でbeforeフィルターを使う。beforeフィルターは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みのこと。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
# editアクション、updateアクションが呼び出されたら、logged_in_userアクションを実行する。
.
.
.
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."
# falseのときはflashを表示
redirect_to login_url
# ログインページへリダイレクト
end
end
end
unless 条件式
条件式が偽の時に実行する処理
end
unless文は条件式が偽の場合の処理を記述するのに使われる。
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "should redirect edit when not logged in" do
get edit_user_path(@user)
# editページにアクセス
assert_not flash.empty?
# flashが表示されていないか?
assert_redirected_to login_url
# ログインページにリダイレクトされたかどうか?
end
test "should redirect update when not logged in" do
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
# @user情報を送信
assert_not flash.empty?
# flashが表示されていないか?
assert_redirected_to login_url
# ログインページにリダイレクトされたかどうか?
end
end
演習 1
デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです(結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか(テストが失敗するかどうか)確かめてみましょう。
確認のみなので省略。
10.2.2 正しいユーザーを要求する
ユーザーが自分の情報だけを編集できるようにする。
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user)
# テストユーザー(:archer)としてログイン
get edit_user_path(@user)
# michaelのeditビューにアクセス
assert flash.empty?
# flashが表示されて
assert_redirected_to root_url
# root_urlにリダイレクトされる
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
# テストユーザー(:archer)としてログイン
patch user_path(@user), params: { user: { name: @user.name, email: @user.email } }
# michaelの情報を更新しようとする
assert flash.empty?
# flashが表示されて
assert_redirected_to root_url
# root_urlにリダイレクトされる
end
end
別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
# editアクション、updateアクションが呼び出されたらcorrect_userアクションを実行する
.
.
.
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])
# 受け取ったログイン情報を@userに代入
redirect_to(root_url) unless current_user?(@user)
# @userと現在ログインしているユーザーが違う場合はroot_urlにリダイレクトさせる
end
end
演習 1
何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
他のユーザーの個人情報の表示、更新ができてしまうから。
演習 2
上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
editアクション。viewが定義されているから。
10.2.3 フレンドリーフォワーディング
リダイレクト先をユーザーがアクセスしたかったページにする。
編集ページにアクセスし、ログインした後に、(デフォルトのプロフィールページではなく)編集ページにリダイレクトされているかどうかをチェックするテスト。
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)
# ログインがまだの状態で、editビューにアクセス
log_in_as(@user)
# テストユーザー(michael)でログイン
assert_redirected_to edit_user_url(@user)
# editビューにリダイレクト
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { 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
ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。
module SessionsHelper
.
.
.
# 記憶したURL(もしくはデフォルト値)にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
# 送られてきたURLがnilでなければ左側を評価する。
session.delete(:forwarding_url)
# 転送用のURLを削除
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
# もしGETリクエストが送られてきたらoriginal_urlをsession[:forwarding_url]に代入する
end
end
演習 1
フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト(プロフィール画面)に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
.
.
.
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
assert_equal session[:forwarding_url], edit_user_url(@user)
log_in_as(@user)
assert_nil session[:forwarding_url]
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { 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
演習 2
7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください(デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう(デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって(コラム 1.2)、落ち着いて対処してみましょう)。
[1, 10] in /home/vagrant/work/sample_app2/app/controllers/sessions_controller.rb
1: class SessionsController < ApplicationController
2:
3: # GET /login
4: def new
5: debugger
=> 6: end
7:
8: # POST /login
9: def create
10: @user = User.find_by(email: params[:session][:email].downcase)
(byebug) session[:forwarding_url]
"http://localhost:3000/users/1/edit"
(byebug) request.get?
true