2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #14 ユーザ投稿表示, ページネーション編

Last updated at Posted at 2019-11-15

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#13 パスワード再設定編
次回:#15 投稿機能, Active Storage編

今回の流れ

  1. 投稿用のモデルをつくる
  • サンプル投稿を表示する
  • テストをつくる

この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回はユーザ投稿の表示とページネーションを実装します。
(投稿機能は#15で紹介します。)

投稿用のモデルをつくる

ここでの手順は以下の通りです。

  • Micropostモデルをつくる
  • Userモデルを編集する
  • バリデーションを追加する
  • Micropostモデルを改良する
  • エラー時の日本語化を行う

Micropostモデルをつくる

まずは投稿のためのMicropostモデルをつくりましょう。
ユーザが投稿できる項目は以下の通りです。

  • 動画視聴時間
  • メモ
  • 画像

加えて投稿はユーザがいて初めて成立するので、Userモデルに所属させます。
そのためには生成時にuser:referencesを加えます。
以上を踏まえてMicropostモデルを生成しましょう。

bash
$ rails g model Micropost time:integer memo:text picture:string user:references

投稿を表示する際、twitterのように最新のものが上部に来てほしいものです。
その準備として、マイグレーションにインデックスを加えます。

db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.2]
  def change
    create_table :microposts do |t|
      t.integer :time
      t.text :memo
      t.string :picture
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

user_idは投稿したユーザ、create_atは投稿時間を管理しています。
これらを複合キーとすることで、望み通り取り出すことが可能です。

Userモデルを編集する

MicropostモデルはUserモデルに所属しました。
この実装はbelongs_to :userにより、user_idとして形になりました。
今度はUserモデルにMicropostモデルを所有してもらいましょう。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
  # 中略
end

has_manyによりUserはMicropostと1対多の関係になりました。
こうすることでmicropostsを指定する時、こんな書き方が可能です。

user = User.new
user.microposts

慣習的にも正しいので、以上の作業は忘れずに行いましょう。

バリデーションを追加する

ここでは以下のバリデーションを追加します。

  • user_idが空の場合受けつけない
  • いずれの値も空の場合(user_idを除く)受けつけない
  • pictureは適切なファイルのみ受けつける
  • pictureは5MBまでを受けつける
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_one_attached :picture
  default_scope { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :memo, length: { maximum: 255 }
  validates :only_user_id, presence: true
  validate :validate_picture
  
  def resize_picture
    return self.picture.variant(resize: '200x200').processed
  end
  
  private
    def only_user_id
      time.presence or memo.presence or picture.attached?
    end
    
    def validate_picture
      if picture.attached?
        if !picture.content_type.in?(%('image/jpeg image/jpg image/png image/gif'))
          errors.add(:picture, 'はjpeg, jpg, png, gif以外の投稿ができません')
        elsif picture.blob.byte_size > 5.megabytes
          errors.add(:picture, "のサイズが5MBを超えています")
        end
      end
    end
end

いずれかという条件の実装にはonly_user_idメソッドを定義しました。
画像の条件にはvalidate_pictureメソッドを定義しました。

参考にさせていただきました↓
「いずれかのカラムが空でなければ良い」というバリデーション
Active Storage移行記:バリデーション編

(これらのバリデーションはエラーになる恐れがあります。)
(その場合、#15Active Storage導入をご覧ください。)

Micropostモデルを改良する

ここでは以下の機能を追加します。

  • 新しい投稿を先にソートする
  • ユーザが削除されたら投稿も削除する
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope { order(created_at: :desc) }
  # 中略
end
app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  # 中略
end

エラー時の日本語化を行う

あとはエラー時の言語を日本語にしましょう。
日本語化にはgemと設定が必要です。#6Railsのバリデーションエラーのメッセージの日本語化を参考に設定を行なってください。

お済みの方は以下のようにファイルを編集してください。

config/locales/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザ
      micropost: 投稿
    attributes:
      user:
        name: 名前
        email: メールアドレス
        password: パスワード
        password_confirmation: パスワード(再入力)
      micropost:
        time: 記録時間
        memo: メモ
        picture: 画像
        user_id: ユーザID
        only_user_id: 入力欄

後ほどビューをつくります。
その際エラー表示を確認するので、ひとまずここで終了です。
最後にデータベースを更新します。

bash
$ rails db:migrate

サンプル投稿を表示する

Micropostモデルは整いました。次は投稿を表示しましょう。
ここでの手順は以下の通りです。

  • サンプル投稿を生成する
  • サンプル投稿を表示する
  • ページネーションを実装する

サンプル投稿を生成する

サンプル投稿を生成するにはfakerを使用します。

gemfile
+ gem 'faker'
bash
$ bundle install

それではサンプル投稿を生成します。
ついでにサンプルユーザも生成しておきます。

db/seeds.rb
User.create!(
  name: "Example User",
  email: "example@railstutorial.org",
  password: "foobar",
  password_confirmation: "foobar",
  activated: true
)

5.times do |n|
  name = Faker::Name.name
  email = "example_#{n+1}@railstutorial.org"
  password = "password"
  User.create!(
    name: name,
    email: email,
    password: password,
    password_confirmation: password,
    activated: true
  )
end

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

最後にデータベースを再構築しましょう。

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

これでサンプルユーザとサンプル投稿が生成されました。

サンプル投稿を表示する

生成を終えたら投稿を表示しましょう。
ここでの手順は以下の通りです。

  • usersコントローラを編集する
  • ビューを編集する
  • ページネーションを追加する

usersコントローラを編集する

投稿を表示させるビューはshow.html.erbです。
つまりこれに対応するコントローラにMicropostを渡す必要があります。
対応するのはusersコントローラです。編集しましょう。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 中略
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.page(params[:page]).per(10)
  end

この1文はページネーションを実装する記述です。

@microposts = @user.microposts.page(params[:page]).per(10)

ページネーションしたい対象に.page〜以下を加えることで動作します。
per(10)は1ページの表示数を表します。今回は10にしました。

ビューを編集する

続いてはビューです。
パーシャルを使用しつつ実装しましょう。

app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="container show-container">
  <div class="row">
    <div class="col bg-primary">
      form
    </div>
    <div class="col bg-secondary">
      figure
    </div>
  </div>
  <div class="row">
    <div class="col">
      <%= render 'layouts/log' %>
    </div>
  </div>
</div>
app/views/layouts/_log.html.erb
<h1 class="log-title col-2">Logs</h1>
<% if @user.microposts.any? %>
  <div class="container">
    <ol class="microposts">
    <% @microposts.each do |micropost| %>
        <li id ="micropost-<%= micropost.id %>">
          <span class="row log-list">
            <span class="col-2 log-timestamp d-none d-md-inline-block log-timestamp-block">
              <span class="log-timestamp"><%= time_ago_in_words(micropost.created_at) %></span>
            </span>
            <span class="col-md-10 col-log-memos">
              <div class="log-time-and-edit">
                <div class="row">
                  <span class="log-time col-3"><%= micropost.time %></span>
                  <span class="col-7 log-timestamp log-timestamp-inline"><%= time_ago_in_words(micropost.created_at) %></span>
                  <span class="log-edit col-2"><%= link_to image_tag('edit.png', class: "log-edit-image"), '#' %></span>
                </div>
              </div>
              <span class="log-memo"><%= micropost.memo %></span>
              <span class="log-picture"><%= micropost.picture %></span>
            </span>
          </span>
        </li>
    <% end %>
    </ol>
  </div>
  <%= paginate @microposts %>
<% else %>
  <span>まだ投稿がありません</span>
<% end %>
app/assets/stylesheets/application.scss.erb
// max-width = 767px
@media (max-width: 767px) {
  .nav-item-extend {
    margin-top: 0.6rem;
  }
  
  .log-title {
    padding: 0;
  }
  
  .log-list {
    border-left: 1px solid $lantern-dark-white;
  }
}

@media (min-width: 768px) {
  .log-timestamp-inline {
    visibility: hidden;
  }
}

// 中略

// logs
ol {
  padding: 0;
  list-style: none;
}

.log-title {
  margin: 1.5rem 0;
  text-align: center;
}

.log-list {
  margin-bottom: 1rem;
  
  .log-timestamp {
    color: gray;
  }
  
  .log-timestamp-block {
    text-align: center;
    padding-bottom: 2rem;
    margin-top: 0.1rem;
    border-right: 1px solid $lantern-dark-white;
  }
  
  .col-log-memos {
    padding: 0 1rem 0 1rem;
    
    .log-time-and-edit {
      .log-time {
        font-size: 1.2rem;
        color: $lantern-yellow;
      }
      
      .log-timestamp-inline {
        text-align: right;    
        margin-top: 0.1rem;
        padding: 0;
      }
      
      .log-edit {
        text-align: right;
        margin-top: 0.1rem;

        .log-edit-image {
          width: 1rem;
        }
      }
    }
    
    .log-memo {
    }
    
    .log-picture {
    }
  }
}

// paginate
.pagination {
  margin-top: 1.6rem;
  
  .page-item {
    .page-link {
      border: 1px solid $lantern-light-white;
      background-color: $lantern-light-white;
      color: $lantern-blue;
    }
  }
  
  .page-item.active .page-link {
    background-color: $lantern-blue;
    border-color: $lantern-blue;
    color: $lantern-light-white;
  }
}
// 中略

_log.html.erbのこの部分もページネーションを実装するための記述です。

<%= paginate @microposts %>

コントローラに引き続き、2つの記述が見受けられました。
これらはkaminariというgemにより動作します。
準備は以上です。後はgemを挿れるだけです。

ページネーションを追加する

それではkaminariを追加し、ページネーションを動作させましょう。
Bootstrap4と日本語化の適用も行います。

gemfile
+ gem 'kaminari'
+ gem 'kaminari-bootstrap'
bash
$ bundle install
$ rails g kaminari:views bootstrap4
config/locales/models/ja.yml
ja:
  # 中略
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: " &rsaquo;"
      truncate: "..."

これらを終えるとビューはこんな感じになります。
PC版↓
lantern_logs_pc.png
スマホ版↓
lantern_logs_iphoneX.png

参考になります↓
【Ruby on Rails】gem(Kaminari)でページネーション機能を追加してBootstrapを適用する。
【Rails初心者】ページネーションを実装して自分好みにデザインを変える

テストをつくる

最後にテストを完成させます。
ここでの手順は以下の通りです。

  • FactoryBotを整える
  • テストを書く

FactoryBotを整える

まずはテストを行う前の準備をしましょう。
今回新たにMicropostモデルが生成されました。
それに伴うテストを行いたいので、FactoryBotで導入しましょう。

ここでの手順は以下の通りです。

  • テスト用のMicropostモデルを生成する
  • テスト用のUserモデルを編集する
spec/factories/microposts.rb
FactoryBot.define do
  factory :memos, class: Micropost do
    trait :memo_1 do
      time { 240 }
      memo { "I just ate an orange!" }
      picture { nil }
      user_id { 1 }
      created_at { 10.minutes.ago }
    end

    trait :memo_2 do
      time { 180 }
      memo { "Check out the @tauday site by @mhartl: http://tauday.com" }
      picture { nil }
      user_id { 1 }
      created_at { 3.years.ago }
    end

    trait :memo_3 do
      time { 59 }
      memo { "Sad cats are sad: http://youtu.be/PKffm2uI4dk" }
      picture { nil }
      user_id { 1 }
      created_at { 2.hours.ago }
    end

    trait :memo_4 do
      time { 207 }
      memo { "Writing a short test" }
      picture { nil }
      user_id { 1 }
      created_at { Time.zone.now }
    end
    association :user, factory: :user
  end
end
spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Michael Example" }
    sequence(:email) { |n| "michael_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
    activated { true }
  end
  
  factory :other_user, class: User do
    name { "Sterling Archer" }
    sequence(:email) { |n| "duchess_#{n}@example.gov" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { true }
  end
  
  factory :no_activation_user, class: User do
    name { "No Activation" }
    sequence(:email) { |n| "no_#{n}@activation.co.jp" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { false }
  end
end

microposts.rbではtraitを使って、Micropostモデル内を区切りました。
users.rbではsequenceを使って、メールアドレスの一意性を保つよう番号をつけました。
以上で準備は完了です。

※ 変更により他のテストが失敗する可能性があるので、適宜変更を加えてください。

Micropostモデルのテスト

それではいよいよテストに入りましょう。
このテストでは以下を確認します。

  • モデルが正しく生成されているか
  • いずれの値も空の場合(user_idを除く)、Micropostは存在しないか
  • カラムが最新のものから順に並んでいるか
  • user_idが存在しないMicropostは存在しないか
  • memoが255文字を超えないか
spec/models/micropost_spec.rb
require 'rails_helper'

RSpec.describe Micropost, type: :model do
  
  let(:user) { create(:user) }
  let(:micropost) { user.microposts.build(time: 240, memo: "Lorem ipsum", user_id: user.id) }

  describe "Micropost" do
    it "should be valid" do
      expect(micropost).to be_valid
    end
    
    it "should not be valid" do
      micropost.update_attributes(time: 1, memo: "  ", picture: nil, user_id: user.id)
      expect(micropost).to be_valid
      micropost.update_attributes(time: nil, memo: "  ", picture: nil, user_id: user.id)
      expect(micropost).to be_invalid
    end
    
    it "should be most recent first" do
      create(:memos, :memo_1, created_at: 10.minutes.ago)
      create(:memos, :memo_2, created_at: 3.years.ago)
      create(:memos, :memo_3, created_at: 2.hours.ago)
      memo_4 = create(:memos, :memo_4, created_at: Time.zone.now)
      expect(Micropost.first).to eq memo_4
    end
  end
    
  describe "user_id" do
    it "should not be present" do
      micropost.user_id = nil
      expect(micropost).to be_invalid
    end
  end
  
  describe "memo" do
    it "should not be at most 255 characters" do
      micropost.memo = "a" * 255
      expect(micropost).to be_valid
      micropost.memo = "a" * 256
      expect(micropost).to be_invalid
    end
  end
end

Userモデルのテスト

このテストでは以下を確認します。

  • ユーザが削除されたら投稿も削除されるか
spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # 中略
  it "destroys assosiated microposts" do
    user.microposts.create!(memo: "Lorem Ipsum")
    expect{ user.destroy }.to change{ Micropost.count }.by(-1)
  end

以上でテストは終了です。
次回は投稿機能を実装します。

前回:#13 パスワード再設定編
次回:#15 投稿機能, Active Storage編

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?