T_Y_CODE

プログラミング学習記録

学習記録 19日目

19日目の学習記録をまとめていきます。

学習計画

学習内容

Ruby on Railsチュートリアル 10章

10.1 ユーザーを更新する
  • ユーザーを編集するedit, データベース上のデータを更新するupdateアクションを作成していきます。
  • トピックブランチを作成します。
$ git checkout -b updating-users
10.1.1 編集フォーム
  • まずはeditアクション, ビューを作成していきます。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  #...

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

  #...
end
$ touch app/views/users/edit.html.erb
# app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, lacal: true) 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 %>
      <%= 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="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>
  • editページへのリンクを追記します。
# app/views/layouts/_header.html.erb
#...
            <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>
#...
演習

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

# app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, lacal: true) do |f| %>
      #...
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

2. リスト 10.5のパーシャルを使って、new.html.erbビュー(リスト 10.6)とedit.html.erbビュー(リスト 10.7)をリファクタリングしてみましょう(コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3 。

  • formパーシャルを作成します。
$ touch app/views/users/_form.html.erb
  • 以下ではerror_messagesパーシャルに変数@userを渡しています。
# app/views/users/_form.html.erb
<%= form_with(model: @user, lacal: true) 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 %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>
# app/views/users/new.html.erb
<% 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>
# app/views/users/edit.html.erb
<% 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="https://gravatar.com/emails" target="_blank" ref="noopener">change</a>
    </div>
  </div>
</div>
10.1.2 編集の失敗
  • updateアクションを作成します。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  #...

  def update
    user = User.find(params[:id])
    if user.update(user_params)

    else
      render 'edit'
    end
  end

  #...
end
演習

1. 編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
f:id:t_y_code:20201024120625p:plain

10.1.3 編集失敗時のテスト
  • ユーザ編集に関する統合テストを作成します。
$ rails g integration_test users_edit
# 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'
    patch user_path(@user), params: { user: {  name: '',
                                                    email: 'foo@invalid',
                                                    password: 'foo',
                                                    password_confirmation: 'foo' } }
    assert_template 'edit'
  end
end
$ rails test:integration
10 tests, 46 assertions, 0 failures, 0 errors, 0 skips
演習

1. リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。

# test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
  #...

  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: 'foo' } }
    assert_template 'edit'
    assert_select 'div.alert', "The form contains 4 errors"
  end
end
$ rails test:integration
10 tests, 47 assertions, 0 failures, 0 errors, 0 skips
10.1.4 TDDで編集を成功させる
  • 次はユーザ編集を成功させる処理を実装していきます。失敗パターンのテストも書いたのでTDD開発していきます。成功パターンのテストを書いていきます。
  • パスワードを入力しなくても編集出来るようにしていきます。
# test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
  #...

  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
end
  • テストはもちろん失敗します。
$ rails test:integration
11 tests, 49 assertions, 1 failures, 0 errors, 0 skips
  • ではまずupdateアクションの中身を編集していきます。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  #...

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end

  #...
end
  • パスワードが空欄のためテストはまだ失敗します。
$ rails test:integration
11 tests, 49 assertions, 1 failures, 0 errors, 0 skips
  • パスワードが空欄の時バリデーションの検証をスキップ出来るallow_nil: trueを追記します。
# app/controllers/user_controller.rb
class User < ApplicationRecord
  #...

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

  #...
end
  • テストが成功になりました。
$ rails t
30 tests, 83 assertions, 0 failures, 0 errors, 0 skips
  • ここで疑問がありました。存在を確認するpresence: trueとnilの時検査をスキップするallow_nil: trueは競合しちゃってないか?ということ。
  • どうもnilの時はバリデーション検査をスキップしてnilじゃなくても例えば" "みたいな空白文字だけの時presence: trueがエラーを吐いてくれるみたいです。以下presence: trueの記載を削除した際のテスト結果です。
$ rails t
 FAIL["test_password_should_be_present_(nonblank)", #<Minitest::Reporters::Suite:0x00007f7fbe5cd7b8 @name="UserTest">, 0.15532700000039767]
 test_password_should_be_present_(nonblank)#UserTest (0.16s)
        Expected true to be nil or false
        test/models/user_test.rb:55:in `block in <class:UserTest>'
30 tests, 83 assertions, 1 failures, 0 errors, 0 skips
  • 該当コードは以下のものでした。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  #...

  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  #...
end
  • has_secure_passwordはパスワード編集時・新規作成時のみ機能しますのでこれで要件を満足できました。
演習

1. 実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。
2. もしGravatarと紐付いていない適当なメールアドレス(foobar@example.comなど)に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。

  • 動作確認のみなので省略
10.2.1 ユーザーにログインを要求する
  • 現状どのユーザからでもURLにアクセス(例えばusers/1/edit)にすればユーザ情報の編集が出来てしまいます。これを防ぐためにbefore_actionメソッドを使用します。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  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
end
  • テストではログインを行っていないので失敗します。
$ rails test:integration
11 tests, 46 assertions, 2 failures, 0 errors, 0 skips
  • テストにログイン処理を追記して再度テストを実行します。
# test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
  #...

  test "unsuccessful edit" do
    log_in_as(@user)
    #...
  end

  test "successful edit" do
    log_in_as(@user)
    #...
  end
end
$ rails test:integration
11 tests, 53 assertions, 0 failures, 0 errors, 0 skips
  • before_actionがちゃんと動作しているかテストを記載します。
# test/controllers/user_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)
    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
$ rails test:controllers
8 tests, 14 assertions, 2 failures, 0 errors, 0 skips
$ rails test:controllers
8 tests, 14 assertions, 0 failures, 0 errors, 0 skips
  • しっかり動作していますね。全体のテストもしておきます。
$ rails t
32 tests, 87 assertions, 0 failures, 0 errors, 0 skips
演習

1. デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです(結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか(テストが失敗するかどうか)確かめてみましょう。

  • 失敗します。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user#, only: [:edit, :update]

  #...
end
$ rails t
32 tests, 81 assertions, 4 failures, 0 errors, 0 skips
10.2.2 正しいユーザーを要求する
  • 現状はログイン状態のみの確認しかしておらずログインしていれば他人のユーザを編集出来てしまいます。他のユーザが編集出来ないテストを書いていきます。まずfixturesに2人目のユーザを作成します。
# test/fixtures/users.yml
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') %>
  • テストを書いていきます。
# test/integration/users_edit_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)
    get edit_user_path(@user)
    assert_not 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 user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to root_url
  end
end
  • まだ処理を書いていないのでテストは失敗します。
$ rails test:integration
13 tests, 57 assertions, 2 failures, 0 errors, 0 skips
  • before_actionを使用して編集するユーザとcurrent_userが一致しているか確かめます。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :corrent_user, only: [:edit, :update]

  #...

  private

    #...

    def corrent_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless @user == current_user
    end
end
  • テストが成功することを確認します。
$ rails test:integration
13 tests, 57 assertions, 0 failures, 0 errors, 0 skips
  • 以下のコードが分かりづらいので(個人的には分かりやすいと思うけど...)ヘルパーメソッドに移し分かりやすくします。
redirect_to(root_url) unless @user == current_user
# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def current_user?(user)
    user && user == current_user
  end
end
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :corrent_user, only: [:edit, :update]

  #...

  private

    #...

    def corrent_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end
演習

1. 何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。

  • editビューが表示出来てしまうと現在登録しているメールアドレスが見えてしまうため。updateアクションが実行出来てしまうとNameやメールアドレス、パスワードが変更出来てしまうため。

2. 上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?

  • GETリクエストのeditアクション。updateアクションはPATCHのためブラウザで気軽にテストは出来ない。
10.2.3 フレンドリーフォワーディング
  • 現在はログイン後はユーザーのプロフィールページ(showアクション)が実行されるようになっている。例えば編集したくて編集ページを開いてみたがログインページにリダイレクトされた場合、ログイン後は編集ページに飛ばしてもらいたいはずです。このような処理(フレンドリーフォワーディング)を実装していきます。
  • まずはテストを書いていきます。
# test/integration/users_edit_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  #...

  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), 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
  • フレンドリーフォワーディングを実装していないためテストは失敗します。
$ rails test:integration
14 tests, 59 assertions, 1 failures, 0 errors, 0 skips
  • 一時セッションを使用して閲覧していたページを保存します。リクエストがGETの時のみセッションに保存するようにします。URLはrequest.original_urlで取得できます。
# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end
  • 先ほどbefore_actionで指定したlogged_in_userメソッド内にセッションに保存するメソッドを記載します。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :corrent_user, only: [:edit, :update]

  #...

  private
    #...

    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in"
        redirect_to login_url
      end
    end

    #...
end
  • ログイン後のリダイレクト先をセッションに保存したページにします。デフォルトはshowページ(@user)に指定します。
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  #...

  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @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
  • これでテストが成功するようになりました。
$ rails test:integration
14 tests, 64 assertions, 0 failures, 0 errors, 0 skips
演習

1. フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト(プロフィール画面)に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。

  • 英語があやしいが以下のようなテストだろうか。
# test/integration/users_edit_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  #...

  test "friendly forwarding URL should be right" do
    get edit_user_path(@user)
    assert_equal edit_user_url(@user), session[:forwarding_url]
    log_in_as(@user)
    get edit_user_path(@user)
    assert session[:forwarding_url].nil?
  end
end

2. 7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください(デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう(デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって(コラム 1.2)、落ち着いて対処してみましょう)。

  • 動作確認のみなので省略
10.3.1 ユーザーの一覧ページ
  • indexアクションを実装してユーザの一覧を表示させます。
  • indexページはログイン済みのユーザしか見せないようにします。テストを書きます。
# test/icontrollers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  $...

  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
end
  • 未実装のためテストは失敗します。
$ rails test:controllers
9 tests, 14 assertions, 0 failures, 1 errors, 0 skips
  • indexアクション実行前にlogged_in_userメソッドを実行するようにします。indexアクションも記載しておきます。すべてのユーザ情報が必要なためインスタンス変数には全ユーザの情報を入れておきます。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :corrent_user, only: [:edit, :update]

  def index
    @users = User.all
  end
  #...
end
  • テストが成功することを確認します。
$ rails test:controllers
9 tests, 15 assertions, 0 failures, 0 errors, 0 skips
  • indexビューを作成します。
# app/views/users/index.html.erb
<% 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>
  • 動作確認をします。

f:id:t_y_code:20201024164413p:plain

演習

1. レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。

# test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "layout links" do

    #...

    # ログインしていない場合
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    # ログインしている場合
    log_in_as(@user)
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, cout: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end
10.3.2 サンプルのユーザー
  • faker gemを使用してユーザー数を増やします。
  • seeds.rbに自動作成したいユーザーを記載していきます。
# db/seeds.rb
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
  • マイグレートしてdb:seedを実行します。
$ rails db:migrate:reset
$ rails db:seed
  • ユーザが作成されたか確認します。
演習

1. 試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。

  • 動作確認のため省略
10.3.3 ページネーション
  • 現在のコードだとindexページに全ユーザを表示してしまいます。10000人分を表示したとするとページの処理が重くなってしまいます。ページネーションをすれば1ページ30人ずつ表示というような処理が可能である。
  • will_paginate, bootstrap-will_pagenate gemを追加します。
  • indexビューにwill_paginateメソッドを追記します。これは各ページへのリンクになります。
# app/views/users/index.html.erb
<% 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 %>
  • インスタンス変数@usersが全ユーザを取得しているためpaginateメソッドを使用して制限する。params[:page]はURL(&page=n)から取得する。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :corrent_user, only: [:edit, :update]

  def index
    @users = User.paginate(page: params[:page])
  end

    #...
end
  • ページネーションが動作していることを確認します。

f:id:t_y_code:20201024171406p:plain

演習

1. Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。

$ rails c --sandbox
> users = User.paginate(page: nil)
> users.each { |user| puts user.name }
Example User
Jacinto Windler PhD
Christopher Cremin
...
Debby Mueller
Tayna Hettinger

2. 先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。

  • 同じクラス。
$ users.class
# => User::ActiveRecord_Relation
$ User.all.class
# => User::ActiveRecord_Relation
10.3.4 ユーザー一覧のテスト
  • テストをするためにfixturesにユーザを追記します。
  • 統合テストを作成します。
$ rails g integration_test users_index
# test/integration/users_index_test.rb
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), test: user.name
    end
  end
end
  • テストが成功するか確認します。
$ rails t
38 tests, 143 assertions, 0 failures, 0 errors, 0 skips
演習

1. 試しにリスト 10.45にあるページネーションのリンク(will_paginateの部分)を2つともコメントアウトしてみて、リスト 10.48のテストが red に変わるかどうか確かめてみましょう。

  • 確認のみのため省略

2. 先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green のままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。

  • テストを修正します。
# test/integration/users_index_test.rb
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', count: 2
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), test: user.name
    end
  end
end
10.3.5 パーシャルのリファクタリング
  • Railsは@usersをUserオブジェクトであると推測しuserパーシャルを呼び出すと自動的にeachしてくれます。(個人的にこの機能はやり過ぎな気がします。一見コードを見ただけじゃどうやって一覧表示の処理をしてるか分からなくなりそうです。)
# app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>
# app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render @users %>
  <% end %>
</ul>

<%= will_paginate %>
$ rails t
38 tests, 143 assertions, 0 failures, 0 errors, 0 skips
10.4.1 管理ユーザー
  • 管理者権限を示すadminカラムをUserテーブルに追加します。
$ rails g migration add_admin_to_users admin:boolean
  • adminのデフォルト値を設定します。(設定しなくてもカラムの内容はnilのため結果的にfalseになりますが明示的にfalseにして設計意図を示しています)
# db/migrate/***_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
  • マイグレートします。
$ rails db:migrate
  • 最初のユーザにのみ管理者権限を与えます。
# db/seeds.rb
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
  • seedを作成します。
$ rails db:migrate:reset
$ railsdb:seed
  • Strong Parametersを使用しているため攻撃者がPATCHリクエストを送ってきてもadminを変更することは出来ません。
演習

1. Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL(/users/:id)に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は red になるはずです。最後の行では、更新済みのユーザー情報をデータベースから読み込めることを確認します( 6.1.5)。

# 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 not allow the admin attribute to be edited via the web" do
    log_in_as(@user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: { user: { poassword: 'password',
                                                    poassword_confirmation: 'password',
                                                    admin: true } }
    assert_not @other_user.reload.admin?
  end
end
10.4.2 destroyアクション
  • ユーザを削除するリンクをindexビューに追加します。
# app/views/users/_user.html.erb
<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アクションを追記します。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  #...

  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_t users_url
  end

  #...
end
  • 現状だと攻撃者がDELETEリクエストを送ると削除出来てしまいます。before_actionで管理者かをチェックするようにします。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :corrent_user, only: [:edit, :update]
  before_action :admin_user, only: [:destroy]

  #...

  private
    #...

    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end
演習

1. 管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?

Started DELETE "/users/10" for 127.0.0.1 at 2020-10-24 17:57:17 +0900
Processing by UsersController#destroy as HTML
  Parameters: {"authenticity_token"=>"xtzP4F/XJoykz+SCQ8FJ9Wsbd0iymyKCtOEX6MNmEGRxZZT11m/2JXpB3wD8bC9W9nKwt05pud4Z/HAIfAOWGQ==", "id"=>"10"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/helpers/sessions_helper.rb:8:in `current_user'
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 10], ["LIMIT", 1]]
  ↳ app/controllers/users_controller.rb:44:in `destroy'
   (0.1ms)  begin transaction
  ↳ app/controllers/users_controller.rb:44:in `destroy'
  User Destroy (0.4ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 10]]
  ↳ app/controllers/users_controller.rb:44:in `destroy'
   (0.8ms)  commit transaction
  ↳ app/controllers/users_controller.rb:44:in `destroy'
Redirected to http://127.0.0.1:3000/users
Completed 302 Found in 6ms (ActiveRecord: 1.4ms | Allocations: 3318)
10.4.3 ユーザー削除のテスト
  • fixturesの最初のユーザに管理者権限を与えます。
  • まずDELETEに関してアクションレベルでテストを書きます。
# test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  #...

  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@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 user_path(@user)
    end
    assert_redirected_to root_url
  end
end
  • テストが成功することを確認します。
$ rails test:controllers
12 tests, 21 assertions, 0 failures, 0 errors, 0 skips
  • 次に統合テストを書いていきます。
# test/integration/users_index_test.rb
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
  • テストが成功することを確認します。
$ rails t
42 tests, 180 assertions, 0 failures, 0 errors, 0 skips
10.5 最後に
  • リモートリポジトリにプッシュしherokuにデプロイします。
$ rails t
$ git add -A
$ git commit -m "Finish user edit, update, index, and destroy actions"
$ git push origin updating-users
$ git checkout master
$ git merge updating-users
$ git push origin master
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
  • 本番環境で動作確認をします。

f:id:t_y_code:20201024182913p:plain

  • 以上で10章は終了です。