学習日記

Ruby on Rails勉強してます

今日やったこと9

第10章 ユーザーの更新・表示・削除

10.1 ユーザーを更新する

ユーザーを編集するためのeditアクションを作成する。

def edit
    @user = User.find(params[:id])
end


editアクションに対応するeditビューを実装
  app/views/users/edit.html.erb


WebブラウザはネイティブではPATCHリクエストを送信できないので、RailsはPOSTリクエストとinputフィールドを利用してPATCHリクエストを「偽造」している。

<input name="_method" type="hidden" value="patch" />


Railsは、form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使う。

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false


ヘッダーに”Setting”リンクを追加する。
editビューへのパスとcurrent_userのヘルパーメソッドを使う。

<%= link_to "Settings", edit_user_path(current_user) %>


演習
・10.1.1.1
target="_blank"で新しいページを開くときには、セキュリティのためrel属性にnoopenerを設定する。

<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>

演習
・10.1.3.1
assert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というエラーメッセージが表示されているかテストする行を追加する。

assert_select "div.alert", "The form contains 4 errors."


Updateアクションを書くまえに編集の成功に対するテストを書く。
テストを先に書くことでユーザーが実際に使うイメージや追加するコードがわかりやすくなる。

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

・
・
・
  
  # 編集の成功に対するテスト
  test "successful edit" do
    get edit_user_path(@user)
    assert_template '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_redirect_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end
end


ユーザー名やメールアドレスだけを編集したいときはパスワード欄を入力する必要はないが空のままだとバリテーションに引っかかってしまうので空のままでも更新できるようにvalidatesにallow_nil: trueというオプションを追加する。

validates :password, presence: true, length: { minimum: 6 }, allow_nil: true


updateアクションを作成しテストする。

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

10.2 認可

今のままでは誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうのでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないようにする。
ログインしていないユーザーが保護されたページにアクセスしようとしたときはログインページに転送して、メッセージも表示するようにする。
許可されていないページに対してアクセスするログイン済みのユーザーがいたら 、ルートURLにリダイレクトさせるようにする。

転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使う。beforeフィルターは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みである。

ユーザーにログインを要求するために、logged_in_userメソッドを定義してbefore_action :logged_in_userという形式で使用する。
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限をかけている。

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


editアクションやupdateアクションをテストする前にログインしておく必要があるので、log_in_asヘルパーを使ってテストを修正する。

test/integration/users_edit_test.rb


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


beforeフィルターが機能しているかどうかのテストを追加する。

test/controllers/users_controller_test.rb


require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
  end
  ・
  ・
  ・
  # editアクションの保護に対するテスト
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end
  
  # updateアクションの保護に対するテスト
  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


ユーザーが自分の情報だけを編集できるようにする必要があるためのテストを追加する。
ユーザーの情報が互いに編集できないことを確認するために、ユーザー用のfixtureファイルに2人目のユーザーを追加する。

test/fixtures/users.yml


michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

# 2人目のユーザーを追加する
archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>


次に、 log_in_asメソッドを使って、editアクションとupdateアクションをテストする。

test/controllers/users_controller_test.rb


require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer) #2人目のユーザー
  end
  .
  .
  .
  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?   #ユーザーが違う際にメッセージが出ているか
    assert_redirected_to root_url #ルートURLにリダイレクトしているか
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end
end


別のユーザーのプロフィールを編集しようとしたらリダイレクトするようにさせるためにcorrect_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする。

app/controllers/users_controller.rb


class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  #editとupdateの各アクションでcorrect_userを呼び出す
  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