noggy’s blog

自分用の備忘録です。。。

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

ここではindexアクションを追加し、すべてのユーザーを一覧表示できるようにする。

その際、「DBにサンプルデータを追加する方法」や、将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするための「ユーザー出力のページネーション(ページ分割)の方法」を学ぶ。

10.3.1 ユーザーの一覧ページ

ユーザーの一覧ページを実装するために、まずはセキュリティモデルについて考えてみる。
ユーザーのshowページについては、今後も(ログインしているかどうかに関わらず)サイトを訪れたすべてのユーザーから見えるようにしておくが、ユーザーのindexページはログインしたユーザーにしか見えないようにし、未登録のユーザーがデフォルトで表示できるページを制限する。

indexアクションのリダイレクト

indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするかを検証するテストを書いてみる。
test/controllers/users_controller_test.rb

  def setup
    @user = users(:micahel)
    @other_user = user(:archer)
  end

  test "should redirect index when not logged in" do
    get users_path ←①          
    assert_redirected_to login_url  ←②                     
  end

①:indexアクションを取得
②:ログインページへリダイレクトできたらtrue

次に、beforeフィルターのlogged_in_userindexアクションを追加して、このアクションを保護する。
controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update] ←indexを追加   
  before_action :correct_user,   only: [:edit, :update]         
  
  def index ←追加
  end

  def show
     @user =  User.find(params[:id])
  end
  :

今度はすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装する。Toyアプリケーションにも同じindexアクションがあったことを思い出す。User.allを使って全DB上のユーザーを取得し、ビューで使えるインスタンス変数@usersに代入させる。
controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update] ←indexを追加   
  before_action :correct_user,   only: [:edit, :update]         
  
  def index
    @users = User.all ←追加
  end
  :
indexページ

実際のindexページを作成するには、ユーザーを列挙してユーザーごとにliタグで囲むビューを作成する必要がある。ここではeachメソッドを使って作成する。
それぞれの行をulで囲いながら、各ユーザーのGravatarと名前を表示する。
views/users/index.html.erb

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

①:Gravatarヘルパーに、7.1.4の演習で使った次のコードを利用して、デフォルト以外のサイズを指定するオプションを渡している。
app/helpers/users_helper.rb

module UsersHelper
  
  #渡されたユーザーのGravatar画像を返す
  def gravatar_for(user, options ={size: 80})
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

Gravatar側の準備が整ったら、SCSSにもちょっぴり手を加える。
app/assets/stylesheets/custom.scss

/* User index */

 .users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-bottom: 1px solid $gray-lighter;
  }
}

最後に、サイト内移動用のヘッダーにユーザー一覧用のリンクを追加する。これにはリンクにはusers_pathを使い、名前つきルートを割りあてる。
views/layouts/_header.html.erb

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

これでユーザーのindexは完全に動くようになり、これでテストは成功。

10.3.2 サンプルのユーザー

indexページに複数のユーザーを表示させるために、ブラウザからユーザー登録ページへ行って手作業で1人ずつ追加するという方法もあるが、Rubyを使って使ってユーザーを一気に作成してみる。

そのため、まずGemfileFaker gemを追加する。これは、実際にいそうなユーザー名を作成するgem。ちなみにfaker gemは開発環境以外では普通使わないが、今回は例外的に本番環境でも適用させる予定なので、次のようにすべての環境で使えるようにする。
Gemfile

source 'https://rubygems.org'
:
gem 'rails', '~>5.2.6'
gem 'bcrypt', '3.1.11'
gem 'faker', '~>1.7'
:

bundle installする

$ bundle install

では、サンプルユーザーを生成するRubyスクリプト(Railsタスクとも言う)を追加してみる。Railsではdb/seed.rbというシードファイルを標準として使う。
作成したコードを次に示す(少し応用的なので、詳細を完全に理解できなくてもOK)。
db/seed.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

上のコードでは、Example Userという名前とメールアドレスを持つ1人のユーザと、それらしい名前とメールアドレスを持つ99人のユーザーを作成する。create!は基本的にcreateメソッドと同じものだが、ユーザーが無効な場合にfalseを返すのではなく例外を発生させる点が異なる。こうしておくと見過ごしやすいエラーを回避できるので、デバッグが容易になる。

それでは、DBをリセットして、Railsタスクを実行(db:seed)してみる。
console

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

DB上にデータを追加するのは遅くなりがちで、システムによっては数分かかることもあり得る。また、Railsサーバーを動かしている状態だとrails db:migrate:resetコマンドがうまく動かない時もあるそうで注意!

10.3.3 ページネーション

これで、最初のユーザーにも仲間ができた。しかし、今度は逆に、1つのページに大量のユーザーを表示されてしまっている。100人でもかなり大きい数。今後はもっとユーザーが増える可能性がある。これを解決するため、ページネーションというmのを実装する。今回は、1つのページに30人だけユーザーを表示してみる。

Railsには豊富なページネーションメソッドがあり、今回はその中で最もシンプルかつ堅牢なwill paginateメソッドを使ってみる。これを使うために、Gemfileにwill_paginate gemとbootstrap-will_paginategemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する必要がある。
まずは各gemをGemfileに追加してみる。
Gemfile

source 'https://rubygems.org'
:
gem 'rails', '~>5.2.6'
gem 'bcrypt', '3.1.11'
gem 'faker', '~>1.7'
gem 'will_paginate','3.1.7' ←追加
gem 'bootstrap-will_paginate','1.0.0' ←追加

次に、bundle install

$ bundle install

新しいgemが正しく読みこまれるよう、webサーバーを再起動する。
ページネーションが動作するには、ユーザーのページネーションを行うようにRailsに指示するコードをindexビューに追加する必要がある。また、indexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もある。
まずは、ビューにwill_paginateメソッドを追加する。
views/users/index.htme.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 %> ←追加

このwill_paginateメソッドは少々不思議なことに、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成している。
ただし、上のビューはこのままでは動かない。理由は、現在の@users変数にはUser.allの結果が含まれているが、will_paginateではpaginateメソッドを使った結果が必要だから。
必要となるデータは次のとおり。

$ rails c
>> User.paginate(page: 1)
  User Load (1.5ms)  SELECT  "users".* FROM "users" LIMIT 30 OFFSET 0 
   (1.7ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1, …

paginateでは、キーが「:page」で値が「ページ番号」のハッシュを引数に取る。
User.paginateでは、:pageパラメーターに基づいて、DBからひとかたまりのデータ(デフォルトでは30)を取り出す。したがって、1ページ目は1から30のユーザー、2ページ目は31から60のユーザーと行った具合にデータが取り出される。ちなみにpagenilの場合、paginateは単に最初のページを返す。

paginateを使うことで、サンプルアプリケーションのユーザーのページネーションを行えるようになる。具体的には、indexアクション内のallpaginateメソッドに置き換える。
ここで、:pageパラメーターにはparams[:page]が使われるが、これはwill_paginateによって自動的に生成される。
controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]  
  before_action :correct_user,   only: [:edit, :update]         
  :
  def index
    @users = User.paginate(page: params[:page]) ←変更       
  end
  :
end

以上で、ユーザー一覧ページは動作するはず。will_pagiateをユーザーリストの上下の両方に配置しているので、ページネーションのリンクもページの上下の両方に表示されている。

10.3.4 ユーザー一覧のテスト

これでユーザーの一覧ページが動くようになったので、ページネーションに対する簡単なテストを書いておく。
今回のテストでは、
(1)ログイン
(2)indexページにアクセス
(3)最初のユーザーのページにユーザーがいることを確認
(4)ページネーションのリンクがあることを確認
といった順でテストしていく。
最後の2つのステップでは、テスト用のDBに31人以上のユーザーがいる必要がある。

fixtureでは埋め込みRubyをサポートしているので、これを利用して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 %>

fixtureファイルができたので、indexページに対するテストを書いてみる。まずは統合テストを生成。

$ rails g integration_test users_index

今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認する。
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 パーシャルのリファクタリング

ユーザー一覧ページにページネーションを実装することができたが、ここで、コンパクトなビューを作成するツールを使って一覧のページのリファクタリングを行うことにする。サンプルアプリのテストは既に完了しているので、Webサイトの機能を損なうことなく安心してリファクタリングに取りかかれる。なるほど!

リファクタリングの第一歩は、ユーザーのlirender呼び出しに置き換えること。
views/users/index.html.erb

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %> ←①
  <% end %>
</ul>

<%= will_paginate %>

①はもともと

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

ここでは、renderをパーシャル(ファイル名の文字列)に対してではなく、Userクラスのuser変数に対して実行している点に注目!この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成。
views/users/_user.html.erb

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

今度はrender@users変数に対して直接実行する。
views/users/index.html.erb

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <%= render @users %> ←変更
</ul>

<%= will_paginate %>

Rails@usersUserオブジェクトのリストであると推測する。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力する。これにより、元のコードは極めてコンパクトになる。