【自分用】rails チュートリアル 10章

やること
ユーザーの更新、表示、削除機能の実装

用語
・パーシャル
部分的なテンプレート
重複しているコードを一つのファイルにまとめておくことで何度も書く手間が省ける
provideメソッドを併用することでパーシャル内の文言を変更できる

・リファクタリング
外部からの挙動はそのままに、内部を整理する


10.1 ユーザーを更新する
ユーザーを編集するための`edit`アクションを作成する
POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成
(sessionsコントローラのnewアクションや、Usersコントローラのnewアクションのようにフォームを用意し、フォームの入力値をupdateアクションに送る動作を実装する)
編集可能なのはユーザー本人のみ

10.1.1 編集フォーム
ユーザー編集ページのURLは/users/ユーザーid/edit
例） /users/1/edit
これを利用してparams[:id]でユーザーのIDを取り出してインスタンス変数に保存しておく
リスト10.2 ユーザー編集ページ
無効な値を入力した時のエラーメッセージは_error_messagesパーシャルを利用
aタブの`target="blank"`で新しいタブでリンク先を開くことが出来る
フォームの入力欄には@user変数に入っている値が自動入力される
実際の編集ページには隠し属性があるが、これはブラウザがPATCHリクエストを送ることができないため、隠しinputフィールドにpatchを指定してRailsがPATCHリクエストを擬似的に行なっている

どうやって新規ユーザー用のPOSTリクエストと既存ユーザー用のPACTHリクエストを区別しているのかというと、
RailsはActiveRecordの`new_record?`メソッドで新規ユーザーと既存ユーザーを判別している

ナビゲーションバーのユーザー設定リンクの更新
リスト10.4
edit_user_path名前付きルートと、current_userヘルパーメソッドを使う

演習
１. target="blank"に設定した場合、現在では書かなくても `rel="noopener"`と同様の処理になった
2.newページとeditページはほぼ共通で、異なる点はsubmitボタンのテキストくらいなので、`provide`メソッドであらかじめテキスト内容を記しておく


10.1.2 編集の失敗
更新に失敗した時の処理の実装
updateアクションでは、editアクションから送信されたparamsを使って更新する(createアクションでparamsを使ってユーザーを作成したのに似ている)
paramsでDBを直接更新するのは危険なので以前定義したuser_paramsメソッドを使う（Strong Parametersを使ってマスアサインメントの脆弱性を防止）
Userモデルのバリデーションとエラーメッセージのパーシャル（_error_messages）があるので無効な情報を送るとエラーメッセージが表示される

10.1.3 編集失敗時のテスト
編集失敗時の統合テストの作成
`$ rails generate integration_test users_edit`
リスト10.9
editページにGETリクエストを送信し、editページが表示されているかテスト
updateアクションにpatchリクエストで無効な値を送信し、editページが再表示されているかテスト
演習
`assert_select 'div.alert', "The form contains 4 errors."`

10.1.4 TDD(テスト駆動開発)で編集を成功させる
機能実装前に統合テストを書き、機能の実装が完了したときにその機能が受け入れ可能かどうか決めるテストを「受け入れテスト」と呼ぶ
リスト10.11
失敗時のテストを参考に成功時のテストを書く
今回は有効な情報を送るよう修正。flashメッセージが空でないかどうか、プロフページにリダイレクトされるかチェック
パスワードとパスワード確認が空
`@user.reload`でDBから最新のユーザー情報を読み込んで正しく更新されているか確認
現状、①flashメッセージを実装していない②リダイレクトの指定をしていない③パスワードが空でバリデーションに引っかかる
リスト10.12
①②を解決するためupdateアクションに成功時の処理を書く

リスト10.13 パスワードが空のままでも更新できるようにする
`allow_nil: true`オプションでパスワードが空の場合でも更新できるようになる
has_secure_passwordがオブジェクト生成時に働くので空のパスワードが新規登録時に有効になることはない
空のパスワードを入力すると存在性のバリデーションと、has_secure_passwordのバリデーションが重複して二つのエラーメッセージが表示される不具合も解消された。

10.2 認可
ユーザーを識別することを認証、ユーザーが行える操作を管理するのが認可
現状誰でもどのアクションにもアクセスできてしまうので、それらを制御していく
未ログイン時にはログインページに転送してflashメッセージを表示する
ログイン済みで他のユーザーにアクセスしようとした場合にはルートU RLに転送する

10.2.1 ユーザーにログインを要求する
Usersコントローラの中で`before`フィルターを使う
リスト10.15`before_action`メソッドでedit、updateアクションが実行される前に必ずログインするようにする
:onlyオプションでeditアクションとupdateアクションの前に限定している

今の段階ではテストは失敗する
ログインしていないとedit、updateアクションを実行できないため
リスト10.17 テスト実行前にログインするようにする
これでテストはパスするようになったが、before_actionの行をコメントアウトしてもテストがパスしてしまう
リスト10.20 edit、updateアクション実行前にログインしているかテストする

10.2.2 正しいユーザーを要求する
本人以外が編集できないようにする
リスト10.23 別人がログインするのを別人がログインする状況を作るためfixtureにユーザー追加

リスト10.24 `@other_user`としてログインして、@userの更新をするテストを書く

リスト10.25 別のユーザーが編集しようとしたらロートURLにリダイレクトさせる`correct_user`メソッドの作成
(`correct_user`メソッド内で@userを定義しているので、edit、updateアクションから@userへの代入文を削除)

リスト10.27、10.28 `current_user?`メソッド作成し、correct_userに組み込む


10.2.3 フレンドリーフォワーディング
ログインしていないユーザーが保護されている編集ページなどにアクセスしようとしたとき、ログイン後はアクセスしようとしていたページに飛ばしてあげる
リスト10.29 ①未ログイン状態で編集ページにアクセス②ログイン③編集ページにリダイレクトされているか
テストが書けたので機能を実装していく

ユーザーを希望のページに転送させるには、行こうとしていたページをどこかに保存し、その場所にリダイレクトさせる
リスト10.30 記憶したページもしくはデフォルトのページに飛ばした後、保存した希望ページ情報を削除する`redirect_back_or`メソッド、希望ページを保存しておく`store_location`メソッド

リスト10.31 store_locationメソッドをbeforeフィルターのlogged_in_userに追加

リスト10.32 redirect_back_orメソッドをcreateアクションに追加
return文やメソッド内の最終行が呼び出されない限り、リダレクトはメソッドの最後に行われる

演習
1.ログイン前に渡されたURLに行っているか、ログイン後は保存されていたURLが消去されているか
渡されたURLに初回のみ転送されていることを確認


10.3 全てのユーザーを表示する
全てのユーザーを一覧表示、DBにサンプルデータを追加する方法、ユーザー出力のページネーション(ページ分割)の方法

10.3.1 ユーザーの一覧ページ
一覧ページはログインしたユーザーのみが見れるようにする
リスト10.34 indexアクションがリダイレクトするかテスト
リスト10.35 beforeフィルターでindexアクションの保護
リスト10.36 User.allで全てのユーザーを取得し@user変数に代入するindexアクション
リスト10.37 indexページのhtml
リスト10.39 indexページのcss
リスト10.40 ヘッダーにリンク追加
演習
log_in_as(@user)でログインした後ルートURLが表示されているか、リンクリンクがあるかテスト

10.3.2 サンプルのユーザー
ユーザーを追加する
リスト10.42 GemfileにFaker gem追加
今回は例外的に本番環境でも適用させる

$ bundle update
$ bundle install

リスト10.43 db/seeds.rbファイルに railsタスク追加

$ rails db:migrate:reset
$ rails db:seed
データベースをリセットしてからrailsタスク実行

10.3.3 ページネーション
1つのページに表示するユーザーを分割する
リスト10.44 Gemfileにwill_pageとbootstrap-will_paginate gem追加

$bundle install
サーバー再起動

ページネーションが動作するにはindexビューとindexアクションを書き換える必要がある
リスト10.45 will_paginateメソッドを上下に置くことでページリンクが上下に配置される

リスト10.46 indexアクションのUser.allをpaginateに書き換え
params[:page]はwill_paginateによって自動的に生成される


10.3.4 ユーザー一覧のテスト
ログイン、indexページにアクセス、最初のページにユーザーがいるか確認、ページネーションのリンクがあるか確認のテストを作成する

リスト10.47 fixtureにユーザー追加

indexページに対する統合テスト作成
$ rails generate integration_test users_index

リスト10.48 paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認

演習
2. count:2で個数指定する


10.3.5 パーシャルのリファクタリング
動作を変えないようコードを整理する
リスト10.50 liをrenderに置き換える
リスト10.51 パーシャル作成
renderはUserクラスのuser変数に対して実行しているのでパーシャル名は`_user.html.erb`
リスト10.52 renderを＠users変数に対して実行
renderは@usersをUserオブジェクトのリストであると推測し、ユーザーのコレクションが与えられていた場合、自動的にそれぞれのユーザーを_user.html.erbパーシャルで出力する


10.4 ユーザーを削除する
ユーザーを削除するためのリンク追加、削除に必要なdestroyアクション実装。その前に管理(admin)ユーザーのクラス作成
承認(authorization)においてこのような特権のセットをroleと呼ぶ

10.4.1 管理ユーザー
Userモデルにadmin属性追加
admin?メソッドが使えるようになったので管理ユーザーの状態を確認できる
$ rails generate migration add_admin_to_users admin:boolean
↑のマイグレーションを実行するとadminカラムがusersテーブルに追加される
リスト10.54 あえてdefault:false引数を与えて意図を明確に示すことができる
$rails db:migrate

リスト10.55 最初のユーザーだけ管理者にする
DBをリセットしてからサンプルデータを再生成

Strong Parametersを使って攻撃者から保護する
具体的にはparamsハッシュにrequireとpermitを使い、任意のユーザーが自身に管理者権限を付与できないようにする

演習
他のユーザーでログインした後、patchリクエストを送るがadminは許可されていないのでassert_not @other_user.reload.admin?がtrueになる

10.4.2 destroyアクション
ユーザー一覧のページにdestroyアクションのリンク追加
リスト10.57 current_userが管理者かつ削除対象が管理者でない場合のみ削除リンクが表示される
リスト10.58 destroyアクションの実装
該当のユーザーを削除し、フラッシュメッセージを表示させてから一覧にリダイレクトする
リスト10.59 beforeフィルターで管理者だけがdestroyアクションにアクセスできるようにする

10.4.3 ユーザー削除のテスト
リスト10.60 fixture内の最初のユーザーを管理者にする

Userコントローラのテストのためにアクション単位でアクセス制御をテストする
削除をテストするためにDELETEリクエストを発行してdestroyアクションを直接動作させる
リスト 10.61 ログインしていないユーザーはログイン画面にリダイレクトさせる。　ログイン済みであっても管理者でなければホーム画面にリダイレクトさせる
assert_no_differenceメソッドでユーザー数が変化していないことを確認

管理者でないユーザーの振る舞いと管理者ユーザーの振る舞いを一緒に確認したい
管理者であれば一覧に削除リンクが表示される仕様を利用してusers_index_test.rbにテスト追加
リスト10.62 first_page_of_usersに1ページ目のユーザーを入れて、１つずつ取り出す。それが管理者であれば名前が表示され、管理者でなければ削除リンクが表示される
assert_difference 'User.count', -1 doで中のコード実行後ユーザーが1人減っていることを確認
管理者以外でログインしたとき削除リンクが存在しないことを確認