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_user
にindex
アクションを追加して、このアクションを保護する。
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を使って使ってユーザーを一気に作成してみる。
そのため、まずGemfile
にFaker 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のユーザーと行った具合にデータが取り出される。ちなみにpage
がnil
の場合、paginate
は単に最初のページを返す。
paginate
を使うことで、サンプルアプリケーションのユーザーのページネーションを行えるようになる。具体的には、index
アクション内のall
をpaginate
メソッドに置き換える。
ここで、: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サイトの機能を損なうことなく安心してリファクタリングに取りかかれる。なるほど!
リファクタリングの第一歩は、ユーザーのli
をrender
呼び出しに置き換えること。
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は@users
をUser
オブジェクトのリストであると推測する。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erb
パーシャルで出力する。これにより、元のコードは極めてコンパクトになる。