↓↓クリックして頂けると励みになります。
【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®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() 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®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() 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®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() 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 | 配達人登録モデル】