らくだ🐫にもできるRailsチュートリアル|10.4(と10.5)

10.4 ユーザーを削除する

RESTに準拠した正統なアプリケーションとなる為に、最後の一つ、destroy機能を実装していく! 削除権限を持つ管理 (admin)ユーザーのクラスを作成し→ユーザーを削除するためのリンクを追加し→destroyアクションを実装する 見本はこちら→RoRT本文

10.4.1 管理ユーザー

論理値をとるadmin属性をUserモデルに追加する → 自動的に論理値を返すadmin?メソッドも使えるようになる → 管理ユーザーの状態をテストできる

admin属性をUserモデルに追加

属性の型をbooleanと指定してマイグレートファイルを作る boolean型 → 論理値を取る型

$ rails generate migration add_admin_to_users admin:boolean
Running via Spring preloader in process 5082
      invoke  active_record
      create    db/migrate/20200219070406_add_admin_to_users.rb

usersテーブルにadminカラムを追加するためのマイグレートファイルが生成されたので default: falseという引数を追加する

これは、デフォルトでは管理者になれないということを示すためです (default: false引数を与えない場合、 adminの値はデフォルトでnilになりますが、これはfalseと同じ意味ですので、必ずしもこの引数を与える必要はありません。ただし、このように明示的に引数を与えておけば、コードの意図をRailsと開発者に明確に示すことができます)。

実は結構重要なのできっちり指定が吉との事(言語・DBによってNull/True/Falseの解釈が違ったりするから)

class AddAdminToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

→マイグレーションの実行

$rails db:migrate

#コンソールで動作を確認できる
$ rails console --sandbox
#userにUser.firstを代入
>> user = User.first
#userは管理者か
>> user.admin?
#false
=> false
#userの以下のキー(admin)の値をtoggle!メソッドで反転
>> user.toggle!(:admin)
#falseが反転してtrueに
=> true
#userは管理者か
>> user.admin?
#true
=> true

10.3で作ったサンプルデータを 最初のユーザーがデフォルトで管理者になるように変更する

# テーブル名.create! 作るデータ対応するカラムと値
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             #管理者に設定
             admin: true)

# 99回繰り返す(timesメソッド)
99.times do |n|
  # nameに代入 Faker::Name.name
  name  = Faker::Name.name
  # emailに代入 example-#{n+1}@railstutorial.org ←それぞれのアドレスが変わるような指定
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

データベースのリセットと再生成

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

Strong Parameters、再び

例えば下記のようなPATCHリクエストを送信されたら?

patch /users/17?admin=1

17番目のユーザーが管理者に変更されてしまう!コワイ!! 危ない危険を防ぐ為に編集してもよい安全な属性だけが更新できるように指定する! 7.3.2と同じくStrong Parametersを使って対策する。

・
・
・
  private

    #許可された属性リストにadminが含まれていない=adminは編集できない!
    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
    
これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できます。

これはとても大切な問題なので編集可能になってはならない属性に対するテストを行う!(演習でやる)

演習

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

/sample_app/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
    # paramsハッシュに以下のデータを持たせてuser_path(@user)にpatchのリクエスト
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    # flashがemptyではない(flashが表示されている→エラー表示)
    assert_not flash.empty?
    # login_urlにリダイレクトされる
    assert_redirected_to login_url
  end
  
  test "should not allow the admin attribute to be edited via the web" do
    #@other_userとしてログイン(test_helper.rbで定義したlog_in_asヘルパー)
    log_in_as(@other_user)
    #@other_userのadmin?はtrueではない
    assert_not @other_user.admin?
    #user_path(@other_user)にpatchのリクエスト →持たせるハッシュ
    patch user_path(@other_user), params: {
                                    user: { password:              'password',
                                            password_confirmation: 'password',
                                            admin: true } }
    #DBから再度取得した@other_userのadmin?はtrueではない
    assert_not @other_user.reload.admin?
  end
・
・
・
end

usersコントローラーのuser_paramsメソッド内にadminを追加するとRED、削除(元に戻す)とGREENになる

10.4.2 destroyアクション

削除機能を完成させる

destroyアクションへのリンクを追加

ユーザー一覧ページに削除用のリンクを表示させる

  • <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to t('.delete'), user, method: :delete, data: { confirm: t('.confirmation') } %> <% end %>
  • ログインしているユーザーが管理者の場合のみ削除用リンクが表示される (管理者自身のユーザー情報には削除用リンクは表示されない)

    ブラウザはネイティブではDELETEリクエストを送信できないため、RailsではJavaScriptを使って偽造します。つまり、JavaScriptがオフになっているとユーザー削除のリンクも無効になるということです。JavaScriptをサポートしないブラウザをサポートする必要がある場合は、フォームとPOSTリクエストを使ってDELETEリクエストを偽造することもできます。こちらはJavaScriptがなくても動作します

    →RailsCastの “JavaScriptを使わない削除” (英語サイト)

    destroyアクションの実装

    destroyアクションでは

    1. 該当するユーザーを見つける
    2. (Active Recordの)destroyメソッドを使って削除
    3. ユーザー一覧へリダイレクト

    ユーザーを削除するためには(管理者として)ログインしていることが必須なので :destroyアクションもlogged_in_userフィルターに追加する

    class UsersController < ApplicationController
      before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
      before_action :correct_user,   only: [:edit, :update]
      .
      .
      .
      def destroy
        # 受け取ったidのUserを削除
        User.find(params[:id]).destroy
        # 削除成功のフラッシュメッセージ
        flash[:success] = "User deleted"
        # ユーザー一覧ページへリダイレクト
        redirect_to users_url
      end
    
      private
      .
      .
      .
    end

    destroyアクションへのアクセス制御

    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
          # root_urlにリダイレクト current_user.admin?(ログイン中のユーザーが管理者であるか)がfalseの場合
          redirect_to(root_url) unless current_user.admin?
        end
    end

    演習

    1. 管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?
    ・
    ・
    ・
       #ここから処理
       (0.1ms)  begin transaction
     #指定したidのユーザーを削除している
      SQL (0.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 6]]
       #ここまで処理
       (6.0ms)  commit transaction
    ・
    ・
    ・

    10.4.3 ユーザー削除のテスト

    ユーザーを削除するといった重要な操作については、期待された通りに動作するか確かめるテストを書くべきです。

    超大事!と言うこと。 準備としてサンプルユーザーの一人を管理者にする。

    michael:
      name: Michael Example
      email: michael@example.com
      password_digest: <%= User.digest('password') %>
      admin: true
    
    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 %>

    アクション単位での削除機能のテスト

    アクション単位→コントローラーテスト

    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
        # ブロックで渡されたものを呼び出す前後でUser.countに違いがない
        assert_no_difference 'User.count' do
          # user_path(@user)にdeleteのリクエスト
          delete user_path(@user)
        end
        # login_urlにリダイレクトされる
        assert_redirected_to login_url
      end
    
      test "should redirect destroy when logged in as a non-admin" do
        # other_userでログインする
        log_in_as(@other_user)
        # ブロックで渡されたものを呼び出す前後でUser.countに違いがない
        assert_no_difference 'User.count' do
          # user_path(@user)にdeleteのリクエスト
          delete user_path(@user)
        end
        # root_urlにリダイレクト
        assert_redirected_to root_url
      end
    end

    ここでは管理者ではないユーザーのふるまいについてテストしている →管理者ユーザーのふるまいと一緒にテストできるともっと良い!

    管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して、リスト 10.48のテスト(users_index_test.rb)に今回のテストを追加していくことにします。
    1. DELETEリクエストを適切なURLに向けて発行
    2. User.countを使ってユーザー数が1減ったか
    require 'test_helper'
    
    class UsersIndexTest < ActionDispatch::IntegrationTest
    
      def setup
        @admin     = users(:michael)
        @non_admin = users(:archer)
      end
    
      test "index including pagination" do
        # @adminでログイン
        log_in_as(@admin)
        # users_pathにgetのリクエスト
        get users_path
        # users/indexが描画される
        assert_template 'users/index'
        # 特定のHTMLタグが存在する div class="pagination"
        assert_select 'div.pagination' , count: 2
        # first_page_of_usersにUser.paginate(page: 1)(ユーザー一覧のページ目)を代入
        first_page_of_users = User.paginate(page: 1)
        # first_page_of_usersからuserを1ずつ取り出す
        first_page_of_users.each do |user|
          # 特定のHTMLタグが存在する a href パスはuser_path(user) 表示テキストはuser.name
          assert_select 'a[href=?]', user_path(user), text: user.name
          # (eachで取り出された)user == @adminがfalseの場合
          unless user == @admin
            # 特定のHTMLタグが存在する a href パスはuser_path(user) 表示テキストは'削除' (管理ユーザー 以 外 に削除の表示があるという状態)
            assert_select 'a[href=?]', user_path(user), text: '削除'
          end
        end
        # ブロックで渡されたものを呼び出す前後でUser.countが-1
        assert_difference 'User.count', -1 do
          # user_path(@non_admin)にdeleteのリクエスト
          delete user_path(@non_admin)
        end
      end
      
      test "index as non-admin" do
        # @non_adminでログイン
        log_in_as(@non_admin)
        # users_pathにgetのリクエスト
        get users_path
        # 特定のHTMLタグが存在する a 表示テキストは'削除' カウントは0個
        assert_select 'a', text: '削除', count: 0
      end
    end

    演習

    動作確認のみにて省略

    10.5 最後に

    5.4でUsersコントローラをご紹介して以来、長い道のりをたどってきました。あの頃はユーザー登録すらありませんでしたが、今は登録もログインもログアウトもできます。プロフィールの表示も、設定の編集も、すべてのユーザーの一覧画面もあります。さらに、一部のユーザーは他のユーザーを削除することすらできるようになりました。

    まじで長かった!がんばったね!!

    章の終わりには

    テストしてaddしてcommitしてbranchにpushしてGitHubで自分にプルリクしてmergeして戻ってmasterにcheckoutしてpullする

    デプロイ!

    #テストして
    $ rails test
    #herokuにpush
    $ git push heroku
    #herokuのDBをリセット
    $ heroku pg:reset DATABASE
    #herokuのDBを初期化
    $ heroku run rails db:migrate
    #herokuのDBにサンプルデータを読み込む(読み込める設定にしているので)
    $ heroku run rails db:seed
    #herokuのリスタート
    $ heroku restart
    実際のWebサイトではサンプルデータを生成したくないという人もいるかと思いますが、これには理由があります (図 10.16)。それは、図 10.16が示すように、サンプルユーザーの表示順序が変化してしまい、図 10.11にあるようなローカル環境での表示順序と異なってしまうことです。これは現時点ではまだデフォルトの表示順序が指定されていないことが原因です。結果として、データベースの内容に応じて表示順序が異なってしまいます。

    図に関してはRoRT本文参照でヒトツ。

    これはデフォルトの表示順序が指定されていないことが原因ということで、13.1.4で解決していくとの事。

    まとめとか感想

    上でもちょっと触れられてるけど、user関連長かったね! でももうちょっとだけ続くんじゃ(メール認証と再設定)

    らくだ🐫にもできるRailsチュートリアルとは

    「ド」が付く素人のらくだ🐫が勉強するRailsチュートリアルの学習記録です。 自分用に記録していますが、お役に立つことがあれば幸いです。

    調べたとはいえらくだ🐫なりの解釈や説明が含まれます。間違っている部分もあるかと思います。そんな所は教えて頂けますと幸いなのですが、このブログにはコメント機能がありません💧お手数おかけしますがTwitterなどでご連絡いただければ幸いです

    タグ: Railsチュートリアル、らくだにも出来るRailsチュートリアル