Welcome back to part 3 of the series! Previously we dived into some details of building the frontend. In this post we will cover some of the Django features that I learnt during the project.


Table of Contents

Pre-requisite Knowledge

Django essentially provides you with a way to define your data models and pass them into views, which in its most basic form are HTML files with Jinja templating.

The biggest difference between a basic Django setup vs. our application is that we’ll be using the Django rest framework (DRF) to build our APIs for the frontend.

Similar to part 2 of the series, I won’t go over the basics of Django. You can read through my beginner tutorial or watch a tutorial on Youtube. I highly recommend Corey Schaufer’s tutorial. 🙃

Setting up a venv

Out of habit, you should always set up a virtual environment when using Python for something more than just a simple script.

Dependency management can be a nightmare once the application grows.

I used virtualenv here but actually I think pipenv is a more modern package.

  1. Make sure you’re in the root directory of the project (ie. one directory above where this README is). You can check this with

    $ pwd

  2. Install virtualenv:

    $ pip3 install virtualenv

  3. Create a virtual environment:

    $ virtualenv venv

  4. Create a shortcut to activating your venv:

    $ echo . ./venv/bin/activate > activate.sh

  5. Activate your virtual environment:

    $ source activate.sh

Initial Setup & Useful Commands

I find it convenient to collect the list of common commands.

Since Django loads a specific environment based on your [settings.py](http://settings.py) (which by the way, we’ll see later how to split it into distinct files for your dev and prod deployments), if you wanted to test some Python code quickly in the command line, you can run

# Settings path example: /app/settings.py 
python3 manage.py shell --settings="<path-to-file>"

# Then in the terminal you can import all your django 
# app assets and use them. 
$ > from app.model import Test

Sometimes you just want to check your database instead. You can jump into a command line shell for your local SQLite by executing the command below.

###
# Settings path example: /app/settings.py 
#
# When working locally, I wipe the database clean and re-populate
# it everytime I make a schema change.
#
# I used this to check that my tables and data were created
# correctly after I run my migrations.
###
python3 manage.py dbshell --settings="<path-to-file>"

Multiple setting files

Often you’ll need different settings for local, dev and prod environments. It’s very easy to separate (and then overwrite the config) in a subsequent file.

# /backend/app_name/settings/base.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent

ALLOWED_HOSTS = [
    "127.0.0.1",
    "localhost",
]

INSTALLED_APPS = [
    "corsheaders",
    "django.contrib.admin",
    ...
    'rest_auth.registration',
    "app_user",
    "content",
]

BASE_MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    ...
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# /backend/app_name/settings/dev.py
import dotenv
import os

# Here we grab everything in base and then overwrite
from .base import *

ALLOWED_HOSTS += [
  'app-dev.com'
]

# If you're 100% augmenting base, you can simply 
# add the 'BASE_' prefix.
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
] + BASE_MIDDLEWARE

CORS_ALLOWED_ORIGINS = [
    "http://127.0.0.1:3000",
    "http://localhost:3000",
    "http://127.0.0.1:8000",
    "http://localhost:8000",
]

You just need to make a slight change in the [wsgi.py](http://wsgi.py) file.

"""
WSGI config for app project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

# Here, you need to add a deployment variable to determine which settings to apply.
DEPLOY_ENV_VAR = os.environ['DEPLOY_ENV']
os.environ.setdefault(
    "DJANGO_SETTINGS_MODULE",
    f"threehundredselective.settings.{DEPLOY_ENV_VAR}"
)

application = get_wsgi_application()

Note: You’ll need to have DEPLOY_ENV set in your environment (we will discuss how to set this up during deployment in part 4).

This is also why we used python3 manage.py shell --settings="<path-to-file>" earlier, as sometimes I would also test dev settings locally.

Django Rest Framework

To configure our Django application as a backend API, we’ll need to install the Django Rest Framework. Let’s install it first and add it to our settings file.

pip3 install djangorestframework
# /backend/app_name/settings/base.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent

ALLOWED_HOSTS = [
    ...
]

INSTALLED_APPS = [
    "rest_framework",
    ...
]

BASE_MIDDLEWARE = [
    ...
]

Typically we return results to the frontend in JSON. Since we use Django’s input data models, it can considerable manual work to convert the Python classes to JSON format.

To make this easier, we utilise Django’s Serialisers to define how exactly the data in the classes will be returned.

Let’s show an example. We create a data model in the content app and define a serialiser to return it to the frontend.

# /backend/content/models.py

from django.db import models

class Subject(models.Model):
    SUBJECTS = (
        ("M", "Math"),
        ("E", "English"),
        ("S", "Science")
    )
    name = models.CharField(
        max_length=25,
        choices=SUBJECTS,
    )

class Question(models.Model):
    text = models.TextField()
    subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
    explanation = models.TextField(null=True)

class Answer(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    text = models.TextField()
    option = models.IntegerField()
    is_correct = models.BooleanField()  # Easier to use
# /backend/content/serializers.py

from rest_framework import serializers
from .models import (
    Question,
)

class QuestionSerializer(serializers.ModelSerializer):
    subject = serializers.SlugRelatedField(
        many=False, read_only=True, slug_field="name"
    )
    class Meta:
        model = Question
        exclude = ["explanation"]

SlugRelatedField is a method that allows us to specify one of the instance variables (i.e. the slug). For Question, the slugs would be text, subject and explanation.

Because we only need to return the subject’s name, we specify slug_field="name".

The other thing to note is that we exclude the explanation field as this API is called to present the question during a test, and hence we don’t want to return answers.

Now we can return this via an HTTP response.

# /backend/content/views.py

from rest_framework.decorators import api_view

from .models import (
    Question,
)

from .serializers import (
    QuestionSerializer,
)

@api_view(["GET"]
def get_question(request, pk):
    question = Question.objects.get(id=pk)
    qt_serializer = QuestionSerializer(question, many=False)

    return Response(qt_serializer.data)
# /backend/content/urls.py

from django.urls import path

from content import views

urlpatterns = [
    path("question/<str:pk>/", views.get_question, name="api-get-question"),
]

Permissions in Django

I distinctively recall watching many tutorials about permissions and how to protect certain APIs behind different permissions.

In the end I settled on subclassing the inbuilt BasePermission class with a simple check.

from rest_framework.permissions import BasePermission

class StudentPerm(BasePermission):
    def has_permission(self, request, view):
        if request.user.has_perm("is_student"):
            return True
        return False

class AdminPerm(BasePermission):
    def has_permission(self, request, view):
        if request.user.has_perm("is_admin"):
            return True
        return False

So it’s pretty straight forward, as you can see. We define a class for each permission scope and then add it as decorator config in views.py

from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated

from .permissions import StudentPerm, AdminPerm

from .models import (
    Question,
)

from .serializers import (
    QuestionSerializer,
)

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def api_overview(request):
    api_urls = {
        "Get a single question": "/api/v1/content/question/<id>/"
    }

    return Response(api_urls)

@api_view(["GET"]
@permission_classes([StudentPerm, AdminPerm])
def get_question(request, pk):
    question = Question.objects.get(id=pk)
    qt_serializer = QuestionSerializer(question, many=False)

    return Response(qt_serializer.data)

So here we can define an array of permission classes for each API route. If the API can be called with any authenticated user, you can use the inbuilt IsAuthenticated.

The last thing is to create a user with the appropriate permissions

from django.contrib.auth.models import User, Permission

name = 'john'
student = User.objects.create_user(
    username=name,
    email=f"local-{name}@test.com",
    password=f"{name}12345"
)
student.save()

student_role = Permission.objects.get(codename="is_student")
student.user_permissions.add(student_role)

Token Authentication

Now we discuss the authentication mechanism used in this application. To enable integrations with other 3rd party applications, I decided to use token authentication as it was easier to understand and implement.

First we install a few packages

pip3 install django-rest-auth django-allauth

We need to add the following packages to settings.py/base.py

# /backend/app_name/settings/base.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent

ALLOWED_HOSTS = [
    ...
]

INSTALLED_APPS = [
    "rest_framework",
    'rest_framework.authtoken',
    'rest_auth',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'rest_auth.registration',
    ...
]

BASE_MIDDLEWARE = [
    ...
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
    ),
}

AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
)

SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_SESSION_REMEMBER = True
###
# We needed to remove this to get our system to work. 
###
# ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_UNIQUE_EMAIL = True
LOGOUT_ON_PASSWORD_CHANGE = True

To create a user and log in, you simply ping the following urls:

# /api/v1/auth/rest-auth/registration/

{
  username: "triplestackdata",
  email: "triplestackdata@gmail.com",
  password1: "password",
  password2: "password",
}
# /api/v1/rest-auth/login/

{
  username: "triplestackdata",
  email: "triplestackdata@gmail.com",
  password: "password"

}

Once you log in, the POST request should return a key

{
  key: "......"
}

and this is the token that you need to call the APIs.

Trailer

In the next instalment, we’ll look at how to deploy the web application using AWS Elastic Beanstalk.

The post will include:

  • How to building React files into the Django code
  • Configuring Django for deployment
  • Configuring AWS EB for deployment
  • Pains with AWS EB

UPDATE: You can read part 4 here