Rails-tutorialのまとめ10.3(すべてのユーザーを表示する)

その10.2から続きます

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

indexアクションを追加しましょう。このアクションは、すべてのユーザーを一覧表示します。
その際、データベースにサンプルデータを追加する方法や、
将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネーション (pagination=ページ分割) の方法を学びます。

10.3.1 ユーザーの一覧ページ

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

indexアクションのリダイレクトをテストする red
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 index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

beforeフィルターのlogged_in_userにindexアクションを追加して、
このアクションを保護します

indexアクションにはログインを要求する green
app/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
  end

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

今度はすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装します。
User.allを使ってデータベース上の全ユーザーを取得し、
ビューで使えるインスタンス変数@usersに代入させます。

ユーザーのindexアクション

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

実際のindexページを作成するには、ユーザーを列挙してユーザーごとにliタグで囲むビューを作成する必要があります。

ここではeachメソッドを使って作成します。
それぞれの行をリストタグulで囲いながら、
各ユーザーのGravatarと名前を表示します。

ユーザーのindexビュー

app/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_forヘルパーにオプション引数を追加する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

CSS (正確には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;
 }
}

サイト内移動用のヘッダーにユーザー一覧表示用のリンクを追加します。
これにはusers_pathを使い、残っている最後の名前付きルートを割り当てます。

ユーザー一覧ページへのリンクを更新するapp/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>
 green
rails test

演習

レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。
ログイン済みユーザーとそうでないユーザーのそれぞれに対して、
正しい振る舞いを考えてください。
ヒント: log_in_asヘルパーを使ってテストを追加してみましょう。

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

10.3.2 サンプルのユーザー

GemfileにFaker gemを追加してユーザーを偽造する

GemfileにFaker gemを追加するGemfile
source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bcrypt',         '3.1.12'
gem 'faker',          '1.7.3'
bundle install

サンプルユーザーを生成するRubyスクリプト (Railsタスクとも呼びます) を
追加してみましょう。Railsではdb/seeds.rbというファイルを標準として使います
コードは少し応用的です。詳細が完全に理解できなくても問題ありません。

データベース上にサンプルユーザーを生成するRailsタスク
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

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

データベースをリセットして、のRailsタスクを実行 (db:seed) してみましょう

rails db:migrate:reset
rails db:seed

db:seedでRailsタスクを実行し終わると、サンプルアプリケーションのユーザーが100人になっています。

演習

1:試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。

動作確認するだけ

10.3.3 ページネーション

Railsには豊富なページネーションメソッドがあります。今回はその中で最もシンプルかつ堅牢なwill_paginateメソッドを使ってみましょう。

これを使うためには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方含め、
Bootstrapのページネーションスタイルを使ってwill_paginateを構成する必要があります。
まずは各gemをGemfileに追加してみましょう。

Gemfilewill_paginateを追加するGemfile
source 'https://rubygems.org'

gem 'rails',                   '5.1.6'
gem 'bcrypt',                  '3.1.12'
gem 'faker',                   '1.7.3'
gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
bundle install

ページネーションが動作するには、ユーザーのページネーションを行うようにRailsに
指示するコードをindexビューに追加する必要があります。また、indexアクション
にあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要も
あります。まずは、ビューに特殊なwill_paginateメソッドを追加しましょう

indexページでpaginationを使う
app/views/users/index.html.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メソッドを使った結果が必要だからです。必要となるデータの例は次のとおりです。

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

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

indexアクションでUsersをページネートするapp/controllers/users_controller.rb
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ページ目のユーザーが取得できることを確認してみましょう。

User.paginate(page: nil)

2:先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか?また、User.allのクラスとどこが違うでしょうか?
比較してみてください。

user = User.paginate(page: 1)
user.class
=> User::ActiveRecord_Relation

user = User.all
user.class
=> User::ActiveRecord_Relation 同じだよ

10.3.4 ユーザー一覧のテスト

fixtureにさらに30人のユーザーを追加する
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 %>

test/fixtures/users.ymlのfixtureファイルができたので、indexページに対するテストを書いてみます。まずは、いつものように統合テストを生成します。

rails generate integration_test users_index

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

ページネーションを含めたUsersIndexのテストgreen
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
green
rails test

演習

1:ページネーションのリンク(will_paginateの部分)を2つともコメントアウトしてみて、テストが redに変わるかどうか確かめてみましょう。

Expected at least 1 element matching “div.pagination”, found 0..
Expected 0 to be >= 1.

2:先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストがgreenのままであることを確認してみましょう。
will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか?
数をカウントするテストを追加してみましょう。

assert_select ‘div.pagination’, count:2

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

indexビューに対する最初のリファクタリングapp/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 %>

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

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>
indexページの完全なリファクタリングgreen
app/views/users/index.html.erb
<% 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パーシャルで出力します。

green
rails test

演習

1:app/views/users/index.html.erbにあるrenderの行をコメントアウトし、テストの結果が redに変わることを確認してみましょう。

コメントアウトしてtestで確認したらOKです

10.4に続く

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です