LoginSignup
1
1

More than 1 year has passed since last update.

【備忘録】【rails】つぶやきアプリ開発の流れ

Last updated at Posted at 2021-06-16

今まで色々な記事を見て学習してきた知識を一旦整理するため、
つぶやきアプリを作る流れを記事に残していきます。
(※長文です)

以下の機能をもつアプリを作ります。

・新規登録・ログイン機能
・投稿・保存・編集・更新・削除機能
・詳細一覧ページ
・コメント機能
・いいね機能
・検索機能

ER図作成

ディレクトリ作成

cdコマンドでProjectディレクトリまで移動した上で下記実行。

ターミナル
$ mkdir 作成したいディレクトリ名

Gemfileの生成

cdコマンドで作成したディレクトリまで移動した上で、下記実行。

ターミナル
$ bundle init

Writing new Gemfile to /path/to/ディレクトリ名/Gemfile
と返ってきたら、Gemfile生成成功。

Gemfile編集

$ vim Gemfileで編集(railsのコメントを外すだけです)

Gemfile
# frozen_string_literal: true
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails"

Railsのインストール

ターミナル
$ bundle install --path vendor/bundle

この時に--path vendor/bundleを忘れずにつけること。
一度オプションをつけてbundle installしたら、次回以降はオプションを付けなくてもvendor/bundle以下に格納されるようになります。

Railsアプリを生成

これで下準備が整ったので、ここからはアプリを作っていきます。

ターミナル
$ bundle exec rails new . -B -d mysql --skip-turbolinks --skip-test

Railsのインストール実行時にGemfileを上書きしていいか聞かれるので、yesにして続行します。

githubで新しいレポジトリを作成

git init → git add → git commit → git remote add origin → git pushの順に実行

ターミナル
$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin https://github.com/githubアカウント名/アプリ名.git
$ git push -u origin master

これでWebアプリの土台が出来上がりました。
次はデータベースの作成です。

データベースの作成

まずはmysql@5.7を立ち上げる。その上で下記実行。

ターミナル
$ bundle exec rails db:create
$ bundle exec rails db:migrate

このとき、mysql2に関するエラーが出ましたが下記記事に沿って

ターミナル
$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
$ bundle install

を実行したところ解決しました。
Sequel Proを使って、ちゃんとデータベースが作成されたか確認。

サーバーを立ち上げる

ターミナル
$ bundle exec rails s

Webpackerがまだインストールされていないですといった内容のエラーが出てくるので、インストールする。

ターミナル
$ bundle exec rails webpacker:install

その上で再度サーバーを立ち上げる

ターミナル
$ bundle exec rails s

成功!

下記の順で実行

ターミナル
$ git add .
$ git commit -m "create_database"
$ git push origin master

Railsで使うライブラリを導入

Gemfile
gem 'pry-rails'
ターミナル
$ bundle install

Gemファイルを追加、修正した場合はサーバーを再起動する。
その時に設定したデータが読み込まれるため。

ターミナル
$ bundle exec rails s

はじめにつぶやき機能を作っていきます。

まずはモデル作成

ターミナル
$ bundle exec rails g model post

マイグレーションファイルを編集

class CreatePost < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :name
      t.string :text
      t.timestamps
    end
  end
end

マイグレーションを実行

ターミナル
$ bundle exec rails db:migrate

ルーティング設定

7つのアクションのルーティングは、resourcesメソッドを使用し一度に設定する。
resourcesとは、7つのアクションへのルーティングを自動生成するメソッドのこと。
resourcesの引数に、:posts というシンボルを指定すると対応するルーティングが生成される。
resourcesにオプションとしてonlyを加えると、指定したアクションのみのルーティングを設定する。

route.rb
Rails.application.routes.draw do
  resources :posts, only: :index
end

コントローラーとビューの作成

ターミナル
$ bundle exec rails g controller posts index #postsコントローラーindexアクションとそれに対応するビューを作成

情報を全て取得できるよう、allメソッドを記述

posts_controller.rb
def index
    @posts = Post.all
end

ビューファイルに下記を記述

views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <div class="content_post" >
      <p><%= post.text %></p>
      <span class="name">
        <%= post.name %>
      </span>
    </div>
  <% end %>
</div>

レイアウトテンプレート

ヘッダーやフッターなど、共通部分をまとめた部分をレイアウトテンプレートという。
Railsではapplication.html.erbファイル。

app/views/layoutsディレクトリ配下にある。

yieldメソッド

レイアウトテンプレートに、各テンプレートファイルを展開するためのメソッド。
yieldを記述することで、コントローラーに対応するビューがyield記述部分へ展開されるようになる。

app/views/layouts/application.html.erb
#途中省略
<body>
    <header class="header">
      <div class="header__bar row">
        <h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
        <div class="user_nav grid-6">
          <a class="post" href="/posts/new">投稿する</a>
        </div>
      </div>
    </header>
    <%= yield %>
    <footer>
      <p>
        Copyright Tsubuyaki 2021.
      </p>
    </footer>
  </body>
</html>

CSSファイルの入力

app/assets/stylesheets/の中にstyle.cssというファイルを作成&以下を記入

app/assets/stylesheets/style.css
.btn,
header.header div.header__bar.row div.grid-6 a.post {
  padding: 8px 20px;
  font-size: 14px;
  border: 2px solid #57C3E9;
  color: #57C3E9;
  font-weight: bold;
  text-align: center;
  border-radius: 3px;
  display: inline-block;
}

.btn:hover,
header.header div.header__bar.row div.grid-6 a.post:hover {
  border-color: #9bdbf2;
  color: #9bdbf2;
}

footer {
  margin: 30px auto;
  padding: 10px;
  color: #D8D8D8;
  text-align: center;
}

header {
  margin: 30px auto;
  padding: 10px;
  color: #D8D8D8;
  text-align: center;
}

.content_post {
  text-align: center;
}

コンソール上でデータを入力するため、bundle exec rails cを実行

[1] pry(main)> Post.create(name: "asami", text: "こんにちは。")

ルートパスの設定

config/routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  get 'posts/index'
  resources :posts
end

一度サーバーを立ち上げて
http://localhost:3000 にアクセスし、表示を確認。

次に投稿ページを作っていく。
まずは投稿画面に遷移するページをnewアクションを使って作る。

ルーティング設定

config/routes.rb
Rails.application.routes.draw do
 root to: 'posts#index'
 resources :posts, only: [:index, :new, :create] #のちにクリエイトアクションも使うので記入
end

コントローラー設定

app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

end

app/views/postsにnew.html.erbというビューファイルを作成

app/views/posts/new.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>投稿する</h3>
    <%= form_with(model: @post, local: true) do |form|  %>
      <%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%>
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>
  </div>
</div>

createアクションでデータを保存

フォームで送られてきたデータを元に、レコードを保存。

プライベートメソッド

クラス外から呼び出すことのできないメソッドのことで
privateと記述した以下のコードがプライベートメソッドになります。

app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  private
  def post_params
    params.require(:post).permit(:name, :text)
  end

end

投稿完了画面作成のため
app/views/postsにcreate.html.erbというビューファイルを作成

app/views/posts/create.html.erb
<div class="contents">
  <div class="success">
    <h3>投稿が完了しました。</h3>
    <a class="btn" href="/">投稿一覧へ戻る</a>
  </div>
</div>

空のツイート投稿ができないよう、バリデーションの設定をする。

バリデーション

データを登録する際に、一定の制約をかけること。
空のデーターを登録できなくしたり、文字数制限をかけたりすることができる。

記述の仕方
validates :カラム名, バリデーションの種類
app/models/post.rb
class Post < ApplicationRecord
  validates :text, presence: true
end

これにより空のつぶやきができなくなる。

cssファイルを整える。

app/assets/stylesheets/style.css
.btn,
.post {
  padding: 8px 20px;
  font-size: 14px;
  border: 2px solid #57C3E9;
  color: #57C3E9;
  font-weight: bold;
  text-align: center;
  border-radius: 3px;
  display: inline-block;
}
.btn:hover,
.post:hover {
  border-color: #9bdbf2;
  color: #9bdbf2;
}

footer {
  margin: 30px auto;
  padding: 10px;
  color: #D8D8D8;
  text-align: center;
}


header {
  margin: 30px auto;
  padding: 10px;
  color: #D8D8D8;
  text-align: center;
}

.content_post {
  text-align: center;
}

.container_box {
 text-align: center;
}

.container {
  margin: auto;
 display: block;
}

.contents {
  text-align: center;
}

新規投稿ができて、一覧表示に遷移・表示される状態になっていればOK。

次は画像投稿機能の実装に入る。

画像のアップロードには、画像の保存・表示・サイズの調整が必要。
そこでRailsのActive Storageという機能を使っていく。
そのためにいくつかの機能をインストールしていく。

画像加工のために必要なImageMagickという画像変換ツールと、
それをRailsから使うためのGemをインストールする。
ImageMagickはGemではなく、ソフトウェアであるためHomebrewからインストールする。

GemではないImageMagickをRubyやRailsで扱うには、MiniMagickというGemが必要。
MiniMagickによって、ImageMagickの機能がRailsで使用できるようになるが、
画像サイズの変更にはもう1つImageProcessingというGemを追加する必要もある。

記述の仕方
$ brew install imagemagick  #imagemagickをインストール

Gemをインストール

Gemfile
gem 'mini_magick'
gem 'image_processing', '~> 1.2'
ターミナル
bundle install

続いてActive Storageのインストール

ターミナル
$ bundle exec rails active_storage:install

マイグレート。

ターミナル
$ bundle exec rails db:migrate

これでactive_storageのテーブルが追加された。
一応Sequel Proでテーブル名が追加されているか、確認してみよう。

次に、Active Storageテーブルに画像を保存するための実装を行う。

【手順】
①Active StorageテーブルとPostsテーブルのアソシエーションを定義
②posts_controller.rbにて、imageカラムの保存を許可

has_one_attachedメソッド

各レコードとファイルを1対1の関係で紐づけるメソッド。
has_one_attachedメソッドを記述したモデルの各レコードは、それぞれ1つのファイルを添付できる。

記述の仕方
class モデル < ApplicationRecord
  has_one_attached :ファイル名
end

では、Postモデルに記述していく。

app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

ストロングパラメーターにimageを保存できるようにするため、追記。

app/controllers/posts_controller.rb
private
  def post_params
    params.require(:post).permit(:name,:text,:image)
  end

次は保存した画像を表示していく。

image_tagメソッド

ヘルパーメソッドで複雑なパスを指定しなくても、
モデルから画像ファイルを呼び出して引数に記述するだけで、画像を表示するimg要素を生成できる。

attached?メソッド

レコードにファイルが添付されているかどうかで、trueかfalseを返すメソッド。

記述の仕方
モデル.ファイル名.attached?

variantメソッド

Active Storageを導入している場合に使用可能なメソッド。
ファイルの表示サイズを指定できる。

記述の仕方
モデル.ファイル名.variant(resize: '幅x高さ')

ビューファイルを変更①

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
        <%= post.name %>
      </span>
    </div>
  <% end %>
</div>

ビューファイルを変更②

app/views/posts/new.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>投稿する</h3>
    <%= form_with(model: @post, local: true) do |form|  %>
      <%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%>
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.file_field :image %>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>
  </div>
</div>

モデルファイルを変更

app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image

  validates :text, presence: true, unless: :was_attached?
  def was_attached?
    self.image.attached?
  end
end

validatesのunlessオプションにメソッド名を指定することで、
「メソッドの返り値がfalseならばバリデーションによる検証を行う」という条件を作っている。

指定されたwas_attached?メソッドは、self.image.attached?という記述により、
画像があればtrue、なければfalseを返す。

これにより、画像が存在しなければテキストが必要となり、画像があればテキストは不要になった。
※画像は拡張子が「.png」または「.jpeg」の画像を投稿すること。

挙動を確認して、画像投稿がうまくできればOK。

次に削除機能を作っていく。
つぶやき削除を行うにはdestroyアクションを使う。

ルーティング設定

config/routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts, only: [:index, :new, :create, :destroy]
end

ビューにリンクを追加

次にlink_toメソッドを使用して、削除ボタンを追加する。
/posts/《postのid》というパスにすることで、削除するツイートを区別でき、パラメーターとして受け取れるようになる。
HTTPメソッドは、DELETEを指定する。

app/views/posts/index.html.erb
<%= link_to '削除', post_path(post.id), method: :delete %>  #追加する

コントローラーにdestroyアクション追加

app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy                              #destroyアクション追加
    post = Post.find(params[:id])
    post.destroy
  end

  private
  def post_params
    params.require(:post).permit(:name,:text,:image)
  end

end

パラメータとして受け取ったparams[:id]をもとに、
ツイートをfindメソッドで取得しdestroyメソッドで削除する、という流れ。

destroyビューの作成

app/views/posts/destroy.html.erb
<div class="contents row">
  <div class="success">
    <h3>削除が完了しました。</h3>
    <a class="btn" href="/">投稿一覧へ戻る</a>
  </div>
</div>

記事の削除ができるかどうか挙動を確認し、できたらOK。

次に、ツイートを編集していく。
まずはeditアクションを使って、ツイート編集ページへ遷移する処理を実装していく。

ルーティング設定

config/routes.rb
Rails.application.routes.draw do
https://guides.rubyonrails.org/routing.html
  root to: 'posts#index'
  resources :posts, only: [:index, :new, :create, :destroy, :edit]
end

editアクションへのルーティングができた。

ビューに編集ボタンと編集ページへのリンクを設定する。

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
        <%= post.name %>
      </span>
      <%= link_to '編集', edit_post_path(post.id), method: :get %>
      <%= link_to '削除', "/posts/#{post.id}", method: :delete %>
    </div>
  <% end %>
</div>

コントローラーにeditアクション追加

新規投稿時と異なる点は、すでに存在しているレコードを選択して中身を上書きする点。
編集したいレコードを@postに代入し、ビューに受け渡すことで編集画面で利用できるようにする。

app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
    @post = Post.find(params[:id]) #編集したいつぶやきを取得して@postへ代入
  end

  private
  def post_params
    params.require(:post).permit(:name,:text,:image)
  end
end

editビューの作成

app/views/postsディレクトリの中にedit.html.erbというビューファイルを作成する。

app/views/posts/edit.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>編集する</h3>
    <%= form_with(model: @post, local: true) do |form|  %>
      <%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%>
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.file_field :image %>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>
  </div>
</div>

つぶやき更新機能の実装

ルーティング設定

つぶやきを更新する際には、/posts/《編集するツイートのid》にPATCHメソッドでアクセスする。
postsコントローラーのupdateアクションが実行されるようにする。

config/routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts, only: [:index, :new, :create, :destroy,:edit,:update]
end

コントローラーにupdateアクション追加

app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
    @post = Post.find(params[:id])
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  private
  def post_params
    params.require(:post).permit(:name,:text,:image)
  end

end

app/views/postsディレクトリの中にupdate.html.erbというビューファイルを作成。

app/views/posts/update.html.erb
<div class="contents row">
  <div class="success">
    <h3>更新が完了しました。</h3>
    <a class="btn" href="/">投稿一覧へ戻る</a>
  </div>
</div>

挙動を確認し、記事の編集&更新ができればOK。

次はつぶやき詳細ページを作っていく。

showアクションのルーティング設定

config/routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts, only: [:index, :new, :create, :destroy, :edit, :update, :show]
end

7つのアクションが揃ったため、onlyオプションは必要なくなる。↓

config/routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts
end

ルーティング設定完了。
次はトップページのビューを編集していく。

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
        <%= post.name %>
      </span>
      <%= link_to '詳細', post_path(post.id), method: :get %>
      <%= link_to '編集', edit_post_path(post.id), method: :get %>
      <%= link_to '削除', "/posts/#{post.id}", method: :delete %>
    </div>
  <% end %>
</div>

コントローラーにshowアクション追加

app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
    @post = Post.find(params[:id])
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
    @post = Post.find(params[:id])
  end

  private
  def post_params
    params.require(:post).permit(:name,:text,:image)
  end
end

show(詳細ページ)ビューの作成

app/views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <%= @post.name %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
    </div>
</div>

詳細ボタンを押して詳細ページへ遷移できれば、機能としては完成。
しかしコントローラーの記述を見てみると、同じ記述が繰り返し使われている。
同じ処理を一まとめにするため、リファクタリングをしていこう。

before_action

コントローラで定義されたアクションが実行される前に、共通の処理を行うことができる。

記述の仕方
class コントローラ名 < ApplicationController
  before_action :処理させたいメソッド名

editアクションとshowアクションの記述が同じであるため、
set_postというアクションにまとめて、before_actionを設定。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
  end

  private

  def post_params
    params.require(:post).permit(:name, :image, :text)
  end

  def set_post
    @post = Post.find(params[:id])
  end
end

次に新規登録、ログイン、ログアウトなどを実装するため、ユーザー管理機能を追加していく。

deviceの利用

Gemfile
gem 'devise'
ターミナル
$ bundle install

Gemを導入した際にはサーバーの再起動が必要。

ターミナル
$ bundle exec rails s

追加したdeviseの「設定関連に使用するファイル」を自動で生成するコマンド

ターミナル
$ bundle exec rails g devise:install

rails g deviseコマンド

モデルとマイグレーションの生成やルーティングの設定などをまとめて処理してくれる。
ルーティングにはdeviseに関連するパスが追加される。

ターミナル
# deviseコマンドでUserモデルを作成
$ bundle exec rails g devise user

ルーティングを確認

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
  resources :posts
end

マイグレーションファイルを確認

db/migrate/########_devise_create_users.rb
class DeviseCreateUser < ActiveRecord::Migration[6.0]
 def change
   create_table :users do |t|
     ## Database authenticatable
     t.string :email,              null: false, default: ""
     t.string :encrypted_password, null: false, default: ""

     ## Recoverable
     t.string   :reset_password_token
     t.datetime :reset_password_sent_at

     ## Rememberable
     t.datetime :remember_created_at

     # 省略

     t.timestamps null: false
   end

   add_index :users, :email,                unique: true
   add_index :users, :reset_password_token, unique: true
   # add_index :users, :confirmation_token,   unique: true
   # add_index :users, :unlock_token,         unique: true
 end
end

Eメールやパスワードなどの設定が自動でされているのが確認できる。

マイグレーション実行

ターミナル
# マイグレーションを実行
$ bundle exec rails db:migrate

テーブル・カラム情報を変更したため、ローカルサーバーを再起動

ターミナル
$ bundle exec rails s

deviseのビューファイルを作成

ビューファイルは自動生成されないため、コマンドを実行する必要がある。

ターミナル
$ bundle exec rails g devise:views

このコマンドでビューファイルを生成できる。
●サインアップ画面のビュー app/views/devise/registrations/new.html.erb
●ログイン画面のビュー
app/views/devise/sessions/new.html.erb

装飾したい場合はこれらのビューを変更すればいい。

app/views/devise/registrations/new.html.erb
<h2 class="contents" >新規登録</h2>

<div class="center">

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "新規登録する" %>
  </div>
<% end %>

</div>
app/views/devise/sessions/new.html.erb
<h2 class="contents" >ログイン</h2>

<div class="center">
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <div class="actions">
    <%= f.submit "ログインする" %>
  </div>
<% end %>

</div>

cssも修正。

app/assets/stylesheets/style.css
.btn,
.post {
  padding: 8px 20px;
  font-size: 14px;
  border: 2px solid #57C3E9;
  color: #57C3E9;
  font-weight: bold;
  text-align: center;
  border-radius: 3px;
  display: inline-block;
  margin: 5px;
}
.btn:hover,
.post:hover {
  border-color: #9bdbf2;
  color: #9bdbf2;
}

footer {
  margin: 30px auto;
  padding: 10px;
  color: #D8D8D8;
  text-align: center;
}


header {
  margin: 30px auto;
  padding: 10px;
  color: #D8D8D8;
  text-align: center;
}

.content_post {
  text-align: center;
}

.container_box {
 text-align: center;
}

.container {
  margin: auto;
 display: block;
}

.contents {
  text-align: center;
}

.top_contents {
  display: flex;
  justify-content: center;
}

.center {
  width: 100vw;
  display: flex;
  justify-content: center;
}

以下にアクセスするとログイン画面が確認できる。
http://localhost:3000/users/sign_in

以下にアクセスするとサインアップ画面が確認できます。
http://localhost:3000/users/sign_up

サインアップ時に登録する情報はメールアドレスとパスワードの2つになっているが、
加えてニックネームを登録できるようにしていこう。

usersテーブルにnicknameカラムをstring型で追加

テーブルにカラムを追加するには、下記を実行しマイグレーションを生成する。

記述の仕方
$ bundle exec rails g migration Addカラム名To追加先テーブル名 追加するカラム名:
ターミナル
# usersテーブルにnicknameカラムをstring型で追加するマイグレーションファイルを作成
$ bundle exec rails g migration AddNicknameToUsers nickname:string
# 作成したマイグレーションを実行
$ bundle exec rails db:migrate

テーブル・カラム情報を変更したため、ローカルサーバーを再起動

ターミナル
$ bundle exec rails s

次はニックネーム情報をフォームから登録できるようにしていく。
そのためにはビューを編集する。
今回は、ニックネームを6文字以内で登録させるようにする。

記述の仕方
#maxlengthオプションの一例
<div class="field">
  <%= f.text_field :nickname, autofocus: true, maxlength: "6" %>
</div>
app/views/devise/registrations/new.html.erb
<h2 class="contents" >新規登録</h2>

<div class="center">

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
        <%= f.label :nickname %> <em>(6 characters maximum)</em><br />
        <%= f.text_field :nickname, autofocus: true, maxlength: "6" %>
      </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "新規登録する" %>
  </div>
<% end %>

</div>

ストロングパラメーターの設定

deviseに関しても、他と同様にストロングパラメーターをコントローラーに記述したい。
しかしdeviseの処理を行うコントローラーはGem内に記述されているため、編集することができない。

devise_parameter_sanitizerメソッド

deviseにおけるparamsのようなメソッドで、deviseのUserモデルに関わる「ログイン」「新規登録」などのリクエストからパラメーターを取得できる。

このメソッドとpermitメソッドを組み合わせることで、deviseに定義されているストロングパラメーターに対し、新しく追加したカラムも指定して含めることができる。

これまでのストロングパラメーターと同じく、新たに定義するプライベートメソッドの中で使用する。deviseの提供元では、新たに定義するメソッド名をconfigure_permitted_parametersと紹介していることから、慣習的にこのメソッド名で定義することが多いよう。

記述の仕方
private
def configure_permitted_parameters  # メソッド名は慣習
  # deviseのUserモデルにパラメーターを許可
  devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
end

devise_parameter_sanitizerに使用するpermitメソッドの引数の指定の仕方は異なる。

記述の仕方
# paramsのpermitメソッド
params.require(:モデル名).permit(:許可するキー)

# devise_parameter_sanitizerのpermitメソッド
devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])

deviseのpermitは、第一引数にdeviseの処理名、第二引数にkeysというキーに対し、配列でキーを指定することで、許可するパラメーターを追加していく。

第一引数の処理名には、deviseで設定されているsign_in, sign_up, account_updateが使用でき、それぞれの処理に対応している。

第一引数で指定した処理に対して、第二引数のkeysで指定された名前と同じキーを持つパラメーターの取得を許可する。ビューに記述した各フォーム部品のname属性値が、フォームから送信されるパラメーターのキーとなる。

deviseにストロングパラメーターを追加するコードは、deviseのコントローラーが編集できないため、application_controller.rbに記述していく。

application_controller.rbを編集

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
  end
end

:devise_controller?というdeviseのヘルパーメソッド名を指定して、deviseに関するコントローラーの処理のときだけメソッドを実行するように設定している。

ビューを整えていく。
●ログインしているとき:ログアウト、新規投稿ボタンを表示
●ログインしていないとき:ログイン、新規登録ボタンを表示

記述の仕方
# ログインしているユーザーのとき
user_signed_in?
#=> true

# ログインしていないユーザーのとき
user_signed_in?
#=> false
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Tsubuyaki</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header class="header">
      <div class="header__bar row">
        <h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
       <div class="top_contents">
        <div class="user_nav grid-6">
       <% if user_signed_in? %>
          <div class="user_nav grid-6">
            <%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post"  %>
            <%= link_to "投稿する", new_post_path, class: "post" %>
          </div>
        <% else %>
          <div class="grid-6">
            <%= link_to "ログイン", new_user_session_path, class: "post" %>
            <%= link_to "新規登録", new_user_registration_path, class: "post" %>
          </div>
        <% end %>

        </div>
      </div>
    </header>
    <%= yield %>
    <footer>
      <p>
        Copyright Tsubuyaki 2021.
      </p>
    </footer>
  </body>
</html>

しかしこのままだと問題あり。
ログインしていなくてもURLを直接入力してしまうと新規投稿ページに行けてしまうのだ。
そこで対策していく。

unless

ifと同様に、条件式の返り値で条件分岐して処理を実行するRubyの構文。
ifは返り値がtrueのときにelseまでの処理が実行されるが、
unlessはfalseのときにelseまでの処理が実行される。

redirect_toメソッド

Railsでリクエストと違うページに返すリダイレクト処理を行う際に使用するメソッド。
コントローラー等での処理が終わった後、アクションに対応するビューファイルを参照せずに、別ページへリダイレクトさせることができる。

記述の仕方
redirect_to action: :リダイレクト先となるアクション名

exceptオプション

before_actionで使用できるオプション。
exceptは「除外する」という意味があるとおり、この中に記述したアクションには処理が適用されずに済む。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
  end

  private
  def post_params
    params.require(:post).permit(:name, :image, :text)
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

挙動を確認してみて、新規登録、ログイン、ログアウトが正しくできればOK。

次に、showアクションを使用してマイページを実装していく。
誰がどのつぶやきをしたか判断するためにつぶやきにuser_idを付与する。

postsテーブルにuser_idカラムをinteger型で追加

ターミナル
$ bundle exec rails g migration AddUserIdToPosts user_id:integer
$ bundle exec rails db:migrate
$ bundle exec rails s #サーバー再起動

ツイートしたユーザー=現在ログインしているユーザーのこと。
そのためpostsテーブルのuser_idカラムには、current_userのidを保存する。

current_userメソッド

device導入後、ログイン中のユーザーの情報を取得できる。
ログイン中のユーザーIDとツイートを一緒に保存したいので、
そのために2つのハッシュを統合するときに使う「mergeメソッド」を利用する。

mergeメソッド

ハッシュを結合させるときに使用するメソッド

post = { name: "スズキ", text: "おはよう!", image: "sun.jpeg" }
id = { user_id: "1" }
post.merge(id)
=> {:name=>"スズキ", :text=>"おはよう!", :image=>"sun.jpeg", :user_id=>"1"}
app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
  end

  private
  def post_params
    params.require(:post).permit(:name, :image, :text).merge(user_id: current_user.id) #current_user_idをマージ
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

has_manyメソッドでUserモデルを編集

app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :posts
end

belongs_toメソッドでPostモデルを編集

app/models/post.rb
class Post < ApplicationRecord
  validates :text, presence: true
  belongs_to :user
end

ユーザーマイページ用のルーティングを設定

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'posts#index'
  resources :posts
  resources :users, only: :show
end

上記で、/users/:id のパスでリクエストした際にusers_controller.rbのshowアクションを実行するルーティングが設定できた。

トップページのビューにマイページへのリンクを追加

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Tsubuyaki</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header class="header">
      <div class="header__bar row">
        <h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
       <div class="top_contents">
        <div class="user_nav grid-6">
       <% if user_signed_in? %>
          <div class="user_nav grid-6">
            <%= link_to "マイページ", "/users/#{current_user.id}", class: "post"  %>
            <%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post"  %>
            <%= link_to "投稿する", new_post_path, class: "post" %>
          </div>
        <% else %>
          <div class="grid-6">
            <%= link_to "ログイン", new_user_session_path, class: "post" %>
            <%= link_to "新規登録", new_user_registration_path, class: "post" %>
          </div>
        <% end %>

        </div>
      </div>
    </header>
    <%= yield %>
    <footer>
      <p>
        Copyright Tsubuyaki 2021.
      </p>
    </footer>
  </body>
</html>

usersコントローラー作成

ターミナル
$ bundle exec rails g controller users

showアクションで表示するページ、つまりマイページには
「ニックネーム」と「ログイン中ユーザーのツイート」が必要。
それぞれ@nickname@postsというインスタンス変数に代入。

@nickname⇒current_userを利用し、
ログイン中ユーザーが持つnicknameカラムの値を取得する。

@posts⇒ログイン中ユーザーのツイートを取得し、インスタンス変数に代入する。

マイページのビューを作成

app/views/usersディレクトリの配下にshow.html.erbというファイルを作成。

app/views/users/show.html.erb
<div class="contents row">
  <p><%= @nickname %>さんの投稿一覧</p>
  <% @posts.each do |post| %>
    <div class="content_post" >
    <p><%= post.text %></p>
    <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name"><%= post.name %></span>
    </div>
  <% end %>
</div>

ここでトップページに行こうとするとエラーが起きる。
理由:先程current_userメソッド、mergeメソッドで
ツイートを投稿した際にuser_idも一緒に保存するようにしたが、
それ以前に投稿したツイートはuser_idがNULLのままになっているため。
⇒sequel proで確認して、以前ツイートした分を一旦削除してしまおう。
エラーが解決する。

他の投稿者のマイページにも行けるよう、投稿一覧ページのビューを編集していく。

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
           <a href="/users/<%= post.user.id %>">
          <span>投稿者</span><%= post.user.nickname %>
        </a>
      </span>
      <%= link_to '詳細', post_path(post.id), method: :get %>
      <%= link_to '編集', edit_post_path(post.id), method: :get %>
      <%= link_to '削除', "/posts/#{post.id}", method: :delete %>
    </div>
  <% end %>
</div>

しかしこのままだと、どの投稿の投稿者名をクリックしても、ログインユーザーの詳細ページに移動してしまう。
この問題は後ほど解決していく。

ツイート詳細画面の編集

ツイート詳細画面の投稿者の部分も@post.nameとnameを使う文になっているため、
アソシエーションを利用する形に変更する。
併せてマイページに飛ぶリンクも設置。

app/views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
    </div>
</div>

N+1問題

ここで起こっているのが、N+1問題。
N+1問題とは、アソシエーションを利用した場合に限り、データベースへのアクセス回数が多くなってしまう問題のこと。
今回の場合postsに関連するusersの情報取得に、ツイート数と同じ回数のアクセスが必要となる。
これはアプリケーションのパフォーマンス低下につながる。
それを解決するのがincludesメソッドだ。

includesメソッド

includesメソッドは、引数に指定された関連モデルを1度のアクセスでまとめて取得できるようにする。
書き方は、includes(:紐付くモデル名)とする。
引数に関連モデルをシンボルで指定する。

記述の仕方
モデル名.includes(:紐付くモデル名)

postsコントローラーを編集

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @posts = Post.includes(:user)
  end

  #中略

end

includesメソッドを使用するとすべてのレコードを取得するため、allメソッドが省略可能。

新規ツイート画面のビューを変更

投稿を表示する際、アソシエーションを利用することで投稿者のニックネームを表示できるようになったため、
nameカラムは必要なくなり、投稿時に「Nickname」の値を入力する必要もなくなった。
なので削除する。

app/views/posts/new.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>投稿する</h3>
    <%= form_with(model: @post, local: true) do |form|  %>
      <%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%> #この行を削除
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.file_field :image %>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>
  </div>
</div>

編集画面のビューも同様に削除

app/views/posts/edit.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>編集する</h3>
    <%= form_with(model: @post, local: true) do |form|  %>
      <%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%> #この行を削除
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.file_field :image %>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>
  </div>
</div>

nameカラムはもう使用しないため、
ツイートの保存時にnameカラムへ情報を保存しないよう、ストロングパラメーターに変更を加える。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @posts = Post.includes(:user)
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
  end

  private
  def post_params
    params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

上記のとおり、:nameカラムを削除した。

テーブルからカラムを削除するためのマイグレーションを作成し、カラムの削除を実行。

記述の仕方
$ bundle exec rails g migration Removeカラム名From削除元テーブル名 削除するカラム名:型
ターミナル
# マイグレーションの作成
$ bundle exec rails g migration RemoveNameFromPosts name:string

# マイグレーションの実行
$ bundle exec rails db:migrate

# 「control + C」でローカルサーバーを停止

# ローカルサーバーを起動
$ bundle exec rails s

マイページのビュー内で、削除されたnameカラムをpost.nameと記述し利用している文があるため、1行まるごと削除

app/views/users/show.html.erb
<div class="contents row">
  <p><%= @nickname %>さんの投稿一覧</p>
  <% @posts.each do |post| %>
    <div class="content_post" >
    <p><%= post.text %></p>
    <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name"><%= post.name %></span> #この行を削除
    </div>
  <% end %>
</div>

さて、ようやくここで以前置き去りにしていた問題を解決していく。
投稿者名をクリックするとログインユーザーのページに遷移してしまうという問題。
・コントローラー内のインスタンス変数を変更する。
・クリックされたユーザーのidから情報を取得し、ビューに受け渡す。
下記の通り。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    user = User.find(params[:id])
    @nickname = user.nickname
    @posts = user.posts
  end
end

ただ、このままだと全てのユーザーに編集ボタンと削除ボタンが表示されてしまっている。
そのためビューに条件を追加し、投稿した本人だけに表示されるようにしていく。

current_user.id == post.user_idと記述することで、
「ログイン中のユーザー」と「ツイートを投稿したユーザー」が同じか否かを判定することができる。
この記述にif文を組み合わせることで表現できる。

一覧ページのビューを編集

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
           <a href="/users/<%= post.user.id %>">
          <span>投稿者</span><%= post.user.nickname %>
        </a>
      </span>
      <%= link_to '詳細', post_path(post.id), method: :get %>
      <% if user_signed_in? && current_user.id == post.user_id %>
      <%= link_to '編集', edit_post_path(post.id), method: :get %>
      <%= link_to '削除', "/posts/#{post.id}", method: :delete %>
      <% end %>
    </div>
  <% end %>
</div>

マイページのビューを編集

app/views/users/show.html.erb
<div class="contents row">
  <p><%= @nickname %>さんの投稿一覧</p>
  <% @posts.each do |post| %>
    <div class="content_post" >
    <p><%= post.text %></p>
    <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
    </div>
  <% end %>
</div>

以上。

部分テンプレート

次に、同じコードをまとめてテンプレートとして共通して使えるようにしていく。
テンプレート自体のファイル名は、命名規則として、アンダースコア_を先頭に記述する。
app/views/postsの配下に_post.html.erbというファイルを作成する。

index.html.erbの以下の部分を選択して切り取る。

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
#ここから切り取る
    <div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
           <a href="/users/<%= post.user.id %>">
          <span>投稿者</span><%= post.user.nickname %>
        </a>
      </span>
      <%= link_to '詳細', post_path(post.id), method: :get %>
      <% if user_signed_in? && current_user.id == post.user_id %>
      <%= link_to '編集', edit_post_path(post.id), method: :get %>
      <%= link_to '削除', "/posts/#{post.id}", method: :delete %>
      <% end %>
    </div>
#ここまで切り取る
  <% end %>
</div>

今index.html.erbから切り取った部分を_post.html.erbに貼り付ける。

app/views/posts/_post.html.erb
<div class="content_post" >
      <p><%= post.text %></p>
       <p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
      <span class="name">
           <a href="/users/<%= post.user.id %>">
          <span>投稿者</span><%= post.user.nickname %>
        </a>
      </span>
      <%= link_to '詳細', post_path(post.id), method: :get %>
      <% if user_signed_in? && current_user.id == post.user_id %>
      <%= link_to '編集', edit_post_path(post.id), method: :get %>
      <%= link_to '削除', "/posts/#{post.id}", method: :delete %>
         <% end %>
    </div>

renderメソッド

部分テンプレートを呼び出す際に利用するメソッド。
呼び出す部分テンプレートは、partialで指定する。

partialオプション

renderメソッドで使用できるオプションであり、部分テンプレート名を指定することで表示できる。

記述の仕方
【例】renderメソッドのpartialオプション
<% render partial: "sample" %>

また、呼び出す部分テンプレートに値を渡すために、localsを使用できる。

localsオプション

renderメソッドで使用できるオプションである。 localsというオプションを付けることで、部分テンプレート内でその変数を使えるようになる。

記述の仕方
【例】renderメソッドのlocalsオプション
<% render partial: "sample", locals: { post: "good!" } %>

上の記述で、部分テンプレート内においてgood!という文字列の代入されたpostという変数が使えるようになる。

ではindex.html.erbを編集する。
先ほど切り取って残った箇所に、部分テンプレートを挿入する。

app/views/posts/index.html.erb
<div class="contents row">
  <% @posts.each do |post| %>
    <%= render partial: "post", locals: { post: post } %>
  <% end %>
</div>

マイページも同じコードであるため、部分テンプレートを適用

使用するファイルを呼び出すが、同じディレクトリではないため、posts/postのようにどこのディレクトリにある部分テンプレートなのかを指定する必要がある。

app/views/users/show.html.erb
<div class="contents row">
  <p><%= @nickname %>さんの投稿一覧</p>
  <% @posts.each do |post| %>
    <%= render partial: "posts/post", locals: { post: post } %>
  <% end %>
</div>

各formも部分テンプレートでまとめる

app/views/postsディレクトリに_form.html.erbファイルを作成。

該当部分を切り取り・・・

app/views/posts/new.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>投稿する</h3>
#ここから切り取る
    <%= form_with(model: @post, local: true) do |form|  %>
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.file_field :image %>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>
#ここまで切り取る
  </div>
</div>

部分テンプレートに貼り付ける。

app/views/posts/_form.html.erb
<%= form_with(model: post, local: true) do |form|  %>
      <%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
      <%= form.file_field :image %>
      <%= form.submit "つぶやく", class: 'container'%>
    <% end %>

#一行目の@postとしてあった部分だけpostに変えておこう。

先ほど切り取って残った箇所に、部分テンプレートを挿入する。new,edit両方同じように記述する。

app/views/posts/new.html.erb,,,app/views/posts/edit.html.erb
<div class="contents_form">
  <div class="container_box">
    <h3>投稿する</h3>
    <%= render partial: "form", locals: { post: @post } %>
  </div>
</div>

問題なく新規投稿・編集ができるかどうか動作を確認してみて、
動作に問題がなければ部分テンプレートを用いた表示は成功。

コメント機能の実装

ツイート詳細画面からツイートに対して、コメントが書き込めるようなコメント機能を実装していく。

コメントは、ツイートが必ず所有する情報ではないため、ツイートと別のテーブルで管理する必要がある。
そのため、commentsテーブルを作る。

さらに、コメントはどのツイートに対してのコメントなのか、どのユーザーが投稿したコメントなのか判る必要もある。
そのため、UserモデルとPostモデルの2つにアソシエーションを組む。

まずはUserモデルとPostモデルに加えて、Commentモデルを作成する。

記述の仕方
$ bundle exec rails g model comment

マイグレーションファイルを編集する。

db/migrate/XXXXXXXXXXXX_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.integer :user_id
      t.integer :post_id
      t.text :text
      t.timestamps
    end
  end
end

ここで注意する点は、カラムにuser_idとpost_id。

「誰が投稿したコメントなのか」が分かるようにするために
関連するユーザーのidを保存する必要がある。
そのidを保存するカラム名をuser_idとした。

加えて「どのツイートに対してのコメントなのか」が分かるようにするために
関連するツイートのidを保存する必要がある。
そのidを保存するカラム名をpost_idとした。

マイグレーションの実行

ターミナル
$ bundle exec rails db:migrate

# 「ctrl + C」でローカルサーバーを終了

# 再度、ローカルサーバーを起動

$ bundle exec rails s

Sequel Proを開き、テーブルが追加されているか確認する。

ここで、3つのテーブル同士の関係性を確認しておく。
テーブルとテーブルをつなぐ線は、1対多の関係を表している。
例としてpostsテーブルからcommentsテーブルに伸びる線は、一つのツイートが複数のコメントを持つ様を表している。

スクリーンショット 2021-06-15 9.59.56.png

comment.rbを編集する。
コメントは、1人のユーザーと1つのツイートに所属するので、belongs_to :モデル単数形と記述することで、アソシエーションを定義する。

コメントモデルを編集

app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post  # postsテーブルとのアソシエーション
  belongs_to :user  # usersテーブルとのアソシエーション
end

ツイートモデルを編集

app/models/post.rb
class Post < ApplicationRecord
  validates :text, presence: true
  belongs_to :user
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end

dependent: :destroyオプションを追加記述することで、
親モデルのPostモデルが削除された際に、子モデルのCommentモデル(インスタンス)も削除される。

ユーザーモデルを編集

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end

コメントを投稿するためのルーティングの設定

コメントを投稿する際に、どのツイートに対するコメントなのかをパスから判断できるようにしておく必要がある。
その一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るため。
ルーティングのネストという方法を使っていく。

【例】ルーティングのネスト
Rails.application.routes.draw do
  resources :親となるコントローラー do
    resources :子となるコントローラー
  end
end

今回は「コメント情報を作る機能」を実装するのでcreateアクションのルーティングとする。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
  resources :posts do
    resources :comments, only: :create
  end
  resources :users, only: :show
end

ルーティングの確認

Prefix Verb           URI Pattern                            Controller#Action
# 中略
post_comments POST   /posts/:post_id/comments(.:format)   comments#create
# 中略

コントローラーの作成

ターミナル
$ bundle exec rails g controller comments
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    Comment.create(comment_params)
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, post_id: params[:post_id])
  end
end

user_idカラムには、ログインしているユーザーのidとなるcurrent_user.idを保存し、
post_idカラムは、paramsで渡されるようにするので、params[:post_id]として保存している。

続けて、コメントしたらツイートの詳細画面に戻る処理をcomments_controller.rbに記述していく。方法:redirect_toメソッド。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    comment = Comment.create(comment_params)
    redirect_to "/posts/#{comment.post.id}"  # コメントと結びつくツイートの詳細画面に遷移する
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, post_id: params[:post_id])
  end
end

postsコントローラーのshowアクションを実行するには、ツイートのidが必要。
そのため、ストロングパラメーターを用いた上で変数commentに代入する。

次に、コメント投稿フォームを作っていく。
コメントはツイートの詳細画面から投稿したいので、
views/posts/show.html.erbにフォームを記述する。

app/views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @post.user_id %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
       <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@post, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
  </div>
</div>

ツイートの詳細画面でツイートと結びつくコメントを表示するためには、ビューを呼び出す前のコントローラーが実行されている時点で、コメントのレコードをデータベースから取得する必要がある。
そのためposts_controller.rbのshowアクションを編集していく。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @posts = Post.includes(:user).order("created_at DESC")
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
  end

  private
  def post_params
    params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

posts/show.html.erbでform_withを使用して、comments#createを実行するリクエストを飛ばしたいので、@comment = Comment.newというインスタンス変数を生成しておかないといけない。

postsテーブルとcommentsテーブルはアソシエーションが組まれているため、@post.commentsとすることで、@postへ投稿されたすべてのコメントを取得できる。

ビューでは誰のコメントか明らかにするため、
アソシエーションを使ってユーザーのレコードを取得する処理を繰り返し行う。
その際「N+1問題」が発生してしまうため、includesメソッドを使って解決している。

コメントを表示させるためにツイート詳細ページを編集する。

app/views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @post.user_id %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
       <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@post, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
     <div class="comments">
      <h4><コメント一覧></h4>
      <% @comments.each do |comment| %>
        <p>
          <strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
          <%= comment.text %>
        </p>
      <% end %>
    </div>
  </div>
</div>

以上でコメント機能の実装が完了。

つぶやき検索機能の実装

投稿が増えてくると、見たい投稿を探すのが大変になってくる。
「検索したテキストを含む投稿」を表示させられると便利なため、検索機能を実装していく。

今回はsearchという命名で、7つの基本アクション以外のアクションを定義する。

ここでcollectionとmemberを学習しておこう。

collectionとmember

collectionとmemberは、ルーティング設定の際に使用する。
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムすることができる。

【両者の違い】
collection ルーティングに:idがつかない
member ルーティングに:idがつく

【例】collectionで定義した場合
Rails.application.routes.draw do
  resources :posts do
    collection do
      get 'search'
    end
  end
end
【例】collectionのルーティング
Prefix           Verb    URI                                 Pattern
search_posts    GET    /posts/search(.:format)              posts#search

上記を見ると、ルーティングに:idが付いていないことがわかる。
続いてmemberの場合を見てみよう。

【例】memberで定義した場合
Rails.application.routes.draw do
  resources :posts do
    member do
      get 'search'
    end
  end
end
【例】memberのルーティング
Prefix           Verb    URI                                 Pattern
search_post      GET    /posts/:id/search(.:format)       posts#search

URLの指定先が、collectionは:idなし、memberが:idありとなっていることが確認できる。
今回の検索機能の場合は、特に:idを指定して特定のページにいく必要はないため、
collectionを使用してルーティングを設定していく。

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
  resources :posts do
    resources :comments, only: :create
    collection do
      get 'search'
    end
  end
  resources :users, only: :show
end

投稿一覧ページのビューを編集

app/views/posts/index.html.erb
<%= form_with(url: search_posts_path, local: true, method: :get, class: "top_contents") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "post" %>
  <%= form.submit "検索", class: "search-btn", class: "post"%>
<% end %>
<div class="contents row">
  <% @posts.each do |post| %>
<%= render partial: "post", locals: { post: post } %>
  <% end %>

次に、Postモデルに検索する処理を記述したメソッドを定義していく。
メソッド名は「searchメソッド」とする。
検索キーワードが含まれた投稿を取得するために、「whereメソッド」と「LIKE句」を利用する。

whereメソッド

モデルが使用できる、ActiveRecordメソッドの1つで
引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得することができる。
引数の条件式は、「検索対象となるカラム」を含めて記述する。

【例】whereメソッド
モデル.where('検索対象となるカラムを含む条件式')

LIKE句

曖昧な文字列の検索をするときに使用するもので、whereメソッドと一緒に使う。

文字列 意味
% 任意の文字列(空白文字列含む)
_ 任意の1文字
実行例 詳細
where('title LIKE(?)', "a%") aから始まるタイトル
where('title LIKE(?)', "%b") bで終わるタイトル
where('title LIKE(?)', "%c%") cが含まれるタイトル
where('title LIKE(?)', "d_") dで始まる2文字のタイトル
where('title LIKE(?)', "_e") eで終わる2文字のタイトル

whereメソッドとLIKE句を使用することで「検索」の処理を作っていく。

app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image

  validates :text, presence: true, unless: :was_attached?
  def was_attached?
    self.image.attached?
  end

  validates :text, presence: true
  belongs_to :user
  has_many :comments

  def self.search(search)
    if search != ""
      Post.where('text LIKE(?)', "%#{search}%")
    else
      Post.all
    end
  end
end

引数のsearchは、検索フォームから送信されたパラメーターが入るため、
if search != ""と記述し、検索フォームに何か値が入力されていた場合を条件としている。

検索フォームに何も入力をせずにボタンを押すと、引数で渡されるsearchの中身は空になり
elseに該当し、Post.allという記述はすべての投稿を取得して表示するためのもの。

ビジネスロジックは基本的にモデルに置く。
実際の開発現場でも、テーブルとのやりとりに関するメソッドはモデルに置くという意識が必要になるとのこと。
コントローラーはあくまでモデルの機能を利用し処理を呼ぶだけで、複雑な処理は組まない。

postsコントローラーの編集

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show, :search]

  def index
    @posts = Post.includes(:user).order("created_at DESC")
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
  end

  def search
    @posts = Post.search(params[:keyword])
  end

  private
  def post_params
    params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

Postモデルに書いたsearchメソッドを呼び出している。
searchメソッドの引数にparams[:keyword]と記述することで、検索結果を渡している。

未ログイン時に検索するとトップページへリダイレクトされてしまうのを回避するために、
before_actionのexceptオプションに:searchを追加している。

それでは、検索結果を表示できるようにsearch.html.erbを作成しよう。
app/views/postsディレクトリの配下にsearch.html.erbを作成。

app/views/posts/search.html.erb
<%= form_with(url: search_posts_path, local: true, method: :get, class: "top_contents") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "post" %>
  <%= form.submit "検索", class: "post" %>
<% end %>
<div class="contents row">
  <% @posts.each do |post| %>
    <%= render partial: "post", locals: { post: post } %>
  <% end %>
</div>

これで検索機能は完成!

最後に、いいね♡機能を追加しよう。

Likeモデルを作る。
「どのユーザー」が「どの投稿」をいいねしたかを記録するために、
データベースに「user_id」と「post_id」2つのカラムを持つlikesテーブルを作成する。

likeモデルを作成

ターミナル
$ bundle exec rails g model like user_id:integer post_id:integer
$ bundle exec rails db:migrate

これで、likesテーブルにinteger型のuser_idカラムとpost_idカラムを作成できた。

モデル間のアソシエーションを組む

Userモデル
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
  has_many :likes, dependent: :destroy
end
Postモデル
class Post < ApplicationRecord
  has_one_attached :image

  validates :text, presence: true, unless: :was_attached?
  def was_attached?
    self.image.attached?
  end
  validates :text, presence: true
  belongs_to :user
  has_many :comments, dependent: :destroy  
  has_many :likes, dependent: :destroy
  def self.search(search)
    if search != ""
      Post.where('text LIKE(?)', "%#{search}%")
    else
      Post.all
    end
  end
end
Likeモデル
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
  resources :posts do
    resources :comments, only: :create
    resources :likes, only: :create
    collection do
      get 'search'
    end
  end
  resources :users, only: :show
end

コントローラーの作成

ターミナル
$ bundle exec rails g controller likes create #likesコントローラーとcreateアクションを一括生成
likes_controller.rb
class LikesController < ApplicationController
  def create
    @like = Like.new(
    user_id: current_user.id,
    post_id: params[:post_id]
    )
    @like.save
    redirect_to("/")
    end
end

投稿詳細ページのビューの編集

views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @post.user_id %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
       <% end %>
        <% if Like.find_by(user_id: current_user.id,post_id: @post.id)%>
       <%= link_to("いいね!済み","/posts/#{@post.id}/likes/#{@like.id}",{method: "delete"}) %>
      <% else %>
      <%= link_to("いいね!","/posts/#{@post.id}/likes",{method: "post"}) %>
      <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@post, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
     <div class="comments">
      <h4><コメント一覧></h4>
      <% @comments.each do |comment| %>
        <p>
          <strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
          <%= comment.text %>
        </p>
      <% end %>
    </div>
  </div>
</div>

いいね取り消しボタンの作成

「いいね」を取り消す機能を作るためには、まずはlikesコントローラにdestroyアクションを作成する。
destroyアクション内では、受け取った@current_user.idとparams[:post_id]をもとに削除すべきLikeデータを取得し、destroyメソッドを用いて削除する。

destroyアクションへのルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  get 'likes/create'
  devise_for :users
  root to: 'posts#index'
  resources :posts do
    resources :comments, only: :create
    resources :likes, only: [:create, :destroy]
    collection do
      get 'search'
    end
  end
  resources :users, only: :show
end

コントローラーにdestroyアクションの追加

likes_controller.rb
class LikesController < ApplicationController
  def create
    @like = Like.new(
    user_id: current_user.id,
    post_id: params[:post_id]
    )
    @like.save
    redirect_to("/posts/#{params[:post_id]}")
    end

    def destroy
      @like = Like.find_by(user_id: current_user.id,post_id: params[:post_id])
      @like.destroy
      redirect_to("/posts/#{params[:post_id]}")
      end
end

次に、いいねボタンを「♡」のアイコンにしていく。

Font Awesome

「Font Awesome」は、様々なアイコンをフォントとして利用できるようにしたもの。
利用するには、タグ内で読み込みをする必要がある。
application.html.erbに読み込み用のタグを追加しよう。
「fa fa-heart」というクラス名をつけることでハートアイコンを表示できる。しかし、下図のようにlink_toメソッド内にHTML要素を記述すると正しく表示することができない。

HTML要素に対してlink_toメソッドを使うには、少し異なる書き方が必要。
下図のように<%= link_to(URL) do %>と<% end %>の間にHTML要素を書くことで、その部分をリンクにすることができる。

ダメなパターン
<%= link_to("表示する文字列","URL") %> #文字列と判断されうまく表示されない

良いパターン
<%= link_to("URL") do %>
<HTML要素の記述>
<% end %>
application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Tsubuyaki</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header class="header">
      <div class="header__bar row">
        <h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
       <div class="top_contents">
        <div class="user_nav grid-6">
       <% if user_signed_in? %>
          <div class="user_nav grid-6">
            <%= link_to "マイページ", "/users/#{current_user.id}", class: "post"  %>
            <%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post"  %>
            <%= link_to "投稿する", new_post_path, class: "post" %>
          </div>
        <% else %>
          <div class="grid-6">
            <%= link_to "ログイン", new_user_session_path, class: "post" %>
            <%= link_to "新規登録", new_user_registration_path, class: "post" %>
          </div>
        <% end %>

        </div>
      </div>
    </header>
    <%= yield %>
    <footer>
      <p>
        Copyright Tsubuyaki 2021.
      </p>
    </footer>
  </body>
</html>

いいねの数を数える

テーブルからデータの件数を取得するには、「countメソッド」を用いる。
配列の要素数を取得するメソッドだが、テーブルのデータ数を取得することもできる。

postsコントローラーにカウントメソッドの追加

controllers/posts_controller
class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show, :search]

  def index
    @posts = Post.includes(:user).order("created_at DESC")

  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
    @like = Like.find_by(user_id: current_user.id, post_id: @post.id)
    @likes_count = Like.where(post_id: @post.id).count #カウントメソッド
  end

  def search
    @posts = Post.search(params[:keyword])
  end

  private
  def post_params
    params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end

ツイート詳細ページのビューも編集して、「♡」と「回数」が表示されるようにする。

posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @post.user_id %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
       <% end %>
        <% if Like.find_by(user_id: current_user.id,post_id: @post.id)%>
       <%= link_to("/posts/#{@post.id}/likes/#{@like.id}",{method: "delete"})do %>
       <span class="fa fa-heart like-btn-unlike"></span>
       <% end %>
      <% else %>
      <%= link_to("/posts/#{@post.id}/likes",{method: "post"})do %>
      <span class="fa fa-heart like-btn"></span>
      <% end %>
       <% end %>
       <%= @likes_count %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@post, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
     <div class="comments">
      <h4><コメント一覧></h4>
      <% @comments.each do |comment| %>
        <p>
          <strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
          <%= comment.text %>
        </p>
      <% end %>
    </div>
  </div>
</div>

以上で、つぶやきアプリの完成です!
masakichi_eng様の記事を参考にさせていただきました。
大変わかりやすい記事をありがとうございましたmm
https://qiita.com/masakichi_eng

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