UI Support

Part 1, Chapter 7

Up until now, we haven't had a reason to track users as drivers or riders. Users can be either. But as soon as we add a UI, we will need a way for users to sign up with a role. Drivers will see a different UI and will experience different functionality than riders.

The first thing we need to do is add support for user groups in our serializers in trips/serializers.py.


First, update the UserSerializer serializer:

# trips/serializers.py

class UserSerializer(serializers.ModelSerializer):
    password1 = serializers.CharField(write_only=True)
    password2 = serializers.CharField(write_only=True)
    group = serializers.CharField() # new

    def validate(self, data):
        if data['password1'] != data['password2']:
            raise serializers.ValidationError('Passwords must match.')
        return data

    def create(self, validated_data): # changed
        group_data = validated_data.pop('group')
        group, _ = Group.objects.get_or_create(name=group_data)
        data = {
            key: value for key, value in validated_data.items()
            if key not in ('password1', 'password2')
        data['password'] = validated_data['password1']
        user = self.Meta.model.objects.create_user(**data)
        return user

    class Meta:
        model = get_user_model()
        fields = (
            'id', 'username', 'password1', 'password2',
            'first_name', 'last_name', 'group', # new
        read_only_fields = ('id',)

Add the import at the top, too.

from django.contrib.auth.models import Group


Next, update the custom user model in trips/models.py to support groups as well:

# trips/models.py

class User(AbstractUser):
    def group(self):
        groups = self.groups.all()
        return groups[0].name if groups else None

We are not adding any database fields so we don't need to create a new migration.


Lastly, add the proper filters to the TripView in trips/views.py:

# trips/views.py

class TripView(viewsets.ReadOnlyModelViewSet):
    lookup_field = 'id'
    lookup_url_kwarg = 'trip_id'
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = TripSerializer

    def get_queryset(self): # new
        user = self.request.user
        if user.group == 'driver':
            return Trip.objects.filter(
                Q(status=Trip.REQUESTED) | Q(driver=user)
        if user.group == 'rider':
            return Trip.objects.filter(rider=user)
        return Trip.objects.none()

Note that we removed the queryset = Trip.objects.all() field.

Add the import at the top:

from django.db.models import Q


Re-run the tests:

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

You should see three failures, with the following errors:

AssertionError: 201 != 400


AssertionError: Element counts were not equal:
First has 1, Second has 0:  '8c1b7e58-6e39-46c2-ac83-b934b6a8c172'
First has 1, Second has 0:  '258f55ed-72c1-488b-bd2d-4ff42cbb8bfd'


AssertionError: 200 != 404

First, within trips/tests/test_http.py, update the create_user() function to take an additional group_name parameter:

# tests/test_http.py

def create_user(username='[email protected]', password=PASSWORD, group_name='rider'):
    group, _ = Group.objects.get_or_create(name=group_name)
    user = get_user_model().objects.create_user(
        username=username, password=password)
    return user

And add the group to the test_user_can_sign_up test in AuthenticationTest:

# tests/test_http.py

def test_user_can_sign_up(self):
    response = self.client.post(reverse('sign_up'), data={
        'username': '[email protected]',
        'first_name': 'Test',
        'last_name': 'User',
        'password1': PASSWORD,
        'password2': PASSWORD,
        'group': 'rider', # new
    user = get_user_model().objects.last()
    self.assertEqual(status.HTTP_201_CREATED, response.status_code)
    self.assertEqual(response.data['id'], user.id)
    self.assertEqual(response.data['username'], user.username)
    self.assertEqual(response.data['first_name'], user.first_name)
    self.assertEqual(response.data['last_name'], user.last_name)
    self.assertEqual(response.data['group'], user.group) # new

Add the import:

from django.contrib.auth.models import Group

Run the tests again. You should see only two failures, with the following errors:


AssertionError: Element counts were not equal:
First has 1, Second has 0:  'd09af51e-040e-4161-954b-2b821dd49b31'
First has 1, Second has 0:  'f67a3d2a-5aa2-4f9e-a467-5c11da7dd617'


AssertionError: 200 != 404

Next, update HttpTripTest:

# tests/test_http.py

class HttpTripTest(APITestCase):

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

    def test_user_can_list_trips(self): # changed
        trips = [
                pick_up_address='A', drop_off_address='B', rider=self.user),
                pick_up_address='B', drop_off_address='C', rider=self.user),
                pick_up_address='C', drop_off_address='D')
        response = self.client.get(reverse('trip:trip_list'))
        self.assertEqual(status.HTTP_200_OK, response.status_code)
        exp_trip_ids = [str(trip.id) for trip in trips[0:2]]
        act_trip_ids = [trip.get('id') for trip in response.data]
        self.assertCountEqual(act_trip_ids, exp_trip_ids)

    def test_user_can_retrieve_trip_by_id(self): # changed
        trip = Trip.objects.create(
            pick_up_address='A', drop_off_address='B', rider=self.user)
        response = self.client.get(trip.get_absolute_url())
        self.assertEqual(status.HTTP_200_OK, response.status_code)
        self.assertEqual(str(trip.id), response.data.get('id'))

After modifications, all tests should pass:

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

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Ran 5 tests in 1.261s

Destroying test database for alias 'default'...

Finally, run the server and test out the DRF Browsable API:

  1. http://localhost:8000/api/sign_up/
  2. http://localhost:8000/api/log_in/

drf sign up page

Mark as Completed