学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Rails6.1 | 民泊予約アプリ作成 | 42 | 通知

↓↓クリックして頂けると励みになります。


41 | 会話表示】 << 【ホーム】 >> 【43 | Herokuアカウントとアプリケーション作成





予約やメッセージが入った時に分かるよう、通知機能を実装します。


ジェネレーターを使って通知モデルを作成します。
コマンド
rails g model Notification content user:references


ユーザーモデルに通知を既読カラムを追加します。
コマンド
rails g migration AddUnreadToUser unread:bigint


記述追加 db\migrate\20200729003608_add_unread_to_user.rb
3行目末尾に「, default: true」の記述追加

class AddUnreadToUser < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :unread, :bigint, default: true
  end
end



マイグレーションを適用します。
コマンド マイグレーション
rails db:migrate


ユーザーモデルに通知モデルを関連付けします。
記述追加 app\models\user.rb
7行目に「has_many :notifications」の記述追加

class User < ApplicationRecord

  has_many :rooms
  has_many :reservations
  has_many :guest_reviews, class_name: "GuestReview", foreign_key: "guest_id"
  has_many :host_reviews, class_name: "HostReview", foreign_key: "host_id"
  has_many :notifications

  has_one_attached :avatar

  validates :full_name, presence: true, length: {maximum: 50}

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :omniauthable

  def is_active_host
    !self.merchant_id.blank?
  end

  def self.from_omniauth(auth)
    user = User.where(email: auth.info.email).first

    if user
      return user
    else
      where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
        user.email = auth.info.email
        user.password = Devise.friendly_token[0, 20]
        user.full_name = auth.info.name   # ユーザーモデルに名前があると仮定
        user.image = auth.info.image # ユーザーモデルに画像があると仮定

        user.uid = auth.uid
        user.provider = auth.provider
      end
    end
  end

end



通知モデルを編集します。
記述追加 app\models\notification.rb
2行目に「after_create_commit { NotificationJob.perform_later self }」の記述追加

class Notification < ApplicationRecord
  after_create_commit { NotificationJob.perform_later self }
  belongs_to :user
end





メッセージモデルを編集します。
「app\models\message.rb」ファイルを以下のように編集します。


1.5行目に以下の記述を追加します。

after_create_commit :create_notification



2.9行目に以下の記述を追加します。

  private
  def create_notification
    if self.conversation.sender_id == self.user_id
      sender = User.find(self.conversation.sender_id)
      Notification.create(content: "#{sender.full_name}から新しいメッセージがあります。", user_id: self.conversation.receiver_id)
    else
      sender = User.find(self.conversation.receiver_id)
      Notification.create(content: "#{sender.full_name}から新しいメッセージがあります。", user_id: self.conversation.sender_id)
    end
  end  



app\models\message.rb

class Message < ApplicationRecord
  belongs_to :user
  belongs_to :conversation

  after_create_commit :create_notification

  validates :content, presence: { message: '空白にはできません' }

  private
  def create_notification
    if self.conversation.sender_id == self.user_id
      sender = User.find(self.conversation.sender_id)
      Notification.create(content: "#{sender.full_name}から新しいメッセージがあります。", user_id: self.conversation.receiver_id)
    else
      sender = User.find(self.conversation.receiver_id)
      Notification.create(content: "#{sender.full_name}から新しいメッセージがあります。", user_id: self.conversation.sender_id)
    end
  end    

end



予約モデルを編集します。
「app\models\reservation.rb」ファイルを以下のように編集します。


1.6行目に以下の記述を追加します

after_create_commit :create_notification



2.13行目に以下の記述を追加します。

  private
  def create_notification
    type = self.status? ? "予約" : "完了処理"
    guest = User.find(self.user_id)
    Notification.create(content: "#{guest.full_name}様の#{type}がありました。", user_id: self.room.user_id)
  end  



app\models\reservation.rb

class Reservation < ApplicationRecord

  # status: {待ち: 0, 済み: 1}
  enum status: {Waiting: 0, Approved: 1}

  after_create_commit :create_notification

  belongs_to :user
  belongs_to :room

  has_many :reviews

  private
  def create_notification
    type = self.status? ? "予約" : "完了処理"
    guest = User.find(self.user_id)
    Notification.create(content: "#{guest.full_name}様の#{type}がありました。", user_id: self.room.user_id)
  end  

end



コントローラーを編集します。
「app\controllers」フォルダに「notifications_controller.rb」ファイルを新規作成して下さい。


app\controllers\notifications_controller.rb(新規作成したファイル)

class NotificationsController < ApplicationController
    before_action :authenticate_user!
  
    def index
      current_user.unread = 0
      current_user.save
      @notifications = current_user.notifications.order(created_at: "DESC").page(params[:page]).per(5)
    end
  end



ジェネレーターを使ってチャンネルを作成します。
コマンド
rails g channel notifications


「app\javascript\channels」フォルダに「notifications.coffee」ファイルを新規作成してください。


app\javascript\channels\notifications.coffee(新規作成したファイル)

$(() ->
  App.notifications = App.cable.subscriptions.create {channel: "NotificationsChannel", id: $('#user_id').val() },
    received: (data) ->
      $('#num_of_unread').html(data.unread)
      $('#notifications').prepend(data.message)
      $('#navbar_num_of_unread').html(data.unread)
)



「app\channels\notifications_channel.rb」ファイルの内容を以下に書き換えます。
書き換え app\channels\notifications_channel.rb

class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notification_#{params[:id]}"
  end
end



ジェネレーターを使ってjobを作成します。
コマンド
rails g job notification


作成された「app\jobs\notification_job.rb」ファイルを以下の内容に書き換えます。
書き換え app\jobs\notification_job.rb

class NotificationJob < ApplicationJob
  queue_as :default

  def perform(notification)
    notification.user.increment!(:unread)
    ActionCable.server.broadcast "notification_#{notification.user.id}", message: render_notification(notification), unread: notification.user.unread
  end

  private

    def render_notification(notification)
      ApplicationController.render(partial: 'notifications/notification', locals: { notification: notification})
    end
end



ルートの設定をします。
記述追加 config\routes.rb
18行目に「get '/notifications' => 'notifications#index'」の記述追加

Rails.application.routes.draw do

  # ルートを app\views\pages\home.html.erb に設定
  root 'pages#home'

  # get
  get '/dashboard', to: 'users#dashboard'
  get 'pages/home'
  get '/users/:id', to: 'users#show', as: 'user'
  get '/your_trips' => 'reservations#your_trips'
  get '/your_reservations' => 'reservations#your_reservations'
  get 'search' => 'pages#search'
  get '/host_calendar' => "calendars#host"
  get 'settings/payment', to: 'users#payment', as: 'settings_payment'
  get 'settings/payout', to: 'users#payout', as: 'settings_payout'
  get '/conversations', to: 'conversations#list', as: "conversations"
  get '/conversations/:id', to: 'conversations#show', as: "conversation_detail"
  get '/notifications' => 'notifications#index'

  # post
  post '/users/edit', to: 'users#update'
  post '/settings/payment', to: 'users#update_payment', as: "update_payment"
  post 'messages', to: 'messages#create'

  resources :rooms, except: [:edit] do
    member do
      get 'listing'
      get 'pricing'
      get 'description'
      get 'photo_upload'
      get 'amenities'
      get 'location'
      get 'preload'
      get 'preview'
      delete :delete_photo
      post :upload_photo
    end
    resources :reservations, only: [:create]
  end

  resources :reservations, only: [:approve, :decline] do
    member do
      post '/approve' => "reservations#approve"
    end
  end

  resources :guest_reviews, only: [:create, :destroy]
  resources :host_reviews, only: [:create, :destroy]

  # device
  devise_for :users, 
              path: '', 
              path_names: {sign_up: 'register', sign_in: 'login', edit: 'profile', sign_out: 'logout'},
              controllers: {omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'}
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end



「app\views\layouts\application.html.erb」ファイルを編集します。


記述追加 app\views\layouts\application.html.erb(52行目)

    <% if current_user %>
        <input type="hidden" id="user_id" value="<%= current_user.id %>">
    <% end %>



app\views\layouts\application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>VacationRental</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

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

    <!-- bootstrap5.3 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>

    <!-- noty3.1.4 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.min.js" integrity="sha512-lOrm9FgT1LKOJRUXF3tp6QaMorJftUjowOWiDcG5GFZ/q7ukof19V0HKx/GWzXCdt9zYju3/KhBNdCLzK8b90Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.min.css" integrity="sha512-0p3K0H3S6Q4bEWZ/WmC94Tgit2ular2/n0ESdfEX8l172YyQj8re1Wu9s/HT9T/T2osUw5Gx/6pAZNk3UKbESw==" crossorigin="anonymous" referrerpolicy="no-referrer" />

    <!-- Google Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Kaisei+Opti&family=Kosugi+Maru&family=Rampart+One&display=swap" rel="stylesheet">

    <!-- Font Awesome -->
    <script src="https://kit.fontawesome.com/dd8c589546.js" crossorigin="anonymous"></script>

    <!-- Dropzone5.5.1 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha512-jytq61HY3/eCNwWirBhRofDxujTCMFEiQeTe+kHR4eYLNTXrUq7kY2qQDKOUnsVAKN5XGBJjQ3TvNkIkW/itGw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.css" integrity="sha512-zoIoZAaHj0iHEOwZZeQnGqpU8Ph4ki9ptyHZFPe+BmILwqAksvwm27hR9dYH4WXjYY/4/mz8YDBCgVqzc2+BJA==" crossorigin="anonymous" referrerpolicy="no-referrer" />

    <!-- 日付ピッカー デザインsunny-->
    <link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/sunny/jquery-ui.css">
    <script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js" integrity="sha256-xLD7nhI62fcsEZK2/v8LsBcb4lG7dgULkuXoXB/j91c=" crossorigin="anonymous"></script>
    <!-- 日付ピッカーの日本語化-->
    <script src="https://rawgit.com/jquery/jquery-ui/master/ui/i18n/datepicker-ja.js"></script>

    <!-- フルカレンダー -->
    <script src='https://cdn.jsdelivr.net/npm/moment@2.27.0/min/moment.min.js'></script>
    <script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js'></script>

  </head>

  <body>

    <!-- ナビゲーションバー -->
    <%= render  "shared/navbar" %>

    <!-- noty -->
    <%= render 'shared/notification' %>

    <!-- 通知用 -->
    <% if current_user %>
        <input type="hidden" id="user_id" value="<%= current_user.id %>">
    <% end %>  

    <%= yield %>
  </body>
</html>



ビューを作成します。
「app\views」フォルダに「notifications」フォルダを新規作成してください。
作成した「notifications」フォルダに「index.html.erb」ファイルを新規作成します。



app\views\notifications\index.html.erb(新規作成したファイル)

<div class="container mt-4">

    <div class="card">
        <div class="card-body">
            <%= link_to 'メッセージ', conversations_path, class: "btn btn-outline-primary mb-4" %>

            <!-- ページネーション -->
            <div style="margin-left: 4rem;"><%= paginate @notifications %></div>

            <div class="card" style="border: none;">
                <div class="card-body">
                    <h4 class="font1">通知内容</h4>

                    <% @notifications.each do |n| %>
                        <ul class="list-group mb-2">
                            <li class="list-group-item" style="border-bottom: none;"><%= n.content %></li>
                            <li class="list-group-item text-secondary"><small><%= time_ago_in_words(n.created_at) %></small></li>
                        </ul>                  
                    <% end %>
                </div>
            </div>
        </div>
    </div>
</div>



ダッシュボードビューを更新します。


app\views\users\dashboard.html.erb

<div class="container mt-4 mb-4">
    <div class="row">
        <!-- 左側 -->
        <div class="col-md-4">

            <!-- お知らせ -->
            <div class="card mb-2">
                <div class="card-body">
                    <h5 class="card-title font2">お知らせ</h5>
                    <%if current_user.unread > 0 %>                             
                        <%= link_to notifications_path, style: "text-decoration: none;" do %>
                            <div class="alert alert-danger" role="alert">
                                <span id="num_of_unread"><%= current_user.unread %></span> 件の通知
                            </div>
                        <% end %>                      
                    <% else %>
                        <%= link_to notifications_path, style: "text-decoration: none;" do %>
                            <div class="alert alert-secondary" role="alert">
                                <span id="num_of_unread">
                                    <%= current_user.unread %>
                                </span> 件の通知
                            </div>
                        <% end %>
                    <% end %>                                  
                </div>
            </div>

            <div class="card mb-4">
                <div class="card-body">
                    <!-- アバター -->
                    <%= image_tag avatar_url(current_user), class: "img-fluid img-thumbnail rounded-pill" %>
                    <h4 class="text-center font2"><%= current_user.full_name %></h4>
                    <!-- 画像アップロードボタン -->
                    <button class="btn btn-info text-light w-100" type="button" data-bs-toggle="collapse" data-bs-target="#collapse1" aria-expanded="false" aria-controls="collapse1">
                        <i class="fa-solid fa-cloud-arrow-up"></i>アバター画像アップロード
                    </button>
                    <div class="collapse" id="collapse1">
                    <div class="card card-body">
                        <%= form_for :user, url: users_edit_url(current_user), action: :update, method: :post do |f| %>
                            <%= f.file_field :avatar, class: "input-group-text", onchange: "this.form.submit();" %>
                        <% end %>   
                    </div>
                </div>
                <hr/>
                <div>
                    <%= link_to "部屋新規登録", new_room_path, class: "btn btn-danger" %>
                </div>
            
                <hr/>
                <!-- ステータス -->
                <div type="button" data-bs-toggle="collapse" data-bs-target="#collapse2" aria-expanded="false" aria-controls="collapse2">

                    <% if current_user.status %>
                        <span class="btn btn-success"><i class="toggle far fa-edit"></i>オンライン</span>
                    <% else %>
                        <span class="btn btn-secondary"><i class="toggle far fa-edit"></i>オフライン</span>
                    <% end %>                
                
                </div>
                <div class="collapse" id="collapse2">
                    <div class="card card-body">

                        <%= form_for :user, url: users_edit_url(current_user), action: :update, method: :post do |f| %>
                            <%= f.select(:status, options_for_select([["オンライン", true], ["オフライン", false]]), {}, {class: "custom-select"}) %>                        
                            <%= f.submit "保存", class: "btn btn-dark" %>
                        <% end %>

                    </div>
                </div>
                <hr/>
                <!-- 自己紹介 -->
                <div class="h5"><%= current_user.about %></div>
                <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapse3" aria-expanded="false" aria-controls="collapse3">
                    自己紹介編集
                </button>

                <div class="collapse" id="collapse3">
                    <div class="card card-body">
                        <%= form_for :user, url: users_edit_url(current_user), action: :update, method: :post do |f| %>
                            <div><%= f.text_area :about, autofocus: true, autocomplete: 'form'%></div>
                            <%= f.submit "保存", class: "btn btn-dark" %>
                        <% end %>                        
                    </div>
                </div>
                <hr/>
                <!-- 電話番号 -->
                <div>
                    <% if !current_user.phone_number.blank? %>
                        <span class="pull-right icon-babu"><i class="far fa-check-circle" style="color:green;"></i></span>&nbsp;&nbsp;電話番号
                    <% else %>
                        <div class="text-danger">電話番号が登録されていません</div>
                        <%= link_to  "電話番号登録", edit_user_registration_path, class: "btn btn-danger" %>
                    <% end %>                    
                </div>
                <hr/>
                <!-- アカウント登録日 -->
                登録:<%= I18n.l(current_user.created_at, format: :full_date) %>                                
                
            </div>
        </div>


        </div>
        <!-- 右側 -->
        <div class="col-md-8">

            <!-- 登録している部屋 -->
            <div class="card">
                <div class="card-body">
                    <h5 class="card-title">登録している部屋(<%= current_user.rooms.count %>)</h5>
                    <h6 class="card-subtitle mb-2 text-body-secondary">
                    </h6>

                    <div class="row">
                        <% current_user.rooms.each do |room| %>
                            <% if room.active? %>
                                <div class="col-md-4">
                                    <div class="card mb-2">
                                        <div class="card-body">
                                            <span><i class="fa fa-star fa-1x" style="color: gold;"></i><%= pluralize(room.average_rating, "") %></span>
                                            <%= link_to room_path(room), data: { turbolinks: false} do %>
                                                <%= image_tag room_cover(room), style: "width: 100%;" %>
                                                <h5 class="card-title">
                                                    <span class="btn btn-light"><%= room.listing_name %></span>
                                                </h5>
                                            <% end %>
                                            <div class="card-text" style="margin-left: 0.5rem;">
                                                <p style="font-size: 0.8rem; margin-bottom: -0.3rem;">Address</p>
                                                <p><%= room.address %></p>
                                                <h5 class="badge rounded-pill bg-danger text-light mb-4" style="font-size: 1rem;">1泊<%= number_to_currency(room.price) %></h5>               
                                                <%= link_to listing_room_path(room), class: "btn btn-outline-primary w-100 mb-4" do %>
                                                    登録内容編集
                                                <% end %>
                                              
                                            </div>
                             
                                        </div>
                                    </div>
                                </div>
                            <% end %>
                        <% end %>
                    </div>

                </div>
            </div>

            <!-- レビュー -->
            <div class="card mt-2">           
                <div class="card-body">
                    <h5 class="card-title"><%= current_user.full_name %>さんへのレビュー</h5>
                        <%= render "reviews/guest_list" %>
                        <%= render "reviews/host_list" %>

                </div>
            </div>

        </div>
     
    </div>

</div>



予約やメッセージのアクションがあると通知がきてわかるようになりました。
ブラウザ確認
http://localhost:3000/dashboard

PCレイアウト
PCレイアウト


モバイルレイアウト
モバイルレイアウト



通知内容詳細です。
http://localhost:3000/notifications

PCレイアウト
PCレイアウト


モバイルレイアウト
モバイルレイアウト

41 | 会話表示】 << 【ホーム】 >> 【43 | Herokuアカウントとアプリケーション作成



↓↓クリックして頂けると励みになります。