今日やったこと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