紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【13章】Ruby on Railsチュートリアル演習まとめ&解答例【13.2 マイクロポストを表示する】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 13章 13.2 マイクロポストを表示するの演習まとめ&回答例です。

個人の解答例なので、誤りがあればご指摘ください。

動作環境

  • cloud9
  • ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
  • Rails 5.0.0.1

13.2.1 マイクロポストの描画

本章での学び

【事前準備】サンプルデータのリセット

yokoyan:~/workspace/sample_app (user-microposts) $ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20170417215343 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0019s
== 20170417215343 CreateUsers: migrated (0.0021s) =============================

== 20170423125906 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0011s
== 20170423125906 AddIndexToUsersEmail: migrated (0.0017s) ====================

== 20170424041610 AddPasswordDigestToUsers: migrating =========================
-- add_column(:users, :password_digest, :string)
   -> 0.0008s
== 20170424041610 AddPasswordDigestToUsers: migrated (0.0008s) ================

== 20170514215307 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0006s
== 20170514215307 AddRememberDigestToUsers: migrated (0.0007s) ================

== 20170614215124 AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.0016s
== 20170614215124 AddAdminToUsers: migrated (0.0017s) =========================

== 20170618221238 AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0007s
-- add_column(:users, :activated, :boolean, {:dafault=>false})
   -> 0.0004s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0005s
== 20170618221238 AddActivationToUsers: migrated (0.0023s) ====================

== 20170703042006 AddResetToUsers: migrating ==================================
-- add_column(:users, :reset_digest, :string)
   -> 0.0007s
-- add_column(:users, :reset_sent_at, :datetime)
   -> 0.0003s
== 20170703042006 AddResetToUsers: migrated (0.0016s) =========================

== 20170719214241 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0016s
-- add_index(:microposts, [:user_id, :created_at])
   -> 0.0009s
== 20170719214241 CreateMicroposts: migrated (0.0032s) ========================
yokoyan:~/workspace/sample_app (user-microposts) $ rails db:seed

【事前準備】Micropostsコントローラの生成

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate controller Microposts
Running via Spring preloader in process 2577
Expected string default value for '--jbuilder'; got true (boolean)
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--assets'; got true (boolean)
      create  app/controllers/microposts_controller.rb
      invoke  erb
      create    app/views/microposts
      invoke  test_unit
      create    test/controllers/microposts_controller_test.rb
      invoke  helper
      create    app/helpers/microposts_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/microposts.coffee
      invoke    scss
      create      app/assets/stylesheets/microposts.scss

【事前準備】micropostパーシャルの作成

yokoyan:~/workspace/sample_app (user-microposts) $ pwd
/home/ubuntu/workspace/sample_app
yokoyan:~/workspace/sample_app (user-microposts) $ touch app/views/microposts/_micropost.html.erb

【View】micropostパーシャルの実装

マイクロポストの内容を一覧表示するパーシャルを作成する。 将来的な拡張を見越して、マイクロポスト毎にidを割り振っている。 はじめて出てきた、time_ago_in_wordsは、ActionViewのヘルパーメソッド 「3分前に投稿」というような文字列を返すらしい。

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

【controller】ページング処理の実装のための事前準備

Usersコントローラから、ユーザのページングを行うのであれば、 ユーザープロフィール画面(View)のwill_paginateの引数に@usersを明示的に渡す必要はない。

今回は、Usersコントローラから、マイクロポストのページング処理を行うため、 同画面(View)のwill_paginateの引数に@microposts変数をwill_paginateに明示的に渡す必要がある。 Usersコントローラにて、@microposts変数を定義する。

  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
    redirect_to root_url and return unless @user.activated?
    #debugger
  end

【View】ユーザープロフィール画面にマイクロポストを表示する

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

演習1

7.3.3で軽く説明したように、今回ヘルパーメソッドとして使った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.month.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.first
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-25 21:45:26", updated_at: "2017-07-25 21:45:26", password_digest: "$2a$10$G03Oh9yI0VMHnGF38MA3iOkoHiLtJ2qnuQ8whUZ8lEp...", remember_digest: nil, admin: true, activation_digest: "$2a$10$M9ShPLhv8mtQE6Yb4.BDMeTjvoWnGI1xfeEAvka1I7Z...", activated: true, activated_at: "2017-07-25 21:45:26", 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", 30], ["OFFSET", 0]]
=> #<ActiveRecord::AssociationRelation []>
>> 
?> microposts.class
=> Micropost::ActiveRecord_AssociationRelation

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

micropostsオブジェクトのクラスは何でしょうか? ヒント: リスト 13.23内のコードににあるように、まずはpaginateメソッド (引数はpage: nil) でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。

本章での学び

【事前準備】サンプルデータにマイクロポストを追加する

サンプルユーザのうち、IDが小さい順に6人呼び出して、50件のマイクロポストを登録する。

ちなみに、lorem ipsumとは、意味のない典型的なダミーテキストのこと。 wiki:lorem ipsum




【事前準備】DBのリセット

yokoyan:~/workspace/sample_app (user-microposts) $ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20170417215343 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0014s
== 20170417215343 CreateUsers: migrated (0.0015s) =============================

== 20170423125906 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0010s
== 20170423125906 AddIndexToUsersEmail: migrated (0.0011s) ====================

== 20170424041610 AddPasswordDigestToUsers: migrating =========================
-- add_column(:users, :password_digest, :string)
   -> 0.0008s
== 20170424041610 AddPasswordDigestToUsers: migrated (0.0013s) ================

== 20170514215307 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0007s
== 20170514215307 AddRememberDigestToUsers: migrated (0.0008s) ================

== 20170614215124 AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean, {:default=>false})
   -> 0.0013s
== 20170614215124 AddAdminToUsers: migrated (0.0014s) =========================

== 20170618221238 AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0011s
-- add_column(:users, :activated, :boolean, {:dafault=>false})
   -> 0.0006s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0005s
== 20170618221238 AddActivationToUsers: migrated (0.0024s) ====================

== 20170703042006 AddResetToUsers: migrating ==================================
-- add_column(:users, :reset_digest, :string)
   -> 0.0010s
-- add_column(:users, :reset_sent_at, :datetime)
   -> 0.0006s
== 20170703042006 AddResetToUsers: migrated (0.0023s) =========================

== 20170719214241 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0019s
-- add_index(:microposts, [:user_id, :created_at])
   -> 0.0010s
== 20170719214241 CreateMicroposts: migrated (0.0035s) ========================

【事前準備】DBにサンプルデータを登録する

yokoyan:~/workspace/sample_app (user-microposts) $ rails db:seed
yokoyan:~/workspace/sample_app (user-microposts) $ 

【動作確認】マイクロポストの表示確認

登録が終わったら、Railsサーバを再起動して、ブラウザで確認。

image

【scss】マイクロポストのCSSを追加

/* 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;
  }
}

【動作確認】マイクロポストの表示確認2

ブラウザで確認。50件のマイクロポストが表示され、ページネーションも動くことを確認。

image

演習1

(1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。

1から10までのうち先頭6件を表示。

?> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]

演習2

先ほどの演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。

演習1と同じ結果となる。

?> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]
>> 

演習3

Fakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント (英語) を眺めながら画面に出力する方法を学び、実際に大学名や電話番号、Hipster IpsumやChuck Norris facts (参考: チャック・ノリスの真実) を画面に出力してみましょう。(訳注: もちろん日本語にも対応していて、例えば沖縄らしい用語を出力するfaker-okinawaもあります。ぜひ遊んでみてください。)

Fakerで大学名を表示する。

?> Faker::University.name
=> "Volkman Institute"
>> 

Fakerで電話番号を表示する。

?> Faker::PhoneNumber.phone_number
=> "1-782-129-1939"
>> 

Hipster(ジャズ?)のテキストを表示する。

?> Faker::Hipster.sentence
=> "Street forage tousled asymmetrical."

チャック・ノリスの真実(ジョーク)を表示する。 量子暗号はチャック・ノリスでは機能しませんwww

?> 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. "
>> 

faker-okinawaを試す。

【事前準備】faker-okinawaのインストール

gemfileを修正する。 ※fakerと同時に入れるとエラーになったため、一旦fakerをコメントアウトする。

# gem 'faker',  '1.6.6'
gem 'faker-okinawa'

バンドルする。

yokoyan:~/workspace/sample_app (user-microposts) $ bundle
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/.........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies...

・・・略・・・

Installing faker-okinawa 0.1.0
Bundle complete! 26 Gemfile dependencies, 84 gems now installed.
Gems in the group production were not installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

gemをインストール。

yokoyan:~/workspace/sample_app (user-microposts) $ gem install faker-okinawa
Successfully installed faker-okinawa-0.1.0
1 gem installed
【動作確認】faker-okinawaを試してみる。

泡盛の名前を取得。

?> Faker::Okinawa::Awamori.name
=> "かねやま"

設定を戻す

動作確認が完了したら、fakerを元に戻す。

Gemfileを戻す。

gem 'faker',  '1.6.6'
#gem 'faker-okinawa'

bundleする。

yokoyan:~/workspace/sample_app (user-microposts) $ bundle

fakerの再インストール。

yokoyan:~/workspace/sample_app (user-microposts) $ gem install faker
Fetching: faker-1.8.4.gem (100%)
Successfully installed faker-1.8.4
1 gem installed

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

本章での学び

【事前準備】統合テストの作成

プロフィール画面用の統合テストを生成する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate integration_test users_profile
Running via Spring preloader in process 1595
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  test_unit
      create    test/integration/users_profile_test.rb

【事前準備】テストデータの生成

マイクロポスト用のfixtureファイルを修正する。 各マイクロポストデータに、user: michaelを追記すると、michaelユーザの投稿として関連付けされる。また、マイクロポストのページャーのテストを行うために、30回マイクロポストを自動生成する。

なお、ymlの各属性の後ろに:をつけないと、エラーが発生するので注意。 詳細は以下参照。

Railsチュートリアル中にcould not find expected ‘:’ while scanning a simple keyが出た時の対処法

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

【test】統合テストの実装

以下の仕様で実装する。

  • GETリクエスト送信(ユーザープロフィールページ)
  • ユーザープロフィールページ(users/show)のテンプレートが表示されること
  • HTMLのtitle属性にユーザーの名前が表示されること
  • HTMLのh1タグにユーザーの名前が表示されること
  • HTMLのh1タグの下に、imgタグ(gravatorクラス付き)が表示されること
  • ユーザーのマイクロポスト数をカウントして文字列に変換する。レスポンスのHTML内に存在するかチェック
  • HTMLにdivタグ(paginationクラス付き)が表示されること
  • ユーザーのマイクロポストから、paginateを使って1ページ目を取得する。取得した結果を繰り返す
    • マイクロポストのcontent属性が、レスポンスのHTML内に存在するかチェック
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.gravator'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1) do |micropost|
      assert_match micropost.content, response.body
    end
  end
end

【test】テスト実行

テスト結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 1230
Started with run options --seed 10439

  54/54: [================================================================================================================================================================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.15487s
54 tests, 238 assertions, 0 failures, 0 errors, 0 skips

演習1

リスト 13.28にある2つの’h1’のテストが正しいか確かめるため、該当するアプリケーション側のコードをコメントアウトしてみましょう。テストが green から redに変わることを確認してみてください。

viewの該当箇所のタグをコメントアウトする。

      <!-- <h1> -->
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>

テストがredになることを確認。 (確認後はコメントアウトを戻すこと)

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 1915
Started with run options --seed 50720

 FAIL["test_profile_display", UsersProfileTest, 1.6931300410069525]
 test_profile_display#UsersProfileTest (1.69s)
        Expected at least 1 element matching "h1", found 0..
        Expected 0 to be >= 1.
        test/integration/users_profile_test.rb:14:in `block in <class:UsersProfileTest>'

  54/54: [================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.94467s
54 tests, 234 assertions, 1 failures, 0 errors, 0 skips

演習2

リスト 13.28にあるテストを変更して、will_paginateが1度のみ表示されていることをテストしてみましょう。ヒント: 表 5.2を参考にしてください。

will_paginateに該当する、div class=paginationが1度のみ表示されていることをテストする。

image.png

  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.gravator'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1) do |micropost|
      assert_match micropost.content, response.body
    end
    assert_select 'div.pagination', count:1 #追加
  end

テストがgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 2471
Started with run options --seed 42382

  54/54: [================================================================================================================================================================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.10291s
54 tests, 239 assertions, 0 failures, 0 errors, 0 skips

おわりに

マイクロポストのテストデータを作成し、テストコードの実装が完了しました。 最新版 (Rails 5.1対応) が出てしまったので、9月中に14章まで完了できるようにがんばります!