10.3 すべてのユーザーを表示する
indexアクションを追加しましょう。このアクションは、すべてのユーザーを一覧表示します。
その際、データベースにサンプルデータを追加する方法や、
将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネーション (pagination=ページ分割) の方法を学びます。
10.3.1 ユーザーの一覧ページ
ユーザーの一覧ページを実装するために、まずはセキュリティモデルについて考えてみましょう。
ユーザーのshowページについては、今後も(ログインしているか
どうかに関わらず) サイトを訪れたすべてのユーザーから見えるようにしておきますが
ユーザーのindexページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限します。
index
アクションのリダイレクトをテストする redtest/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
アクションにはログインを要求する greenapp/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>
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に追加してみましょう。
Gemfile
にwill_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メソッドを追加しましょう
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
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 パーシャルのリファクタリング
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>
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パーシャルで出力します。
rails test
演習
1:app/views/users/index.html.erb
にあるrender
の行をコメントアウトし、テストの結果が redに変わることを確認してみましょう。
コメントアウトしてtestで確認したらOKです