【Ruby on Rails】通知機能を実装してみた

#はじめに
今回は通知機能を作成しました。
こちらの記事を参考にさせていただいております。
https://qiita.com/nekojoker/items/80448944ec9aaae48d0a
https://qiita.com/yuto_1014/items/2db1dd4fcd7945b980f7

###作るもの
コメント・いいね・フォローされた時に通知を作成して確認・削除できる機能を作成します。すでに上記の機能はあるものとして作成してます。どれか一つでも機能があればOK。参考に過去に投稿したコメント・フォロー機能についてリンクを載せておきます。
コメント機能：https://qiita.com/E6YOteYPzmFGfOD/items/ef776d34908872ea19f7
フォロー機能：https://qiita.com/E6YOteYPzmFGfOD/items/ec0492b509962c3b7ae4

#ER図
参考に今回作成する通知機能のER図です。（今回関係ないタグ機能がありますが無視してください。）
<img width="732" alt="スクリーンショット 2020-04-04 16.22.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563841/0c1d3977-e516-a735-7b22-67b674d714a0.png">


#作成の流れ
1.Notificationモデルの作成
2.各モデルの関連づけ
3.通知メゾットの作成
4.通知機能をコントローラーへ埋め込む
5.通知一覧画面と通知削除作成

#モデルの作成

```
rails g model Notification visitor:references visited:references micropost:references comment:references action:string checked:boolean
```

```ruby:db/migrate/
class CreateNotifications < ActiveRecord::Migration[5.1]
  def change
    create_table :notifications do |t|
      t.references :visitor, foreign_key:{ to_table: :users }, null: false
      t.references :visited, foreign_key:{ to_table: :users }, null: false
      t.references :micropost, foregin_key: true
      t.references :comment, foregin_key: true
      t.string :action, null: false
      t.boolean :checked, null: false, default: false

      t.timestamps
    end
  end
end
```
vistor_id
→通知するユーザー（いいね、コメント、フォローする側）
visted_id
→通知をうけるユーザー
rails g model カラム名：references とすると自動で
・カラム名にidをつけた形でカラムを作成。
・インデックス付与。
foreign_key:true
・外部キー制約を付与
to_table: :user 
・visitorとvisitedカラムの参照テーブルを指定してます。
nul: false
・空の状態で保存できなくする。

#モデルの関連付け

```ruby:app/models/user.rb
has_many :active_notifications, foreign_key:"visitor_id", class_name: "Notification", dependent: :destroy
has_many :passive_notifications, foreign_key:"visited_id", class_name: "Notification", dependent: :destroy
```

```ruby:app/models/micropost.rb
has_many :notifications, dependent: :destroy
```

```ruby:app/models/comment.rb
has_many :notifications, dependent: :destroy
```

```ruby:app/models/notification.rb
class Notification < ApplicationRecord
  default_scope -> { order(created_at: :desc) }
  belongs_to :visitor, class_name: "User", optional: true
  belongs_to :visited, class_name: "User", optional: true
  belongs_to :micropost, optional: true
  belongs_to :comment, optional: true
  validates :visitor_id, presence: true
  validates :visited_id, presence: true
  ACTION_VALUES = ["like", "follow", "comment"]
  validates :action,  presence: true, inclusion: {in:ACTION_VALUES}
  validates :checked, inclusion: {in: [true,false]}
end
```

各オプション
foreign_key:"vistor_id", class_name: "Notification"
→関連づけるカラム名とテーブル名を指定します。
dependent: :destroy
→投稿が削除された際に関連付いている通知を削除。
default_scope -> { order(created_at: :desc) }
→通知が新しい順に並ぶ。
optional: true: nullの値を許可する。
inclusion
→保存できる値を制限しています。

#通知メゾットの作成
###いいね

```ruby:app/models/micropost.rb
def create_notification_like!(current_user)
    temp = Notification.where(["visitor_id = ? and visited_id = ? and micropost_id = ? and action = ? ",
                                  current_user.id, user_id, id, 'like'])
    if temp.blank?
      notification = current_user.active_notifications.new(
        micropost_id: id,
        visited_id: user_id,
        action: 'like'
      )
      
      if notification.visitor_id == notification.visited_id
         notification.checked = true
      end
      notification.save if notification.valid?
    end
  end
```

始めに通知が作成済みでないか確認してます。（何度も作成しないように）登録済みの場合と自分の投稿に対するいいねの場合は通知を確認済みとして作成します。（checkedをtrueにする。）active_notificationsはuserモデルでhas_many関連付けを行った際に名付けた関連名です。

###コメント

```ruby:app/models/micropost.rb
def create_notification_comment!(current_user, comment_id)
    #同じ投稿にコメントしているユーザーに通知を送る。（current_userと投稿ユーザーのぞく）
    temp_ids = Comment.where(micropost_id: id).where.not("user_id=? or user_id=?", current_user.id,user_id).select(:user_id).distinct
    #取得したユーザー達へ通知を作成。（user_idのみ繰り返し取得）
    temp_ids.each do |temp_id|
      save_notification_comment!(current_user, comment_id, temp_id['user_id'])
    end
    #投稿者へ通知を作成
    save_notification_comment!(current_user, comment_id, user_id)
end

def save_notification_comment!(current_user, comment_id, visited_id)
    notification = current_user.active_notifications.new(
      micropost_id: id,
      comment_id: comment_id,
      visited_id: visited_id,
      action: 'comment'
    )
    if notification.visitor_id == notification.visited_id
      notification.checked = true
    end
    notification.save if notification.valid?
end
```

コメントの場合は同じ投稿に対してコメントしているユーザーにもコメントを送るようにします。マイクロポストの投稿者には必ず通知が１件作成されるようにします。
「where.(micropost_id: id)」
→同じ投稿にコメント
「where.not("user_id=? or user_id=?", current_user.id,user_id)」
→コメントしたユーザーと投稿者は除く。
distinct
→複数回コメントしている人も通知は一件のみにする
select(:user_id)
→user_idのみ取得しています。

取得したuser_idを下で定義している通知作成メゾットに渡して繰り返し通知を作成します。

###フォロー

```ruby:app/models/user.rb
def create_notification_follow!(current_user)
    #すでに通知が作成されているか確認
    temp = Notification.where(["visitor_id = ? and visited_id = ? and action = ? ",current_user.id, id, 'follow'])
    if temp.blank?
      notification = current_user.active_notifications.new(
        visited_id: id,
        action: 'follow'
      )
      notification.save if notification.valid?
    end
end
```

#通知機能を各コントローラーへ埋め込み
各通知メゾットをコメント等のアクションを起こした際に起動するようにコントローラー内へ埋め込んでいきます。
###コメント
```ruby:app/controlloers/comments_controller.rb
def create
    @comment = current_user.comments.build(comment_params)
    @comment.micropost_id = params[:micropost_id]
    if @comment.save
      flash[:success] = 'コメントしました'
      #通知機能用
      @micropost=@comment.micropost
      @micropost.create_notification_comment!(current_user, @comment.id)
      #ここまで通知機能
      redirect_to @comment.micropost
    else
      comments_get
      render template: 'microposts/show'
end
```
###フォロー

```ruby:app/controllers/follow_controlloer.rb
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    #通知機能追加
    @user.create_notification_follow!(current_user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js 
    end
end
```
###いいね

```ruby:app/controllers/like_controlloer.rb
def create
    @user = current_user
    @micropost = Micropost.find(params[:micropost_id])
    current_user.like(@micropost)
    #通知機能追加
    @micropost.create_notification_like!(current_user)
    respond_to do |format|
      format.html { redirect_back(fallback_location: root_url) }
      format.js
    end
end
```

#通知一覧画面と通知削除作成
まずは通知ページ表示ようのコントローラ、ルーディングの設定です。

```
rails g controller notifications
```

```ruby:config/routes.rb
resources :notifications, only: [:index, :destroy]
```

```ruby:app/controllers/notifications_controlloer.rb
def index
    @notifications = current_user.passive_notifications
    #通知画面を開くとcheckedをtrueにして通知確認済にする
    @notifications.where(checked: false).each do |notification|
      notification.update_attributes(checked: true)
    end
end
  
def destroy
    @notifications =current_user.passive_notifications.destroy_all
    redirect_to notifications_path
end
```
続いて通知一覧画面の作成です。

```html:app/views/notifications/index.html.erb
<h3 class="text-center">通知</h3>
    <%= link_to "通知削除", notification_path(@notifications), method: :delete ,class: "fas fa-trash" %>
    <% if @notifications.exists? %>
    <div class="notification-index">
        <%= render @notifications %>
    </div>
<% else %>
    <p>通知はありません</p>
<% end %>
```

```html:app/views/notifications/_notification.html.erb
<div class="notification-view">
  <%= notification_form(notification) %><span class="moderate-font"><%= " (#{time_ago_in_words(notification.created_at)}前)" %></span>
  <br>
  <% if !@comment.nil? %>
    <p class="notification-comment"><%= @comment %></p>
  <% end %>
</div>
```

いいね、フォロー、コメントの通知によって表示する内容を変更するために、
ヘルパーメゾットを作成してviewで呼び出します。

```ruby/app/helprs/notifications_helper.rb
module NotificationsHelper
  def notification_form(notification)
    #通知を送ってきたユーザーを取得
    @visitor = notification.visitor
    #コメントの内容を通知に表示する
    @comment = nil
    @visitor_comment = notification.comment_id
    # notification.actionがfollowかlikeかcommentかで処理を変える
    case notification.action
    when 'follow'
      #aタグで通知を作成したユーザーshowのリンクを作成
      tag.a(notification.visitor.name, href: user_path(@visitor)) + 'があなたをフォローしました'
    when 'like'
      tag.a(notification.visitor.name, href: user_path(@visitor)) + 'が' + tag.a('あなたの投稿', href: micropost_path(notification.micropost_id)) + 'にいいねしました'
    when 'comment' then
      #コメントの内容と投稿のタイトルを取得　      
      @comment = Comment.find_by(id: @visitor_comment)
      @comment_content =@comment.content
      @micropost_title =@comment.micropost.title
      tag.a(@visitor.name, href: user_path(@visitor)) + 'が' + tag.a("#{@micropost_title}", href: micropost_path(notification.micropost_id)) + 'にコメントしました'
    end
end
```
  
以上となります。

だいぶ長くなりましたがここまでお読みいただきありがとうございました！！