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

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

Rails7.1 | 民泊予約アプリ作成 | 43 | 通知

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



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




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


GemFileに以下の記述を追加します。

# 通知
gem 'coffee-rails', '~> 5.0'



バンドルインストールします。
コマンド
bundle


ジェネレーターを使って通知モデルを作成します。
コマンド
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[7.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.17行目に以下の記述を追加します。

  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

  def self.ransackable_attributes(auth_object = nil)
    ["created_at", "end_date", "id", "id_value", "price", "room_id", "start_date", "status", "total", "updated_at", "user_id"]
  end

  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', to: 'notifications#index'」の記述追加

Rails.application.routes.draw do

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

  # get
  get 'pages/home'
  get '/dashboard', to: 'users#dashboard'
  get '/users/:id', to: 'users#show', as: 'user'
  get '/your_trips', to: 'reservations#your_trips'
  get '/your_reservations', to: 'reservations#your_reservations'
  get 'search', to: 'pages#search'
  get '/host_calendar', to: "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', to: '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] do
    member do
      post '/approve', to: "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'}

  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
end



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


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

    <% 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>VacationRental7</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>

    <!-- 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">

    <!-- 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" />

    <!-- JQuery 3.7.1 -->
    <script
    src="https://code.jquery.com/jquery-3.7.1.min.js"
    integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
    crossorigin="anonymous"></script>

    <!-- JQuery-UI 1.13.2 -->
    <script
    src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"
    integrity="sha256-lSjKY0/srUM9BE3dPm+c4fBo1dky2v27Gdjm2uoZaL0="
    crossorigin="anonymous"></script>

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

    <!-- ratyjs 3.1.1 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/raty/3.1.1/jquery.raty.min.js" integrity="sha512-Isj3SyFm+B8u/cErwzYj2iEgBorGyWqdFVb934Y+jajNg9kiYQQc9pbmiIgq/bDcar9ijmw4W+bd72UK/tzcsA==" crossorigin="anonymous" referrerpolicy="no-referrer"></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", data: { turbo: false} %>

            <!-- ページネーション -->
            <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-4">
                <div class="card-body">
                    <!-- アバター -->
                    <%= image_tag avatar_url(current_user), class: "img-fluid img-thumbnail rounded-pill" %>
                    <h4 class="text-center"><%= 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 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">
                <div class="card-body">
                    <h5 class="card-title">登録している部屋</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">
                                        <%= link_to room_path(room), data: { turbo: false} do %>
                                            <%= image_tag room_cover(room), style: "width: 100%;", class: "card-image-top" %>
                                        <% end %>
                                        <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: { turbo: false} do %>
                                                <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レイアウト


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


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





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