こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#13 パスワード再設定編
次回:#15 投稿機能, Active Storage編
今回の流れ
- 投稿用のモデルをつくる
- サンプル投稿を表示する
- テストをつくる
この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回はユーザ投稿の表示とページネーションを実装します。
(投稿機能は#15で紹介します。)
投稿用のモデルをつくる
ここでの手順は以下の通りです。
- Micropostモデルをつくる
- Userモデルを編集する
- バリデーションを追加する
- Micropostモデルを改良する
- エラー時の日本語化を行う
Micropostモデルをつくる
まずは投稿のためのMicropostモデルをつくりましょう。
ユーザが投稿できる項目は以下の通りです。
- 動画視聴時間
- メモ
- 画像
加えて投稿はユーザがいて初めて成立するので、Userモデルに所属させます。
そのためには生成時にuser:referencesを加えます。
以上を踏まえてMicropostモデルを生成しましょう。
$ rails g model Micropost time:integer memo:text picture:string user:references
投稿を表示する際、twitterのように最新のものが上部に来てほしいものです。
その準備として、マイグレーションにインデックスを加えます。
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モデルを所有してもらいましょう。
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までを受けつける
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モデルを改良する
ここでは以下の機能を追加します。
- 新しい投稿を先にソートする
- ユーザが削除されたら投稿も削除する
class Micropost < ApplicationRecord
belongs_to :user
default_scope { order(created_at: :desc) }
# 中略
end
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
# 中略
end
エラー時の日本語化を行う
あとはエラー時の言語を日本語にしましょう。
日本語化にはgemと設定が必要です。#6やRailsのバリデーションエラーのメッセージの日本語化を参考に設定を行なってください。
お済みの方は以下のようにファイルを編集してください。
ja:
activerecord:
models:
user: ユーザ
micropost: 投稿
attributes:
user:
name: 名前
email: メールアドレス
password: パスワード
password_confirmation: パスワード(再入力)
micropost:
time: 記録時間
memo: メモ
picture: 画像
user_id: ユーザID
only_user_id: 入力欄
後ほどビューをつくります。
その際エラー表示を確認するので、ひとまずここで終了です。
最後にデータベースを更新します。
$ rails db:migrate
サンプル投稿を表示する
Micropostモデルは整いました。次は投稿を表示しましょう。
ここでの手順は以下の通りです。
- サンプル投稿を生成する
- サンプル投稿を表示する
- ページネーションを実装する
サンプル投稿を生成する
サンプル投稿を生成するにはfakerを使用します。
+ gem 'faker'
$ bundle install
それではサンプル投稿を生成します。
ついでにサンプルユーザも生成しておきます。
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
最後にデータベースを再構築しましょう。
$ rails db:migrate:reset
$ rails db:seed
これでサンプルユーザとサンプル投稿が生成されました。
サンプル投稿を表示する
生成を終えたら投稿を表示しましょう。
ここでの手順は以下の通りです。
- usersコントローラを編集する
- ビューを編集する
- ページネーションを追加する
usersコントローラを編集する
投稿を表示させるビューはshow.html.erbです。
つまりこれに対応するコントローラにMicropostを渡す必要があります。
対応するのはusersコントローラです。編集しましょう。
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にしました。
ビューを編集する
続いてはビューです。
パーシャルを使用しつつ実装しましょう。
<% 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>
<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 %>
// 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と日本語化の適用も行います。
+ gem 'kaminari'
+ gem 'kaminari-bootstrap'
$ bundle install
$ rails g kaminari:views bootstrap4
ja:
# 中略
views:
pagination:
first: "« 最初"
last: "最後 »"
previous: "‹ 前"
next: "次 ›"
truncate: "..."
これらを終えるとビューはこんな感じになります。
PC版↓
スマホ版↓
参考になります↓
【Ruby on Rails】gem(Kaminari)でページネーション機能を追加してBootstrapを適用する。
【Rails初心者】ページネーションを実装して自分好みにデザインを変える
テストをつくる
最後にテストを完成させます。
ここでの手順は以下の通りです。
- FactoryBotを整える
- テストを書く
FactoryBotを整える
まずはテストを行う前の準備をしましょう。
今回新たにMicropostモデルが生成されました。
それに伴うテストを行いたいので、FactoryBotで導入しましょう。
ここでの手順は以下の通りです。
- テスト用のMicropostモデルを生成する
- テスト用のUserモデルを編集する
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
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文字を超えないか
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モデルのテスト
このテストでは以下を確認します。
- ユーザが削除されたら投稿も削除されるか
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
以上でテストは終了です。
次回は投稿機能を実装します。