# 10章 edit, update, index, destroyアクションを加え、RESTアクションを完成させる。 ユーザの削除は、管理者ユーザという特権クラスを作成し、このユーザにのみ削除できるようにする。   ## ユーザを更新する POSTリクエストに応答するcreateの代わりに、PATCHリクエストに応答するupdateアクションを作成する。 ユーザ登録は誰でもできるが、ユーザ更新はユーザ自身しかできないようにする。   ## 編集フォーム Userコントローラにeditアクションを追加して、ユーザをデータベースから取り出し、内容を表示する必要がある。ユーザのidは **params[:id]** で取り出す。   **app/controllers/users_controller.rb** ```ruby class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end #################################### def edit @user = User.find(params[:id]) end #################################### private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end ``` 次に、ユーザ編集のビューファイルを作成する。登録フォームに似ていることから、パーシャルにまとめることができる。   **app/views/users/edit.html.erb** ```html <% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) 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, "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="http://gravatar.com/emails" target="_blank">change</a> </div> </div> </div> ``` error_messagesパーシャルを再利用している。また、下のほうでは、target="_blank"使っているが、セキュリティ上小さな問題がある。 @userインスタンス変数をつかうと。自動的に値が引き出される。   *ここで重要なのが、新規登録フォームのPOSTリクエストとユーザ編集用のPATCHリクエストの* ***form_for(@user)*** *が完全一致していてい区別がつかない* どのようにつかいわけているのか... Railsでは、Active Recordのnew_record?論理値メソッドを使って区別している。 ```bash $ rails console >> User.new.new_record? => true >> User.first.new_record? => false ``` すなわち、 ***trueの時はPOST, falseの時はPATCHを使い分けている。***   ## 演習 1.先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。 A. ```html <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a> ```   2.リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3。(関連するリスト 7.27の演習課題を既に解いている場合、この演習課題をうまく解けない可能性があります。うまく解けない場合は、既存のコードのどこに差異があるのか考えながらこの課題に取り組んでみましょう。例えば筆者であれば、リスト 10.5で用いた変数を渡すテクニックを使って、リスト 10.6やリスト 10.7で必要になるURLをリスト 10.5に渡してみるでしょう。) A. リストのようにformパーシャルを作成し、newとeditを簡略化でぉた。   ## 編集の失敗 編集に失敗した場合について扱う。適切なデータが送信された場合は、update_attributesを使い、ユーザを更新する。無効な情報の場合、結果として、 **false** が返され、elseに分岐し、編集ページにレンダリングするようにする。これは、createアクションに似ている。   **app/controllers/users_controller.rb** ```ruby class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end ############################################ def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # 更新に成功した場合を扱う。 else render 'edit' end end ############################################ private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end ``` private内で定義されているuseer_paramsを使うことで脆弱性を防止している。   ## 演習 1.編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。 A. nameを空欄にしたら、「Name can't be blank」がでた。   ## 編集失敗時のテスト エラーを検知する統合テストを作成する。 ```bash $ rails generate integration_test users_edit ``` テストは以下のように書く。 **test/integration/users_edit_test.rb** ```ruby 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' # editビューが描画されるか patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } # 無効な情報をおくる assert_template 'users/edit' # editビューが再描画されるか end end ``` これでテストはGREENになる。 本当はGREENになるはずが、うまくいかなかったので、以下を参照 https://qiita.com/shiomarukun/items/12937507b39108336aef pathを指定する必要があるらしい。   ## 演習 1.リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。 A. テストの最後の行に assert_select "div.alert", "The form contains 4 errors." を追加。   ## TDDで編集を成功させる 「編集の成功」を実装する前に統合テストを先に書いてみる。これが一般的らしい。 **test/integration/users_edit_test.rb** ```ruby require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit" do get edit_user_path(@user) # 編集ページにアクセスできるか assert_template 'users/edit' # 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? # flashメッセージがからでないか assert_redirected_to @user # プロフィールページにリダイレクトされるか @user.reload # データベースから最新のユーザー情報を読み込み assert_equal name, @user.name # 正しく更新されたか assert_equal email, @user.email # 正しく更新されたか end end ```   次に、updateアクションをflashが出るように、リダイレクトされるように変更。 **app/controllers/users_controller.rb** ```ruby 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 ```   しかし、このままでは、パスワードに関する部分で、引っかかってしまう。そこで、ユーザモデルのバリデーションを追加する。   **app/models/user.rb** ```ruby class User < ApplicationRecord attr_accessor :remember_token before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password #################################################### validates :password, presence: true, length: { minimum: 6 }, allow_nil: true #################################################### . . . end ```   空のパスワードでユーザ登録できるわけではない。(存在性のバリデーションは保たれているから。) よって、編集に対するテストと実装ができた。   ## 演習 1.実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。 A. nameを「Rails Tutorial」に変更できた。   2,もしGravatarと紐付いていない適当なメールアドレス (foobar@example.comなど) に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。 A. ![Gravatar](data:image/png;base64,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)   ## 認可 どのユーザでもあらゆるアクションにアクセス出来てしまうため、だれでも情報の編集ができてしまう。ここでは、ユーザにログインを要求し、かつ自分以外のユーザ情報を変更できないように制御する。   ## ユーザーにログインを要求する before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する。 **app/controllers/users_controller.rb** ```ruby 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 ``` beforeフィルターは全てのメソッドに適応されるが、onlyオプションを使うことで、onlyの中の:editと:updateアクションだけにこのフィルタが適応されるようになる。   一度ログアウトして、ユーザ編集ページ(/users/1/edit) にアクセスしてみることで確認できる。 editアクションやupdateアクションでログインを要求するようにテストが書かれていたため、失敗する。   このため、editアクションや、updateアクションをテストする前にログインしておく必要がある。log_in_asヘルパーをedit, updateのテストで用いる。 **test/integration/users_edit_test.rb** ```ruby 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フィルターをコメントアウトしてもテストが通ってしまう。 そこで、users_controller_test.rbにbeforeフィルターを仮定したテストを書く必要がある。 **test/controllers/users_controller_test.rb** ```ruby 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? # flash空ではないか assert_redirected_to login_url # loginページにリダイレクトされたか end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } # PATCHリクエストを送信したか assert_not flash.empty? # flash空ではないか assert_redirected_to login_url # loginページにリダイレクトされたか end end ``` これでやっとテストが通る。 これで、誰でも編集できてしまうバグがあっても、検知できるようになる。   ## 演習 1.デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。 A. エラーが検知された。   ## 正しいユーザーを要求する ユーザが自分の情報だけを編集できるようにする。テスト駆動開発を進めながら、セキュリティモデルが正しく実装されているか確認する。 まず、サンプルユーザを追加する。 **test/fixtures/users.yml** ```bash 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') %> ```   次に@other_userを定義し、log_in_asメソッドを使い他のユーザにアクセスさせるようにする。 **test/controllers/users_controller_test.rb** ```ruby 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 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 flash.empty? assert_redirected_to root_url end end ```   別のユーザのプロフィールを編集しようとしたときリダイレクトさせるために、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする。 **app/controllers/users_controller.rb** ```ruby class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] 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 ```   これでテストが通る。 最後に、current_user?という論理値を返すメソッドを実装する。このようにすることで、少しわかりやすくする。 **app/helpers/sessions_helper.rb** ```ruby # 渡されたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user end ``` これで、実装できたので、 app/controllers/users_controller.rb ```ruby # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end ``` このように変更できる。   ## 演習 1.何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。 A. getリクエスト、patchリクエストでどちらもHTTTPメソッドでパスも異なるため。   2.上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか? A. editアクション。1にログインした人が、edit/2のURLをたたくだけだから。   ## フレンドリーフォワーディング ログインしていないユーザが自身の編集ページにアクセスにログインしたあと、編集ページにリダイレクトさせる。 **test/integration/users_edit_test.rb** ```ruby test "successful edit with friendly forwarding" do get edit_user_path(@user) # 編集ページにアクセスできるか log_in_as(@user) # loginできるか assert_redirected_to edit_user_url(@user) # 自身のeditページにリダイレクトされたか .... end ``` ユーザの希望のページに飛びたいとき、リクエスト時点のページをどこかに保存し、その場所にリダイレクトさせる必要がある。この動作をstore_locationとredirect_back_orの2つのメソッドを使って実装する。 **app/helpers/sessions_helper.rb** ```ruby module SessionsHelper . . . # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.original_url if request.get? end end ``` store_locationメソッドでは、 リクエストが送られたURLをsession変数の:forwarding_urlキーに格納する。GETリクエストのみ。これにより、ログインしていないユーザーがフォームを使って送信した場合を防ぐ。このため、if request.get?という条件文を使ってこのケースに対応させる。 これを反映させる。 **app/controllers/users_controller.rb** ```ruby # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end end ```   フォワーディング自体を実装するには、redirect_back_orメソッドを使う。リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトするように実装する。   ```bash session[:forwarding_url] || default ``` **app/controllers/sessions_controller.rb** ```ruby def create user = User.find_by(email: params[:session][:email].downcase) if user && 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 ``` テストが通るか確認。   ## 演習 1.フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト (プロフィール画面) に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。 A. **test/integration/users_edit_test.rb** ```ruby test "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_url(@user) assert_nil session[:forwarding_url] ... ``` このように書く   2.7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください (デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう (デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって (コラム 1.1)、落ち着いて対処してみましょう)。 A. **app/controllers/sessions_controller.rb** ```ruby def new debugger end ``` 結果 ```bash (byebug) session[:forwarding_url] "http://localhost:3000/users/1/edit" (byebug) request.get? true ```   ## すべてのユーザーを表示する indexアクションを追加する。すべてのユーザを一覧表示し、ページネーションを実装する。また、管理者権限を作成し、管理者ならユーザを削除できる機能も実装する。   ## ユーザーの一覧ページ indexページはログインしたユーザにしか見せないようにする。また、未登録のユーザがデフォルトで表示できるページを制限する。 まずはindexアクションが正しくリダイレクトするかを検証するテスト   **test/controllers/users_controller_test.rb** ```ruby require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end ######################################################### test "should redirect index when not logged in" do get users_path assert_redirected_to login_url end ######################################################### . . . end ``` 次に、beforeフィルターのlogged_in_userにindexアクションを追加。 **app/controllers/users_controller.rb** ```ruby class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update] def index end def show @user = User.find(params[:id]) end . . . end ``` 次に、すべてのユーザを表示するようにする。 User.allを使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@usersに代入させる。   **app/controllers/users_controller.rb** ```ruby def index @users = User.all end ``` 実際のindexページを作成するには、ユーザを列挙してユーザごとにliタグで囲むビューを作成する必要がある。ここではeachメソッドを使って作成。   **app/views/users/index.html.erb** ```html <% 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> ``` html作成に伴い、cssも追加。 **app/assets/stylesheets/custom.scss** ```css /* Users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-bottom: 1px solid $gray-lighter; } } ``` 最後にサイトのヘッダーにユーザ一覧用のリンクを追加。 **app/views/layouts/_header.html.erb** ```html <header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <!-- ################################################ --> <li><%= link_to "Users", users_path %></li> <!-- ################################################ --> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <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> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header> ``` 最後に、テストが通るか確認。   ## 演習 1.レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。 A. 最終的なsite_layout_test.rb **test/integration/site_layout_test.rb** ```ruby require 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest 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=?]", about_path assert_select "a[href=?]", contact_path assert_select "a[href=?]", login_path get contact_path assert_select "title", full_title("Contact") get signup_path assert_select "title", full_title("Sign up") end def setup @user = users(:michael) end test "layout links when logged in" do log_in_as(@user) get root_path assert_template 'static_pages/home' 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 end end ``` となる。   ## サンプルのユーザー indexに複数のユーザを追加する。 まず、GemfileにFaker gemを追加。 **Gemfile** ```bash gem 'faker', '1.7.3' ``` 次に、bundle install サンプルユーザを生成するRubyスクリプトを追加する。 **db/seeds.rb** ```ruby 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することでユーザを生成することができる。 ```bash $ rails db:migrate:reset $ rails db:seed ``` また、リセットの部分でエラーが発生したため https://qiita.com/Toshiki23/items/f366504844fd22ad87d9 で解決。   ## 演習 1.試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。 A. root画面にリダイレクトされる。   ## ページネーション indexページに表示されるユーザを分割する。 まず、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方追加する。 **Gemfile** ```bash gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0' ``` 次にbundle installそして、rails s を再起動 ページネーションの手順として、 1.Railsに指示するコードをindexビューに追加する 2.indexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える   まずは、ビューに特殊なwill_paginateメソッドを追加。 **app/views/users/index.html.erb** ```ruby <% 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 % ``` このwill_paginateメソッドは、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成してくれるが、paginateメソッドを使った結果が必要が必要なので、@usersを **app/controllers/users_controller.rb** ```ruby class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.paginate(page: params[:page]) end . . . end ``` このように書き換えることによって、ユーザ一覧ページが動作するようになる。   ## 演習 1.Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。 A. irb(main):001:0> users = User.paginate(page: nil) User Load (7.3ms) SELECT "users".* FROM "users" LIMIT ? OFFSET ? [["LIMIT", 11], ["OFFSET", 0]] (1.0ms) SELECT COUNT(*) FROM "users" => #<ActiveRecord::Relation [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-03-05 06:25:21", updated_at: "2020-03-05 06:25:21", password_digest: "$2a$10$W.IpSi3wdcygiruX5aOBTOsXPIV2tUfPRoTE.INZjqU...", remember_digest: nil, admin: true>, #<User id: 2, name: "Josiah Orn", email: "example-1@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$m0R7RoquRK0m3TvR3YJ.B.wvGG1eyQi1xqikPApHP.....", remember_digest: nil, admin: false>, #<User id: 3, name: "Emma Bergstrom", email: "example-2@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$maPeZiS2OnVm7W40ZswpoOppADOARU4rr1WSP.LIkh8...", remember_digest: nil, admin: false>, #<User id: 4, name: "Hayley Pollich", email: "example-3@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$N34.OrvJ4jQoteuuT3ho5ONWh7Mvvcud/OZV/fH7/lm...", remember_digest: nil, admin: false>, #<User id: 5, name: "Pauline Brakus", email: "example-4@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$PsjEtV546VUIKHLFxwCooO8IXtIv2mjpdzh8ZzYS4n8...", remember_digest: nil, admin: false>, #<User id: 6, name: "Abdullah Olson", email: "example-5@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$HeI2UsVCWvnL4ILESf9rDe6SdwkqTjvkSfUAHHHRdUN...", remember_digest: nil, admin: false>, #<User id: 7, name: "Saul Legros", email: "example-6@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$NcSxtqvMMu/y8vhFmDsMferxhUTHQ4qYmE47CnQHEmM...", remember_digest: nil, admin: false>, #<User id: 8, name: "Ford Stanton", email: "example-7@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$Ve5rGiq0ndgavQjLbwel3uSn.3klStSVTBfOzzsxLIH...", remember_digest: nil, admin: false>, #<User id: 9, name: "Allie Doyle", email: "example-8@railstutorial.org", created_at: "2020-03-05 06:25:23", updated_at: "2020-03-05 06:25:23", password_digest: "$2a$10$9QvpSCdHlXE0I45AYnKxieQ0OM4GhJb4bH3crJTMKCO...", remember_digest: nil, admin: false>, #<User id: 10, name: "Cyril Larkin", email: "example-9@railstutorial.org", created_at: "2020-03-05 06:25:24", updated_at: "2020-03-05 06:25:24", password_digest: "$2a$10$5VyUcwAvpUtHrOkGB1AAvOCZ3kWYtKew6.Jm.Yy8Wn....", remember_digest: nil, admin: false>, ...]>   2.先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。 A. users.class => User::ActiveRecord_Relation User.all.class => User::ActiveRecord_Relation 同じ。   ## ユーザー一覧のテスト ページネーションに対する簡単なテストを書く。 1.indexページにアクセス 2.最初のページにユーザがいることを確認 3.ページネーションのリンクがあることを確認   fixtureの埋め込みrubyを利用して30人のユーザを追加する。 **test/fixtures/users.yml** ```ruby 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') %> 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 %> ``` 次にindexページに対する統合テストを書く。 ```bash $ rails generate integration_test users_index ``` 今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザがいることを確認する。 **test/integration/users_index_test.rb** ```ruby 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 # indexページにアクセスできるか assert_template 'users/index' # indexビューが描写されてるか assert_select 'div.pagination' # paginationクラスのdivタグがあるか User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name # ユーザが存在するか end end end ``` テストが通る。   ## 演習 1.試しにリスト 10.45にあるページネーションのリンク (will_paginateの部分) を2つともコメントアウトしてみて、リスト 10.48のテストが redに変わるかどうか確かめてみましょう。 A. redに変わった。   2.先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが greenのままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。 A. テストはGREENのまま、 2つともテストしたい場合、 **test/integration/users_index_test.rb** ```ruby assert_select 'div.pagination', count:2 ``` count 2を追加する。   ## パーシャルのリファクタリング リファクタリング(動作を変えずにコードを整理すること) で、コンパクトなビューを作成できる。 第一歩として、ユーザのliをrender呼び出しに置き換える。 **app/views/users/index.html.erb** ```html <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <!-- ################################################ --> <%= render user %> <!-- ################################################ --> <% end %> </ul> <%= will_paginate %> ``` ここでは、renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する必要がある。 **app/views/users/_user.html.erb** ```html <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> ``` さらにrenderを@users変数に対して直接実行。 **app/views/users/index.html.erb** ```html <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %> ``` Railsは@users をUserオブジェクトのリストであると推測する。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力する。   これに限らず、リファクタリングを行う場合は必ずテストすることを心掛ける必要がある。   ## 演習 1.リスト 10.52にあるrenderの行をコメントアウトし、テストの結果が redに変わることを確認してみましょう。 A. redに変わる。   ## ユーザーを削除する destoryを実装し、ユーザを削除するリンクを作成する。 その前に、削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成する。   ## 管理ユーザー 特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加。 変更後のデータモデルは以下のようになる。 ![users](https://railstutorial.jp/chapters/images/figures/user_model_admin_3rd_edition.png) まず、マイグレーションを実行。 ```bash $ rails generate migration add_admin_to_users admin:boolean ``` adminカラムが追加される。また、明示的にデフォルトでは管理者になれないということを示すため、default: falseという引数をadd_columnに追加される。 **db/migrate/[timestamp]_add_admin_to_users.rb** ```ruby class AddAdminToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :admin, :boolean, default: false end end ``` 最後にマイグレーションする。 ```bash $ rails db:migrate ``` ここで、Railsコンソールで動作を確認すると、期待どおりadmin属性が追加されて論理値を取ることができる。   ```bash $ rails console --sandbox >> user = User.first >> user.admin? => false >> user.toggle!(:admin) => true >> user.admin? => true ``` ここではtoggle!メソッドを使って admin属性の状態をfalseからtrueに反転しています。   最後に最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新する。 **db/seeds.rb** ```ruby 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.rbが書けたので、 データベースをリセットしてサンプルデータを生成 ```bash $ rails db:migrate:reset $ rails db:seed ``` ### Strong Parameters、再び ここで、 patch /users/17?admin=1 のようなリクエストが送信された場合、 17番目のユーザーを管理者に変えてしまいます。   ここの対策として、Strong Parametersを使ってparamsハッシュに対してrequireとpermitを呼び出す。 ```bash def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end ``` 上のコードは、許可された属性リストにadminが含まれていない。 これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できる。   ## 演習 1.Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は redになるはずです。 A. **test/controllers/users_controller_test.rb** ```ruby 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, admin: true } } assert_not @other_user.reload.admin? end ```   ## destroyアクション destroyアクションへのリンクを追加する。 まず、ユーザindexページの各ユーザに削除用リンクを追加し、続いて管理ユーザーへのアクセスを制限します。これによって、現在のユーザーが管理者のときに限り [delete] リンクが表示されるようになります。   **app/views/users/_user.html.erb** ```html <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> ``` この削除リンクが動作するためには、、destoryアクションを追加する必要がある。このアクションでは、destroyメソッドを使って削除し、最後にユーザindexに移動するようにする。 また、ユーザを削除するためにはログインしていなくてはならないので、destoryアクションもlogged_in_userフィルターに追加する。   **app/controllers/users_controller.rb** ```ruby class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] . . . def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end private . . . end ``` 最後に、destroyアクションにもアクセス制限を行う。これを行えば、管理者だけがユーザを削除できるようになる。 beforeフィルターを使って、destoryアクションのアクセスを制御する。 **app/controllers/users_controller.rb** ```ruby 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 redirect_to(root_url) unless current_user.admin? end end ```   ## 演習 1.管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか? A. DELETE FROM "users" WHERE "users"."id" = ? [["id", 6]] DELETE FROM "users" WHERE "users"."id" = ? [["id", 7]] このようなログが確認された。   ## ユーザー削除のテスト ユーザを削除するような重要な操作はテストを書くべき。 まずは、ユーザー用fixtureファイルを修正し、今いるサンプルユーザーの一人を管理者にする。 **test/fixtures/users.yml** ```bash 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 %> ``` Usersコントローラをテストするために、アクション単位でアクセス制御をテストする。 2つのケースをチェックする。 1つ目は、ログインしていないユーザであれば、ログイン画面にリダイレクトされる。 2つ目はログイン済みであっても管理者でなければ、ホーム画面にリダイレクトされること。   **test/controllers/users_controller_test.rb** ```ruby 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 delete user_path(@user) end assert_redirected_to root_url end end ``` このとき、assert_no_differenceメソッドを使って、ユーザー数が変化しないことを確認している。 また、管理者ならば、 ```bash assert_difference 'User.count', -1 do delete user_path(@other_user) end ``` このように書け、DELETEリクエストを適切なURLに向けて発行し、User.countを使ってユーザー数が 1 減ったかどうかを確認している。   最終的にまとめられたindexの統合テストは **test/integration/users_index_test.rb** ```ruby 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 ``` このようになる。最後にテストで確認。   ## 演習 1.試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が redに変わることを確認してみましょう。 A. redに変わる。     ## 最後に ```bash $ git add -A $ git commit -m "Finish user edit, update, index, and destroy actions" $ git checkout master $ git merge updating-users $ git push $ rails test $ git push heroku $ heroku pg:reset DATABASE $ heroku run rails db:migrate $ heroku run rails db:seed $ heroku restart ```