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

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

Rails7.1 | 動画学習アプリ作成 | 39 | 通知

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



38 | 会話表示】 << 【ホーム





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


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 :projects
  has_many :subscriptions
  has_many :projects, through: :subscriptions
  has_many :reviews
  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
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")
    end
end



ダッシュボード用にコントローラーを編集します。
「app/controllers/users_controller.rb」ファイルのdashboadメソッドを以下のように書き換えます。

  def dashboard
    if !current_user.nil?
      @projects = current_user.projects
    end    
    @reviews = Review.all
  end



記述編集 【app/controllers/users_controller.rb】

class UsersController < ApplicationController
  
  before_action :authenticate_user!

  def dashboard
    if !current_user.nil?
      @projects = current_user.projects
    end    
    @reviews = Review.all
  end

  def show
    @user = User.find(params[:id])
    @projects = Project.all
    @reviews = Review.all
  end

  def update
    @user = current_user
    if @user.update(current_user_params)
      flash[:notice] = "保存しました"
    else
      flash[:alert] = "更新できません"
    end
    redirect_to dashboard_path
  end

  def update_payment
    if !current_user.stripe_id
      customer = Stripe::Customer.create(
        email: current_user.email,
        source: params[:stripeToken]
      )
    else
      customer = Stripe::Customer.update(
        current_user.stripe_id,
        source: params[:stripeToken]
      )
    end
    if current_user.update(stripe_id: customer.id, stripe_last_4: customer.sources.data.first["last4"])
      flash[:notice] = "新しいカード情報が登録されました"
    else
      flash[:alert] = "無効なカードです"
    end
    redirect_to request.referrer
  rescue Stripe::CardError => e
    flash[:alert] = e.message
    redirect_to request.referrer
  end

  private

  def current_user_params
    params.require(:user).permit(:about, :status, :avatar)
  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
14行目に「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 '/myprojects', to: 'projects#list'
  get 'settings/payment', to: 'users#payment', as: 'settings_payment'
  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 '/free', to: 'charges#free'
  post '/settings/payment', to: 'users#update_payment', as: "update_payment"
  post '/reviews', to: 'reviews#create'
  post 'messages', to: 'messages#create'

  # 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'}

  resources :projects do
    member do
      get 'naming'
      get 'pricing'
      get 'description'
      get 'photo_upload'
      delete :delete_photo
      post :upload_photo
    end
    resources :tasks, only: [:show, :index]
  end

  resources :tasks, except: [:edit] do
    member do
      get 'naming'
      get 'description'
      get 'video'
      get 'code'
    end
  end

  resources :reviews, only: [:create, :destroy]

  # 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(46行目)

    <% 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>StreamAcademe</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 %>

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

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

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


  </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.erb73行目

<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">
                <div class="card-body">
                    <h5 class="card-title">購入済みプロジェクト</h5>
                    <div class="row">
                    <% if @projects.count > 0 %>
                        <% @projects.each do |project| %>
                                <div class="col-md-4">
                                    <div class="card mb-2">
                                        <%= link_to project_path(project), data: { turbo: false} do %>
                                            <%= image_tag project_cover(project), style: "width: 100%;", class: "card-img-top" %>
                                        <% end %>                                    
                                        <div class="card-body">    
                                            <span class="star-review">
                                                <i class="fa fa-star text-warning"></i>
                                                <%= project.average_rating %>
                                                <span class="has-text-primary">(<%= project.reviews.count %>)</span>
                                            </span>      
                                            <%= link_to project_path(project), data: { turbo: false} do %>
                                                <h5 class="card-title">
                                                    <span class="btn btn-light"><%= project.name %></span>
                                                </h5>
                                            <% end %>
                                            <div><strong>タスク数: <i class="far fa-clock"></i> <%= project.tasks.count %></strong></div>
                                            <div class="badge bg-danger fs-5 mb-4 mt-2"><%= number_to_currency(project.price) %></div>
                                            <div class="btn btn-success">購入済み</div>           
                                        </div>
                                    </div>
                                </div>
                        <% end %>
                    <% 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レイアウト


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



38 | 会話表示】 << 【ホーム




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