noggy’s blog

自分用の備忘録です。。。

13.2 マイクロポストの表示

Web経由でマイクロポストを作成する方法は現時点ではないが、マイクロポストを表示することと、テストすることならできる。
ここでは、Twitterのような独立したマイクロポストのindexページを作らず、次のモックアップのように、ユーザーのshowページで直接マイクロポストを表示させることにする。

ユーザープロフィールにマイクロポストを表示させるため、最初に極めてシンプルなERbテンプレートを作成する。
次に、10.3.2のサンプルデータ生成タスクにマイクロポストのサンプルを追加して、画面にサンプルデータが表示されるようにしてみる。

f:id:nogicchi:20210822202617p:plain
マイクロポストが表示されたプロフィールページのモックアップ

13.2.1 マイクロポストの描画

ここでは、ユーザーのプロフィールの画面(show.html.erb)で、そのユーザーのマイクロポストを表示させたり、これまでに投稿した総数も表示させたりしていく。(10.3で実装したユーザーを表示する部分と似ている。)

一度DBをリセットし、サンプルデーターを再生成

$ rails db:migrate:reset
$ rails db:seed

まずは、Micropostのコントローラを生成。
(今回使うのはビューだけで、Micropostsコントローラは13.3から使う。)

$ rails g controller Microposts

ユーザーごとにすべてのマイクロポストを描画

10.3.5で見た次のコードでは、

<ul class="users">
  <%= render @users %>
</ul>

_user.html.erbパーシャルを使って自動的に@users変数内のそれぞれのユーザーを出力していた。
これを参考に、_micropost.html.erbパーシャルを使ってマイクロポストのコレクションを表示する

<ol class="microposts"> #①
  <%= render @microposts %>
</ol>

①:ユーザー一覧と違いulタグではなく、順序付きリストのolタグを使っている点に注目!マイクロポストが特定の順序(新しい→古い)に依存しているため。

1つのマイクロポストを表示するパーシャル

<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 classs="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at ) %> ago.  #①投稿時間         
  </span>
</li>

①:time_ago_in_wordsというヘルパーメソッドで「3分前に投稿」といった文字列を出力
②:各マイクロポストに対してCSSのidを割り振っている。これは一般的に良いとされる慣習とのこと

ページネーションを使っての表示

一度にすべてのマイクロポストが表示されてしまうことを防ぐため。ユーザー一覧ページと同じ方法でwill_paginateメソッドを使う。

<%= will_paginate @microposts %>

ユーザー一覧画面のコードと比較すると、少し違っている。以前は次のようなコードだった。

<%= will_paginate %>

実は、上のコードは引数なしで動作していた。これはwill_paginateが、Usersコントローラのコンテキストにおいて、@usersインスタンス変数が存在していることを前提としているため。
@usersインスタンス変数は、10.3.3でも述べたようにActiveRecord: :Relationクラスのインスタンス

しかし、今回の場合はUsersコントローラのコンテキストからマイクロポストをページネーションしたいため(つまりコンテキストが異なるため)、明示的に@microposts変数をwill_paginateに渡す必要がある。なるほど!

したがって、そのようなインスタンス変数をUsersコントローラのshowアクションで定義しなければならない。

  :
  def show
    @user = User.find(params[:id])     
    @microposts = @user.microposts.paginate(page: params[:page])  #追加①
  end
  :
end

①:paginateメソッドは、マイクロポストの関連付けを経由してmicropostテーブルに到達し、必要なマイクロポストのページを引き出してくれる。

マイクロポストの投稿数を表示

関連付けをとおしてcountメソッドを呼び出すことができる。

user.microposts.count

重要なことは、countメソッドではDB上のマイクロポストを全部読みだしてから結果の配列に対してlengthを呼ぶ、といった無駄な処理はしていないという点。そんなことをしたら、マイクロポストの数が増加するにつれて効率が低下してしまう。
そうではなく、(DB内での計算は高度に最適化されているので)DBに代わりに計算してもらい、特定のuser_idに紐付いたマイクロポストの数をDBに問い合わせている。

すべての要素が揃ったので実装

プロフィール画面にマイクロポストを表示させてみる。(このとき、if @user.microposts.any?を使って、ユーザーのマイクロポストが1つもない場合には空のリストを表示させていない点にも注目!)

<% 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>

新しくなったユーザー詳細ページをブラウザで確認しても、マイクロポストがないので表示が変わらない!
次でマイクロポストのサンプルを作成。

13.2.2 マイクロポストのサンプル

マイクロポスのサンプルデータを追加して、表示を確認できるようにしたい。

すべてのユーザーにマイクロポストを追加すると時間が掛かり過ぎる。
よって、takeメソッドを使って最初の6人だけに追加

User.order(:created_at).take(6)
orderメソッド

上のコードではorderメソッドを経由することで、明示的に最初の(IDが小さい順に)6人を呼び出すようにしている。
pikawaka.com

Faker gemでサンプルを追加

Faker gemにLorem.sentenceというメソッドを使って、サンプルを追加する。
6人については、1ページの表示限界数(30)を越えさせるために、それぞれ50個分のマイクロポストを追加するようにしている。

下の①で50.times.douser.eachを後に書いてる理由は、ユーザーごとに50個分のマイクロポストを作成してしまうと、ステータスフィードに表示される投稿がすべて同じユーザーになってしまい、視覚的な見栄えが悪くなってしまうから。

:
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) } #①
end

DBに反映
$ rails db:migrate:reset
$ rails db:seed

生成し終わったら、Railsサーバーを一度落として、起動し直す。次の図

f:id:nogicchi:20210822213557p:plain
プロフィールとマイクロポスト(CSSは未適用)


マイクロポスト固有のスタイルが与えられていないので、CSSを追加

:
/* micrposts */

.microposts {
  list-styles: 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分前に投稿"など)が表示されていることに注目!これはtime_ago_in_wordsメソッドによるもの。数分待ってからページを再度読み込むと、このテキストは自動的に新しい時間に基づいて更新される。

f:id:nogicchi:20210822213840p:plain
プロフィールとマイクロポスト(/users/1)

13.2.3 プロフィール画面のマイクロポストをテストする

アカウントを有効化したばかりのユーザーはプロフィール画面にリダイレクトされるので、そのプロフィール画面が正しく描画されてることは、単体テストで確認済み。

ここでは、プロフィール画面で表示されるマイクロポストに対して、統合テストを書いていく。まずは、プロフィール画面用の統合テストを生成してみる。

テストの準備

$ rails g integration_test users_profile
Running via Spring preloader in process 18157
      invoke  test_unit
      create   test/integration/users_profile_test.rb

プロフィール画面におけるマイクロポストをテストするためには、ユーザーに紐付いたマイクロポストのテスト用データが必要になる。Railsの慣習に従って、関連付けされたテストデータをfixtureファイルに追加すると、次のようになる。

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael #①

①:usermichaelという値を渡すと、Railsはfixtureファイル内の対応するユーザーを探し出して、(もし見つかれば)マイクロポストに関連付けてくれる。

michael:
  name: Michael Example
  email: michael@example.com
  :

また、マイクロポストのページネーションをテストするためには、埋め込みRubyを使い、マイクロポスト用のfixtureにテストデータを追加

<% 30.times do |n| %>
 micropost_<%= n %> #埋め込みRuby
 content: <%= Faker::Lorem.sentence(5) %> #
 created_at: <%= 42.days.ago %> #
 user: michael
<% end %>

これらのコードを1つにまとめると、マイクロポスト用のfixtureファイルは次のようになる。

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/PKffm2ul4dk"
  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 %>

テストデータを実装

今回のテストでは、プロフィール画面にアクセスした後に、ページタイトルとユーザー名、Gravatar、マイクロポストの投稿数、ページ分割されたマイクロポスト、といった順でテストしていく。

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

①②:Applicationヘルパーを読み込んだことでfull_titleヘルパーが利用できている点に注目!

response.body

③:マイクロポストの投稿数をチェックするために、12章の演習で使ったresponse.bodyを使用。
response.bodyにはそのページの完全なHTMLが含まれている(HTMLのbodyタグだけではない)。
bodyの内容だけでなくて、ページ内のすべてのHTMLが含まれるということ。
したがって、そのページのどこかしらにマイクロポストの投稿数が存在するのであれば、次のように探し出してマッチできるはず。

assert_match @user.microposts.count.to_s response.body

これはassert_selectよりもずっと抽象的なメソッド。特に、assert_selectではどのHTMLタグを探すのか伝える必要があるが、assert_matchメソッドではその必要がない点が違う。

④:assert_selectの引数では、ネストした文法を使っている点にも注目。このように書くことで、h1タグの内側にある、garavatarクラス付きのimgタグがあるかどうかチェックできる。

これでテストは成功する。