├── app ├── __init__.py ├── tests │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ ├── test_http.cpython-36.pyc │ │ ├── test_http.cpython-36-pytest-5.4.2.pyc │ │ └── test_web_sockets.cpython-36-pytest-5.4.2.pyc │ ├── test_http.py │ └── test_web_sockets.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ ├── 0002_trip.cpython-36.pyc │ │ ├── 0001_initial.cpython-36.pyc │ │ └── 0003_auto_20200526_0756.cpython-36.pyc │ ├── 0003_auto_20200526_0756.py │ ├── 0002_trip.py │ └── 0001_initial.py ├── apps.py ├── __pycache__ │ ├── urls.cpython-36.pyc │ ├── admin.cpython-36.pyc │ ├── models.cpython-36.pyc │ ├── views.cpython-36.pyc │ ├── __init__.cpython-36.pyc │ ├── consumers.cpython-36.pyc │ └── serializers.cpython-36.pyc ├── urls.py ├── admin.py ├── views.py ├── models.py ├── serializers.py └── consumers.py ├── taxiapp ├── __init__.py ├── __pycache__ │ ├── urls.cpython-36.pyc │ ├── __init__.cpython-36.pyc │ ├── routing.cpython-36.pyc │ ├── settings.cpython-36.pyc │ └── middleware.cpython-36.pyc ├── wsgi.py ├── asgi.py ├── urls.py ├── routing.py ├── middleware.py └── settings.py ├── .env ├── pytest.ini ├── manage.py ├── requirements.txt └── README.md /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /taxiapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://apptaxi:password@localhost:5432/apptaxi -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = taxiapp.settings 3 | -------------------------------------------------------------------------------- /app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppConfig(AppConfig): 5 | name = 'app' 6 | -------------------------------------------------------------------------------- /app/__pycache__/urls.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/urls.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/admin.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/admin.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/models.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/models.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/views.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/views.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/consumers.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/consumers.cpython-36.pyc -------------------------------------------------------------------------------- /taxiapp/__pycache__/urls.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/taxiapp/__pycache__/urls.cpython-36.pyc -------------------------------------------------------------------------------- /app/__pycache__/serializers.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/__pycache__/serializers.cpython-36.pyc -------------------------------------------------------------------------------- /taxiapp/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/taxiapp/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /taxiapp/__pycache__/routing.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/taxiapp/__pycache__/routing.cpython-36.pyc -------------------------------------------------------------------------------- /taxiapp/__pycache__/settings.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/taxiapp/__pycache__/settings.cpython-36.pyc -------------------------------------------------------------------------------- /app/tests/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/tests/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /taxiapp/__pycache__/middleware.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/taxiapp/__pycache__/middleware.cpython-36.pyc -------------------------------------------------------------------------------- /app/tests/__pycache__/test_http.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/tests/__pycache__/test_http.cpython-36.pyc -------------------------------------------------------------------------------- /app/migrations/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/migrations/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /app/migrations/__pycache__/0002_trip.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/migrations/__pycache__/0002_trip.cpython-36.pyc -------------------------------------------------------------------------------- /app/migrations/__pycache__/0001_initial.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/migrations/__pycache__/0001_initial.cpython-36.pyc -------------------------------------------------------------------------------- /app/tests/__pycache__/test_http.cpython-36-pytest-5.4.2.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/tests/__pycache__/test_http.cpython-36-pytest-5.4.2.pyc -------------------------------------------------------------------------------- /app/migrations/__pycache__/0003_auto_20200526_0756.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/migrations/__pycache__/0003_auto_20200526_0756.cpython-36.pyc -------------------------------------------------------------------------------- /app/tests/__pycache__/test_web_sockets.cpython-36-pytest-5.4.2.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gautamamber/Real-time-Taxi-app-with-Django-Channels/HEAD/app/tests/__pycache__/test_web_sockets.cpython-36-pytest-5.4.2.pyc -------------------------------------------------------------------------------- /taxiapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for taxiapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taxiapp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from . import views 3 | from rest_framework_simplejwt.views import TokenRefreshView 4 | 5 | 6 | urlpatterns = [ 7 | path('v1/signup', views.SignupApiView.as_view(), name="sign_up"), 8 | path('v1/login', views.LoginApiView.as_view(), name="login"), 9 | path('v1/token/refresh', TokenRefreshView.as_view(), name="token_refresh"), 10 | path('v1/trip', views.TripApiView.as_view(), name="trip_list"), 11 | path('v1/trip/', views.TripDetailApiView.as_view(), name="trip_detail") 12 | ] 13 | -------------------------------------------------------------------------------- /taxiapp/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for taxiapp project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | 9 | Question: Try to answer the “why” along with the “what” and “how”. For example, why did we use Redis over an in-memory 10 | layer for Django Channels? 11 | 12 | """ 13 | 14 | import os 15 | 16 | from django.core.asgi import get_asgi_application 17 | 18 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taxiapp.settings') 19 | 20 | application = get_asgi_application() 21 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'taxiapp.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /taxiapp/urls.py: -------------------------------------------------------------------------------- 1 | """taxiapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include('app.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin 3 | from . import models 4 | 5 | 6 | @admin.register(models.User) 7 | class UserAdmin(DefaultUserAdmin): 8 | """ 9 | Representation in django admin panel 10 | """ 11 | pass 12 | 13 | 14 | @admin.register(models.Trip) 15 | class TripAdmin(admin.ModelAdmin): 16 | """ 17 | Representation in django admin panel 18 | """ 19 | fields = ( 20 | 'id', 'pick_up_address', 'drop_off_address', 'status', 21 | 'driver', 'rider', 22 | 'created', 'updated', 23 | ) 24 | list_display = ( 25 | 'id', 'pick_up_address', 'drop_off_address', 'status', 26 | 'driver', 'rider', 27 | 'created', 'updated', 28 | ) 29 | list_filter = ( 30 | 'status', 31 | ) 32 | readonly_fields = ( 33 | 'id', 'created', 'updated', 34 | ) 35 | -------------------------------------------------------------------------------- /app/migrations/0003_auto_20200526_0756.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-26 07:56 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('app', '0002_trip'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='trip', 17 | name='driver', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='trips_as_driver', to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AddField( 21 | model_name='trip', 22 | name='rider', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='trips_as_rider', to=settings.AUTH_USER_MODEL), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.3.1 2 | asgiref==3.2.7 3 | async-timeout==3.0.1 4 | attrs==19.3.0 5 | autobahn==20.4.3 6 | Automat==20.2.0 7 | cffi==1.14.0 8 | channels==2.4.0 9 | channels-redis==2.4.2 10 | constantly==15.1.0 11 | cryptography==2.9.2 12 | daphne==2.5.0 13 | Django==3.0.6 14 | django-environ==0.4.5 15 | djangorestframework==3.11.0 16 | djangorestframework-simplejwt==4.4.0 17 | hiredis==1.0.1 18 | hyperlink==19.0.0 19 | idna==2.9 20 | importlib-metadata==1.6.0 21 | incremental==17.5.0 22 | more-itertools==8.3.0 23 | msgpack==0.6.2 24 | packaging==20.4 25 | Pillow==7.1.2 26 | pluggy==0.13.1 27 | psycopg2==2.8.5 28 | py==1.8.1 29 | pyasn1==0.4.8 30 | pyasn1-modules==0.2.8 31 | pycparser==2.20 32 | PyHamcrest==2.0.2 33 | PyJWT==1.7.1 34 | pyOpenSSL==19.1.0 35 | pyparsing==2.4.7 36 | pytest==5.4.2 37 | pytest-asyncio==0.12.0 38 | pytest-django==3.9.0 39 | pytz==2020.1 40 | service-identity==18.1.0 41 | six==1.15.0 42 | sqlparse==0.3.1 43 | Twisted==20.3.0 44 | txaio==20.4.1 45 | wcwidth==0.1.9 46 | zipp==3.1.0 47 | zope.interface==5.1.0 48 | -------------------------------------------------------------------------------- /app/migrations/0002_trip.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-25 18:34 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('app', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Trip', 16 | fields=[ 17 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 18 | ('created', models.DateTimeField(auto_now_add=True)), 19 | ('updated', models.DateTimeField(auto_now=True)), 20 | ('pick_up_address', models.CharField(max_length=255)), 21 | ('drop_off_address', models.CharField(max_length=255)), 22 | ('status', models.CharField(choices=[('REQUESTED', 'REQUESTED'), ('STARTED', 'STARTED'), ('IN_PROGRESS', 'IN_PROGRESS'), ('COMPLETED', 'COMPLETED')], default='REQUESTED', max_length=100)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real-time-Taxi-app-with-Django-Channels 2 | 3 | ##### Features: 4 | 1. Real time ride sharing application 5 | 2. Messaging between rider and driver using Django channels 6 | 3. JWT token based authentication 7 | 8 | ###### In this application below packages are used: 9 | 1. Python 10 | 2. Django 11 | 3. Django Channels 12 | 4. Django Rest Framework 13 | 5. PostgresSQL 14 | 15 | ###### More features to add 16 | 17 | 1. The rider cancels his request after a driver accepts it. 18 | 2. The server alerts all other drivers in the driver pool that someone has accepted a request. 19 | 3. The driver periodically broadcasts his location to the rider during a trip. 20 | 4. The server only allows a rider to request one trip at a time. 21 | 5. The rider can share his trip with another rider, who can join the trip and receive updates. 22 | 6. The server only shares a trip request to drivers in a specific geographic location. 23 | 7. If no drivers accept the request within a certain timespan, the server cancels the request and returns a message to the rider. 24 | -------------------------------------------------------------------------------- /taxiapp/routing.py: -------------------------------------------------------------------------------- 1 | from channels.routing import ProtocolTypeRouter, URLRouter 2 | from django.urls import path 3 | from app import consumers 4 | from taxiapp import middleware 5 | 6 | 7 | application = ProtocolTypeRouter( 8 | { 9 | # Here, we’re wrapping our URL router in our middleware stack, so all incoming connection requests will go 10 | # through our authentication method. 11 | # Channels implicitly handles the HTTP URL configuration, we need to explicitly handle WebSocket routing. 12 | # (A router is the Channels counterpart to Django’s URL configuration.) 13 | # the app initializes an HTTP router by default if one isn’t explicitly specified. 14 | # If an http argument is not provided, it will default to the Django view system’s 15 | # ASGI interface, channels.http.AsgiHandler , which means that for most projects 16 | # that aren’t doing custom long-poll HTTP handling, you can simply not specify an 17 | # http option and leave it to work the “normal” Django way. 18 | "websocket": middleware.TokenAuthMiddlewareStack(URLRouter([ 19 | path('taxi/', consumers.TaxiConsumer), 20 | ])) 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import generics, permissions, viewsets 3 | from . import serializers, models 4 | from rest_framework_simplejwt.views import TokenObtainPairView 5 | 6 | 7 | class SignupApiView(generics.CreateAPIView): 8 | """ 9 | Sign up api view 10 | """ 11 | queryset = get_user_model().objects.all() 12 | serializer_class = serializers.UserSerializer 13 | 14 | 15 | class LoginApiView(TokenObtainPairView): 16 | """ 17 | Login api view with jwt token 18 | """ 19 | serializer_class = serializers.LoginSerializer 20 | 21 | 22 | class TripApiView(generics.ListAPIView): 23 | """ 24 | Trip api view 25 | """ 26 | permission_classes = (permissions.IsAuthenticated,) 27 | queryset = models.Trip.objects.all() 28 | serializer_class = serializers.TripSerializer 29 | 30 | 31 | class TripDetailApiView(generics.RetrieveAPIView): 32 | """ 33 | Get trip details 34 | """ 35 | lookup_field = 'id' 36 | lookup_url_kwarg = 'trip_id' 37 | permission_classes = (permissions.IsAuthenticated, ) 38 | queryset = models.Trip.objects.all() 39 | serializer_class = serializers.TripSerializer 40 | 41 | 42 | """ 43 | users can participate in trips in one of two ways – they either drive the 44 | cars or they ride in them. A rider initiates the trip with a request, which 45 | is broadcast to all available drivers. A driver starts a trip by accepting the 46 | request. At this point, the driver heads to the pick-up address. The rider is instantly 47 | alerted that a driver has started the trip and other drivers are notified that the trip 48 | is no longer up for grabs. 49 | """ 50 | -------------------------------------------------------------------------------- /taxiapp/middleware.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import parse_qs 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.models import AnonymousUser 4 | from django.db import close_old_connections 5 | from channels.auth import AuthMiddlewareStack 6 | from rest_framework_simplejwt.tokens import AccessToken 7 | 8 | 9 | User = get_user_model() 10 | 11 | 12 | class TokenMiddleware: 13 | """ 14 | Token middleware 15 | """ 16 | 17 | def __init__(self, inner): 18 | self.inner = inner 19 | 20 | def __call__(self, scope): 21 | close_old_connections() 22 | query_strings = parse_qs(scope['query_string'].decode()) 23 | token = query_strings.get('token') 24 | if not token: 25 | scope['user'] = AnonymousUser() 26 | return self.inner(scope) 27 | try: 28 | access_token = AccessToken(token[0]) 29 | user = User.objects.get(id=access_token['id']) 30 | except Exception as exception: 31 | scope['user'] = AnonymousUser() 32 | return self.inner(scope) 33 | if not user.is_active: 34 | scope['user'] = AnonymousUser() 35 | return self.inner(scope) 36 | scope['user'] = user 37 | return self.inner(scope) 38 | 39 | 40 | def TokenAuthMiddlewareStack(inner): 41 | """ 42 | Our new middleware class plucks the JWT access token from the query string and retrieves the associated user. Once 43 | the WebSocket connection is opened, all messages can be sent and received without verifying the user again. Closing 44 | the connection and opening it again requires re-authorization. 45 | :param inner: 46 | :return: 47 | """ 48 | return TokenMiddleware(AuthMiddlewareStack(inner)) 49 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | import uuid 4 | from taxiapp import settings 5 | from django.shortcuts import reverse 6 | 7 | 8 | class User(AbstractUser): 9 | """ 10 | We are using Django built in user with Abstract Base user 11 | for designing application with requirements 12 | """ 13 | pass 14 | 15 | 16 | class Trip(models.Model): 17 | """ 18 | Trip model 19 | """ 20 | REQUESTED = 'REQUESTED' 21 | STARTED = 'STARTED' 22 | IN_PROGRESS = 'IN_PROGRESS' 23 | COMPLETED = 'COMPLETED' 24 | STATUSES = ( 25 | (REQUESTED, REQUESTED), 26 | (STARTED, STARTED), 27 | (IN_PROGRESS, IN_PROGRESS), 28 | (COMPLETED, COMPLETED), 29 | ) 30 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 31 | created = models.DateTimeField(auto_now_add=True) 32 | updated = models.DateTimeField(auto_now=True) 33 | pick_up_address = models.CharField(max_length=255) 34 | drop_off_address = models.CharField(max_length=255) 35 | rider = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.DO_NOTHING, 36 | related_name="trips_as_rider") 37 | driver = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.DO_NOTHING, 38 | related_name="trips_as_driver") 39 | status = models.CharField( 40 | max_length=100, choices=STATUSES, default=REQUESTED 41 | ) 42 | 43 | def __str__(self): 44 | """ 45 | string representation 46 | :return: 47 | """ 48 | return "{}".format(self.id) 49 | 50 | def get_absolute_url(self): 51 | return reverse('trip_detail', kwargs={'trip_id': self.id}) 52 | -------------------------------------------------------------------------------- /app/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | from . import models 4 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | """ 9 | User serializer 10 | """ 11 | password1 = serializers.CharField(write_only=True) 12 | password2 = serializers.CharField(write_only=True) 13 | 14 | class Meta: 15 | model = get_user_model() 16 | fields = ( 17 | 'id', 'username', 'password1', 'password2', 'first_name', 'last_name' 18 | ) 19 | read_only_fields = ('id',) 20 | 21 | def validate(self, attrs): 22 | """ 23 | Match password1 and password2 24 | :param attrs: 25 | :return: 26 | """ 27 | if attrs['password1'] != attrs['password2']: 28 | raise serializers.ValidationError("Password do not match") 29 | return attrs 30 | 31 | def create(self, validated_data): 32 | """ 33 | Create user 34 | :param validated_data: 35 | :return: 36 | """ 37 | data = { 38 | key: value for key, value in validated_data.items() 39 | if key not in ('password1', 'password2') 40 | } 41 | data['password'] = validated_data['password1'] 42 | return self.Meta.model.objects.create_user(**data) 43 | 44 | 45 | class LoginSerializer(TokenObtainPairSerializer): 46 | """ 47 | Login serializer 48 | LogInSerializer that serializes the User object and adds the data to the token payload as private claims. 49 | (We avoid overwriting the id claim, since the token already includes it by 50 | """ 51 | @classmethod 52 | def get_token(cls, user): 53 | token = super().get_token(user) 54 | user_data = UserSerializer(user).data 55 | for key, value in user_data.items(): 56 | if key != 'id': 57 | token['key'] = value 58 | return token 59 | 60 | 61 | class TripSerializer(serializers.ModelSerializer): 62 | """ 63 | Trip serializer 64 | """ 65 | 66 | class Meta: 67 | model = models.Trip 68 | fields = "__all__" 69 | read_only_fields = ('id', 'created', 'updated',) 70 | 71 | 72 | class NestedTripSerializer(serializers.ModelSerializer): 73 | class Meta: 74 | model = models.Trip 75 | fields = '__all__' 76 | depth = 1 -------------------------------------------------------------------------------- /app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-25 16:33 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0011_update_proxy_permissions'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 33 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 34 | ], 35 | options={ 36 | 'verbose_name': 'user', 37 | 'verbose_name_plural': 'users', 38 | 'abstract': False, 39 | }, 40 | managers=[ 41 | ('objects', django.contrib.auth.models.UserManager()), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /app/tests/test_http.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | from django.contrib.auth import get_user_model 4 | from rest_framework import status 5 | from rest_framework.reverse import reverse 6 | from rest_framework.test import APITestCase 7 | from app import models 8 | 9 | # Dummy password 10 | PASSWORD = "password@12345" 11 | 12 | 13 | def create_user(username="user@example.com", password=PASSWORD): 14 | """ 15 | Create user function with dummy username and password 16 | :param username: 17 | :param password: 18 | :return: 19 | """ 20 | return get_user_model().objects.create_user( 21 | username=username, 22 | password=password, 23 | last_name="User", 24 | first_name="Test" 25 | ) 26 | 27 | 28 | class AuthenticationTest(APITestCase): 29 | """ 30 | Test user authentication 31 | """ 32 | 33 | def test_user_sign_up(self): 34 | """ 35 | user signup test case 36 | :return: 37 | """ 38 | response = self.client.post(reverse("sign_up"), data={ 39 | 'username': "amber@nickelfox.com", 40 | "first_name": "Amber", 41 | "last_name": "Mike", 42 | "password1": PASSWORD, 43 | "password2": PASSWORD 44 | }) 45 | user = get_user_model().objects.last() 46 | self.assertEqual(status.HTTP_201_CREATED, response.status_code) 47 | self.assertEqual(response.data['id'], user.id) 48 | self.assertEqual(response.data['first_name'], user.first_name) 49 | self.assertEqual(response.data['username'], user.username) 50 | 51 | def test_user_login(self): 52 | """ 53 | User login test case 54 | :return: 55 | """ 56 | user = create_user() 57 | response = self.client.post(reverse("login"), data={ 58 | "username": user.username, 59 | "password": PASSWORD 60 | }) 61 | 62 | # Parse payload data from access token 63 | access = response.data['access'] 64 | header, payload, signature = access.split('.') 65 | decode_payload = base64.b64decode(f'{payload}==') 66 | payload_data = json.loads(decode_payload) 67 | self.assertEqual(status.HTTP_200_OK, response.status_code) 68 | self.assertIsNotNone(response.data['refresh']) 69 | self.assertEqual(payload_data['id'], user.id) 70 | 71 | 72 | class TripTest(APITestCase): 73 | """ 74 | Test case for Trips 75 | """ 76 | 77 | def setUp(self): 78 | """ 79 | Setup the database 80 | :return: 81 | """ 82 | user = create_user() 83 | response = self.client.post(reverse('login'), data={ 84 | "username": user.username, 85 | "password": PASSWORD 86 | }) 87 | self.access = response.data['access'] 88 | 89 | def test_user_can_list_trips(self): 90 | """ 91 | User can see his all trips 92 | :param self: 93 | :return: 94 | """ 95 | trips = [ 96 | models.Trip.objects.create(pick_up_address='A', drop_off_address='B'), 97 | models.Trip.objects.create(pick_up_address='B', drop_off_address='C') 98 | ] 99 | response = self.client.get(reverse('trip_list'), 100 | HTTP_AUTHORIZATION=f'Bearer {self.access}') 101 | self.assertEqual(status.HTTP_200_OK, response.status_code) 102 | exp_trip = [str(trip.id) for trip in trips] 103 | act_trip = [trip.get('id') for trip in response.data] 104 | self.assertCountEqual(exp_trip, act_trip) 105 | 106 | def test_user_can_retrieve_trip(self): 107 | """ 108 | Get trip using uuid 109 | :return: 110 | """ 111 | trip = models.Trip.objects.create(pick_up_address='A', drop_off_address='B') 112 | response = self.client.get(trip.get_absolute_url(), 113 | HTTP_AUTHORIZATION=f'Bearer {self.access}' 114 | ) 115 | self.assertEqual(status.HTTP_200_OK, response.status_code) 116 | self.assertEqual(str(trip.id), response.data.get('id')) 117 | -------------------------------------------------------------------------------- /taxiapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for taxiapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | import datetime 13 | import os 14 | import environ 15 | 16 | # django-environ package setup. 17 | # this will visible thrown the application. 18 | ROOT_DIR = environ.Path(__file__) - 2 19 | APPS_DIR = ROOT_DIR.path('config') 20 | 21 | env = environ.Env() 22 | # reading .env file 23 | environ.Env.read_env(env_file=ROOT_DIR('.env')) 24 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 25 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 26 | 27 | 28 | # Quick-start development settings - unsuitable for production 29 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 30 | 31 | # SECURITY WARNING: keep the secret key used in production secret! 32 | SECRET_KEY = 'z*$8i%uvmm+@__w=b+1e)t@uf$=ix)=it76rn(x^uj=hcc#=0+' 33 | 34 | # SECURITY WARNING: don't run with debug turned on in production! 35 | DEBUG = True 36 | 37 | ALLOWED_HOSTS = [] 38 | 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = [ 43 | 'django.contrib.admin', 44 | 'django.contrib.auth', 45 | 'django.contrib.contenttypes', 46 | 'django.contrib.sessions', 47 | 'django.contrib.messages', 48 | 'django.contrib.staticfiles', 49 | 'channels', 50 | 'rest_framework', 51 | 'app' 52 | ] 53 | AUTH_USER_MODEL = 'app.User' 54 | MIDDLEWARE = [ 55 | 'django.middleware.security.SecurityMiddleware', 56 | 'django.contrib.sessions.middleware.SessionMiddleware', 57 | 'django.middleware.common.CommonMiddleware', 58 | 'django.middleware.csrf.CsrfViewMiddleware', 59 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 60 | 'django.contrib.messages.middleware.MessageMiddleware', 61 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 62 | ] 63 | 64 | ROOT_URLCONF = 'taxiapp.urls' 65 | 66 | TEMPLATES = [ 67 | { 68 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 69 | 'DIRS': [], 70 | 'APP_DIRS': True, 71 | 'OPTIONS': { 72 | 'context_processors': [ 73 | 'django.template.context_processors.debug', 74 | 'django.template.context_processors.request', 75 | 'django.contrib.auth.context_processors.auth', 76 | 'django.contrib.messages.context_processors.messages', 77 | ], 78 | }, 79 | }, 80 | ] 81 | 82 | WSGI_APPLICATION = 'taxiapp.wsgi.application' 83 | 84 | 85 | # Database 86 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 87 | 88 | DATABASES = { 89 | 'default': env.db(), # Looks for DATABASE_URL in environment file, 90 | } 91 | 92 | ASGI_APPLICATION = 'taxiapp.routing.application' 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | # Configure the CHANNEL_LAYERS by settings a default Redis and routing 113 | 114 | REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379') 115 | 116 | CHANNEL_LAYERS = { 117 | 'default': { 118 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 119 | 'CONFIG': { 120 | 'hosts': [REDIS_URL], 121 | }, 122 | }, 123 | } 124 | # Internationalization 125 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 126 | 127 | LANGUAGE_CODE = 'en-us' 128 | 129 | TIME_ZONE = 'UTC' 130 | 131 | USE_I18N = True 132 | 133 | USE_L10N = True 134 | 135 | USE_TZ = True 136 | 137 | 138 | # Static files (CSS, JavaScript, Images) 139 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 140 | 141 | STATIC_URL = '/static/' 142 | 143 | REST_FRAMEWORK = { 144 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 145 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 146 | 'rest_framework.authentication.SessionAuthentication', 147 | ) 148 | } 149 | 150 | SIMPLE_JWT = { 151 | 'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=5), 152 | 'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1), 153 | 'USER_ID_CLAIM': 'id', 154 | } 155 | -------------------------------------------------------------------------------- /app/consumers.py: -------------------------------------------------------------------------------- 1 | from channels.generic.websocket import AsyncWebsocketConsumer 2 | from .models import Trip 3 | from .serializers import NestedTripSerializer, TripSerializer 4 | from channels.db import database_sync_to_async 5 | 6 | 7 | class TaxiConsumer(AsyncWebsocketConsumer): 8 | """ 9 | Taxi consumer 10 | A Channels consumer is like a Django view with extra steps to support the WebSocket protocol. Whereas a Django view 11 | can only process an incoming request, a Channels consumer can send and receive messages to the WebSocket 12 | connection being opened and closed. 13 | """ 14 | 15 | @database_sync_to_async 16 | def _get_user_group(self, user): 17 | return user.groups.first().name 18 | 19 | @database_sync_to_async 20 | def _get_trip_ids(self, user): 21 | user_groups = user.groups.values_list('name', flat=True) 22 | if 'driver' in user_groups: 23 | trip_ids = user.trips_as_driver.exclude( 24 | status=Trip.COMPLETED 25 | ).only('id').values_list('id', flat=True) 26 | else: 27 | trip_ids = user.trips_as_rider.exclude( 28 | status=Trip.COMPLETED 29 | ).only('id').values_list('id', flat=True) 30 | return map(str, trip_ids) 31 | 32 | async def connect(self): 33 | """ 34 | connect with web socket 35 | :return: 36 | """ 37 | user = self.scope['user'] 38 | if user.is_anonymous: 39 | await self.close() 40 | else: 41 | """ 42 | Add user in driver group 43 | """ 44 | user_group = await self._get_user_group(user) 45 | if user_group == 'driver': 46 | await self.channel_layer.group_add( 47 | group="drivers", 48 | channel=self.channel_name 49 | ) 50 | for trip_id in await self._get_trip_ids(user): 51 | await self.channel_layer.group_add( 52 | group=trip_id, 53 | channel=self.channel_name 54 | ) 55 | 56 | await self.accept() 57 | 58 | async def echo_message(self, message): 59 | await self.send_json(message) 60 | 61 | async def disconnect(self, code): 62 | """ 63 | disconnect with web socket 64 | :param code: 65 | :return: 66 | """ 67 | user = self.scope['user'] 68 | user_group = await self._get_user_group(user) 69 | if user_group == 'driver': 70 | await self.channel_layer.group_discard( 71 | group='drivers', 72 | channel=self.channel_name 73 | ) 74 | 75 | # new 76 | for trip_id in await self._get_trip_ids(user): 77 | await self.channel_layer.group_discard( 78 | group=trip_id, 79 | channel=self.channel_name 80 | ) 81 | 82 | await super().disconnect(code) 83 | 84 | async def receive_json(self, content, **kwargs): 85 | """ 86 | The receive_json() function is responsible for processing all messages that come to the server. Our message is 87 | an object with a type and a data payload. 88 | Passing a type is a Channels convention that serves two purposes. First, it helps differentiate incoming 89 | messages and tells the server how to process them. Second, the type maps directly to a consumer function when 90 | sent from another channel layer. 91 | :param content: 92 | :param kwargs: 93 | :return: 94 | """ 95 | message_type = content.get('type') 96 | if message_type == 'create.trip': 97 | await self.create_trip(content) 98 | elif message_type == 'echo.message': 99 | await self.echo_message(content) 100 | 101 | async def create_trip(self, message): 102 | data = message.get('data') 103 | trip = await self._create_trip(data) 104 | trip_data = NestedTripSerializer(trip).data 105 | 106 | # Send rider requests to all drivers. 107 | await self.channel_layer.group_send(group='drivers', message={ 108 | 'type': 'echo.message', 109 | 'data': trip_data 110 | }) 111 | 112 | await self.channel_layer.group_add( 113 | group=f'{trip.id}', 114 | channel=self.channel_name 115 | ) 116 | 117 | await self.send_json({ 118 | 'type': 'echo.message', 119 | 'data': trip_data, 120 | }) 121 | 122 | @database_sync_to_async 123 | def _create_trip(self, data): 124 | serializer = TripSerializer(data=data) 125 | serializer.is_valid(raise_exception=True) 126 | return serializer.create(serializer.validated_data) 127 | -------------------------------------------------------------------------------- /app/tests/test_web_sockets.py: -------------------------------------------------------------------------------- 1 | from channels.testing import WebsocketCommunicator 2 | import pytest 3 | from channels.layers import get_channel_layer 4 | from django.contrib.auth.models import Group 5 | from rest_framework_simplejwt.tokens import AccessToken 6 | from django.contrib.auth import get_user_model 7 | from channels.db import database_sync_to_async 8 | from taxiapp.routing import application 9 | 10 | 11 | TEST_CHANNEL_LAYERS = { 12 | 'default': { 13 | 'BACKEND': 'channels.layers.InMemoryChannelLayer' 14 | } 15 | } 16 | 17 | 18 | @database_sync_to_async 19 | def create_user(username, password, group="rider"): 20 | """ 21 | Create dummy username password 22 | helper function creates a new user in the database and then generates an access token for it. 23 | :param group: 24 | :param username: 25 | :param password: 26 | :return: 27 | """ 28 | user = get_user_model().objects.create_user( 29 | username=username, 30 | password=password 31 | ) 32 | user_group, _ = Group.objects.get_or_create(name=group) # new 33 | user.groups.add(user_group) 34 | user.save() 35 | access = AccessToken.for_user(user) 36 | return user, access 37 | 38 | 39 | @pytest.mark.asyncio 40 | @pytest.mark.django_db(transaction=True) 41 | class TestWebSocket: 42 | """ 43 | Test web sockets 44 | We’re also using coroutines that were introduced with the asyncio module 45 | Django Channels mandates the use of both pytest and asyncio 46 | """ 47 | 48 | async def test_can_connect_to_server(self, settings): 49 | """ 50 | Connect of test server 51 | WebSocket test proves that a client can connect to the server. 52 | :param settings: 53 | :return: 54 | """ 55 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 56 | _, access = await create_user( # new 57 | 'test.user@example.com', 'pAssw0rd' 58 | ) 59 | communicator = WebsocketCommunicator( 60 | application=application, 61 | path=f'/taxi/?token={access}' # changed 62 | ) 63 | connected, _ = await communicator.connect() 64 | assert connected is True 65 | await communicator.disconnect() 66 | 67 | async def test_join_driver_pool(self, settings): 68 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 69 | _, access = await create_user( 70 | 'test.user@example.com', 'password123', 'driver' 71 | ) 72 | communicator = WebsocketCommunicator( 73 | application=application, 74 | path=f'/taxi/?token={access}' 75 | ) 76 | connected, _ = await communicator.connect() 77 | message = { 78 | 'type': 'echo.message', 79 | 'data': 'This is a test message.', 80 | } 81 | channel_layer = get_channel_layer() 82 | await channel_layer.group_send('drivers', message=message) 83 | response = await communicator.receive_json_from() 84 | assert response == message 85 | await communicator.disconnect() 86 | 87 | async def test_request_trip(self, settings): 88 | """ 89 | Test case for trip 90 | When a rider requests a trip, the server will create a new Trip record and will broadcast the request to the 91 | driver pool. But from the rider’s perspective, he will only get a message back confirming the creation of a new 92 | trip. That’s what this test does. 93 | :param settings: 94 | :return: 95 | """ 96 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 97 | user, access = await create_user( 98 | 'test.user@example.com', 'password123', 'rider' 99 | ) 100 | communicator = WebsocketCommunicator( 101 | application=application, 102 | path=f'/taxi/?token={access}' 103 | ) 104 | await communicator.send_json_to({ 105 | 'type': 'create.trip', 106 | 'data': { 107 | 'pick_up_address': '123 Main Street', 108 | 'drop_off_address': '456 Piney Road', 109 | 'rider': user.id, 110 | }, 111 | }) 112 | response = await communicator.receive_json_from() 113 | response_data = response.get('data') 114 | assert response_data['id'] is not None 115 | assert response_data['pick_up_address'] == '123 Main Street' 116 | assert response_data['drop_off_address'] == '456 Piney Road' 117 | assert response_data['status'] == 'REQUESTED' 118 | assert response_data['rider']['username'] == user.username 119 | assert response_data['driver'] is None 120 | await communicator.disconnect() 121 | 122 | async def test_driver_alert_on_request(self, settings): 123 | """ 124 | A ride request should be broadcast to all drivers in the driver pool the moment it is sent. Let’s create a 125 | test to capture that behavior. 126 | We start off by creating a channel layer and adding it to the driver pool. Every message that is broadcast to 127 | the drivers group will be captured on the test_channel . Next, we establish a connection to the server as a 128 | rider, and we send a new request message over the wire. Finally, we wait for the broadcast message to reach the 129 | drivers group, and we confirm the identity of the rider who sent it. 130 | :param settings: 131 | :return: 132 | """ 133 | settings.CHANNEL_LAYERS = TEST_CHANNEL_LAYERS 134 | channel_layer = get_channel_layer() 135 | await channel_layer.group_add( 136 | group="drivers", 137 | channel="test_channel" 138 | ) 139 | 140 | user, access = await create_user( 141 | "test.user@example.com", "password", "rider" 142 | ) 143 | communicator = WebsocketCommunicator( 144 | application=application, 145 | path=f'/taxi/?token={access}' 146 | ) 147 | connected, _ = await communicator.connect() 148 | 149 | # Request a trip 150 | 151 | await communicator.send_json_to({ 152 | 'type': 'create.trip', 153 | 'data': { 154 | 'pick_up_address': '123 Main Street', 155 | 'drop_off_address': '456 Piney Road', 156 | 'rider': user.id, 157 | }, 158 | }) 159 | # Receive JSON message from server on test channel. 160 | response = await channel_layer.receive('test_channel') 161 | response_data = response.get("data") 162 | assert response_data['id'] is not None 163 | assert response_data['rider']['username'] == user.username 164 | assert response_data['driver'] is None 165 | 166 | await communicator.disconnect() 167 | --------------------------------------------------------------------------------