kouの技術的メモ

学習した内容の定着やアウトプット用に開設しました

Railsチュートリアル 10章 ユーザーの更新・表示・削除

10章ではUsersのRESTアクションのうち、これまで未実装だったedit、update、index、destroyアクションを加え、userコントローラーのアクションを完成させます。

復習

10.1演習でパーシャルが出てくるが、パーシャルのファイル名の頭にはアンダースコアを書く。 renderを使って呼び出す場合は、アンダースコアを除いたパーシャル名のファイル名でできる。

10.1.4 TDDで編集を成功させる

今回は先にテストを書いて、それに合わせてコーディングするTDDを実行します。

ここまでは割とスムーズに理解。

10.2 認可 editアクションとupdateアクションは、セキュリティ上の大穴が1つ空いています。 ログインしていない人でも、他人のユーザーidを含めたURLに直接アクセスして他人のユーザー情報変更ページ(/users/id/edit)を表示できてしまう事と、users/idにpostアクセスして直接情報を弄ってしまえる事です。 どのユーザーでもあらゆるアクションにアクセスできるため、誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうのです。

そこでUserコントローラ内でeditアクションと、updateアクションにbefore_actionメソッドを定義し、最初にログインの判定をします。

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]   #edit,updateアクションの直前ここが実行される
  .
  .
  .
\\
    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?                                                              #判定メソッド
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

そしてそれ用のテストを書く

・test/controllers/users_controller_test.rb


  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.3 フレンドリーフォワーディング

ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作です。リダイレクト先は、ユーザーが開こうとしていたページにしてあげるのが親切というものです。

これを実現するのがフレンドリーフォワーディング機能です

リスト 10.30: フレンドリーフォワーディングの実装

  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)     #ここでredirect_toがあっても。
    session.delete(:forwarding_url)                                         #ここまで実行される
  end

最初にredirect文を実行しても、セッションが削除される。 実は、明示的にreturn文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しない。 したがって、redirect文の後にあるコードでも、そのコードは実行さます。

リスト 10.29: フレンドリーフォワーディングのテスト

  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)                              #ここのedit_user_url(@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

edit_user_urlはURLヘルパーと言って、pathヘルパーedit_user_pathとほぼ同じ意味だそうです。 Railsガイドによると、 urlヘルパーは、pathの前に現在のホスト名、ポート番号、パスのプレフィックスが追加されている点が異なるのだそうです。

10.3 すべてのユーザーを表示する

すべてのユーザーを一覧表示します。 ユーザ一覧表示にはindexアクションを使用し、ユーザーの一覧、ページネーション機能を実装します。 また、管理者権限を新たに実装し、ユーザーの一覧ページから (管理者であれば) ユーザーを削除できる機能も実装します

10.3.2 サンプルのユーザー

テストをするには多くのデータをデータベースに入れる必要がありますが、手動だと面倒くさいので、GemfileにFaker gemを使って、自動で適当なユーザー情報を作ります。

Gemfileにgem 'faker', '1.7.3'を追加した後、bundle installした際にエラー。

過去のようにbundle updateをしてください的なメッセージが出たので、bundle update後、無事にインストール完了

サンプルユーザーを生成するRubyスクリプト (Railsタスクとも呼びます)をdb/seeds.rbというファイルに書きます。

一人のExample Userと99人の適当なユーザーが生成されるコードです。

リスト 10.43: データベース上にサンプルユーザーを生成するRailsタスク 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

そしてデータベースをリセットして、Railsタスクを実行 (db:seed)

$ rails db:migrate:reset
$ rails db:seed

これで必要な仮のデータセットは揃いました

10.3.3 ページネーション

膨大な量のユーザー一覧を表示すると大変なので、1ページで表示される人数を制限するページネーション機能を付け足します。 Gemファイルに以下を付け足し、

gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'

そしてbundle install。

実際にページネーションしたい領域を<%= 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.each用のコントローラー内の@usersですが、ページネーション用に少し変える必要があります。

  def index
    @users = User.paginate(page: params[:page])
  end
10.4 ユーザーを削除する

あと残るREST機能としてはDestroyのみになります。 ユーザーを削除するということですが、これは管理者権限(admin)を持つユーザーだけができます。 Userテーブルにadminカラムを追加し、これで判定するようにします。

マイグレーションファイルの作成

rails generate migration add_admin_to_users admin:boolean

それからマイグレーション

rails db:migrate
10.4.1 管理ユーザー

作ったadminカラムを他人が/users/idにpatchアクセスしてadmin属性を勝手にtrueに書き換えしようとするときちんと失敗するか、テスト

演習 リスト 10.56: admin属性の変更が禁止されていることをテストする 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 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

  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: {
                                    user: { password:              @other_user.password,
                                            password_confirmation: @other_user.password_confirmation,
                                            admin: true} }
    assert_not @other_user.@other_user.reload.admin?     #@other?userを読み込み直す
  end
  .
  .
  .
end
10.4.2 destroyアクション

まずhtmlの実装です。

リスト 10.57: ユーザー削除用リンクの実装 (管理者にのみ表示される) app/views/users/_user.html.erb

 <li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>       #!current_user?(user) は、管理者のデータデリートリンクを表示しないと言う意図。
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

ブラウザは本来ネイティブではDELETEリクエストを送信できないため、RailsではJavaScriptを使って偽造します。 つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になるということです。

10.4.3 ユーザー削除のテスト

assert_no_differenceメソッドを使います。

リスト 10.61: 管理者権限の制御をアクションレベルでテストする green 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 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                       #7章にも出てきましたが、 assert_no_difference 'XXXXX' do   
      delete user_path(@user)                                        #                                           ...  end
    end                                                                           #で、間に入る式をしてもXXXXの値が変化しないかをテストできる
    assert_redirected_to root_url
  end
end
10.5 最後に

今回開発で適当なサンプルデータを作成しましたが、それを本番環境でも使うために heroku pg:reset DATABASEでDBをリセットさせ、 $ heroku run rails db:migrateし、 $ heroku run rails db:seedでseedをデータベースで使えるようにし、 $ heroku restartでherokuをリスタートします。

$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
$ heroku restart

heroku pg:reset DATABASEをコマンドすると

 ▸    WARNING: Destructive action
 ▸    postgresql-XXXXX-〇〇〇〇 will lose all of its data
 ▸    
 ▸    To proceed, type アプリ名 or re-run this command with --confirm アプリ名

みたいなワーニングが出てきました。 postgresql-XXXXX-〇〇〇〇のデータをすべてを失うことになります。 次のように入寮してください アプリ名。または--confirmアプリケーション名を指定してこのコマンドを再実行してください。

とのことなので言われたとおりアプリ名を入力。

Resetting postgresql-〇〇〇-△△△... doneと出力され、無事pg:reset成功のようです

10章まとめ
  • ユーザーは、編集フォームからPATCHリクエストをupdateアクションに対して送信し、情報を更新する

  • Strong Parametersを使うことで、安全にWeb上から更新させることができる

  • beforeフィルターを使うと、特定のアクションが実行される直前にメソッドを呼び出すことができる

  • beforeフィルターを使って、認可 (アクセス制御) を実現した

  • 認可に対するテストでは、特定のHTTPリクエストを直接送信する低級なテストと、ブラウザの操作をシミュレーションする高級なテスト (統合テスト) の2つを利用した

  • フレンドリーフォワーディングとは、ログイン成功時に元々行きたかったページに転送させる機能である

  • ユーザー一覧ページでは、すべてのユーザーをページ毎に分割して表示する

  • rails db:seedコマンドは、db/seeds.rbにあるサンプルデータをデータベースに流し込む

  • render @usersを実行すると、自動的に_user.html.erbパーシャルを参照し、各ユーザーをコレクションとして表示する

  • boolean型のadmin属性をUserモデルに追加すると、admin?という論理オブジェクトを返すメソッドが自動的に追加される

  • 管理者が削除リンクをクリックすると、DELETEリクエストがdestroyアクションに向けて送信され、該当するユーザーが削除される

  • fixtureファイル内で埋め込みRubyを使うと、多量のテストユーザーを作成することができる