HTTP

Part 1, Chapter 5


After users log in, they should be taken to a dashboard that displays an overview of their user-related data. Even though we plan to use WebSockets for user-to-user communication, we still have a use for run-of-the-mill HTTP requests. Users should be able to query the server for information about their past, present, and future trips. Up-to-date information is vital to understanding where the user has travelled from or for planning where she is traveling next.

Our HTTP-related tests capture these scenarios.

All Trips

First, let's add a feature to let users view all of the trips associated with their accounts. As an initial step, we will allow users to see all existing trips; later on in this tutorial, we will add better filtering.

Test

Add the following test case to the bottom of our existing tests in example/tests/test_http.py:

# example/tests/test_http.py
class HttpTripTest(APITestCase):

    def setUp(self):
        user = create_user()
        self.client = APIClient()
        self.client.login(username=user.username, password=PASSWORD)

    def test_user_can_list_trips(self):
        trips = [
            Trip.objects.create(pick_up_address='A', drop_off_address='B'),
            Trip.objects.create(pick_up_address='B', drop_off_address='C')
        ]
        response = self.client.get(reverse('trip:trip_list'))
        self.assertEqual(status.HTTP_200_OK, response.status_code)
        exp_trip_nks = [trip.nk for trip in trips]
        act_trip_nks = [trip.get('nk') for trip in response.data]
        self.assertCountEqual(exp_trip_nks, act_trip_nks)

Update the imports as well:

# example/tests/test_http.py
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.test import APIClient, APITestCase

from example.serializers import TripSerializer, UserSerializer # new
from example.models import Trip # new

Our test creates two trips and then makes a call to the "trip list" API, which should successfully return the trip data.

For now, the tests should fail:

(env)$ python manage.py test example.tests

Error:

ImportError: cannot import name 'TripSerializer'

We have a lot of work to do in order to get the tests passing.

Model

First, we need to create a model that represents the concept of a trip. Update the example/models.py file as follows:

# example/models.py
import datetime # new
import hashlib # new

from django.db import models # new
from django.shortcuts import reverse # new
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    pass


class Trip(models.Model): # new
    REQUESTED = 'REQUESTED'
    STARTED = 'STARTED'
    IN_PROGRESS = 'IN_PROGRESS'
    COMPLETED = 'COMPLETED'
    STATUSES = (
        (REQUESTED, REQUESTED),
        (STARTED, STARTED),
        (IN_PROGRESS, IN_PROGRESS),
        (COMPLETED, COMPLETED),
    )

    nk = models.CharField(max_length=32, unique=True, db_index=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    pick_up_address = models.CharField(max_length=255)
    drop_off_address = models.CharField(max_length=255)
    status = models.CharField(
        max_length=20, choices=STATUSES, default=REQUESTED)

    def __str__(self):
        return self.nk

    def get_absolute_url(self):
        return reverse('trip:trip_detail', kwargs={'trip_nk': self.nk})

    def save(self, **kwargs):
        if not self.nk:
            now = datetime.datetime.now()
            secure_hash = hashlib.md5()
            secure_hash.update(
                f'{now}:{self.pick_up_address}:{self.drop_off_address}'.encode(
                    'utf-8'))
            self.nk = secure_hash.hexdigest()
        super().save(**kwargs)

Since a trip is simply a transportation event between a starting location and a destination, we included a pick-up address and a drop-off address. At any given point in time, a trip can be in a specific state, so we added a status to identify whether a trip is requested, started, in progress, or completed. Lastly, we need to have a consistent way to identify and track trips that is also difficult for someone to guess. So, we used an MD5 hash as a natural key for our Trip model.

We want our model to generate natural keys on its own, based on the time that the record is created and the pick-up and drop-off addresses. We will enforce this later on with our Trip serializer.

Let's make a migration for our new model and run it to create the corresponding table.

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Serializer

Like the user data, we need a way to serialize the trip data to pass it between the client and the server, so add a new serializer to the bottom of the example/serializers.py file:

# example/serializers.py
class TripSerializer(serializers.ModelSerializer):
    class Meta:
        model = Trip
        fields = '__all__'
        read_only_fields = ('id', 'nk', 'created', 'updated',)

Add at the top the import:

from .models import Trip

By identifying certain fields as "read only", we can ensure that they will never be created or updated via the serializer. In this case, we want the server to be responsible for creating the id, nk, created, and updated fields.

View

Add the TripView to example/apis.py:

# example/apis.py
class TripView(viewsets.ReadOnlyModelViewSet):
    permission_classes = (permissions.IsAuthenticated,)
    queryset = Trip.objects.all()
    serializer_class = TripSerializer

As you can see, our TripView is incredibly basic. We leveraged the DRF ReadOnlyModelViewSet to support our trip list and trip detail views. For now, our view will return all trips. Note that like the LogOutView, a user needs to be authenticated in order to access this API.

Update the imports like so:

# example/apis.py
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.forms import AuthenticationForm
from rest_framework import generics, permissions, status, views, viewsets # new
from rest_framework.response import Response

from .models import Trip # new
from .serializers import TripSerializer, UserSerializer # new

URLs

Include the trip-specific URL configuration in the main URLs file, example_taxi/urls.py:

# example_taxi/urls.py
from django.contrib import admin
from django.urls import include, path # new

from example.apis import SignUpView, LogInView, LogOutView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/sign_up/', SignUpView.as_view(), name='sign_up'),
    path('api/log_in/', LogInView.as_view(), name='log_in'),
    path('api/log_out/', LogOutView.as_view(), name='log_out'),
    path('api/trip/', include('example.urls', 'trip',)), # new
]

Then, add our first trip-specific URL, which enables our TripView to provide a list of trips. Create an example/urls.py file and populate it as follows:

# example/urls.py
from django.urls import re_path

from .apis import TripView

app_name = 'example_taxi'

urlpatterns = [
    re_path('', TripView.as_view({'get': 'list'}), name='trip_list'),
]

If curious, you can read more about the need to set app_name here.

When we run our tests again, we get our list of trips:

(env)$ python manage.py test example.tests

Single Trip

Our next, and last, HTTP test covers the trip detail feature. With this feature, users are able to retrieve the details of a trip identified by its natural key (nk) value.

Add the following test to HttpTripTest in example/tests/test_http.py:

# example/tests/test_http.py
class HttpTripTest(APITestCase):
    ...
    def test_user_can_retrieve_trip_by_nk(self):
        trip = Trip.objects.create(pick_up_address='A', drop_off_address='B')
        response = self.client.get(trip.get_absolute_url())
        self.assertEqual(status.HTTP_200_OK, response.status_code)
        self.assertEqual(trip.nk, response.data.get('nk'))

Here, we leveraged the use of the handy get_absolute_url function on our Trip model to identify the location of our Trip resource. We added asserts that get the serialized data of a single trip and a success status.

Of course, we create a failing test to begin:

(env)$ python manage.py test example.tests

Error:

django.urls.exceptions.NoReverseMatch: Reverse for 'trip_detail' not found.
'trip_detail' is not a valid view function or pattern name.

Update the Tripview in example/apis.py, like so:

# example/apis.py
class TripView(viewsets.ReadOnlyModelViewSet):
    lookup_field = 'nk' # new
    lookup_url_kwarg = 'trip_nk' # new
    permission_classes = (permissions.IsAuthenticated,)
    queryset = Trip.objects.all()
    serializer_class = TripSerializer

Supporting our new functionality is as easy as adding two variables to our TripView:

  1. The lookup_field variable tells the view to get the trip record by its nk value.
  2. The lookup_url_kwarg variable tells the view what named parameter to use to extract the nk value from the URL.

Add the URL to example/urls.py:

# example/urls.py
from django.urls import re_path

from .apis import TripView

app_name = 'example_taxi'

urlpatterns = [
    re_path(r'^$', TripView.as_view({'get': 'list'}), name='trip_list'),
    re_path(r'^(?P<trip_nk>\w{32})/$', TripView.as_view(
        {'get': 'retrieve'}), name='trip_detail'), # new
]

In our URL configuration, we identified a trip_nk, which should be 32 characters long—the length of the MD5 hash.

Ensure the tests pass:

(env)$ python manage.py test example.tests



Mark as Completed