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
- Setting up a venv
- Initial Setup & Useful Commands
- Multiple setting files
- Django Rest Framework
- Permissions in Django
- Token Authentication
- Trailer
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.
-
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
-
Install
virtualenv
:$ pip3 install virtualenv
-
Create a virtual environment:
$ virtualenv venv
-
Create a shortcut to activating your venv:
$ echo . ./venv/bin/activate > activate.sh
-
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/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