Code Snippets for Django Development

This post contains a few code snippets I came up with when starting development of a Django web application.

Using Django with Jinja Templates

By default Django uses its own template engine to generate HTML output. While it might be fast and sufficient for most tasks, I think it lacks one really useful feature: macros. Macros in Jinja are parameterized templates which can be "called" in other templates. This helps very much to ensure a consistent structure and layout of the HTML.

To enable the Jinja template engine in addition to the Django template engine (you will stil need this, e.g. for admin pages, etc.), you need to update your sessings file and additionally write a function to provide the Environment instance to Jinja:

Here's the settings file:

TEMPLATES = [
    {
        # Configuration for Django templates
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [os.path.join(BASE_DIR, 'loc/templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'environment': 'dbloc.jinja2.environment',
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
    {
        # Configuration for Jinja2 templates
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

The line 'environment': 'dbloc.jinja2.environment', references a function which supplies the Jinja environment. I have implemented it in a file jinja2.py in my project directory (here called dbloc):

from django.templatetags.static import static
from django.urls import reverse

from jinja2 import Environment

def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': static,
        'url': reverse,
    })

    return env

Logging Setup

As I run my web application inside a Docker container, I came up with the following setup for logging. All messages are logged to STDOUT, so they are captured by the docker daemon. And I decided to colorize messages, so especially during debugging sessions it is easier to distinguish a flood of debug messages from any important warning or error messages. For colored logs to work you need to install the colorlog package.

Here's the fragment from the Django project settings related to logging setup:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,

    'formatters': {
        # verbose log message formatter
        'verbose': {
            '()': 'colorlog.ColoredFormatter',
            'format': '[{asctime}] {log_color}{levelname:<8}{reset} {name}.{funcName}:{lineno} {message}',
            'datefmt': '%Y-%m-%d %H:%M:%S',
            'style': '{',
        },

        # simple log message format, currently not in use
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },

    'handlers': {
        # log messages to console (stdout)
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'verbose',
        },

        # Handler to log everything up to DEBUG level to file with log
        # rotation. Currently not in use, as docker container handles log data
        # written to console
        'file': {
            'level': 'DEBUG',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': os.path.abspath(os.path.join(BASE_DIR, 'log', 'varweb.log')),
            'formatter': 'verbose',
            'maxBytes': 1024 * 1024 * 5, # 5 MB
            'backupCount': 5,
        },
    },

    # root logger catches all log messages. Everything warning and above is sent
    # to the console and to the log file. Change the level e.g. to 'DEBUG' for
    # the development settings
    'root': {
        'handlers': ['console'],
        'level': 'WARNING',
    },

    # Loggers below the django hierarchy log to console with warning level, but
    # do not propagate to the root logger
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': os.getenv('DJANGO_LOG_LEVEL', 'WARNING'),
            'propagate': False,
        },
    },
}

Dockerfile for Django Project

Here's and example to package your Django project into a Docker container based on Alpine linux. This leads to nice small container images.

Here's the Dockerfile:

FROM python:3.8-alpine

ENV PYTHONUNBUFFERED 1
ENV APP_DIR /usr/src/app

# Patches for apk and pip to use Chinese mirrors
COPY tools/patches/pip.conf /etc/xdg/pip/pip.conf
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

# Install nginx
RUN apk update \
        && apk --no-cache add nginx \
        && mkdir /run/nginx
COPY tools/nginx.conf /etc/nginx/conf.d/default.conf

RUN mkdir -p ${APP_DIR} ${APP_DIR}/media
WORKDIR /${APP_DIR}

# Install python packages (needs -dev packages for building, can be removed later)
ADD requirements ${APP_DIR}/requirements
RUN apk --no-cache add build-base jpeg-dev zlib-dev musl-dev \
        && python3 -m pip install --no-cache-dir -r requirements/prod.txt \
        && apk --no-cache del build-base jpeg-dev zlib-dev musl-dev \
        && apk --no-cache add musl zlib jpeg \
        && rm -rf /var/cache/apk/*

# Add the application
ADD . ${APP_DIR}
RUN python3 manage.py collectstatic --no-input

EXPOSE 80

CMD ["./tools/start-server.sh"]

As I'm living in China, I patch the package sources for apk (the Alpine Linux package manager) and pip to fetch packages from fast Chinese mirrors, as the default sources are extremely slow when accessed from the Chinese internet.

In this example I have Pillow, the Python image library, as one dependency. When it is installed for Alpine Linux, it is built from source (parts of it are implemented in C). To be able to do this while building the container image, I have one RUN command with these steps:

  • first install all build dependencies (apt add jpeg-dev zlib-dev ...),

  • then install the required Python packages with python3 -m pip install ...,

  • then remove the build dependencies (apk del build-base ...) and replace them by the corresponding runtime libraries (apt add musl zlib jpeg)

I did not fully trust the --no-cache option to apk and still remove the complete cache directory at the end.

Currently with Python 3.8 and a very simple Django application, the Docker image size is 154MB.

Application Start Script respecting CTRL-C

Initially I had some trouble to set up the Docker container, so that I can easily terminate it with Ctrl-C. Most of the time the container waits for ca. 10 seconds before (I guess forcefully) terminating the application.

To fix this, there are two things to observe:

  • The CMD line in the Dockerfile must use the Javascript array notation ["..."].

  • The actual start script, a shell script in my case, must use exec.

This ensures that the main process of your application is the one receiving signals like SIGTERM or SIGINT (which is sent when you press Ctrl-C) instead of any wrapper process or a shell instance.

Here's the start-server.sh script I use:

#!/usr/bin/env sh
#
# Start script for running inside docker container
#

# Set up database
python3 manage.py migrate

# Create admin user (password is shown during startup in the logs)
python3 manage.py initadmin

# If nothing else is set already, we run the testing config for the container
if [ -z "${DJANGO_SETTINGS_MODULE}" ]
then
    export DJANGO_SETTINGS_MODULE=dbloc.settings.testing
fi

# Start nginx for serving static and media files and act as a reverse proxy for
# gunicorn. It starts as a daemon automatically.
nginx -c /etc/nginx/nginx.conf

exec gunicorn dbloc.wsgi

Creating an Admin User at First Start

Usually you use the manage.py createsuperuser command to create the first user with admin rights for your application. I wanted to automate this, so I created an additional command for manage.py to automatically create and admin user with a random password. The user is only created if no users exist in the database yet.

'''Admin command implementation for 'initadmin' command.'''

import random
import string

from django.core.management.base import BaseCommand
from django.contrib.auth.models import User


def gen_password(length=8):
    '''Generate a random password.'''

    # we use upper and lower case letters and digits for the password
    letters = string.ascii_letters + string.digits

    return ''.join(random.choices(letters, k=length))


class Command(BaseCommand):
    '''Admin command to create an admin user with random password.

    The admin user is only created if no other users are already in the
    database.

    '''

    def handle(self, *args, **options):
        if User.objects.count() == 0:
            print('No users yet in database. Creating admin user.')

            username = 'admin'
            email = 'admin@localhost'
            password = gen_password(8)

            print('Creating account for %s (%s)' % (username, email))

            admin = User.objects.create_superuser(
                email=email,
                username=username,
                password=password)

            admin.is_active = True
            admin.is_admin = True

            admin.save()

            print("Account crated with password '{0}'.".format(password))
            print("Please change password immediately.")
        else:
            print('Admin accounts can only be initialized if no Accounts exist')

When starting the application the first time, you can see the initial admin password in the log output. Of course you should immediately change it.

LinkedIn logo mail logo