↓↓クリックして頂けると励みになります。
【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">
© クラウドソーシング
</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」を起動します。

「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で配達依頼人を表示させます。


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

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

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

写真を撮影すれば、リアルタイムで連動します。
一通り動作を確認してください。
↓↓クリックして頂けると励みになります。
【57 | 配達依頼を受けた時の連動】 << 【ホーム】 >> 【59 | Heroku デプロイ】