日別アーカイブ: 2021年12月4日

Rails-tutorialのまとめ(13.2.1 マイクロポストの描画 主に演習)

その13から続く

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タグがあるかどうかをチェックできます。

green
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

13.3に続く