ぞえの技術めも

Ruby on Rails勉強中

【149日目】【1日20分のRailsチュートリアル】【第11章】マイクロポストのビューを追加する

Ruby on Railsチュートリアル(第3版)

今日は「11.2.1 マイクロポストの描画」から。

11.2.1 マイクロポストの描画

この項では、ユーザーのプロフィール画面 (show.html.erb) でそのユーザーのマイクロポストを表示させ、また、これまでに投稿した総数も表示するようにしていきます。

ふむふむ。Twitterのプロフィール画面のイメージかな。

まずは、Micropostのコントローラとビューを作成するために、コントローラを生成しましょう。

生成しましょう。

$ rails generate controller Microposts
      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

user.html.erbパーシャルと同じようにmicropost.html.erbパーシャルを定義してマイクロポストを表示するようにする。

パーシャル定義しようとしたらファイルがなかったので作成しておく。

$ touch app/views/microposts/_micropost.html.erb

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>

今回の場合は、ユーザーコントローラのコンテキストにおいて、マイクロポストをページネーションしたいため、明示的に@microposts変数を will_paginateに渡す必要があります。

will_paginateについての説明が全くピンと来てないけど明示的に変数を渡す必要があるのは分かった。

チュートリアルのコードにはredirect_toの行のコードがないけどとりあえず下に@microposts変数追加しといた。

app/controllers/users_controller.rb

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

これですべての要素が揃ったので、プロフィール画面にマイクロポストを表示させてみましょう (リスト11.23)。

マイクロポストの投稿数はuser.microposts.countで取得できて、それなりに高速らしい。いいね!
アイコンの表示の下にマイクロポストを表示させるようにコードを追加。

app/views/users/show.html.erb

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

ここで、改良した新しいプロフィール画面をブラウザで見てみましょう (図11.5)。
…何とも寂しいページで、がっかりですね。マイクロポストが1つもないのでは無理もありません。

今までのユーザーページと何も変わっていない…!ので動作確認はマイクロポスト追加してからにしよう。

今日の学習時間は【20分】

次は「11.2.2 マイクロポストのサンプル」から。

【148日目】【1日20分のRailsチュートリアル】【第11章】ユーザーが破棄された場合、マイクロポストも破棄されるようにする

Ruby on Railsチュートリアル(第3版)

今日は「11.1.4 マイクロポストを改良する」の「Dependent: destroy」から。

11.1.4 マイクロポストを改良する

Dependent: destroy

サイト管理者はユーザーを破棄する権限を持ちます。ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべきです。

マイクロポストとユーザーの関連性を切るのもありだと思ったけど、残しておいても容量圧迫するもんねぇ。。。要らないか。

この振る舞いは、has_manyメソッドにオプションを渡してあげることで実装できます (リスト11.18)。

元々書いてたhas_manyメソッドにdependent: :destroyオプションを追加。
これだけでユーザーを削除したらそのユーザーに紐付いたマイクロポストも削除されるらしい…!楽!シンプル!

app/models/user.rb

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  :

次に、リスト11.18が正しく動くかどうか、テストを使ってUserモデルを検証してみます。

マイクロポストを生成して、次にユーザーを削除してマイクロポストの数が1減っているかを確認するテスト。

test/models/user_test.rb

  test "associated microposts should be destroyed" do
    @user.save
    @user.microposts.create!(content: "Lorem ipsum")
    assert_difference 'Micropost.count', -1 do
      @user.destroy
    end
  end

問題なく動きました。

$ bundle exec rake test
54 tests, 223 assertions, 0 failures, 0 errors, 0 skips

11.2 マイクロポストを表示する

Twitterのような独立したマイクロポストのindexページは作らずに、図11.4モックアップに示したように、ユーザーのshowページで直接マイクロポストを表示させることにします。

まずは特定のユーザーのマイクロポストを表示させるようにユーザーのプロフィールページを修正する。

今日の学習時間は【15分】

次は「11.2.1 マイクロポストの描画」から。

【147日目】【1日20分のRailsチュートリアル】【第11章】マイクロポストを特定の順序で取得できるようにする

Ruby on Railsチュートリアル(第3版)

今日は「11.1.4 マイクロポストを改良する」から。

11.1.4 マイクロポストを改良する

具体的には、ユーザーのマイクロポストを特定の順序で取得できるようにしたり、マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにしていきます。

ふむふむ。

デフォルトのスコープ

まずデータベース上の最初のマイクロポストが、fixture内のマイクロポスト (most_recent) と同じであるか検証するテストを書いていきましょう (リスト11.13)。

まずテストを書いてみる。

test/models/micropost_test.rb

  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end

リスト11.13では、マイクロポスト用のfixtureファイルからサンプルデータを読み出しているので、次のfixtureファイルも必要になります (リスト11.14)。

マイクロポスト用のfixtureファイルを作成する。

test/fixtures/microposts.yml

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

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>

ほとんどのシステムでは上から順に作成されるので、fixtureファイルでも意図的に順序をいじっています。
(中略)
この振る舞いは恐らくシステムに依存していて崩れやすいので、(本来は) この振る舞いに依存したテストは書くべきでは無いでしょう。

崩れやすいからオススメではないしいつも使えるわけではない?けど、今回はとりあえず大丈夫なのかな…??

この段階ではマイクロポストをソートしてないのでテストは失敗する。

$ bundle exec rake test TEST=test/models/micropost_test.rb TESTOPTS="--name test_order_should_be_most_recent_first"
Finished in 0.11341s
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

残念ながらデフォルトの順序が昇順となっているので、このままでは数の小さい値から大きい値にソートされてしまいます (最も古い投稿が最初に表示されてしまいます)。
順序を逆にしたい場合は、一段階低いレベルの技術ではありますが、次のように生のSQLを引数に与える必要があります。

新しい投稿から古い投稿の順になるようソートする。

app/models/micropost.rb

  default_scope -> { order(created_at: :desc) }

これでテストが通るようになりました。

$ bundle exec rake test
Finished in 1.94684s
53 tests, 222 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【25分】

次は「11.1.4 マイクロポストを改良する」の「Dependent: destroy」から。

【146日目】【1日20分のRailsチュートリアル】【第11章】UserモデルとMicropostモデルの関連付け

Ruby on Railsチュートリアル(第3版)

今日は「11.1.3 User/Micropostの関連付け」から。

11.1.3 User/Micropostの関連付け

それぞれのマイクロポストは1人のユーザーと関連付けられ、それぞれのユーザーは (潜在的に) 複数のマイクロポストと関連付けられます。

ふむふむ。
UserとそのMicropostは1対多の関係、と。

これらのメソッドは使うと、紐付いているユーザーを通してマイクロポストを作成することができます (慣習的に正しい方法です)。

なんか不思議…!でもWebアプリケーションの作りには合ってる感じする。
前回の記事の「慣習的に間違っている」行はこのことらしい。

Micropostモデルの方では、belongs_to :userというコードが必要になるのですが、これは リスト11.1マイグレーションによって自動的に生成されているはずです (リスト11.9)。 一方、Userモデルの方では、has_many :micropostsと追加する必要があります。ここは自動的に生成されないので、手動で追加してください (リスト11.10)。

Micropostモデルは下記コードが元々生成されていることを確認。

app/models/micropost.rb

class Micropost < ActiveRecord::Base
  belongs_to :user
  :
end

Userモデルは自動的に生成されないので下記コードを追加。

app/models/user.rb

class User < ActiveRecord::Base
  has_many :microposts
  :
end

正しく関連付けができたら、リスト11.2のsetupメソッドを修正して、慣習的に正しくマイクロポストを作成してみます (リスト11.11)。

慣習的云々のコードを下記のように修正。

test/models/micropost_test.rb

    @micropost = @user.microposts.build(content: "Lorem ipsum")

テストも問題なし。

$ bundle exec rake test
52 tests, 221 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【17分】

次は「11.1.4 マイクロポストを改良する」から。

【145日目】【1日20分のRailsチュートリアル】【第11章】Micropostモデルのバリデーション

Ruby on Railsチュートリアル(第3版)

今日は「11.1.2 Micropostのバリデーション」から。

11.1.2 Micropostのバリデーション

Micropostの初期テストはUserモデルの初期テスト (リスト6.7) と似ています。

まずはMicropostモデル単体のテストを作成する。

test/models/micropost_test.rb

require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

  def setup
    @user = users(:michael)
    # 次の行は慣習的に間違っている
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end

  test "should be valid" do
    assert @micropost.valid?
  end

  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
end

慣習的に間違っているコードは後で修正するそう。

また、有効性に対するテストは成功しますが、存在性に対するテストは失敗するはずです。これは、Micropostモデルのバリデーションがまだ何もないことが原因です。

"test_user_id_should_be_present"のテストで失敗しますね。

$ bundle exec rake test:models
 FAIL["test_user_id_should_be_present", MicropostTest, 2017-05-26 15:23:17 +0000]
 test_user_id_should_be_present#MicropostTest (1495812197.48s)
        Expected true to be nil or false
        test/models/micropost_test.rb:17:in `block in <class:MicropostTest>'

14 tests, 23 assertions, 1 failures, 0 errors, 0 skips

これを修正するためには、ユーザーidに対するバリデーションを追加する必要があります (リスト11.4)。

Micropostモデルにバリデーションを追加。

app/models/micropost.rb

class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
end

これでテストが通るようになりました。

$ bundle exec rake test:models
14 tests, 23 assertions, 0 failures, 0 errors, 0 skips

user_id属性と同様に、content属性も存在する必要があり、さらにマイクロポストが140文字より長くならないよう制限を加えます。

content属性とマイクロポストの制限に関するテストを追加。

test/models/micropost_test.rb

  test "content should be present" do
    @micropost.content = "   "
    assert_not @micropost.valid?
  end

  test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
  end

バリデーション追加してないのでテストは失敗する。

$ bundle exec rake test:models
 FAIL["test_content_should_be_at_most_140_characters", MicropostTest, 2017-05-26 15:23:17 +0000]
 test_content_should_be_at_most_140_characters#MicropostTest (1495812197.13s)
        Expected true to be nil or false
        test/models/micropost_test.rb:27:in `block in <class:MicropostTest>'

 FAIL["test_content_should_be_present", MicropostTest, 2017-05-26 15:23:17 +0000]
 test_content_should_be_present#MicropostTest (1495812197.13s)
        Expected true to be nil or false
        test/models/micropost_test.rb:22:in `block in <class:MicropostTest>'
16 tests, 25 assertions, 2 failures, 0 errors, 0 skips

これに対応するアプリケーション側の実装は、Userのname用バリデーション (リスト6.16) と全く同じです。

Userモデルと同じようなバリデーションをMicropostモデルに追加。

app/models/micropost.rb

  validates :content, presence: true, length: { maximum: 140 }

これでテストは通るようになりました。

$ bundle exec rake test:models
16 tests, 25 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【18分】

次は「11.1.3 User/Micropostの関連付け」から。

【144日目】【1日20分のRailsチュートリアル】【第11章】Micropostモデルを生成する

Ruby on Railsチュートリアル(第3版)

今日は「第11章 ユーザーのマイクロポスト」から。ようやく第11章…!

第11章 ユーザーのマイクロポスト

全ての準備が整った今、ユーザーが短いメッセージを投稿できるようにするためのリソース「マイクロポスト」を追加していきます。

Twitterみたいなアプリケーションにするそう。

11.1 Micropostモデル

Git をバージョン管理に使っている場合は、いつものようにトピックブランチを作成しておきましょう。

作成しておきましょう。

$ git checkout master
$ git checkout -b user-microposts

11.1.1 基本的なモデル

Micropostモデルは、マイクロポストの内容を保存するcontent属性と、特定のユーザーとマイクロポストを関連付けるuser_id属性の2つの属性だけを持ちます。

基本的な属性はこの2つらしい。

図11.1のモデルでは、マイクロポストの投稿にString型ではなくtext型を使っている点に注目してください。

文字列を扱うにも色んな型があるんだな…。

リスト6.1でUserモデルを生成したときと同様に、Railsのgenerate modelコマンドを使ってMicropostモデルを生成してみます。

Micropostモデルを生成してみました。

$ rails generate model Micropost content:text user:references
      invoke  active_record
      create    db/migrate/20170525023044_create_microposts.rb
      create    app/models/micropost.rb
      invoke    test_unit
      create      test/models/micropost_test.rb
      create      test/fixtures/microposts.yml

このgenerateコマンドはmicropostsテーブルを作成するためのマイグレーションファイルを生成します。

マイグレーションファイル生成されてますね。

user_idとcreated_at両方のカラムを1つの配列に含めることで、Active Recordで両方のキーを同時に使用する複合キーインデックスを作成できます。

うーん、データベースから検索しやすくするため、って感じかな。。。たぶん
下記をマイグレーションファイルに追加することでuser_idとcreated_atカラムにインデックスが付与できる?っぽい。

db/migrate/[timestamp]_create_microposts.rb

    add_index :microposts, [:user_id, :created_at]

マイグレーションしてデータベースを更新。

$ bundle exec rake db:migrate
  :
-- create_table(:microposts)
-- add_index(:microposts, [:user_id, :created_at])
  :

今日の学習時間は【23分】

次は「11.1.2 Micropostのバリデーション」から。

【143日目】【1日20分のRailsチュートリアル】【第10章】演習の3.と期限切れの比較の証明

Ruby on Railsチュートリアル(第3版)

今日は「10.5 演習」の3.から。

10.5 演習

3.リスト10.42では、activateメソッドとcreate_reset_digestメソッドの両方でupdate_attributeを呼び出しており、それぞれのアクセスによってデータベーストランザクションが個別に発生してしまう点が残念です。
リスト10.59のテンプレートに記入することで、個別のupdate_attribute呼び出しを単一のupdate_columns呼び出しに統合し、データベースアクセスが1回で済むようにしてください。
変更後にテストを実行し、GREENになることを確認してください。

2行で書いてるupdate_attribute(=2回データベースアクセスが発生する)をupdate_columnsで1回のデータベースアクセスにしよう、ってことか。
まとめるだけだから引数は変えなくて問題ないかな。

app/models/user.rb

class User < ActiveRecord::Base
  :
  # アカウントを有効にする
  def activate
    update_columns(activated: true, activated_at: Time.zone.now)
  end
  :
  # パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest:  User.digest(reset_token),
                   reset_sent_at: Time.zone.now)
  end

テスト実行して問題ないことを確認。

$ bundle exec rake test
48 tests, 217 assertions, 0 failures, 0 errors, 0 skips

演習終わり!

10.6 証明: 期限切れの比較

この説では、10.2.4で用いたパスワード期限切れの期間の比較が正しいことを証明します。

私の中では納得できている話なのでさらっと読むだけ。

数式で書くと順を追って理解できるから分かりやすいね。

今日の学習時間は【14分】

これにて第10章終わり!

次は「第11章 ユーザーのマイクロポスト」から。