LoginSignup
0
0

More than 3 years have passed since last update.

【Railsチュートリアル】第10章 ユーザーの更新・表示・削除 10.2まで

Posted at

はじめに

これまで未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる。

10.1 ユーザーを更新する

PATCHリクエストに応答するupdateアクションを作成する。

10.1.1 編集フォーム

まず、Usersコントローラにeditアクションを追加して、それに対応するeditビューを実装する。
ユーザー編集ページのURLは/users/1/edit。ユーザーidはparams[:id]変数で取り出すことができる。

app/controllers/users_controller.rb
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/1PATCHでリクエストをするとユーザーを、Railsが既存のユーザーである(すでにDBに存在する)ということを区別し、更新をしてくれる。

演習 1

先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。

app/views/users/edit.html.erb
.
.
.
<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 編集失敗時のテスト

エラーを検知するための統合テストを実装する。

test/integration/users_edit_test.rb

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/integration/users_edit_test.rb
.
.
.
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で編集を成功させる

編集の成功に対するテストを実装する。

test/integration/users_edit_test.rb
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

パスワードが空でも更新できるようにしているが、バリデーションがかかっているため、まだエラーになる。

app/models/user.rb
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メソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みのこと。

app/controllers/users_controller.rb
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文は条件式が偽の場合の処理を記述するのに使われる。

test/controllers/users_controller_test.rb
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 正しいユーザーを要求する

ユーザーが自分の情報だけを編集できるようにする。

test/controllers/users_controller_test.rb
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フィルターからこのメソッドを呼び出すようにする。

app/controllers/users_controller.rb
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 フレンドリーフォワーディング

リダイレクト先をユーザーがアクセスしたかったページにする。

編集ページにアクセスし、ログインした後に、(デフォルトのプロフィールページではなく)編集ページにリダイレクトされているかどうかをチェックするテスト。

test/integration/users_edit_test.rb
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

ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。

app/helpers/sessions_helper.rb
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]が正しい値かどうか確認するテストを追加してみましょう。

test/integration/users_edit_test.rb
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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0