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

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

Django3.2 | クラウドソーシングアプリの構築 | 53 | Firebaseメッセージ

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


52 | 報酬支払いの実装】 << 【ホーム】 >> 【54 | Web Socketの設定


「core/models.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/models.py】23行目

import uuid
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

# Create your models here.
class Customer(models.Model):
  user = models.OneToOneField(User, on_delete=models.CASCADE)
  avatar = models.ImageField(upload_to='customer/avatars/', blank=True, null=True)
  phone_number = models.CharField(max_length=50, blank=True)
  stripe_customer_id = models.CharField(max_length=255, blank=True)
  stripe_payment_method_id = models.CharField(max_length=255, blank=True)
  stripe_card_last4 = models.CharField(max_length=255, blank=True)

  def __str__(self):
    return self.user.get_full_name()

class Courier(models.Model):
  user = models.OneToOneField(User, on_delete=models.CASCADE)
  lat = models.FloatField(default=0)
  lng = models.FloatField(default=0)
  paypal_email = models.EmailField('PayPalメールアドレス', max_length=255, blank=True)
  fcm_token = models.TextField(blank=True)

  def __str__(self):
    return self.user.get_full_name()


class Category(models.Model):
  slug = models.CharField(max_length=255, unique=True)
  name = models.CharField(max_length=255)

  def __str__(self):
    return self.name

class Job(models.Model):
  SMALL_SIZE = "small"
  MEDIUM_SIZE = "medium"
  LARGE_SIZE = "large"
  SIZES = (
    (SMALL_SIZE, '小'),
    (MEDIUM_SIZE, '中'),
    (LARGE_SIZE, '大'),
  )

  CREATING_STATUS = 'creating'
  PROCESSING_STATUS = 'processing'
  PICKING_STATUS = 'picking'
  DELIVERING_STATUS = 'delivering'
  COMPLETED_STATUS = 'completed'
  CANCELED_STATUS = 'canceled'
  STATUSES = (
    (CREATING_STATUS, 'Creating'),
    (PROCESSING_STATUS, 'Processing'),
    (PICKING_STATUS, 'Picking'),
    (DELIVERING_STATUS, 'Delivering'),
    (COMPLETED_STATUS, 'Completed'),
    (CANCELED_STATUS, 'Canceled'),
  )

  # Step 1
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  customer = models.ForeignKey(Customer, on_delete=models.CASCADE, verbose_name='配達依頼人')
  courier = models.ForeignKey(Courier, on_delete=models.CASCADE, null=True, blank=True)
  name = models.CharField('配達依頼名', max_length=255)
  description = models.CharField('備考', max_length=255)
  category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='カテゴリー')
  size = models.CharField('サイズ', max_length=20, choices=SIZES, default=MEDIUM_SIZE)
  quantity = models.IntegerField('数量', default=1)
  photo = models.ImageField('写真', upload_to='job/photos/')
  status = models.CharField('状態', max_length=20, choices=STATUSES, default=CREATING_STATUS)
  created_at = models.DateTimeField(default=timezone.now, verbose_name='登録日')

  # Step 2
  pickup_address = models.CharField('荷物届先住所', max_length=255, blank=True)
  pickup_lat = models.FloatField('荷物届先緯度', default=0)
  pickup_lng = models.FloatField('荷物届先経度', default=0)
  pickup_name = models.CharField('依頼人氏名', max_length=255, blank=True)
  pickup_phone = models.CharField('依頼人電話番号', max_length=50, blank=True)

  # Step 3
  delivery_address = models.CharField('配達先住所', max_length=255, blank=True)
  delivery_lat = models.FloatField('配達先緯度', default=0)
  delivery_lng = models.FloatField('配達先経度',default=0)
  delivery_name = models.CharField('配達先氏名',max_length=255, blank=True)
  delivery_phone = models.CharField('配達先電話番号',max_length=50, blank=True)

  # Step 4
  duration = models.IntegerField('移動時間' ,default=0)
  distance = models.FloatField('距離' ,default=0)
  price = models.IntegerField('料金', default=0)

  # その他
  pickup_photo = models.ImageField(upload_to='job/pickup_photos/', null=True, blank=True)
  pickedup_at = models.DateTimeField(null=True, blank=True)

  delivery_photo = models.ImageField(upload_to='job/delivery_photos/', null=True, blank=True)
  delivered_at = models.DateTimeField(null=True, blank=True)  

  def __str__(self):
    return self.description

class Transaction(models.Model):

  IN_STATUS = "in"
  OUT_STATUS = "out"
  STATUSES = (
    (IN_STATUS, 'In'),
    (OUT_STATUS, 'Out'),
  )

  stripe_payment_intent_id = models.CharField(max_length=255, unique=True)
  job = models.ForeignKey(Job, on_delete=models.CASCADE)
  amount = models.FloatField(default=0)
  status = models.CharField(max_length=20, choices=STATUSES, default=IN_STATUS)
  created_at = models.DateTimeField(default=timezone.now)

  def __str__(self):
    return self.stripe_payment_intent_id



マイグレーションファイルを作成します。
コマンド
python manage.py makemigrations


マイグレーションを適用します。
コマンド
python manage.py migrate


「crowdsource/urls.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/crowdsource/urls.py】36行目

from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from django.conf import settings
from django.conf.urls.static import static
from django.views.generic import TemplateView

from core import views

from core.customer import views as customer_views
from core.courier import views as courier_views, apis as courier_apis

customer_urlpatters = [
    path('', customer_views.home, name="home"),
    path('profile/', customer_views.profile_page, name="profile"),
    path('payment_method/', customer_views.payment_method_page, name="payment_method"),
    path('create_job/', customer_views.create_job_page, name="create_job"),

    path('jobs/current/', customer_views.current_jobs_page, name="current_jobs"),
    path('jobs/archived/', customer_views.archived_jobs_page, name="archived_jobs"),
    path('jobs/<job_id>/', customer_views.job_page, name="job"),
]

courier_urlpatters = [
    path('', courier_views.home, name="home"),
    path('jobs/available/', courier_views.available_jobs_page, name="available_jobs"),
    path('jobs/available/<id>/', courier_views.available_job_page, name="available_job"),
    path('jobs/current/', courier_views.current_job_page, name="current_job"),
    path('jobs/current/<id>/take_photo/', courier_views.current_job_take_photo_page, name="current_job_take_photo"),
    path('jobs/complete/', courier_views.job_complete_page, name="job_complete"),
    path('jobs/archived/', courier_views.archived_jobs_page, name="archived_jobs"),
    path('profile/', courier_views.profile_page, name="profile"),
    path('payout_method/', courier_views.payout_method_page, name="payout_method"),

    path('api/jobs/available/', courier_apis.available_jobs_api, name="available_jobs_api"),
    path('api/jobs/current/<id>/update/', courier_apis.current_job_update_api, name="current_job_update_api"),
    path('api/fcm-token/update/', courier_apis.fcm_token_update_api, name="fcm_token_update_api"),

]


urlpatterns = [
    path('admin/', admin.site.urls),
    path('oauth/', include('social_django.urls', namespace='social')),
    path('', views.home),

    path('sign-in/', auth_views.LoginView.as_view(template_name="sign_in.html")),
    path('sign-out/', auth_views.LogoutView.as_view(next_page="/")),
    path('sign-up/', views.sign_up),

    path('customer/', include((customer_urlpatters, 'customer'))),
    path('courier/', include((courier_urlpatters, 'courier'))),
    path('firebase-messaging-sw.js', (TemplateView.as_view(template_name="firebase-messaging-sw.js", content_type="application/javascript",))),

]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)



「core/courier/apis.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/courier/apis.py】46行目

from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone

from core.models import *

@csrf_exempt
@login_required(login_url="/courier/sign-in/")
def available_jobs_api(request):
  jobs = list(Job.objects.filter(status=Job.PROCESSING_STATUS).values())

  return JsonResponse({
    "success": True,
    "jobs": jobs
  })

@csrf_exempt
@login_required(login_url="/courier/sign-in/")
def current_job_update_api(request, id):
  job = Job.objects.filter(
    id=id,
    courier=request.user.courier,
    status__in=[
      Job.PICKING_STATUS,
      Job.DELIVERING_STATUS
    ]
  ).last()

  if job.status == Job.PICKING_STATUS:
    job.pickup_photo = request.FILES['pickup_photo']
    job.pickedup_at = timezone.now()
    job.status = Job.DELIVERING_STATUS
    job.save()

  elif job.status == Job.DELIVERING_STATUS:
    job.delivery_photo = request.FILES['delivery_photo']
    job.delivered_at = timezone.now()
    job.status = Job.COMPLETED_STATUS
    job.save()

  return JsonResponse({
    "success": True
  })

@csrf_exempt
@login_required(login_url="/courier/sign-in/")
def fcm_token_update_api(request):
  request.user.courier.fcm_token = request.GET.get('fcm_token')
  request.user.courier.save()

  return JsonResponse({
    "success": True
  })



「core/templates」フォルダに「load_firebase.html」ファイルを新規作成してください。


新規作成 【Desktop/crowdsource/core/templates/load_firebase.html】

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/8.2.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.2.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.2.1/firebase-messaging.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->

<script>
  // ご自分のFirebaseConfigに置き換えてください。
  const firebaseConfig = {
    apiKey: "AIzaSyCGdXmGoLenh7BiRl7pGyrO",
    authDomain: "clowdsource-f1701.firebaseapp.com",
    projectId: "clowdsource-f1701",
    storageBucket: "clowdsource-f1701.appspot.com",
    messagingSenderId: "64788569",
    appId: "1:647885698103:web:a8fc303ada90e"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
</script>



「core/templates/customer/profile.html」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/templates/customer/profile.html】6行目

{% extends 'customer/base.html' %}
{% load bootstrap4 %}

{% block head %}

{% include 'load_firebase.html' %}

{% endblock %}

{% block main %}
<!-- 基本情報 -->
<b class="text-secondary">基本情報</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body">
    <form method="POST" enctype="multipart/form-data">
      {% csrf_token %}
      {% bootstrap_form user_form %}
      {% bootstrap_form customer_form %}
      <input type="hidden" name="action" value="update_profile">
      <button type="submit" class="btn btn-danger">保存</button>
    </form>
  </div>
</div>

<!-- パスワード -->
<b class="text-secondary">パスワード更新</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body">
    <form method="POST" enctype="multipart/form-data">
      {% csrf_token %}
      {% bootstrap_form password_form %}
      <input type="hidden" name="action" value="update_password">
      <button type="submit" class="btn btn-danger">保存</button>
    </form>
  </div>
</div>

<!-- 電話番号 -->
<b class="text-secondary">電話番号</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body">

    <div id="recaptcha-container"></div>

    <div id="get-code" class="input-group mb-3 {% if request.user.customer.phone_number %} d-none {% endif %}">
      <input type="text" class="form-control" placeholder="+81+あなたの電話番号を入力">
      <div class="input-group-append">
        <button class="btn btn-info" type="button">PINコード生成</button>
      </div>
    </div>

    <div id="verify-code" class="input-group mb-3 d-none">
      <input type="text" class="form-control" placeholder="PINコード入力">
      <div class="input-group-append">
        <button class="btn btn-info" type="button">PINコード送信</button>
      </div>
    </div>

    <div id="change-phone" class="input-group mb-3 {% if not request.user.customer.phone_number %} d-none {% endif %}">
      <input type="text" class="form-control" disabled value="{{ request.user.customer.phone_number }}">
      <div class="input-group-append">
        <button class="btn btn-info" type="button">更新</button>
      </div>
    </div>

  </div>
</div>

<script>

  function onVerify(idToken) {
    var form = document.createElement("form");
    form.method = "POST";

    var element1 = document.createElement("input");
    element1.name = "id_token";
    element1.value = idToken;
    form.appendChild(element1);

    var element2 = document.createElement("input");
    element2.name = "action";
    element2.value = "update_phone";
    form.appendChild(element2);

    var element3 = document.createElement("input");
    element3.name = "csrfmiddlewaretoken";
    element3.value = "{{ csrf_token }}";
    form.appendChild(element3);

    document.body.appendChild(form);
    form.submit();
  }

  window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container', {
    'size': 'invisible'
  });

  $("#get-code button").on('click', function () {
    const phoneNumber = $("#get-code input").val();
    console.log(phoneNumber);

    firebase.auth().signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier)
      .then((confirmationResult) => {
        // SMS sent. Prompt user to type the code from the message, then sign the
        // user in with confirmationResult.confirm(code).
        console.log(confirmationResult);
        window.confirmationResult = confirmationResult;

        $("#get-code").addClass("d-none");
        $("#verify-code").removeClass("d-none");
      }).catch((error) => {
        // Error; SMS not sent
        toast(error.message, 'error');
      });


  });

  $("#verify-code button").on('click', function () {
    const code = $("#verify-code input").val();

    confirmationResult.confirm(code).then((result) => {
      // User signed in successfully.
      const user = result.user;
      console.log(user.phoneNumber);

      user.getIdToken().then(function (idToken) {
        onVerify(idToken);
      });
    }).catch((error) => {
      // User couldn't sign in (bad verification code?)
      toast(error.message, 'error');
    });
  });

  $("#change-phone button").on('click', function () {
    $("#change-phone").addClass("d-none");
    $("#get-code").removeClass("d-none");
  })
</script>



{% endblock %}



FireBaseの設定で「Cloud Messaging」のタブに移動します。

Cloud Messaging
Cloud Messaging



下部にある「Generate key pair」をクリックし、キーを生成します。

キーを生成
キーを生成



ここで生成されたキーペアをコピーしておいてください。


「core/templates/courier/base.html」ファイルを編集します。
18行目の「validKey:」にご自分のキーペアを入力してください。


記述編集 【Desktop/crowdsource/core/templates/courier/base.html】

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <title>配達人 | クラウドソーシングアプリ</title>
        {% load bootstrap4 %}
        {% bootstrap_css %}
        {% bootstrap_javascript jquery='full' %}

        {% include 'load_firebase.html' %}

        <script>
          const messaging = firebase.messaging();
          messaging
            .getToken({
              <!-- ご自分のkey pairを入れてください -->
              validKey: "BNKwo-bS7wQjKLaB4RkxL6Ta1lkbk-cJ7BBaw8FW-9Wim8fRY7HHkgXv9yLR8FxNvqh9qv5"
            })
            .then((currentToken) => {
              console.log(currentToken);
              if (currentToken) {
                fetch('{% url "courier:fcm_token_update_api" %}?fcm_token=' + currentToken);
              }
            })
            .catch((err) => {
              console.log('トークンの取得中にエラーが発生しました。', err);
            })
        </script>
      

        <script>
            let vh = window.innerHeight * 0.01;
            document.documentElement.style.setProperty('--vh', `${vh}px`);
        </script>

        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.9.0/css/all.css">

        <style>
            #content {
              height: calc(var(--vh, 1vh) * 100);
            }
        </style>

        {% block head %}{% endblock %}
    </head>
    <body>
        <div id="content">
            {% block content %}{% endblock %}
        </div>        

        <script src="https://unpkg.com/bootoast@1.0.1/dist/bootoast.min.js"></script>
        <link rel="stylesheet" href="https://unpkg.com/bootoast@1.0.1/dist/bootoast.min.css">
      
        <script>
          function toast(message, type) {
            bootoast.toast({
              position: 'centerBottom',
              message,
              type,
            });
          }
      
          {% if messages %}
      
          {% for message in messages %}
          toast('{{ message }}', '{{ message.tags }}');
          {% endfor %}
      
          {% endif %}
        </script>        

    </body>
</html>



「core/templates」フォルダに「firebase-messaging-sw.js」ファイルを新規作成します。
FIrebaseの情報はご自分のIDとKeyを入れてください。



新規作成 【Desktop/crowdsource/core/templates/firebase-messaging-sw.js】

importScripts("https://www.gstatic.com/firebasejs/8.2.1/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.1/firebase-messaging.js");

firebase.initializeApp({
    apiKey: "AIzaSyCGdXmGoLenh7BiRl7pGyr",
    authDomain: "clowdsource-f1701.firebaseapp.com",
    projectId: "clowdsource-f1701",
    storageBucket: "clowdsource-f1701.appspot.com",
    messagingSenderId: "647885698103",
    appId: "1:647885698103:web:a8fc303a"
});

const messaging = firebase.messaging();



ブラウザでJsonViewを開いてTokenが表示されているのを確認します。
http://localhost:8000/courier/profile/

Token確認
Token確認



管理サイトでもTokenが保存されていることを確認します。

Token保存確認
Token保存確認



通知を送信できるよう実装していきます。


https通信を可能にしするため、「crowdsource/settings.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/crowdsource/settings.py】29行目

ALLOWED_HOSTS = ['*',]



ngrokを使ってローカル開発環境で実行されているWebサーバーを、一時的にインターネット上でアクセス可能にします。
ngrok.com


ホーム右上の「Download」をクリックします。

Downloadをクリック
Downloadをクリック



「Download ZIP file」をクリックします。

Download ZIP File
Download ZIP File



ZIPファイルを解凍すると、ngrokの実行ファイルができるので、それを適当な場所に置きます。
今回はDeskTopに置くことにします。


ngrokを利用するにはWebサービス認証を済ます必要があります。
認証にはトークンをngrokのマイページから取得して設定します。
認証作業は初回のみです。


サインアップします。

サインアップ
サインアップ



認証アプリケーション「Salesforce Authenticator」をスマートフォンにダウンロードし、表示されるQRコードを読み取れば、認証コードを取得することができます。


ログインできたら、認証コードをコピーします。

認証コードをコピー
認証コードをコピー



Visual Studio Codeでターミナルの分割をします。

ターミナルの分割
ターミナルの分割



分割したターミナルでDeskTopに移動します。
コマンド
cd ~/DeskTop


コピーした認証トークンを以下のコマンドの後ろに貼り付け、実行します。
コマンド
./ngrok config add-authtoken ここにコピーした認証トークンを貼り付け

認証トークン貼り付け
認証トークン貼り付け



ngrokを起動します。
コマンド
./ngrok http 8000


表示されているhttpsのアドレスにアクセスしてみます。

https
https



無事トップページが表示されました。

トップページ
トップページ



「crowdsource/settings.py」ファイルを編集します。
ご自分のngrokのURLを貼り付けてください。


記述編集 【crowdsource/settings.py】173行目(末尾)

NOTIFICATION_URL = "https://aeb9-240d-18-85.ngrok-free.app/"



「core/customer/views.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/customer/views.py】

import requests
import firebase_admin
from firebase_admin import credentials, auth, messaging
import stripe

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from core.customer import forms

from django.contrib import messages
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.conf import settings

from core.models import *

cred = credentials.Certificate(settings.FIREBASE_ADMIN_CREDENTIAL)
firebase_admin.initialize_app(cred)

stripe.api_key = settings.STRIPE_API_SECRET_KEY

@login_required()
def home(request):
    return redirect(reverse('customer:profile'))

@login_required(login_url="/sign-in/?next=/customer/")
def profile_page(request):
    user_form = forms.BasicUserForm(instance=request.user)
    customer_form = forms.BasicCustomerForm(instance=request.user.customer)
    password_form = PasswordChangeForm(request.user)

    if request.method == "POST":
 
        if request.POST.get('action') == 'update_profile':
            user_form = forms.BasicUserForm(request.POST, instance=request.user)
            customer_form = forms.BasicCustomerForm(request.POST, request.FILES, instance=request.user.customer)

            if user_form.is_valid() and customer_form.is_valid():
                user_form.save()
                customer_form.save()

                messages.success(request, 'プロフィールが更新されました。')
                return redirect(reverse('customer:profile'))

        elif request.POST.get('action') == 'update_password':
            password_form = PasswordChangeForm(request.user, request.POST)
            if password_form.is_valid():
                user = password_form.save()
                update_session_auth_hash(request, user)

                messages.success(request, 'パスワードが更新されました。')
                return redirect(reverse('customer:profile'))

        elif request.POST.get('action') == 'update_phone':
            # Get Firebase user data
            firebase_user = auth.verify_id_token(request.POST.get('id_token'))

            request.user.customer.phone_number = firebase_user['phone_number']
            request.user.customer.save()
            messages.success(request, '電話番号が更新されました。')
            return redirect(reverse('customer:profile'))

    return render(request, 'customer/profile.html', {
        "user_form": user_form,
        "customer_form": customer_form,
        "password_form": password_form,
    })

@login_required(login_url="/sign-in/?next=/customer/")
def payment_method_page(request):
    current_customer = request.user.customer

    # Remove existing card
    if request.method == "POST":
        stripe.PaymentMethod.detach(current_customer.stripe_payment_method_id)
        current_customer.stripe_payment_method_id = ""
        current_customer.stripe_card_last4 = ""
        current_customer.save()
        return redirect(reverse('customer:payment_method'))

    # Save stripe customer infor
    if not current_customer.stripe_customer_id:
        customer = stripe.Customer.create()
        current_customer.stripe_customer_id = customer['id']
        current_customer.save()    

    # Get Stripe payment method
    stripe_payment_methods = stripe.PaymentMethod.list(
        customer = current_customer.stripe_customer_id,
        type = "card",
    )

    print(stripe_payment_methods)

    if stripe_payment_methods and len(stripe_payment_methods.data) > 0:
        payment_method = stripe_payment_methods.data[0]
        current_customer.stripe_payment_method_id = payment_method.id
        current_customer.stripe_card_last4 = payment_method.card.last4
        current_customer.save()
    else:
        current_customer.stripe_payment_method_id = ""
        current_customer.stripe_card_last4 = ""
        current_customer.save()


    if not current_customer.stripe_payment_method_id:
        intent = stripe.SetupIntent.create(
            customer = current_customer.stripe_customer_id
        )

        return render(request, 'customer/payment_method.html', {
            "client_secret": intent.client_secret,
            "STRIPE_API_PUBLIC_KEY": settings.STRIPE_API_PUBLIC_KEY,
        })
    else:
        return render(request, 'customer/payment_method.html')

@login_required(login_url="/sign-in/?next=/customer/")
def create_job_page(request):
    current_customer = request.user.customer

    if not current_customer.stripe_payment_method_id:
        return redirect(reverse('customer:payment_method'))

    has_current_job = Job.objects.filter(
        customer = current_customer,
        status__in = [
            Job.PROCESSING_STATUS,
            Job.PICKING_STATUS,
            Job.DELIVERING_STATUS
        ]
    ).exists()

    if has_current_job:
        messages.warning(request, "現在依頼中の仕事があります")
        return redirect(reverse('customer:current_jobs'))


    creating_job = Job.objects.filter(customer=current_customer, status=Job.CREATING_STATUS).last()
    step1_form = forms.JobCreateStep1Form(instance=creating_job)
    step2_form = forms.JobCreateStep2Form(instance=creating_job)
    step3_form = forms.JobCreateStep3Form(instance=creating_job)

    if request.method == "POST":
        if request.POST.get('step') == '1':
            step1_form = forms.JobCreateStep1Form(request.POST, request.FILES)
            if step1_form.is_valid():
                creating_job = step1_form.save(commit=False)
                creating_job.customer = current_customer
                creating_job.save()
                return redirect(reverse('customer:create_job'))

        elif request.POST.get('step') == '2':
            step2_form = forms.JobCreateStep2Form(request.POST, instance=creating_job)
            if step2_form.is_valid():
                creating_job = step2_form.save()
                return redirect(reverse('customer:create_job'))

        elif request.POST.get('step') == '3':
            step3_form = forms.JobCreateStep3Form(request.POST, instance=creating_job)
            if step3_form.is_valid():
                creating_job = step3_form.save()

                try:
                    r = requests.get("https://maps.googleapis.com/maps/api/distancematrix/json?origins={}&destinations={}&language=ja&mode=driving&region=ja&key={}".format(
                        creating_job.pickup_address,
                        creating_job.delivery_address,
                        settings.GOOGLE_MAP_API_KEY,
                    ))

                    print(r.json()['rows'])

                    distance = r.json()['rows'][0]['elements'][0]['distance']['value']
                    duration = r.json()['rows'][0]['elements'][0]['duration']['value']
                    creating_job.distance = round(distance / 1000, 2) #距離
                    creating_job.duration = int(duration / 60) #移動時間
                    creating_job.price = int(creating_job.distance * 35) # kmあたり35円
                    creating_job.save()                    

                except Exception as e:
                    print(e)
                    messages.error(request, "残念ながら、この距離での配送はサポートされていません。")                

        elif request.POST.get('step') == '4':
            if creating_job.price:
                try:
                    payment_intent = stripe.PaymentIntent.create(
                        amount=int(creating_job.price),
                        currency='jpy',
                        customer=current_customer.stripe_customer_id,
                        payment_method=current_customer.stripe_payment_method_id,
                        off_session=True,
                        confirm=True,
                    )

                    Transaction.objects.create(
                        stripe_payment_intent_id = payment_intent['id'],
                        job = creating_job,
                        amount = creating_job.price,
                    )

                    creating_job.status = Job.PROCESSING_STATUS
                    creating_job.save()

                    # すべての配達業者にプッシュ通知を送信する
                    couriers = Courier.objects.all()
                    registration_tokens = [i.fcm_token for i in couriers if i.fcm_token]

                    message = messaging.MulticastMessage(
                        notification = messaging.Notification(
                            title = creating_job.name,
                            body = creating_job.description,
                        ),
                        webpush = messaging.WebpushConfig(
                            notification = messaging.WebpushNotification(
                                icon = creating_job.photo.url,
                            ),
                            fcm_options = messaging.WebpushFCMOptions(
                                link = settings.NOTIFICATION_URL + reverse('courier:available_jobs'),
                            ),
                        ),
                        tokens = registration_tokens 
                    )
                    response = messaging.send_multicast(message)
                    print('{0}個のメッセージが送信されました。'.format(response.success_count))                    

                    return redirect(reverse('customer:home'))

                except stripe.error.CardError as e:
                    err = e.error
                    # Error code will be authentication_required if authentication is needed
                    print("Code is: %s" % err.code)
                    payment_intent_id = err.payment_intent['id']
                    payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)

    # 現在のステップ
    if not creating_job:
        current_step = 1
    elif creating_job.delivery_name:
        current_step = 4
    elif creating_job.pickup_name:
        current_step = 3        
    else:
        current_step = 2
    
    return render(request, 'customer/create_job.html', {
        "job": creating_job,
        "step": current_step,
        "step1_form": step1_form,
        "step2_form": step2_form,
        "step3_form": step3_form,
        "GOOGLE_MAP_API_KEY": settings.GOOGLE_MAP_API_KEY,
    })

@login_required(login_url="/sign-in/?next=/customer/")
def current_jobs_page(request):
    jobs = Job.objects.filter(
        customer=request.user.customer,
        status__in=[
            Job.PROCESSING_STATUS,
            Job.PICKING_STATUS,
            Job.DELIVERING_STATUS
        ]
    )

    return render(request, 'customer/jobs.html', {
        "jobs": jobs
    })

@login_required(login_url="/sign-in/?next=/customer/")
def archived_jobs_page(request):
    jobs = Job.objects.filter(
        customer=request.user.customer,
        status__in=[
            Job.COMPLETED_STATUS,
            Job.CANCELED_STATUS
        ]
    )

    return render(request, 'customer/jobs.html', {
        "jobs": jobs
    })

@login_required(login_url="/sign-in/?next=/customer/")
def job_page(request, job_id):
    job = Job.objects.get(id=job_id)

    if request.method == "POST" and job.status == Job.PROCESSING_STATUS:
        job.status = Job.CANCELED_STATUS
        job.save()
        return redirect(reverse('customer:archived_jobs'))

    return render(request, 'customer/job.html', {
        "job": job,
        "GOOGLE_MAP_API_KEY": settings.GOOGLE_MAP_API_KEY,
    
    })



Chromeの通知設定をシステム設定で許可にしておきます。

システム設定→通知→Chromeを許可
システム設定→通知→Chromeを許可



「core/templates/courier/available_jobs.html」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/templates/courier/available_jobs.html】68行目

{% extends 'courier/base.html' %}

{% block head %}
<script
  src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap&libraries=places&v=weekly"
  defer></script>

<script>
  function initMap() {
  const map = new google.maps.Map(document.getElementById("map"), {
    zoom: 13,
    center: { lat: 43.062087, lng: 141.354404 },
    });

    // Get available jobs via API
    fetch("{% url 'courier:available_jobs_api' %}")
      .then(response => response.json())
      .then(json => {
        // console.log(json);

        // Create a new viewpoint bound
        var bounds = new google.maps.LatLngBounds();

        for (let i = 0; i < json.jobs.length; i++) {
            const job = json.jobs[i];
            const position = { lat: job.pickup_lat, lng: job.pickup_lng };
            const marker = new google.maps.Marker({
                position,
                map,
            });

            // Increase the bounds to take this point
            bounds.extend(position);

            new google.maps.InfoWindow({
                content: "<small><b>" + job.name + "</b></small><br/><small>" + job.distance + " Km</small>"
            }).open(map, marker);

            // Click event for each job
            marker.addListener("click", () => {
                showJobDetails(job);
            });

            // Fit these bounds to the map
            map.fitBounds(bounds);

        }
      })
}

function showJobDetails(job) {
    $("#job-details").css("display", "block");
    $("#job-name").html(job.name);

    $("#job-photo").attr('src', "/media/" + job.photo);
    $("#pickup-address").html(job.pickup_address);
    $("#delivery-address").html(job.delivery_address);
    $("#duration").html(job.duration);
    $("#distance").html(job.distance);
    $("#price").html(job.price);

    $("#job-details").on("click", function () {
      window.location.href = "/courier/jobs/available/" + job.id + "/";
    })    

  }

  messaging.onMessage((payload) => {
    window.location.reload();
  })
    
</script>

<style>
    .gm-ui-hover-effect {
        display: none !important;
    }    

    #map {
      flex: 1;
    }

    small {
        font-size: 12px;
        line-height: 1.2rem;
    }

    .card {
        border: none;
    }

    #job-details {
        display: none;
    }    

  </style>

{% endblock %}

{% block content %}

<div class="d-flex flex-column h-100" style="padding-bottom: 60px">
    <div id="map"></div>  

    <div id="job-details" class="card">
        <div class="card-body p-2">
          <div class="media">
            <img id="job-photo" class="rounded-lg mr-3" width="50px" height="50px">
            <div class="media-body">
              <b id="job-name"></b>
              <div class="d-flex">
                <div class="flex-grow-1 mr-2">
    
                  <small class="text-success">
                    <i class="fas fa-car"></i> <span id="distance"></span> km
                    <i class="far fa-clock ml-2"></i> <span id="duration"></span></small>
    
                  <div class="d-flex align-items-center mt-2">
                    <i class="fas fa-map-marker-alt"></i>
                    <small id="pickup-address" class="text-secondary ml-2"></small>
                  </div>
    
                  <div class="d-flex align-items-center mt-2">
                    <i class="fas fa-flag-checkered"></i>
                    <small id="delivery-address" class="text-secondary ml-2"></small>
                  </div>
    
                </div>
                <h3 id="price"></h3></div>
            </div>
          </div>
        </div>
    </div>    
</div>



{% include 'courier/bottom_tabs.html' %}

{% endblock %}



動作を確認します。
動作確認は、ngrokのURLで行います。
配送依頼を作成すると通知が出て、自動で仕事一覧ページが更新されるのを確認します。

仕事を始める
仕事を始める


通知
通知


画面自動更新
画面自動更新


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


52 | 報酬支払いの実装】 << 【ホーム】 >> 【54 | Web Socketの設定