学習日記

Ruby on Rails勉強してます

今日やったこと14

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


13.1 Micropostモデル

Micropostデータモデルの構造

microposts

カラム名
id integer
content text
user_id integer
create_at datetime
update_at datetime

マイクロポストの投稿にString型ではなくText型を使っている理由は、Text型の方が投稿フォームを表現豊かにしたり、変更に柔軟に対応できるため。

Micropostモデルを生成。

$ rails generate model Micropost content:text user:references

references型を利用することで、自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、UserとMicropostを関連付けする下準備をしてくれる。
生成されたマイグレーションファイルでuser_idとcreated_atカラムにインデックスを付与する。こうすることで、user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくなる。

db/migrate/[timestamp]_create_microposts.rb

class CreateMicroposts < ActiveRecord::Migration[5.0]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at] # インデックスを付与する
  end
end

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

$ rails db:migrate

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

  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
end

この時点ではテストは失敗するのでMicropostモデルのバリデーションを追加する。

app/models/micropost.rb

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

これでテストに成功しました。

UserモデルとMicropostモデルを関連付ける。
関連付けにはbelongs_to/has_manyを使う。
Micropostモデルの方では、belongs_to :userというコードが必要になるが、マイグレーションによって自動生成されている。Userモデルはhas_many :micropostsと追加する必要があるので追加していく。

app/models/user.rb

class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end

ユーザーの最も新しいマイクロポストを最初に表示するようにするために、default scopeというテクニックを使う。
まずはマイクロポストの順序付けのテストを先に作っていく。

test/models/micropost_test.rb

require 'test_helper'

class MicropostTest < ActiveSupport::TestCase
  .
  .
  .
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end
end

テストは失敗。
テストではマイクロポスト用のfixtureファイルからサンプルデータを読み出しているので、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 %>

default_scopeを使いマイクロポストを投稿の古い順番に並べる。

app/models/micropost.rb

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

再びテストし成功。


dependent: :destroyというオプションを使うと、ユーザーが削除されたときに、そのユーザーに関連づいたマイクロポストも一緒に削除されるようになる。

app/models/user.rb

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  .
  .
  .
end

正しく動くかどうか、テストする。

test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  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
end


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

ユーザーのプロフィール画面 (show.html.erb) でそのユーザーのマイクロポストや投稿した総数を表示させる。
まずは、Micropostのコントローラとビューを作成。

$ rails generate controller Microposts

マイクロポストを表示するパーシャルを用意して、ユーザーのshowページ (プロフィール画面) にマイクロポストを追加する。

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

@microposts変数をwill_paginateに渡す必要があるので、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

サンプルデータにマイクロポストを追加し、開発環境用のデータベースで再度サンプルデータを生成する。

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を追加する。

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

プロフィール画面で表示されるマイクロポストに対して、テストを書いていく。

$ rails generate integration_test users_profile

テストのために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プロフィール画面に対するテスト

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
$ rails t