↓↓クリックして頂けると励みになります。
【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」のタブに移動します。
下部にある「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が保存されていることを確認します。
通知を送信できるよう実装していきます。
https通信を可能にしするため、「crowdsource/settings.py」ファイルを編集します。
記述編集 【Desktop/crowdsource/crowdsource/settings.py】29行目
ALLOWED_HOSTS = ['*',]
ngrokを使ってローカル開発環境で実行されているWebサーバーを、一時的にインターネット上でアクセス可能にします。
ngrok.com
ホーム右上の「Download」をクリックします。
「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のアドレスにアクセスしてみます。
無事トップページが表示されました。
「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®ion=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の通知設定をシステム設定で許可にしておきます。
「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の設定】