13.2.1 マイクロポストの描画
ユーザーのプロフィール画面 (show.html.erb) でそのユーザーの
マイクロポストを表示させたり、これまでに投稿した総数も表示させたりしていきます。
Micropostのコントローラとビューを作成するために、コントローラを生成しましょう。なお、今回使うのはビューだけで、Micropostsコントローラは 13.3から使っていきます。
rails db:migrate:reset rails db:seed rails generate controller Microposts
_micropost.html.erbパーシャルを使ってマイクロポストのコレクションを
表示しようとすると、次のようになります。
<ol class=”microposts”>
<%= render @microposts %>
</ol>
順序無しリストのulタグではなく、順序付きリストのolタグを使っている点に注目してください。
これは、マイクロポストが特定の順序 (新しい→古い)
に依存しているためです。
次に、対応するパーシャルを示します。
1つのマイクロポストを表示するパーシャル
app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. </span> </li>
time_ago_in_wordsという↑ヘルパーメソッドを使っています。
メソッド名の表すとおりですが、「3分前に投稿」といった文字列を出力します。
各マイクロポストに対してCSSのidを割り振っています。
<li id=”micropost-<%= micropost.id %>”>
これは一般的に良いとされる慣習で、例えば将来、JavaScriptを使って
各マイクロポストを操作したくなったときなどに役立ちます。
一度にすべてのマイクロポストが表示されてしまう潜在的問題に対処します。10ではページネーションを使いましたが、今回も同じ方法でこの問題を解決します。
前回同様、will_paginateメソッドを使うと次のようになります。
<%= will_paginate @microposts %>
以前は次のように単純なコードでした。
<%= will_paginate %>
実は、上のコードは引数なしで動作していました。
これはwill_paginateが、Usersコントローラのコンテキストに
おいて、@usersインスタンス変数が存在していることを前提としているためです
今回の場合はUsersコントローラのコンテキストからマイクロポストをページネーションしたいため (つまりコンテキストが異なるため)、
明示的に@microposts変数をwill_paginateに渡す必要があります。
したがって、そのようなインスタンス変数をUsersコントローラの
showアクションで定義しなければなりません
@micropostsインスタンス変数をshowアクションに追加する
app/controllers/users_controller.rb
class UsersController < ApplicationController . . . def show @user = User.find(params[:id]) @microposts = @user.microposts.paginate(page: params[:page]) end . . . end
マイクロポストの投稿数を表示することですが、これはcountメソッドを使うことで解決できます。
user.microposts.count
paginateと同様に、関連付けをとおしてcountメソッドを呼び出すことができます。
大事なことは、countメソッドではデータベース上の
マイクロポストを全部読みだしてから結果の配列に対してlengthを呼ぶ、といった無駄な処理はしていないという点です。
そんなことをしたら、
マイクロポストの数が増加するにつれて効率が低下してしまいます。
そうではなく、(データベース内での計算は高度に最適化されているので)データベースに代わりに計算してもらい、
特定のuser_idに紐付いた
マイクロポストの数をデータベースに問い合わせています。
すべての要素が揃ったので、プロフィール画面にマイクロポストを
表示させてみましょう(このとき、if @user.microposts.any?を使って、ユーザーのマイクロポストが1つもない場合には空のリストを
表示させていない点にも注目してください。)
マイクロポストをユーザーのshowページ (プロフィール画面) に追加する
app/views/users/show.html.erb
<% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> </aside> <div class="col-md-8"> <% if @user.microposts.any? %> <h3>Microposts (<%= @user.microposts.count %>)</h3> <ol class="microposts"> <%= render @microposts %> </ol> <%= will_paginate @microposts %> <% end %> </div> </div>
演習
今回ヘルパーメソッドとして使ったtime_ago_in_wordsメソッドは、
Railsコンソールのhelperオブジェクトから呼び出すことができます。
このhelperオブジェクトのtime_ago_in_wordsメソッドを使って、
3.weeks.agoや6.months.agoを実行してみましょう。
helper.time_ago_in_words(3.weeks.ago) => "21 days" >> helper.time_ago_in_words(6.months.ago) => "6 months"
2:helper.time_ago_in_words(1.year.ago)と実行すると、どういった結果が返ってくるでしょうか?
helper.time_ago_in_words(1.year.ago) => "about 1 year"
3:micropostsオブジェクトのクラスは何でしょうか? ヒント: リスト 13.23内のコードにあるように、まずはpaginateメソッド (引数はpage: nil) でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。
user = User.find(1) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-04-10 02:03:10", updated_at: "2020-04-10 02:03:10", password_digest: "$2a$10$cwFqu4HHjAMUEhZpbM294.4JUOfjV08XlIzGu37zTSD...", remember_digest: nil, admin: true, activation_digest: "$2a$10$mRd3DEhj6awejp8QoTEbouySA73h7GJW4h5MQYUJ3ax...", activated: true, activated_at: "2020-04-10 02:03:09", reset_digest: nil, reset_sent_at: nil> >> microposts = user.microposts.paginate(page:nil) Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ? [["user_id", 1], ["LIMIT", 11], ["OFFSET", 0]] => #<ActiveRecord::AssociationRelation []> >> microposts.class => Micropost::ActiveRecord_AssociationRelation
13.2.2 マイクロポストのサンプル
すべてのユーザーにマイクロポストを追加しようとすると時間が掛かり過ぎるので、takeメソッドを使って最初の6人だけに追加します。
User.order(:created_at).take(6)
このとき、orderメソッドを経由することで、
作成されたユーザーの最初の6人を明示的に呼び出すようにしています。
この6人については、1ページの表示限界数(30)を越えさせるために、
それぞれ50個分のマイクロポストを追加するようにしています。
また、各投稿内容についてですが、Faker gem
にLorem.sentenceという便利なメソッドがあるので、これを使います。
サンプルデータにマイクロポストを追加する
db/seeds.rb
users = User.order(:created_at).take(6) 50.times do content = Faker::Lorem.sentence(5) users.each { |user| user.microposts.create! (content: content) } end
rails db:migrate:reset rails db:seed
マイクロポスト用のCSS (本章で利用するCSSのすべて)
app/assets/stylesheets/custom.scss
. . /* microposts */ .microposts { list-style: none; padding: 0; li { padding: 10px 0; border-top: 1px solid #e8e8e8; } .user { margin-top: 5em; padding-top: 0; } .content { display: block; margin-left: 60px; img { display: block; padding: 5px 0; } } .timestamp { color: $gray-light; display: block; margin-left: 60px; } .gravatar { float: left; margin-right: 10px; margin-top: 5px; } } aside { textarea { height: 100px; margin-bottom: 5px; } } span.picture { margin-top: 10px; input { border: 0; } }
演習
1:(1..10).to_a.take(6)というコードの実行結果を推測できますか?
推測した値が合っているかどうか、コンソールを使って確認してみましょう。
(1..10).to_a.take(6) => [1, 2, 3, 4, 5, 6]
2:先ほどの演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。
(1..10).take(6) => [1, 2, 3, 4, 5, 6]
3:Fakerのドキュメント (英語) を眺めながら画面に出力する方法を学び、実際に大学名や電話番号を画面に出力してみましょう。
Faker::Hipster.words => ["whatever", "schlitz", "kickstarter"] >> Faker::ChuckNorris.fact => "Quantum cryptography does not work on Chuck Norris. When something is being observed by Chuck it stays in the same state until he's finished."
13.2.3 プロフィール画面のマイクロポストをテストする
プロフィール画面で表示されるマイクロポストに対して、
統合テストを書いていきます。まずは、プロフィール画面用の統合テストを生成してみましょう。
rails generate integration_test users_profile
プロフィール画面におけるマイクロポストをテストするためには、
ユーザーに紐付いたマイクロポストのテスト用データが必要になります。
Railsの慣習に従って、関連付けされたテストデータをfixtureファイルに追加すると、次のようになります。
orange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michael
userにmichaelという値を渡すと、
Railsはfixtureファイル内の対応するユーザーを探し出して、
(もし見つかれば) マイクロポストに関連付けてくれます。
マイクロポストのページネーションをテストするためには、
マイクロポスト用のfixtureにいくつかテストデータを追加する必要が
ありますが、これはリスト 10.47でユーザーを追加したときと同様に、埋め込みRubyを使うと簡単です。
<% 30.times do |n| %> micropost_<%= n %>: content: <%= Faker::Lorem.sentence(5) %> created_at: <%= 42.days.ago %> user: michael <% end %>
ユーザーと関連付けされたマイクロポストのfixture
test/fixtures/microposts.yml
orange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michael tau_manifesto: content: "Check out the @tauday site by @mhartl: http://tauday.com" created_at: <%= 3.years.ago %> user: michael cat_video: content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk" created_at: <%= 2.hours.ago %> user: michael most_recent: content: "Writing a short test" created_at: <%= Time.zone.now %> user: michael <% 30.times do |n| %> micropost_<%= n %>: content: <%= Faker::Lorem.sentence(5) %> created_at: <%= 42.days.ago %> user: michael <% end %>
Userプロフィール画面に対するテストgreen
test/integration/users_profile_test.rb
require 'test_helper' class UsersProfileTest < ActionDispatch::IntegrationTest include ApplicationHelper def setup @user = users(:michael) end test "profile display" do get user_path(@user) assert_template 'users/show' assert_select 'title', full_title(@user.name) assert_select 'h1', text: @user.name assert_select 'h1>img.gravatar' assert_match @user.microposts.count.to_s, response.body assert_select 'div.pagination' @user.microposts.paginate(page: 1).each do |micropost| assert_match micropost.content, response.body end end end
response.bodyにはそのページの完全なHTMLが含まれています
(HTMLのbodyタグだけではありません)。つまり、そのページのどこかしらに
マイクロポストの投稿数が存在するのであれば、
次のように探し出してマッチできるはずです。
assert_match @user.microposts.count.to_s, response.body
これはassert_selectよりもずっと抽象的なメソッドです。
特に、assert_selectではどのHTMLタグを探すのか伝える必要が
ありますが、assert_matchメソッドではその必要がない点が違います。
assert_selectの引数では、
ネストした文法を使っている点にも注目してください。
assert_select 'h1>img.gravatar'
h1タグ (トップレベルの見出し) の内側にある、
gravatarクラス付きのimgタグがあるかどうかをチェックできます。
rails test
演習
1:2つの’h1’のテストが正しいか確かめるため、
該当するアプリケーション側のコードをコメントアウトしてみましょう。greenからredに変わることを確認してみてください。
Expected at least 1 element matching "h1", found 0.. Expected 0 to be >= 1. REDになる
2:テストを変更して、will_paginateが1度のみ表示されていることを
テストしてみましょう。ヒント: 表 5.2を参考にしてください。
assert_select 'div.pagination', count:1