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

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

Django3.2 | クラウドソーシングアプリの構築 | 49 | 支払いページ

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


48 | 配達人プロフィールページ】 << 【ホーム】 >> 【50 | 取引の更新


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


記述追加 【Desktop/crowdsource/core/models.py】22行目

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

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

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

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

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


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

  def __str__(self):
    return self.name

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

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

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

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

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

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

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

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

  def __str__(self):
    return self.description

class Transaction(models.Model):

  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」ファイルを編集します。


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

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, apis as courier_apis

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

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

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

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

]


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/courier」フォルダに「forms.py」ファイルを新規作成します。


新規作成 【Desktop/crowdsource/core/courier/forms.py】

from django import forms
from core.models import Courier

class PayoutForm(forms.ModelForm):
  class Meta:
    model = Courier
    fields = ('paypal_email',)



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


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

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.conf import settings
from django.contrib import messages

from core.models import *
from core.courier import forms

@login_required(login_url="/sign-in/?next=/courier/")
def home(request):
    return redirect(reverse('courier:available_jobs'))

@login_required(login_url="/sign-in/?next=/courier/")
def available_jobs_page(request):
    return render(request, 'courier/available_jobs.html', {
        "GOOGLE_MAP_API_KEY": settings.GOOGLE_MAP_API_KEY
    })

@login_required(login_url="/sign-in/?next=/courier/")
def available_job_page(request, id):
 
    job = Job.objects.filter(id=id, status=Job.PROCESSING_STATUS).last()

    if not job:
        return redirect(reverse('courier:available_jobs'))

    if request.method == 'POST':
        job.courier = request.user.courier
        job.status = Job.PICKING_STATUS
        job.save()

        return redirect(reverse('courier:available_jobs'))    

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

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

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

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

    if not job:
        return redirect(reverse('courier:current_job'))

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

@login_required(login_url="/sign-in/?next=/courier/")
def job_complete_page(request):
    return render(request, 'courier/job_complete.html')

@login_required(login_url="/sign-in/?next=/courier/")
def archived_jobs_page(request):
    jobs = Job.objects.filter(
        courier=request.user.courier,
        status=Job.COMPLETED_STATUS
    )

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

@login_required(login_url="/sign-in/?next=/courier/")
def profile_page(request):
    jobs = Job.objects.filter(
        courier=request.user.courier,
        status=Job.COMPLETED_STATUS
    )

    total_earnings = round(sum(job.price for job in jobs) * 0.8) #配送業者の収入は総額の8割
    total_jobs = len(jobs)
    total_km = sum(job.distance for job in jobs)

    return render(request, 'courier/profile.html', {
        "total_earnings": total_earnings,
        "total_jobs": total_jobs,
        "total_km": total_km
    })

@login_required(login_url="/sign-in/?next=/courier/")
def payout_method_page(request):
    payout_form = forms.PayoutForm(instance=request.user.courier)

    if request.method == 'POST':
        payout_form = forms.PayoutForm(request.POST, instance=request.user.courier)
        if payout_form.is_valid():
            payout_form.save()

            messages.success(request, "支払い用 Eメールアドレスを更新しました。")
            return redirect(reverse('courier:profile'))

    return render(request, 'courier/payout_method.html', {
        'payout_form': payout_form
    })



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


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

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

{% block head %}
<style>
    .header {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      height: 60px;
      display: flex;
      align-items: center;
      padding: 0 20px;
      background-color: #6f00be;
      box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
    }
  </style>
{% endblock %}

{% block content %}
<div class="header">
    <a href="{% url 'courier:profile' %}" class="mr-2">
      <i class="fas fa-chevron-left text-light"></i>
    </a>
    <h5 class="mt-1 mb-0 text-light">プロフィール</h5>
  </div>

<div class="container-fluid" style="padding-top: 80px">
  <form method="POST">
    {% csrf_token %}
    {% bootstrap_form payout_form %}
    <button type="submit" class="btn btn-block btn-danger">保存する</button>
  </form>
</div>

{% endblock %}



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


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

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

{% block content %}

<div class="media align-items-center bg-info text-light p-3">
  <img src="{% static 'img/avatar.png' %}" class="rounded-circle" width="60" height="60">
  <div class="media-body ml-4">
    <h4 class="mb-0">{{ request.user.get_full_name }}</h4>
  </div>
</div>

<div class="mt-4 p-2 mb-5">
    <b class="text-secondary">お支払い</b>
    <a href="{% url 'courier:payout_method' %}" class="btn btn-outline-secondary btn-block btn-md mt-2">
      登録する
    </a>
  </div>

<div class="mt-2 p-2">
  <b class="text-secondary">配達記録</b>
  <hr />

  <div class="d-flex text-center">
    <div class="flex-grow-1">
      <h4 class="text-success">{{ total_earnings }}円</h4>
      <span class="text-secondary">総収入</span>
    </div>
    <div class="flex-grow-1">
      <h4 class="text-success">{{ total_jobs }} 件</h4>
      <span class="text-secondary">配達完了</span>
    </div>
    <div class="flex-grow-1">
      <h4 class="text-danger">{{ total_km }} Km</h4>
      <span class="text-secondary">総配達距離</span>
    </div>
  </div>
  <hr />
</div>

<div class="p-2">
  <a href="/sign-out/" class="btn btn-block btn-danger btn-md mt-2">
    <i class="fas fa-sign-out-alt mr-1"></i>
    ログアウト
  </a>
</div>

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

{% endblock %}



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


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

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

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

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

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

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

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

    </body>
</html>



ブラウザを確認します。
支払い用メールアドレスを登録できるようになりました。

メールアドレス登録
メールアドレス登録
メールアドレス保存
メールアドレス保存


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


48 | 配達人プロフィールページ】 << 【ホーム】 >> 【50 | 取引の更新