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

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

Django3.2 | クラウドソーシングアプリの構築 | 34 | 配達依頼詳細ページ

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


33 | 配達依頼確認ページ】 << 【ホーム】 >> 【35 | 配達人登録モデル


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


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

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 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='配達依頼人')
  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):

  stripe_payment_intent_id = models.CharField(max_length=255, unique=True)
  job = models.ForeignKey(Job, on_delete=models.CASCADE)
  amount = models.FloatField(default=0)
  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」20行目ファイルを編集します。


記述追加 【Desktop/crowdsource/crowdsource/urls.py】

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 core import views

from core.customer import views as customer_views
from core.courier import views as courier_views

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"),
]


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'))),

]

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



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


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

import requests
import firebase_admin
from firebase_admin import credentials, auth
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()

                    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)

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



「core/templates/customer」フォルダに「job.html」ファイルを新規作成します。


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

{% extends 'customer/base.html' %}
{% block main %}

{{ job.name }}

{% endblock %}



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


記述編集 【Desktop/crowdsource/core/templates/customer/jobs.html】

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

{% block main %}

{% url 'customer:current_jobs' as current_jobs_url %}
{% url 'customer:archived_jobs' as archived_jobs_url %}

<style>
.nav-pills .nav-link.active{
  background-color: rgb(27, 73, 66);
}

/* タブの文字色変更 */
.nav-pills .nav-link{
  color: rgb(27, 73, 66);
}
</style>

<!-- タブ -->
<ul class="nav nav-pills nav-fill">
  <li class="nav-item">
    <a class="nav-link {% if request.path == current_jobs_url %}active{% endif %}"
      href="{% url 'customer:current_jobs' %}">依頼中の配達</a>
  </li>
  <li class="nav-item">
    <a class="nav-link {% if request.path == archived_jobs_url %}active{% endif %}"
      href="{% url 'customer:archived_jobs' %}">終了した配達依頼</a>
  </li>
</ul>

<!-- 仕事リスト -->
<div class="list-group mt-3">
  {% if jobs %}

  {% for job in jobs %}
  <a href="{% url 'customer:job' job.id %}" class="list-group-item list-group-item-action">
    <div class="media">
      <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="100" height="100">
      <div class="media-body">
        <div class="row">

          <div class="col-lg-9">
            <h5 class="mb-1">{{ job.name }}</h5>
            <p>{{ job.description }}</p>

            <div class="d-flex mb-4">
              <i class="fas fa-map-marker-alt"></i>
              <div class="ml-2">
                <b>{{ job.pickup_name }}</b><br />
                <small>{{ job.pickup_address }}</small>
              </div>
            </div>
            <div class="d-flex">
              <i class="fas fa-flag-checkered"></i>
              <div class="ml-2">
                <b>{{ job.delivery_name }}</b><br />
                <small>{{ job.delivery_address }}</small>
              </div>
            </div>
          </div>

          <div class="col-lg-3 text-right d-flex flex-column justify-content-between">
            <div>
                <span class="badge badge-warning mb-2">
                    {% if job.get_status_display == 'Processing' %}
                        進行中 {{ job.get_status_display }}
                    {% elif job.get_status_display == 'Canceled' %}
                        キャンセル {{ job.get_status_display }}
                    {% else %}
                        完了 {{ job.get_status_display }}
                    {% endif %}
                    
                </span>
            </div>
            <h2 class="mb-1">{{ job.price }}円</h2>
          </div>

        </div>
      </div>
    </div>
  </a>

  {% endfor %}

  {% else %}
  <div class="card">
    <div class="card-body text-center">
      表示できる配達依頼がありません
    </div>
  </div>
  {% endif %}
</div>

{% endblock %}



クリックすると詳細ページが表示されるようになりました。

詳細ページ表示
詳細ページ表示



仕事詳細ページのレイアウトを更新します。


記述更新 【Desktop/crowdsource/core/templates/customer/job.html】

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

{% block head %}

<style>
  .photo {
    object-fit: cover;
  }

  .photo-blank {
    border: 2px dashed #DFDFDF;
    height: 130px;
    width: 130px;
    border-radius: 5px;
    align-items: center;
    display: flex;
    justify-content: center;
    text-align: center;
    padding: 10px;
  }
</style>

{% endblock %}

{% block main %}

<!-- JOB DESCRIPTION -->
<div class="media mb-4">
  <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="150" height="150">
  <div class="media-body">

    <h4>{{ job.name }}</h4>
    <p class="text-secondary">{{ job.description }}</p>

    <div class="row">
      <div class="col-lg-3">
        <small class="text-secondary">カテゴリー</small><br />
        <span><b>{{ job.category.name }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">サイズ</small><br />
        <span><b>{{ job.get_size_display }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">料金</small><br />
        <span><b>{{ job.price }}円</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">数量</small><br />
        <span><b>{{ job.quantity }}</b></span>
      </div>
    </div>
  </div>
</div>

<!-- DELIVERY INFORMATION -->
<b class="text-secondary">配達依頼情報</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body p-4">

    <h4 class="mb-3">
      荷物受取先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>荷物受取先住所</b><br />
        <span>{{ job.pickup_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.pickup_name }}</b><br />
        <span>{{ job.pickup_phone }}</span>
      </div>
      <div id="pickup_photo" class="col-lg-4">
        {% if job.pickup_photo %}
        <img src="{{ job.pickup_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>

    </div>

    <hr class="my-4" />

    <h4 class="mb-3">
      配達先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>配達先住所</b><br />
        <span>{{ job.delivery_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.delivery_name }}</b><br />
        <span>{{ job.delivery_phone }}</span>
      </div>
      <div id="delivery_photo" class="col-lg-4">
        {% if job.delivery_photo %}
        <img src="{{ job.delivery_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>

    </div>

  </div>
</div>


{% endblock %}



配達依頼詳細ページを確認します。

配達依頼詳細ページ確認
配達依頼詳細ページ確認



Googleマップを表示させます。


記述更新 【Desktop/crowdsource/core/templates/customer/job.html】

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

{% block head %}

<style>
  .photo {
    object-fit: cover;
  }

  .photo-blank {
    border: 2px dashed #DFDFDF;
    height: 130px;
    width: 130px;
    border-radius: 5px;
    align-items: center;
    display: flex;
    justify-content: center;
    text-align: center;
    padding: 10px;
  }
</style>

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

<script>
  var pickupLat = parseFloat(" {{ job.pickup_lat }} ");
  var pickupLng = parseFloat(" {{ job.pickup_lng }} ");
  var deliveryLat = parseFloat(" {{ job.delivery_lat }} ");
  var deliveryLng = parseFloat(" {{ job.delivery_lng }} ");

  function initMap() {
  const directionsService = new google.maps.DirectionsService();
  const directionsRenderer = new google.maps.DirectionsRenderer();
  const map = new google.maps.Map(document.getElementById("map"), {
    zoom: 7,
    center: { lat: 43.062087, lng: 141.354404 },
  });

  directionsRenderer.setMap(map);

  calculateAndDisplayRoute(map, directionsService, directionsRenderer);

}

function calculateAndDisplayRoute(map, directionsService, directionsRenderer) {
  directionsService
    .route({
        origin: new google.maps.LatLng(pickupLat, pickupLng),
        destination: new google.maps.LatLng(deliveryLat, deliveryLng),
        travelMode: google.maps.TravelMode.DRIVING,
    },)
    .then((response) => {

        new google.maps.DirectionsRenderer({
            map: map,
            directions: response,
            suppressMarkers: true,
            polylineOptions: {
              strokeColor: "red",
              strokeWeight: 5,
              strokeOpacity: 0.8
            }
          });

          var leg = response.routes[0].legs[0];
          new google.maps.Marker({
            position: leg.start_location,
            map: map,
            icon: "{% static 'img/start.png' %}"
          });

          new google.maps.Marker({
            position: leg.end_location,
            map: map,
            icon: "{% static 'img/end.png' %}"
          });


    })
    .catch((e) => window.alert("Directions request failed due to " + status));
}

window.initMap = initMap;

</script>

{% endblock %}

{% block main %}

<!-- JOB DESCRIPTION -->
<div class="media mb-4">
  <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="150" height="150">
  <div class="media-body">

    <h4>{{ job.name }}</h4>
    <p class="text-secondary">{{ job.description }}</p>

    <div class="row">
      <div class="col-lg-3">
        <small class="text-secondary">カテゴリー</small><br />
        <span><b>{{ job.category.name }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">サイズ</small><br />
        <span><b>{{ job.get_size_display }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">料金</small><br />
        <span><b>{{ job.price }}円</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">数量</small><br />
        <span><b>{{ job.quantity }}</b></span>
      </div>
    </div>
  </div>
</div>

<!-- DELIVERY INFORMATION -->
<b class="text-secondary">配達依頼情報</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body p-4">

    <h4 class="mb-3">
      荷物受取先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>荷物受取先住所</b><br />
        <span>{{ job.pickup_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.pickup_name }}</b><br />
        <span>{{ job.pickup_phone }}</span>
      </div>
      <div id="pickup_photo" class="col-lg-4">
        {% if job.pickup_photo %}
        <img src="{{ job.pickup_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>

    </div>

    <hr class="my-4" />

    <h4 class="mb-3">
      配達先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>配達先住所</b><br />
        <span>{{ job.delivery_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.delivery_name }}</b><br />
        <span>{{ job.delivery_phone }}</span>
      </div>
      <div id="delivery_photo" class="col-lg-4">
        {% if job.delivery_photo %}
        <img src="{{ job.delivery_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>

    </div>

  </div>
</div>

<!-- マップ -->
<div class="d-flex justify-content-between">
    <b class="text-secondary">配達の追跡</b>
    <div>
        <span id="job_status" class="badge badge-warning">
            {% if job.get_status_display == 'Processing' %}
            進行中 {{ job.get_status_display }}
        {% else %}
            完了 {{ job.get_status_display }}
        {% endif %}
    
        </span>
    </div>
  </div>
  
  <div class="card bg-white mt-2">
    <div class="card-body p-0">
      <div id="map" style="height: 500px;"></div>
    </div>
  </div>

{% endblock %}



「core/customer/views.py」ファイルに記述を追加します。


記述追加 【Desktop/crowdsource/core/customer/views.py】269行目

import requests
import firebase_admin
from firebase_admin import credentials, auth
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()

                    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)

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



「core/static/img」フォルダに何でもいいので「start.png」「end.png」の2つを保存しておいてください。


ルートが表示されるようになりました。

ルート表示
ルート表示



配達依頼のキャンセルボタンを実装します。


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


記述編集 【Desktop/crowdsource/core/templates/customer/job.html】

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

{% block head %}

<style>
  .photo {
    object-fit: cover;
  }

  .photo-blank {
    border: 2px dashed #DFDFDF;
    height: 130px;
    width: 130px;
    border-radius: 5px;
    align-items: center;
    display: flex;
    justify-content: center;
    text-align: center;
    padding: 10px;
  }
</style>

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

<script>
  var pickupLat = parseFloat(" {{ job.pickup_lat }} ");
  var pickupLng = parseFloat(" {{ job.pickup_lng }} ");
  var deliveryLat = parseFloat(" {{ job.delivery_lat }} ");
  var deliveryLng = parseFloat(" {{ job.delivery_lng }} ");

  function initMap() {
  const directionsService = new google.maps.DirectionsService();
  const directionsRenderer = new google.maps.DirectionsRenderer();
  const map = new google.maps.Map(document.getElementById("map"), {
    zoom: 7,
    center: { lat: 43.062087, lng: 141.354404 },
  });

  directionsRenderer.setMap(map);

  calculateAndDisplayRoute(map, directionsService, directionsRenderer);

}

function calculateAndDisplayRoute(map, directionsService, directionsRenderer) {
  directionsService
    .route({
        origin: new google.maps.LatLng(pickupLat, pickupLng),
        destination: new google.maps.LatLng(deliveryLat, deliveryLng),
        travelMode: google.maps.TravelMode.DRIVING,
    },)
    .then((response) => {

        new google.maps.DirectionsRenderer({
            map: map,
            directions: response,
            suppressMarkers: true,
            polylineOptions: {
              strokeColor: "red",
              strokeWeight: 5,
              strokeOpacity: 0.8
            }
          });

          var leg = response.routes[0].legs[0];
          new google.maps.Marker({
            position: leg.start_location,
            map: map,
            icon: "{% static 'img/start.png' %}"
          });

          new google.maps.Marker({
            position: leg.end_location,
            map: map,
            icon: "{% static 'img/end.png' %}"
          });


    })
    .catch((e) => window.alert("Directions request failed due to " + status));
}

window.initMap = initMap;

</script>

{% endblock %}

{% block main %}

<!-- JOB DESCRIPTION -->
<div class="media mb-4">
  <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="150" height="150">
  <div class="media-body">

    {% if job.status == 'processing' %}
    <form method="POST" class="float-right">
      {% csrf_token %}
      <button type="submit" class="btn btn-warning">配達依頼をキャンセル</button>
    </form>
    {% endif %}

    <h4>{{ job.name }}</h4>
    <p class="text-secondary">{{ job.description }}</p>

    <div class="row">
      <div class="col-lg-3">
        <small class="text-secondary">カテゴリー</small><br />
        <span><b>{{ job.category.name }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">サイズ</small><br />
        <span><b>{{ job.get_size_display }}</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">料金</small><br />
        <span><b>{{ job.price }}円</b></span>
      </div>
      <div class="col-lg-3">
        <small class="text-secondary">数量</small><br />
        <span><b>{{ job.quantity }}</b></span>
      </div>
    </div>
  </div>
</div>

<!-- DELIVERY INFORMATION -->
<b class="text-secondary">配達依頼情報</b><br />
<div class="card bg-white mt-2 mb-5">
  <div class="card-body p-4">

    <h4 class="mb-3">
      荷物受取先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>荷物受取先住所</b><br />
        <span>{{ job.pickup_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.pickup_name }}</b><br />
        <span>{{ job.pickup_phone }}</span>
      </div>
      <div id="pickup_photo" class="col-lg-4">
        {% if job.pickup_photo %}
        <img src="{{ job.pickup_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>

    </div>

    <hr class="my-4" />

    <h4 class="mb-3">
      配達先
    </h4>
    <div class="row">
      <div class="col-lg-4">
        <b>配達先住所</b><br />
        <span>{{ job.delivery_address }}</span>
      </div>
      <div class="col-lg-4">
        <b>{{ job.delivery_name }}</b><br />
        <span>{{ job.delivery_phone }}</span>
      </div>
      <div id="delivery_photo" class="col-lg-4">
        {% if job.delivery_photo %}
        <img src="{{ job.delivery_photo.url }}" class="rounded-lg photo" width="130" height="130">
        {% else %}
        <div class="photo-blank">写真があればここに表示されます。</div>
        {% endif %}
      </div>

    </div>

  </div>
</div>

<!-- マップ -->
<div class="d-flex justify-content-between">
    <b class="text-secondary">配達の追跡</b>
    <div>
        <span id="job_status" class="badge badge-warning">
            {% if job.get_status_display == 'Processing' %}
            進行中 {{ job.get_status_display }}
        {% else %}
            完了 {{ job.get_status_display }}
        {% endif %}
    
        </span>
    </div>
  </div>
  
  <div class="card bg-white mt-2">
    <div class="card-body p-0">
      <div id="map" style="height: 500px;"></div>
    </div>
  </div>

{% endblock %}



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


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

import requests
import firebase_admin
from firebase_admin import credentials, auth
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()

                    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,
    
    })



これで配達依頼をキャンセルすることができるようになりました。

キャンセルボタン
キャンセルボタン


終了した配達依頼
終了した配達依頼


ステータス確認
ステータス確認


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


33 | 配達依頼確認ページ】 << 【ホーム】 >> 【35 | 配達人登録モデル