LoginSignup
1
0

More than 3 years have passed since last update.

【第10章】Railsチュートリアル 5.1(第4版) ユーザーの更新・表示・削除

Last updated at Posted at 2020-01-20

はじめに

個人的な理解・備忘録を目的としてます。
筆者自身は動画版Railsチュートリアルで進めているため、アプリ作成中コード・ブランチ名などの若干の違いがありますので参考程度に流し見して頂けたら嬉しいです。
理解不足のため、何かありましたらコメント等ご指摘してくださると幸いです(^_^;)

10.0 目標

未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる。

その他 個人的進行
単数形と複数形
モデル(概念的)→単
それ以外→複数(ほぼ全部)

10.1 ユーザーを更新する

10.1.1 編集フォーム

編集フォームのモックアップ
公式より参考)

スクリーンショット 2020-01-17 13.53.22.png

まずはフィーチャーブランチを作成。

$ git checkout -b updating-users

最初はeditアクションを実装する。

app/controllers/users_controller.rb
  # GET /users/:id/edit
 def edit
    @user = User.find(params[:id])
  #=> app/views/users/edit.html.erb
  end
  end

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の送信先を指定-->
    <%= form_for(@user) do |f| %>
    <!--エラーメッセージ-->
      <%= render 'shared/error_messages' %>

       <!--入力formを作成-->
      <%= 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>

        
ユーザーのeditビュー画面の表示と、saveを押すとupdateアクションに移行しているか(エラー画面)を確認する。

スクリーンショット 2020-01-17 14.50.26.png

スクリーンショット 2020-01-17 14.54.01.png

Webブラウザは通常GETリクエストとPOSTの2つのリクエストのみのため、PATCHリクエストを送信できないので、RailsはPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを「偽造」している。

edit.heml.erbnew.html.erbはform_for(@user)...と構造は同じだが、
editにはDBに入っている値、newはDBにない新しいインスタンスが入り、これをRailsのActive Recordにあるnew_record?メソッドが判断する。

Ruby on Rails チュートリアル 第10章 ユーザー更新 beforeフィルター フレンドリーフォワーディング adminまで

最後に、サイト内移動用のヘッダーSettingsにユーザー一覧表示用のリンクを追加する。

app/views/layouts/_header.html.erb

<li><%= link_to "Users", users_path %></li>

<li><%= link_to "Settings", edit_user_path(current_user) %></li>

10.1.2 編集の失敗

ユーザー登録に失敗したときと似た方法で、編集に失敗した場合について扱う。updateアクションを追加して失敗時の処理表示を実装する。

app/controllers/users_controller.rb

# PATCH /users/:id
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # Success
    else
      # Failure
      #=> @user.errors.full_messages()
      render 'edit'
    end
  end

パスワードなしで更新すると、エラーメッセージが出る。

スクリーンショット 2020-01-17 17.24.08.png

10.1.3 編集失敗時のテスト

統合テストを生成

$ rails generate integration_test users_edit

テスト内容を記載する。流れは下記の通り。

  1. まず編集ページにアクセス
  2. editビュー(テンプレート)が描画されるかどうかをチェック
  3. その後、無効な情報を送信
  4. editビューが再描画されるかどうかをチェック

この特徴として、PATCHリクエストを送るためにpatchメソッドを使っているというものがある。patchメソッドはとは、getやpost、deleteメソッドと同じように、HTTPリクエストを送信するためのメソッド。

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: "bar" } }

    assert_template 'users/edit'
  end
end

テストが通過すればok!

10.1.4 TDDで編集を成功させる

TDD

TDDとはテスト駆動開発(Test-Driven Development: TDD)の名称で、プログラム実装前にテストコードを書き(テストファーストと呼ばれる)、動作する必要最低限な実装をとりあえず行った後にコードを改善していく手法である。

基本スタイルは
1. (RED:失敗する)テストコードを書く
2. テストに通る(GREEN:成功する)最低限のコードを書く
3. コードの重複を除去する(リファクタリング)
を繰り返すもので、アジャイル開発等でよく用いられる。
(※本記事では(公式
の理解を目的とするため、REDは一部省略してリファクタリングに移る場合もあります)

この節では編集フォームが動作するようにする。
今回はアプリケーション用のコードを実装する前に統合テストとして受け入れテスト (Acceptance Tests)を行う。
 受け入れテストとは、ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決める(成功したらどうなるか?の)テストとされている。
先ほどのテストをベースとして、

  1. 今回はユーザー情報を更新する有効な情報を送信する
  2. 次に、flashメッセージが空でないかどうか
  3. プロフィールページにリダイレクトされるかどうか
  4. DBのユーザー情報をインスタンスに上書きする(リロード)
  5. データベース内のユーザー情報が正しく変更されたかどうか
test/integration/users_edit_test.rb

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

もちろん成功部を実装していないためテストしてもRED。

updateアクションif文に成功パターンとして、flashと@userでリダイレクト動作を追加する。

app/controllers/users_controller.rb

def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # Success
      flash[:success] = "Profile updated"
      redirect_to @user
    else

先ほどのテストでパスワードが空で渡しているためバリデーションで弾かれるが、例外処理としてallow_nil: trueというオプションをvalidatesに追加してテストを通過 & 更新flashの表示を確認。

app/models/user.rb

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

image.png

10.2 認可

editアクションとupdateアクションの動作導入はできたが、今のままでは誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうので、ユーザーにログインを要求し、かつ自分以外のユーザー情報を勝手に変更できないように制御する。こういったセキュリティ上の制御機構をセキュリティモデルと呼ぶ。
公式より参考)
(実際のところ、本人でもセッションが切れてしまった場合も含む)
この節では、ログインしていないユーザーが保護された(自分の権限のない)ページにアクセスしようとしたらログインを促すよう対処する。

認証と認可

日本語だと似たような印象になるが、
認証(英:Authentication, AuthN)
 → 何者であるかを特定すること。
ex.「〇〇ですか?」と尋ねられる、職務質問で身分証の提示を求められるなど
Railsでは、*** authenticateメソッド***

認可(英:Authorization, AuthZ)
 → 行動やリソースの使用を許可すること。
ex.「△△の資格がありますね。あのカウンターへどうぞ」と権限を認められる。
Railsでは、beforeメソッド`

<参考>
認証と認可

「認証と認可」について調べたので、違いをざっくり整理した

10.2.1 ユーザーにログインを要求する

beforeフィルター

beforeフィルターとは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みのこと。

今回はユーザーにログインを要求するためコントローラーに追加する。

before_actionの後にメソッド名をシンボルでlogged_in_userメソッドを定義、その後に:onlyオプション (ハッシュ) で渡されたeditアクション、updateアクションを入れることで、「only以下のアクション(edit、updateアクション)が実行される前に、最初に定義したメソッド(logged_in_user)を実行してね」という内容になる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]

省略

# beforeアクション

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

end

スクリーンショット 2020-01-17 22.45.55.png

この段階ではテストしててもRED。原因としては、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったため。対処としては、editアクションやupdateアクションをテストする前にログインしておくよう、log_in_asヘルパーを実装する。

test/integration/users_edit_test.rb

test "unsuccessful edit" do
    log_in_as(@user) #=> Michaelとしてログイン

省略

 test "successful edit" do
    log_in_as(@user) #=> Michaelとしてログイン

テストはGREEN。
しかし、実はまだbeforeフィルターの実装はまだ終わっていない。セキュリティモデルに関する実装を取り外してもテストが通ってしまうか、beforeフィルターをコメントアウトしてテスト確認。

app/controllers/users_controller.rb

class UsersController < ApplicationController
  # before_action :logged_in_user, only: [:edit, :update]
0 failures, 0 errors, 0 skips

通過してしまった。
beforeフィルターは基本的にアクションごとに適用していくので、Usersコントローラのテストもアクションごとに書いていく。手順としては、
1.routesからedit、updateの正しい種類のHTTPリクエスト確認
2. そのリクエストを使ってeditアクションとupdateアクションをそれぞれ実行
3. flashにメッセージが代入されたかどうか
4. ログイン画面にリダイレクトされたかどうか

$ rails routes
           Prefix Verb   URI Pattern                  Controller#Action
     sessions_new GET    /sessions/new(.:format)      sessions#new
             root GET    /                            static_pages#home
static_pages_home GET    /static_pages/home(.:format) static_pages#home
             help GET    /help(.:format)              static_pages#help
            about GET    /about(.:format)             static_pages#about
          contact GET    /contact(.:format)           static_pages#contact
           signup GET    /signup(.:format)            users#new
                  POST   /signup(.:format)            users#create
            login GET    /login(.:format)             sessions#new
                  POST   /login(.:format)             sessions#create
           logout DELETE /logout(.:format)            sessions#destroy
            users GET    /users(.:format)             users#index
                  POST   /users(.:format)             users#create
         new_user GET    /users/new(.:format)         users#new
        edit_user GET    /users/:id/edit(.:format)    users#edit
             user GET    /users/:id(.:format)         users#show
                  PATCH  /users/:id(.:format)         users#update
                  PUT    /users/:id(.:format)         users#update
                  DELETE /users/:id(.:format)         users#destroy

editとupdateアクションの保護に対するテスト追加。
beforeフィルターが入っているかの確認(ユーザー:Michael追加)。具体的には、
1. ログインしてない状況でgetリクエスト→ユーザーの編集ページに
2. flashが出て
3. ログインにリダイレクトされるか

もう一つはpatchリクエスト(ブラウザ以外からもある)にもneforeの確認を行うもの。

test/controllers/users_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

テストして

2 failures, 0 errors, 0 skips

エラーでなく失敗したのok(コメントアウト解除)

10.2.2 正しいユーザーを要求する

ユーザーが自分の情報だけを編集できるようにしたい。まずはユーザーの情報が互いに編集できないことを確認するために、ユーザー用のfixtureファイル(YAML)に2人目のユーザー(Archer)を追加する。

test/fixtures/users.yml

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

次に、log_in_asメソッドを使ってeditアクションとupdateアクションをテスト。このとき、既にログイン済みのユーザーを対象として(①ArcherさんでログインしてMichaelさん入ろうとする、②Archerさんでログインしてpatchを送ろうとする)、ログインページではなくルートURLにリダイレクトしている点に注意。

test/controllers/users_controller_test.rb

def setup
    @user       = users(:michael)
    @other_user = users(:archer) #=> 他ユーザー追加
  end

テストではエラーになるので、beforeアクションに書き足す。

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update] #順番に注意! 上から順番に「ログインしたユーザー」且つ正しいユーザー

# beforeアクション

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

    # 正しいユーザーかどうか確認
  def correct_user
    # GET   /users/:id/edit
    # PATCH /users/:id
    @user = User.find(params[:id])
    redirect_to(root_url) unless @user == current_user
  end

テストは通過。

リファクタリングとして、一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装。correct_userの中で使えるようにしたいので、Sessionsヘルパーの中にこのメソッドを追加する。

(旧)unless @user == current_user
(新)unless current_user?(@user)
app/controllers/users_controller.rb

redirect_to(root_url) unless current_user?(@user)  #=> @user == current_user
app/helpers/sessions_helper.rb

# 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end

テスト通過。

10.2.3 フレンドリーフォワーディング

フレンドリーフォワーディング

フレンドリーフォワーディングとは、ユーザーがログインした後、ログイン直前に閲覧していたページヘとリダイレクトさせる(あると便利な)機能のこと。

フレンドリーフォワーディングのテストは、ログイン手前でログインページへ(ユーザさんにログインしてもらう)
ログインした後に編集ページへアクセスするという順序を逆にするもの。

test/integration/users_edit_test.rb

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

実装してないのでテストして失敗(failure)。

ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があり、store_locationredirect_back_orの2つのメソッドを使って対応する。

app/helpers/sessions_helper.rb

# 記憶した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

redirect_back_orメソッド
転送先のURLを保存する場所は(今回は一時的なものなので)DBでなくsessionを使い、もともとユーザーが行きたかった場所を保存しておいてURLがある場合はリダイレクトし、ない場合(sessionが切れたり分からなくなったら)デフォルト値にユーザーのページを表示する。終わったらsessionを消す。
デフォルトのURLは、sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクトします

store_locationメソッド
リクエストが送られたURLをsession変数のforwarding_urlキーに格納。ただし、GETリクエストが送られたときのみ(後置if)。

ログインユーザー用beforeフィルターにstore_locationメソッドを追加する。

app/controllers/users_controller.rb

# ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location  #=> アクセスしようとしたURLを覚えておく
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
app/controllers/sessions_controller.rb

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 #=>  フレンドリーフォワーディングを備える

これでテストは通過する。
Settingsの確認もok.

image.png

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

この節ではすべての(大量の)ユーザーをページごとに一覧表示、
かつsignupしたユーザーのみが閲覧できるindexアクションを実装する。
それに伴い、①DBにサンプルデータを追加する方法、②将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネーション (pagination=ページ分割) の方法、を学ぶ。

モックアップ
公式より参考)

スクリーンショット 2020-01-19 13.31.38.png

10.3.1 ユーザーの一覧ページ

indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテスト。

test/controllers/users_controller_test.rb
#=> 習慣として、indexに関するテストは一番上に書く
  test "should redirect index when not logged in" do
    get users_path #=> user(s)_pathでindexのurl(/users)へgetリクエスト
    assert_redirected_to login_url
  end

beforeフィルターに何もないため失敗するので、beforeフィルターのlogged_in_userindexアクションを追加して、このアクションを保護する。すべてのユーザーを表示するために、User.allを使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@usersに代入。

app/controllers/users_controller.rb

before_action :logged_in_user, only: [:index, :edit, :update] #=> 「:index」追加

  def index
    @users = User.all
  end

ユーザーのindexビュー(app/views/users/index.html.erb)を新規に作成。
userはハッシュを受け取らないので、引数に2つ(gravatar_for userとsize: 50)を与えるとエラーが起こる。


<% 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>

image.png

app/helpers/users_helper.rb

module UsersHelper
  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 }) #=> デフォでsize80追加
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size] #=>変数size ,下記で「?s=#{size}」追加
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

SCSSの追記

app/assets/stylesheets/custom.scss

/* 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)にユーザー一覧ページへのリンクを更新する。


<% if logged_in? %>
  <li><%= link_to "Users", users_path %></li> 

テストして通過。

10.3.2 サンプルのユーザー

indexページに複数のユーザーを表示させてみる。
まずはGemfileFaker gemを追加する。

Gemfile

gem 'bcrypt',         '3.1.12'
gem 'faker',          '1.7.3' #=> 追加 

データベース上にサンプルユーザーを生成するRailsタスク(サンプルユーザーを生成するRubyスクリプト)を追加。
Railsではdb/seeds.rbというファイルを標準とする。
中身としては、
1. まずユーザー(Example User)を作る
2. Fakerの「.name」メソッドからそれっぽいユーザーを99人増やす

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

そしてbundle install。だが、筆者の場合失敗

エラー対応:GemfileにFaker gemを追加できない

bundle installしようとするとエラー。
サーバを止めてもダメ。

環境
Rails v5.1.6
faker v1.7.3

$ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Bundler could not find compatible versions for gem "i18n":
  In snapshot (Gemfile.lock):
    i18n (= 1.7.0)

  In Gemfile:
    rails (= 5.1.6) was resolved to 5.1.6, which depends on
      activesupport (= 5.1.6) was resolved to 5.1.6, which depends on
        i18n (>= 0.7, < 2)

    faker (= 1.7.3) was resolved to 1.7.3, which depends on
      i18n (~> 0.5)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

対応策
fakerのバージョンを指定しない

Gemfile

#旧 gem 'faker',          '1.7.3'
gem 'faker' #=> バージョン指定なし

再度bundle install実行。


$ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Using rake 13.0.1
Using concurrent-ruby 1.1.5
Using i18n 1.7.0
Using minitest 5.10.3
Using thread_safe 0.3.6
Using tzinfo 1.2.5
Using activesupport 5.1.6
Using builder 3.2.3
Using erubi 1.9.0
Using mini_portile2 2.4.0
Using nokogiri 1.10.5
Using rails-dom-testing 2.0.3
Using crass 1.0.5
Using loofah 2.3.1
Using rails-html-sanitizer 1.3.0
Using actionview 5.1.6
Using rack 2.0.7
Using rack-test 1.1.0
Using actionpack 5.1.6
Using nio4r 2.5.2
Using websocket-extensions 0.1.4
Using websocket-driver 0.6.5
Using actioncable 5.1.6
Using globalid 0.4.2
Using activejob 5.1.6
Using mini_mime 1.0.2
Using mail 2.7.1
Using actionmailer 5.1.6
Using activemodel 5.1.6
Using arel 8.0.0
Using activerecord 5.1.6
Using ansi 1.5.0
Using execjs 2.7.0
Using autoprefixer-rails 9.7.2
Using bcrypt 3.1.12
Using bindex 0.8.1
Using rb-fsevent 0.10.3
Using ffi 1.11.2
Using rb-inotify 0.10.0
Using sass-listen 4.0.0
Using sass 3.7.4
Using bootstrap-sass 3.3.7
Using bundler 1.17.3
Using byebug 9.0.6
Using coderay 1.1.2
Using coffee-script-source 1.12.2
Using coffee-script 2.4.1
Using method_source 0.9.2
Using thor 0.20.3
Using railties 5.1.6
Using coffee-rails 4.2.2
Fetching faker 2.10.1
Installing faker 2.10.1
Using formatador 0.2.5
Using ruby_dep 1.5.0
Using listen 3.1.5
Using lumberjack 1.0.13
Using nenv 0.3.0
Using shellany 0.0.1
Using notiffany 0.1.3
Using pry 0.12.2
Using guard 2.13.0
Using guard-compat 1.2.1
Using guard-minitest 2.4.4
Using multi_json 1.14.1
Using jbuilder 2.7.0
Using jquery-rails 4.3.1
Using ruby-progressbar 1.10.1
Using minitest-reporters 1.1.14
Using puma 3.9.1
Using sprockets 3.7.2
Using sprockets-rails 3.2.1
Using rails 5.1.6
Using rails-controller-testing 1.0.2
Using tilt 2.0.10
Using sass-rails 5.0.6
Using spring 2.0.2
Using spring-watcher-listen 2.0.1
Using sqlite3 1.3.13
Using turbolinks-source 5.2.0
Using turbolinks 5.0.1
Using uglifier 3.2.0
Using web-console 3.5.1
Bundle complete! 24 Gemfile dependencies, 82 gems now installed.
Gems in the group production were not installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

無事終了

(とてもありがたかった)ご参考先
Railsチュートリアルでfakerがインストールできない場合の対処法

本編へ戻ります

  
DBリセット(これまでの登録ユーザー初期化)、Railsタスクを実行 (db:seed) 。

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

サンプルですが、たくさんのユーザーさん登場。

スクリーンショット 2020-01-19 15.44.25.png

10.3.3 ページネーション

ユーザーが増えたのはいいが、今度は逆に1つのページに大量のユーザーが表示されて(仮に1万人とかになったときに)重くなってしまう。
そこで解決するのが、ページネーション (pagination) **というもの。
ページネーションとは、検索などに使われてるような
「1つのページに一度に〇〇個だけ表示する」**というもの。
今回は1つのページに一度に30人だけ表示するのに、シンプルとされるwill_paginateメソッドを使う。そのためには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する。

Gemfile

gem 'faker'
gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
$ bundle install

新たにpaginateメソッドを追加したため、念のためここでサーバーの再起動を行っておく。
indexページ(app/views/users/index.html.erb)でpaginationを使う


<% 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オブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成してくれる。ただし、現在の@users変数にはUser.allの結果が含まれているが 、will_paginateではpaginateメソッドを使った結果が必要となる。

必要となるデータの例は次のとおり
paginateでは、キーが:pageで値がページ番号のハッシュを引数に1を渡すと1~30までのユーザーまで出る
ちなみにpageがnilの場合、 paginateは単に最初のページを返す。


$ rails console
> User.paginate(page: 1)
  User Load (1.0ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
   (0.1ms)  SELECT COUNT(*) FROM "users"
 => #<ActiveRecord::Relation [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-01-19 06:42:48", updated_at: "2020-01-19 06:42:48", password_digest: "$2a$10$xDXvcjV4nyrflH.nVpxu2uWGCeBYR5quXeo1ERVKIUE...", remember_digest: nil>, #<User id: 2,... 
省略

paginateを使うことで、このアプリでユーザーのページネーションを行えるようになる。
具体的には、indexアクション内のallをpaginateメソッドに置き換えて、indexアクションでUsersをページネートする

app/controllers/users_controller.rb

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

image.png

現在の位置(ページネーションの番号)と下のデータが一致。

image.png

10.3.4 ユーザー一覧のテスト

ユーザーの一覧ページが動くようになったので、ページネーションに対するテストを行う。

今回のテストでは、
1. ログイン
2. indexページにアクセス
3. 最初のページにユーザーがいることを確認
4. ページネーションのリンクがあることを確認
の順でテストを行う。

まずはfixtureにさらに30人のユーザーを追加する。
今後必要になるので、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') %>

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 %>

統合テストを生成。


$ rails generate integration_test users_index
Running via Spring preloader in process 12447
      invoke  test_unit
      create    test/integration/users_index_test.rb test/integration/users_index_test.rb

ページネーションを含めたUsersIndexのテスト内容を記述。
具体的には、
1. Michael(何かのユーザー)でログイン
2. ユーザーのindexページへ移動(テンプレート)
3. ページネーションクラスがあるか
4. ユーザーの名前(変数user)をクリックするとそのprofileページに行くか

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), text: user.name
    end
  end
end

テストは通過。
  

10.3.5 パーシャルのリファクタリング

いくつかリファクタリングを行う。

リファクタリングの第一歩は、ユーザーのliをrender呼び出しに置き換える。(app/views/users/index.html.erb)
renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している。これは、renderにモデルのインスタンスオブジェクトを渡したときのデフォルトの挙動。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、各ユーザーを表示するパーシャルを作成する。


<ul class="users">
  <% @users.each do |user| %>
     <%= render user %> 
     <!-- => app/views/リソース名/_モデル名.html.erb-->
     <!-- => app/views/users/_user.html.erb-->
  <% end %>
</ul>

各ユーザーを表示するパーシャル
app/views/users/_user.html.erb


<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

今度はrenderを@users変数にして、最終的に下記に。


<ul class="users">
  <%= render @users %>
</ul>

Railsは@usersをUserオブジェクトのリストであると推測する。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力するので、each文がなくなりコードは短くなった。

一応テストして通過。

10.4 ユーザーを削除する

destroyの実装。この節では、ユーザーを削除するためのリンクを追加する。もちろん、ユーザーを削除(delete)できるのは管理権限を持ったユーザーのみ。

モックアップは以下の形式。(公式より参考)

image.png

ただしその前に、削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成する。

10.4.1 管理ユーザー

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加する。
こうすると自動的にadmin?メソッド (論理値booleanを返す) も使えるようになるため、これを使って管理ユーザーの状態をテストする。
変更後のデータモデルは以下(公式より参考)

スクリーンショット 2020-01-20 18.02.29.png

まずはマイグレーションを実行してadmin属性を追加(属性の型をbooleanに指定)


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

マイグレーションを実行するとadminカラムがusersテーブルに追加される。デフォルトでは管理者になれないことを示す+nilが入るケースを防ぐため、default: false引数を与える。

db/migrate/[timestamp]_add_admin_to_users.rb

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

マイグレーションを実行。

$ rails db:migrate

コンソール(sandbox)で動作を確認すると、期待どおりadmin属性が追加されて論理値をとり、さらに疑問符の付いたadmin?メソッドも利用できるようになっている。


$ rails console --sandbox
> user = User.first
> user.admin?
 => false 
> user.toggle!(:admin)
=> true 
> user.admin?
 => true 

ここではtoggle!メソッドを使って admin属性の状態をfalseからtrueに反転している。
toggle!メソッドの「!」は破壊的メソッドで、「書き換えたらもう元には戻らない」ことを示している。

演習用として、最初のユーザーだけをデフォルトで管理者にするよう(admin→true)、サンプルデータを更新しておく。

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

データベースをリセットして、サンプルデータを再度生成。

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

10.4.2 destroyアクション

まず、destroyアクションへのリンクを追加する。ユーザーindexページの各ユーザーに削除用のリンクを追加+管理ユーザーへのアクセスを制限が目標。

ユーザー削除用リンクの実装 (管理者にのみ表示される)
(app/views/users/_user.html.erb)
※admin権限を持っていても、自分自身は消せないように && !current_user? で確認を取っている。


<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>

実際にユーザExample Userでログインしてみると、アクションまで(エラー画面で)確認できる。

スクリーンショット 2020-01-20 18.46.33.png

スクリーンショット 2020-01-20 18.46.58.png

実際に動作するdestroyアクションを追加する。このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーのindexページにリダイレクトさせる。ユーザーを削除するためにはログインしていなくてはならないため、destroyアクションもlogged_in_userフィルター(before_action)に追加している。

ただしこれでは、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除される可能性があるため、destroyアクションにもadmin_userフィルターを入れてアクセス制御を実装する。

app/controllers/users_controller.rb

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]

省略

  # DELETE /users/:id
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end

private

# 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

10.4.3 ユーザー削除のテスト

fixtureファイルの一番上(Michael)を管理者にする。

test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true

管理者権限の制御をアクションレベルでテストする。
「ユーザーがログインしてないときにDELETEリクエスト送ったらだめ」
「ログインしていたとしても、adminじゃなかったらやはりだめ」
という内容。

test/controllers/users_controller_test.rb

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

テストは通過。

最後に、削除リンクとユーザー削除に対する統合テストとして「ユーザーを削除したらユーザーの総数が1つ消えてるよ?」というテストを付け加える(先のテストを大幅に改造)。
上のテストは、
1. サンプルとしてMichaelさん(admin)、Archerさん(non_admin)のユーザーデータを持ってくる
2. ログイン(ユーザーパスが見えるはず)
3. ページネーション見える
4. ユーザーがadminかどうかチェック(adminならdeleteが見えるはず)
5. 選択すればArcherさん(non_admin)は消えるはず

下のテストは,
1. non_adminとしてログイン
2. deleteリンクは見えないはずなのでcountは0か?

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

テストは通過。

最後にherokuへデプロイ。

$ git add -A
$ git commit -m "Finish ch10"
$ git checkout master
$ git merge updating-users
$ git push heroku master

本番環境として
・ DBリセットは危険なので本来あまりやらない
・ 本番環境にrun rails db:seedで擬似データを送る。これもあまりやらない
・ リモートのリンクのfetchをクリック


$ heroku pg:reset DATABASE
     WARNING: Destructive action

     To proceed, type sample-app or re-run this command with --confirm sample-app

> sample-app
$ heroku run rails db:migrate
$ heroku run rails db:seed
$ git remote -v
※リンク確認

スクリーンショット 2020-01-20 19.50.58.png

本番環境でログインしてユーザー削除の確認ができたので終了!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0