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

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

Rails7.1 | 仕事売買アプリ作成 | 48 | 通知

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



47 | Full Calendar】 << 【ホーム】 >> 【49 | 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
8行目に「has_many :notifications」の記述追加

class User < ApplicationRecord

  has_many :gigs
  has_many :requests
  has_many :offers
  has_many :buying_orders, foreign_key: "buyer_id", class_name: "Order"
  has_many :selling_orders, foreign_key: "seller_id", class_name: "Order"
  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 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

  def is_active_host
    !self.merchant_id.blank?
  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\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
23行目に「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 '/selling_orders', to: 'orders#selling_orders'
  get '/buying_orders', to: 'orders#buying_orders'
  get '/all_requests', to: 'requests#list'
  get '/request_offers/:id', to: 'requests#offers', as: 'request_offers'
  get '/my_offers', to: 'requests#my_offers'
  get '/search', to: 'pages#search'
  get 'settings/payment', to: 'users#payment', as: 'settings_payment'
  get 'settings/payout', to: 'users#payout', as: 'settings_payout'
  get '/gigs/:id/checkout/:pricing_type', to: 'gigs#checkout', as: 'checkout'
  get '/conversations', to: 'conversations#list', as: "conversations"
  get '/conversations/:id', to: 'conversations#show', as: "conversation_detail"
  get '/orders/:id', to: 'orders#show', as: "order_detail"
  get '/host_calendar', to: "calendars#host"
  get '/notifications', to: 'notifications#index'

  # post
  post '/users/edit', to: 'users#update'
  post '/offers', to: 'offers#create'
  post '/reviews', to: 'reviews#create'
  post '/search', to: 'pages#search'
  post '/settings/payment', to: 'users#update_payment', as: "update_payment"
  post 'messages', to: 'messages#create'
  post '/comments', to: 'comments#create'

  # put
  put '/orders/:id/complete', to: 'orders#complete', as: 'complete_order'
  put '/offers/:id/accept', to: 'offers#accept', as: 'accept_offer'
  put '/offers/:id/reject', to: 'offers#reject', as: 'reject_offer'

  resources :gigs do
    member do
      delete :delete_photo
      post :upload_photo
    end
    resources :orders, only: [:create]
  end

  resources :requests
  resources :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(51行目)

    <% 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>GigHub7</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">

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

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

    <!-- 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">
                <div class="card-body">
                    <!-- アバター -->
                    <%= image_tag avatar_url(current_user), class: "img-fluid img-thumbnail rounded-pill" %>
                    <h4 style="margin-left: 5.5rem;"><%= current_user.full_name %></h4>
                    <!-- 画像アップロードボタン -->
                    <button class="btn btn-dark 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/>
                登録:<%= I18n.l(current_user.created_at, format: :full_date) %>
                <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-secondary" 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>       
                    
                </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 mt-2">
                <div class="card-body">
                    <h5 class="card-title">登録している仕事</h5>

                    <div class="row">
                        <% current_user.gigs.each do |gig| %>
                            <% if gig.active? %>
                                <div class="col-md-4">
                                    <div class="card mb-2">
                                    <%= link_to gig_path(gig), data: { turbolinks: false} do %>
                                        <%= image_tag gig_cover(gig), style: "width: 100%;", class: "card-img-top" %>
                                    <% end %>                                    
                                        <div class="card-body">

                                            <span class="star-review"><i class="fa fa-star text-warning"></i>
                                                <%= gig.average_rating %>
                                                <span class="has-text-primary">(<%= gig.reviews.count %>)</span>
                                            </span>          

                                            <%= link_to gig_path(gig), data: { turbolinks: false} do %>
                                                <h5 class="card-title">
                                                    <span class="btn btn-light"><%= gig.title %></span>
                                                </h5>
                                            <% end %>
                                            <div class="card-text" style="margin-left: 0.5rem;">
                                                <p><%= gig.summary %></p>          
                                                <%= link_to edit_gig_path(gig), 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">登録しているリクエスト</h5>

                    <div class="row">
                    <% current_user.requests.each do |r| %>

                        <div class="col-md-4">
                            <div class="card mb-2">
                                <div class="card-body">
                                    <div>
                                        <% if r.offers.count == 0 %>
                                            <span class="badge bg-secondary">まだオファーがありません</span>
                                        <% else %>
                                            <%= link_to request_offers_path(r) do %>
                                                <span class="badge bg-danger"><%= r.offers.count %>件のオファーが入りました</span>
                                            <% end %>
                                        <% end %>                                    
                                    </div>
                                    <ul class="list-group mt-4">
                                        <li class="list-group-item" style="border: none;">
                                            <span class="font1">リクエスト日:</span><%= I18n.l(r.created_at, format: :full_date) %>
                                        </li>
                                        <li class="list-group-item" style="border: none;">
                                            <span class="font1">リクエスト名:</span>
                                            <%= link_to r.title, request_path(r), class: "btn btn-light" %>
                                        </li>               
                                        <li class="list-group-item" style="border: none;">
                                            <span class="font1">期日:</span>   <%= r.delivery %></li>
                                        <li class="list-group-item" style="border: none;">
                                            <span class="font1">価格:</span><%= number_to_currency(r.budget) %>
                                        </li>
                                        <% if r.attachment_file.attached? %>
                                            <div class="mt-2">
                                                <%= link_to url_for(r.attachment_file), 
                                                                class: "badge bg-success",  
                                                                download: "Attachment_#{r.attachment_file.id}",
                                                                style: "text-decoration: none;" do %>
            
                                                    <i class="fas fa-paperclip fa-lg p-r-5"></i><span class="fs-6"><%= r.attachment_file.filename %></span>
            
                                                <% end %>
                                            </div>
                                        <% end %>
                                        <li class="list-group-item mt-4" style="border: none;">
                                        <%= link_to edit_request_path(r), class: "btn btn-outline-primary w-100 mb-4" do %>
                                            登録内容編集
                                        <% end %>
                                        </li>                                      
                                    </ul>
                                </div>
                            </div>
                        </div>
                    <% end %>
                    </div>
                </div>
            </div>

            <!-- レビュー -->
            <div class="card mb-2 mt-2">           
                <div class="card-body">
                    <h5 class="card-title font1">投稿したレビュー</h5>
                    <%= render "reviews/list" %>
                </div>
            </div>            
        </div>
    </div>
</div>



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

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


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



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

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


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




47 | Full Calendar】 << 【ホーム】 >> 【49 | Herokuアカウントとアプリケーション作成






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