There are many ways to structure a Django project. We’ve opted for a layout that hits, for us, the sweet spot between flexibility and ease-of-use.

This layout is specifically geared towards projects that use a combination of Django, React, and SASS, but works for other projects as well.

The layout consists of a project root (a wrapper around everything), an assets directory (scripts, images, stylesheets etc), an env directory that contains the virtual environment (this should not version controlled, but we keep it in a standardized location to simplify automation between developers), a project directory (where the actual Django project and project-specific apps live), and a couple of other files and directories:

├── .babelrc
├── .gitignore
├── assets
│   ├── images
│   ├── script
│   │   └── app.js
│   └── style
│       └── main.scss
├── db.sqlite3
├── env/ (not version controlled)
├── manage.py
├── myproject
│   ├── app1
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── __init__.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── __init__.py
│   ├── local_settings.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── node_modules/ (not version controlled)
├── package.json
└── package-lock.json

We try to minimize external dependencies in the default layout, but have been using the excellent django-compressor app in many of our projects and decided to standardize the implementation as described below.

Note: An acceptable alternative to integrating the ui with Django using compressor is to see the interface as an external project and use the standard JavaScript stack (WebPack, Gulp/Grunt et al). We prefer to do it our way for a couple of reasons:

  • No Gulp or Grunt.
  • Straight-forward configuration.
  • Allows us to build isolated parts of the system (where it makes sense) using regular Django components (forms, generic classes, templates, sessions etc), while sharing stylesheets and other resources.
  • Developers can clone a single repo and start everything with one command.
  • Once configured, Compressor rarely requires any additional tweaking. This is useful if you have a system with multiple interfaces (external docs, admin-facing ui, client-facing ui, isolated pages etc – just specify the paths to the root files and Compressor will handle everything from transpiling to cache busting automatically).
  • No Gulp or Grunt.

Set up the structure

First create a project root:

mkdir myproject_root

The root directory should contain the actual Django project (project directory), assets, .git directory, README, requirements.txt, license(s), package.json etc.

Enter the directory and create a local virtualenv named env:

cd myproject_root
virtualenv env

Activate the virtualenv and install django and django-compressor:

source env/bin/activate
pip install django django-compressor

Create a couple directories to hold standard assets:

mkdir -p assets/style
mkdir -p assets/script
mkdir -p assets/images

And touch JavaScript and SASS entry points:

touch assets/style/main.scss
touch assets/script/app.js

And, finally, create the actual project (note the trailing dot):

django-admin startproject myproject .

Settings

You’ll need to make a couple of changes to the settings generated by Django. Open settings.py and add the following to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'compressor',
    'myproject',
]

Note: The reason we add myproject to the settings is to find templates placed in the myproject/templates directory we’ll create later. It is also to have a generic app where we can add one-off views (like home) that do not belong to any particular application.

And then add the following to the bottom of the file:

STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'compressor.finders.CompressorFinder',
)

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "assets"),
    os.path.join(BASE_DIR, "node_modules"),
)

STATIC_ROOT = os.path.join(BASE_DIR, 'static')

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

COMPRESS_OFFLINE = False

COMPRESS_OUTPUT_DIR = ''

try:
    from local_settings import *
except ImportError:
    pass

NODE_ENV = 'development' if DEBUG else 'production'

COMPRESS_PRECOMPILERS = (
    ('text/x-scss', 'node_modules/.bin/node-sass {infile} {outfile}'),
    ('text/jsx', 'NODE_ENV={} node_modules/.bin/browserifyinc '
                 '--debug '
                 '-t babelify {{infile}} -o {{outfile}}'.format(NODE_ENV)),
)

if not DEBUG:
    COMPRESS_OFFLINE = True
    COMPRESS_PRECOMPILERS = (
    ('text/x-scss', 'node_modules/.bin/node-sass {infile} {outfile}'),
    ('text/jsx', 'NODE_ENV=production node_modules/.bin/browserify '
                 '-t babelify {infile} -o {outfile}'),
)

The idea is that settings.py should contain whatever settings (excluding API keys and passwords, which are well-documented in the README, naturally) a developer needs to start the project locally. We can then override certain settings and keys by adding them to a local_settings.py file.

Note: There are many different ways to structure settings in Django. Relying on a local, unversioned settings module might feel like an anti-pattern. And it is. At-least if you put logic in the local settings file. We do, however, see it more as a file in which we store things like API keys, passwords, and things we do not want version controlled anyway. We also almost always generate initial settings (e.g. database passwords and allowed hosts) for our server environments automatically during provisioning.

Note: We should rename local_settings.py to settings_local.py to align them in project trees.

Set up the JavaScript Environment

Create a file named package.json in your root directory containing the following (or run npm init and answer the questions):

{
    "name": "myproject"
}

Install JavaScript dependencies:

npm install --save @babel/core @babel/preset-env @babel/preset-react
npm install --save browserify browserify-incremental uglify babelify

Add another file named .babelrc in your root directory if any babel presets were installed:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react",
    ],
}

Continue by installing whatever parts of the react-redux eco system you need. For example:

npm install --save react redux redux-saga

And development utils:

npm install --save-dev eslint eslint-config-airbnb eslint-plugin-importeslint-plugin-react

Set up the CSS Environment

npm install --save node-sass

Create a Base Template

First create the template directory:

mkdir -p myproject/templates

We then typically create a template named base.html that contains the basic stuff that we need:

{% load static %}
{% load compress %}

<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
        <title>{% block title %}{% endblock %}</title>
        {% compress css %}
        <link rel="stylesheet" href="{% static 'style/main.scss' %}" type="text/x-scss" media="screen">
        {% endcompress %}
    </head>

    <body class="{% block bodyclass %}{% endblock %}">
        {% block content %}{% endblock %}

        {% compress js %}
        <script type="text/javascript" src="{% static 'script/app.js' %}"></script>
        {% endcompress %}
    </body>
</html>

This template can be extended by other templates and content injected using the block named content.

.gitignore

While you’re add it, add the following to your .gitignore:

/env
/media
/static
/db.sqlite3
/node_modules

local_settings.py
.module-cache
.sass-cache
.tmp

.DS_Store
__pycache__
*.py[co]

Commonly Used Features, Conveniences, and Patterns

Serving Media Files Using the Development Server

You need to add the following to the bottom of urls.py in order to serve media files using Django’s development server:

from django.conf.urls.static import static
from django.conf import settings

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Adding a Body Class

You might’ve noticed that we have a block named bodyclass in the HTML body element. It can be used in any template extending base.html to add one or more classes.

A very common pattern is to add classes to the body to aid in overriding generalized styling of components or even highlighting links in menus.

If you have a detail page for a blog entry, for example, you can add the following two classes to it:

  • .blog-page: because the page is part of the blog
  • .blog-entry-detail-page: because the page is displaying a specific entry

It could look something like:

{% extends "base.html" %}

{% block bodyclass %}blog-plage blog-detail-page{% endblock %}

{% block content %}
    ...
{% endblock %}

If you also have an element in your main navigation with the class .blog-link you can now use the following css to highlight the link whenever a page with .blog-page is displayed:

.blog-page .blog-link a {
    color: tomato;
}

Or, if you have styled an .author-info component that you use in a couple of places but want to add some styles specific to the entry detail page:

.blog-entry-detail-page .author-info {
    padding: 1.5rem;
}

URLs

Refrain from declaring url patterns directly in the root urls file (myproject.urls). Make use of namespaces and includes where it makes sense instead.

urlpatterns = [
    # bad:
    path('blog/', views.entry_list, name='entry_list'),

    # good:
    path('blog/', include('trell.blog.urls', namespace='blog')),
]

Use DRF for APIs

We are heavy proponents of django-restframework. It is well documented, supported, has a vibrant eco system of plugins, integrates well with different Django components, and is, in general, a pleasure to work with.

Debug your projects

Debug and profile your views/endpoints and queries with django-debug-toolbar (for regular Django views) or django-silk (DRF projects).

E-mail on errors

A very useful feature in Django is it’s ability to send emails containing stacktraces to administrators of the site. Add yourself to ADMINS:

ADMINS = (
    ('Gustaf', 'gs@trell.se'),
)

And set SERVER_EMAIL to the verified e-mail address used to send mail from the server:

SERVER_EMAIL = 'info@trell.se'
Related entries from the blog
Posted in Processes on May 23, 2018
Posted in Processes on Apr 12, 2018