#ユーザーの更新・表示・削除
■第10章
edit
、update
、index
、destroy
アクションを加え、RESTアクションを完成させる。まず、ユーザーが自分のプロフィールを自分で更新できるようにできる。
##10.1 ユーザーを更新する
ユーザー情報を編集するパターンは、新規ユーザーの作成と似てるらしい。
###10.1.1 編集フォーム
Usersコントローラにedit
アクションを追加して、それに対応するeditビューを実装する。
def edit
@user = User.find(params[:id])
end
editビューも作成。
<% 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>
リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまうため、target="_blank"
で新しいページを開くときはセキュリティ上の問題がある。
###10.1.2 編集の失敗
編集に失敗した場合について扱っていく。以下のコードを追加。
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
end
end
###10.1.3 編集失敗時のテスト
統合テスト生成から始める。
$ rails generate integration_test users_edit
中身も追記する。無事テストGREENに。
###10.1.4 TDDで編集を成功させる
今度は編集フォームが動作するようにする。
編集の成功に対するテストを追記。
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/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
コントローラのupdate
アクションを弄って、パスワードが空のままでも更新できるようにする。空だったときの例外処理を加えるallow_nil: true
を追記。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
これでテストがGREEN。
##10.2 認可
ログインすると誰でも他のユーザー情報を編集できる状態なので、他の人のユーザー編集ページにアクセスしようとしたらルートURLにリダイレクトするようにする。
###10.2.1 ユーザーにログインを要求する
ログイン画面に転送するために、beforeフィルターを使う。
beforeフィルターにlogged_in_user
を追加。
before_action :logged_in_user, only: [:edit, :update]
.
.
.
private
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
このままじゃテストRED。原因は、editアクションやupdateアクションでログインを要求するようになったためなので、ログインしておくようにする。
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
beforeフィルターの実装が終わってないので追加してく。
edit
とupdate
アクションの保護に対するテストする
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)
assert_not flash.empty?
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 } }
assert_not flash.empty?
assert_redirected_to login_url
end
end
今の状態だとまだログインさえしてしまえば他の人のページを編集できてしまうので、次以降でに進めていく。
###10.2.2 正しいユーザーを要求する
ユーザーが自分の情報だけを編集できるようにする。
ユーザーの情報が互いに編集できないことを確認するために、2人目のユーザーを追加。
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
間違ったユーザーが編集しようとしたときのテスト。
def setup
@user = users(:michael)
@other_user = users(:archer)
end
current_user?
メソッドも追加。
論理値を返すメソッドにする。
# 渡されたユーザーがログイン済みユーザーであればtrueを返す
def current_user?(user)
user == current_user
end
beforeフィルターを使って編集/更新ページを保護する。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
テストも無事GREEN。
###10.2.3 フレンドリーフォワーディング
アクセスしたページ→ログイン→アクセスしたページとなるにする。
統合テストの書き換え。
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_url(@user)
失敗するテストが書けた。
希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。store_location
とredirect_back_or
の2つのメソッドでこの動作を実現させる。
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.original_url if request.get?
end
end
GETリクエストが送られたときだけ格納するようにしておく。
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
フォワーディング自体を実装するには、redirect_back_or
メソッドを使う。
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
テストも無事GREEN。
##10.3 すべてのユーザーを表示する
index
アクションを追加していく。
###10.3.1 ユーザーの一覧ページ
index
ページを不正なアクセスから守るために、まずはindex
アクションが正しくリダイレクトするか検証するテストを書いていく。
以下を追記。
test "should redirect index when not logged in" do
get users_path
assert_redirected_to login_url
end
次にbeforeフィルターのlogged_in_user
にindex
アクションを追加して、このアクションを保護する。
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
User.all
を使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@users
に代入させる。一気に読み込むと遅いが、それはあとで修正。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.all
end
.
.
.
end
index
ビューの作成。;
<% 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>
gravatar_for
ヘルパーにオプション引数を追加。
module UsersHelper
# 渡されたユーザーのGravatar画像を返す
def gravatar_for(user, options = { size: 80 })
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
size = options[:size]
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
CSSもちょっといじくって、そして、ユーザー一覧ページへのリンクを更新するusers_path
を使う。以下を追加。
<li><%= link_to "Users", users_path %></li>
テストして問題ないことを確認。
###10.3.2 サンプルのユーザー
Gemfile
にFaker gemを追加。これでダミーのユーザーがたくさん作れるらしい。
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
その後データベースをいじったら、無事ダミーユーザーがたくさん出てきました。
###10.3.3 ページネーション
1つのページに大量のユーザーが表示されているので、人数制限みたいなのをする。これを解決するのがページネーション。
Gemfile
に新たなgemを追加。
indexページでpaginationを使う。
<% 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 %>
indexアクションでUsersをページネートする。
def index
@users = User.paginate(page: params[:page])
end
###10.3.4 ユーザー一覧のテスト
ページネーションに対する簡単なテストも書いておく。
fixtureにユーザー追加。
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') %>
malory:
name: Malory 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 %>
統合テスト作成。
$ rails generate integration_test users_index
最後にpagination
クラスを持ったdiv
タグチェックをして、無事テスト完了。
###10.3.5 パーシャルのリファクタリング
ユーザーのli
をrender
呼び出しに置き換える。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
各ユーザーを表示するパーシャル。
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
今度はrenderを@users変数に対して直接実行する。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %>
</ul>
<%= will_paginate %>
テストもGREEN。
##10.4 ユーザーを削除する
ユーザーを削除するためのリンクの作成。管理者権限を持つユーザーしか削除できないようにする。
###10.4.1 管理ユーザー
論理値をとるadmin
属性をUserモデルに追加する。型はboolean
に。
$ rails generate migration add_admin_to_users admin:boolean
デフォルトでは管理者になれないということを示すために、default: false
という引数をadd_column
に追加。
boolean型のadmin
属性をUserに追加するマイグレーション
class AddAdminToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :admin, :boolean, default: false
end
end
最初のユーザーだけ管理者にしとく。
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true)
データベース周りをいじって終わり。
###10.4.2 destroyアクション
管理者のみ削除用のリンクを表示。
<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>
削除リンク動作のために、destroy
アクションを追加する。
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
private
.
.
.
end
サイトを正しく防衛するために、destroy
アクションにもアクセス制御を行う。
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
###10.4.3 ユーザー削除のテスト
1人だけadmin:true
で管理者にする。
ログインしていないユーザーであれば、ログイン画面にリダイレクトされること、ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされることの2つをチェックする。
テストの中身を書き換えたら完了。
##感想
やっぱりセキュリティ周りのことはとにかくテストが大事なんだなという印象を受けました。あとテストでエラーが出ても、自力で解決できる力がさらについた気がします。