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

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

Django3.2 | クラウドソーシングアプリの構築 | 54 | Web Socketの設定

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


53 | Firebaseメッセージ】 << 【ホーム】 >> 【55 | 配達人のマップ表示


Django Channelsをインストールします。
コマンド
pip install channels==3.0.3


バージョンを確認します。
コマンド
pip freeze > requirements.txt


内容確認 【Desktop/crowdsource/requirements.txt】

asgiref==3.7.2
attrs==23.1.0
autobahn==23.6.2
Automat==22.10.0
beautifulsoup4==4.12.2
CacheControl==0.13.1
cachetools==5.3.1
certifi==2023.7.22
cffi==1.15.1
channels==3.0.3
chardet==3.0.4
charset-normalizer==3.2.0
constantly==15.1.0
cryptography==41.0.3
daphne==3.0.2
defusedxml==0.7.1
Django==3.2.20
django-bootstrap4==2.3.1
firebase-admin==4.4.0
google-api-core==1.34.0
google-api-python-client==2.97.0
google-auth==2.22.0
google-auth-httplib2==0.1.0
google-cloud-core==2.3.3
google-cloud-firestore==2.11.1
google-cloud-storage==2.10.0
google-crc32c==1.5.0
google-resumable-media==2.5.0
googleapis-common-protos==1.60.0
grpcio==1.57.0
grpcio-status==1.48.2
httplib2==0.22.0
hyperlink==21.0.0
idna==2.10
incremental==22.10.0
msgpack==1.0.5
oauthlib==3.2.2
paypalrestsdk==1.13.1
Pillow==10.0.0
proto-plus==1.22.3
protobuf==3.20.3
pyasn1==0.5.0
pyasn1-modules==0.3.0
pycparser==2.21
PyJWT==2.8.0
pyOpenSSL==23.2.0
pyparsing==3.1.1
python3-openid==3.2.0
pytz==2023.3
requests==2.25.0
requests-oauthlib==1.3.1
rsa==4.9
service-identity==23.1.0
six==1.16.0
social-auth-app-django==4.0.0
social-auth-core==4.4.2
soupsieve==2.5
sqlparse==0.4.4
stripe==2.55.1
Twisted==23.8.0
txaio==23.1.1
typing_extensions==4.7.1
uritemplate==4.1.1
urllib3==1.26.16
zope.interface==6.0



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


記述追加 【Desktop/crowdsource/crowdsource/settings.py】45行目

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    #'core', #削除
    'bootstrap4', 
    'social_django',
    'core.apps.CoreConfig', 
    'channels',  #追加
]



記述追加 【Desktop/crowdsource/crowdsource/settings.py】176行目(末尾)

ASGI_APPLICATION = "crowdsource.asgi.application"



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


記述編集 【Desktop/crowdsource/crowdsource/asgi.py】

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application

from crowdsource.urls import websocket_urlpatterns


os.environ.setdefault("DJANGO_SETTINGS_MODULE", "crowdsource.settings")


application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
      URLRouter(websocket_urlpatterns)
    )
})



「crowdsource/core」フォルダに「consumers.py」ファイルを新規作成します。
作成した「consumers.py」ファイルを以下のように編集します。



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

import json
from channels.generic.websocket import WebsocketConsumer
from . import models

class JobConsumer(WebsocketConsumer):
  def connect(self):
    self.accept()

  def disconnect(self, close_code):
    pass

  def receive(self, text_data):
    text_data_json = json.loads(text_data)
    job = text_data_json['job']

    print("Job", job)



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


記述編集 【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 django.views.generic import TemplateView

from core import views, consumers

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"),
    path('api/fcm-token/update/', courier_apis.fcm_token_update_api, name="fcm_token_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'))),
    path('firebase-messaging-sw.js', (TemplateView.as_view(template_name="firebase-messaging-sw.js", content_type="application/javascript",))),

]

websocket_urlpatterns = [
    path('ws/jobs/<job_id>/', consumers.JobConsumer.as_asgi())
]

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



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


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

{% 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://" + 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 %}



ブラウザを確認します。
配達人の現在位置が表示されているページをアップデートします。
http://127.0.0.1:8000/courier/jobs/current/

ブラウザ更新
ブラウザ更新



ターミナルで配達人の位置が表示されるのを確認します。

WebSocket HANDSHAKING /ws/jobs/3d8f6ddc-caca-45b0-b39a-3a1422efbb96/ [127.0.0.1:56598]
WebSocket CONNECT /ws/jobs/3d8f6ddc-caca-45b0-b39a-3a1422efbb96/ [127.0.0.1:56598]
Job {'courier_lat': 4338, 'courier_lng': 1492}
Job {'courier_lat': 496, 'courier_lng': 1496974}



引き続き「core/consumers.py」ファイルを編集します。


記述編集 【Desktop/crowdsource/core/consumers.py】18行目

import json
from channels.generic.websocket import WebsocketConsumer
from . import models

class JobConsumer(WebsocketConsumer):
  def connect(self):
    self.accept()

  def disconnect(self, close_code):
    pass

  def receive(self, text_data):
    text_data_json = json.loads(text_data)
    job = text_data_json['job']

    # print("Job", job)

    if job.get('courier_lat') and job.get('courier_lng'):
      self.scope['user'].courier.lat = job['courier_lat']
      self.scope['user'].courier.lng = job['courier_lng']
      self.scope['user'].courier.save()



配達人ページを更新して、管理サイトを確認します。
配達人の位置がデータベースに格納されているのがわかります。

管理サイト確認
管理サイト確認


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


53 | Firebaseメッセージ】 << 【ホーム】 >> 【55 | 配達人のマップ表示