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

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

Django3.2 | クラウドソーシングアプリの構築 | 58 | PWA

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


57 | 配達依頼を受けた時の連動】 << 【ホーム】 >> 【59 | Heroku デプロイ


PWA(Progressive Web App)は、ウェブアプリケーションの一種であり、モバイルデバイスやデスクトップコンピューターで動作するための新しいアプローチです。
PWAは、モバイルアプリケーションのような機能をウェブアプリケーションに取り入れることができます。


何でもいいので、「logo.png」ファイルを「core/static/img」フォルダに入れておいてください。


「core/static」フォルダに「manifest.json」ファイルを新規作成します。


新規作成 【Desktop/crowdsource/core/static/manifest.json】

{
    "short_name": "Crowd Source",
    "name": "ようこそ! クラウドソーシングアプリへ",
    "description": "商品を低価格でお届けします",
    "icons": [
      {
        "src": "/static/img/logo.png",
        "type": "image/png",
        "sizes": "512x512"
      }
    ],
    "start_url": "/courier/",
    "display": "standalone"
  }
  



「core/templates/base.html」を編集します。


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

{% load static %}

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <title>クラウドソーシングアプリ</title>
        <link rel="shortcut icon" href="{% static 'img/logo.png' %}">
        
        {% load bootstrap4 %}
        {% bootstrap_css %}
        {% bootstrap_javascript jquery='full' %}

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

        {% block head %}{% endblock %}
    </head>
    <body>
        <nav class="navbar {% if not request.user.is_authenticated %} navbar-expand-lg {% endif %} navbar-dark bg-dark">
            <a class="navbar-brand" href="/">クラウドソーシングアプリ</a>
            {% if not request.user.is_authenticated %}
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
          
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
              <ul class="navbar-nav">
                <li class="nav-item mr-2 ml-4 {% if request.GET.next != '/courier/' %} active {% endif %}">
                 <a class="nav-link btn btn-light {% if request.GET.next != '/courier/' %}bg-success{% else %}bg-dark{% endif %}" href="/sign-in/?next=/customer/">依頼人</a>
                </li>
                <li class="nav-item {% if request.GET.next == '/courier/' %} active {% endif %}">
                  <a class="nav-link btn btn-light {% if request.GET.next == '/courier/' %}bg-primary{% else %}bg-dark{% endif %}" href="/sign-in/?next=/courier/">配達人</a>
                </li>
              </ul>
            </div>
            {% else %}
            <form class="form-inline">
              <span class="mr-4 text-light">
                {{ request.user.get_full_name | title }}
              </span>
              <span>
                <a href="/sign-out" class="btn btn-outline-dark bg-light">ログアウト</a>
              </span>
            </form>
            {% endif %}
        </nav>
        {% block content %}{% endblock %}
          
          
        <footer class="text-center mt-5 mb-5">
            &copy; クラウドソーシング
        </footer>
        <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: 'leftBottom',
                message,
                type: 'success',
                animationDuration: 300,
                dismissible: true,
              });
            }
        
            {% if messages %}
        
              {% for message in messages %}
                toast('{{ message }}', '{{ message.tags }}');
              {% endfor %}
        
            {% endif %}
        </script>
    </body>
</html>



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


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

{% load static %}

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <title>配達人 | クラウドソーシングアプリ</title>
        <link rel="shortcut icon" href="{% static 'img/logo.png' %}">
        <link rel="apple-touch-icon" sizes="180x180" href="{% static 'img/logo.png' %}">
        <link rel="manifest" href="{% static 'manifest.json' %}">
        
        {% load bootstrap4 %}
        {% bootstrap_css %}
        {% bootstrap_javascript jquery='full' %}

        {% include 'load_firebase.html' %}

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

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

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

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

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

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

    </body>
</html>



Visual Studio Codeのターミナルで「bash」を起動します。

ターミナルでbashを起動
ターミナルでbashを起動



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


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


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

{% extends 'courier/base.html' %}
{% load static %}
{% block head %}
<script
  src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap&libraries=places&v=weekly"
  defer></script>
<script>
  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() {
    if (!document.getElementById("map")) {
      return;
    }
    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,
      },
      (response, status) => {
        if (status === "OK") {
          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' %}"
          });
          updateCourierPosition(map);
        } else {
          window.alert("Directions request failed due to " + status);
        }
      }
    );
  }
  function updateCourierPosition(map) {
 
    const jobSocket = new WebSocket(
      "ws{% if request.get_host != 'localhost:8000' %}s{% endif %}://" + window.location.host + "/ws/jobs/{{ job.id }}/"
    );

    navigator.geolocation.watchPosition(
      pos => {
        var courierPostion = new google.maps.LatLng(pos.coords.latitude, pos.coords.longitude);
        if (!window.courierMarker) {
          window.courierMarker = new google.maps.Marker({
            position: courierPostion,
            map,
            icon: "{% static 'img/courier.png' %}"
          });
        } else {
          window.courierMarker.setPosition(courierPostion);
        }
        map.panTo(courierPostion);

        try {
          jobSocket.send(JSON.stringify({
            job: {
              courier_lat: pos.coords.latitude,
              courier_lng: pos.coords.longitude,
            }
          }))
        } catch (error) {
          console.log(error);
        }
      },
      pos => console.log(pos))
  }
</script>

<style>
  #map {
    flex: 1;
  }
  small {
    font-size: 12px;
    line-height: 1.2rem;
  }
  .card {
    border: none;
  }
</style>
{% endblock %}
{% block content %}
<div class="d-flex flex-column h-100" style="padding-bottom: 60px">
    <div class="text-center">
        <div class="btn-group mt-1 mb-1 align-item-center" role="group">
        <a href="{% url 'courier:current_job' %}" class="btn btn-info">進行中の配達</a>
        <a href="{% url 'courier:archived_jobs' %}" class="btn btn-outline-info">終了した配達</a>
        </div>
    </div>
    {% if job %}
    <div id="map"></div>
    <div class="card">
        <div class="card-body p-2">
            <div class="media">
                <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="50px" height="50px">
                <div class="media-body">
                    <b>{{ job.name }}</b>
                    <div class="d-flex">
                        <div class="flex-grow-1 mr-2">
                            <small class="text-success">
                                <i class="fas fa-car"></i> <span>{{ job.distance }}</span> km
                                <i class="far fa-clock ml-2"></i> <span>{{ job.duration }}</span></small>
                            <div class="d-flex align-items-center mt-2">
                                <i class="fas fa-map-marker-alt"></i>
                                <small class="text-secondary ml-2">{{ job.pickup_address }}</small>
                            </div>
                            <div class="d-flex align-items-center mt-2">
                                <i class="fas fa-flag-checkered"></i>
                                <small class="text-secondary ml-2">{{ job.delivery_address }}</small>
                            </div>
                        </div>
                        <h3>{{ job.price }}</h3></div>
                </div>
            </div>
            <a href="{% url 'courier:current_job_take_photo' job.id %}" class="btn btn-block btn-info btn-md mt-3">
                {% if job.status == 'picking' %}配達開始の写真を撮影{% else %}配達完了の写真を撮影{% endif %}
            </a>
        </div>
    </div>
    {% else %}
    <div id="main" class="text-center">
        <p>
            現在、何も配達依頼を受けていません。<br/>配達依頼を選んで受けてみましょう。
        </p>
    </div>
    
    {% endif %}
</div>
{% include 'courier/bottom_tabs.html' %}
{% endblock %}



「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 }} ");

  var courierLat = parseFloat(" {{ job.courier.lat }} ");
  var courierLng = parseFloat(" {{ job.courier.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,
      },
      (response, status) => {
        if (status === "OK") {
          new google.maps.DirectionsRenderer({
            map: map,
            directions: response,
            suppressMarkers: true,
            polylineOptions: {
              strokeColor: "#000",
              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' %}"
          });

          window.courierMarker = new google.maps.Marker({
            position: new google.maps.LatLng(courierLat, courierLng),
            map,
            icon: '/static/img/courier.png',
          })
        } else {
          window.alert("リクエストは次の理由で失敗しました:" + status);
        }
      }
    );
  }

  const jobSocket = new WebSocket(
    "ws{% if request.get_host != 'localhost:8000' %}s{% endif %}://" + window.location.host + "/ws/jobs/{{ job.id }}/"
    );

  // このページが WebSocket 経由でイベントを受信するたびにこの関数を実行します
  jobSocket.onmessage = function (e) {
    var data = JSON.parse(e.data);
    var job = data.job;
    console.log(job);

    if (job.courier_lat && job.courier_lng) {
      var courierPosition = new google.maps.LatLng(job.courier_lat, job.courier_lng);
      window.courierMarker.setPosition(courierPosition);
    }

    if (job.status) {
      $("#job_status").html(job.status);
      $("form").css("display", "none"); //配達依頼が配達人に受けられた時、キャンセルボタンを消す
    }

    if (job.pickup_photo) {
      $("#pickup_photo").html('<img src="' + job.pickup_photo + '" class="rounded-lg photo" width="130" height="130">');
    }

    if (job.delivery_photo) {
      $("#delivery_photo").html('<img src="' + job.delivery_photo + '" class="rounded-lg photo" width="130" height="130">');
    }

  }  

</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 %}



ngrokのURLで配達依頼人を表示させます。

ngrokのURLでログイン
ngrokのURLでログイン
配達依頼人を表示させる
配達依頼人を表示させる



次にスマートフォンのChromeでngrokのURLを開きます。

スマートフォンで表示
スマートフォンで表示



配達人でログインします。

配達人でログイン
配達人でログイン



ホーム画面にURLを登録すると、登録したアイコンでショートカットを作ることができます。

ホーム画面にショートカット
ホーム画面にショートカット



写真を撮影すれば、リアルタイムで連動します。
一通り動作を確認してください。

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


57 | 配達依頼を受けた時の連動】 << 【ホーム】 >> 【59 | Heroku デプロイ