Architecture and Configuration of a Real Project

First lesson of the Complete Application with Django course, free to read: about 18 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Split a project into coherent apps and define a custom user model from the start
  • Move configuration (secrets, DEBUG, hosts, database) into environment variables
  • Organize templates, static files and uploaded files

Project architecturePer-environment configurationCustom user model

The running project

In this course, you build Atelier, a project management application: each user creates projects, adds labeled tasks to them, invites members, and views a dashboard. These are the needs of a real SaaS: accounts, permissions, relational data, a responsive interface, a JSON API, tests and production deployment.

Splitting into apps

atelier/
├── manage.py
├── config/                 # settings, root URLs, wsgi/asgi
│   ├── settings.py
│   └── urls.py
├── comptes/                # user model, sign-up, profile
├── projets/                # Projet, Tache, Etiquette, views and API
├── templates/              # base.html and shared templates
├── static/                 # the project's CSS, JS, images
├── .env.example            # expected variables, WITHOUT secret values
└── requirements.txt

One app = one coherent functional domain. If an app grows too large, split it; if two apps constantly depend on each other, merge them.

The custom user model

The Django documentation recommends defining a custom user model from the very start, even one identical to the default: changing it after the first migration is very tricky.

# comptes/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class Utilisateur(AbstractUser):
    # fields added later without pain: avatar, time zone...
    bio = models.TextField(blank=True)
# config/settings.py
AUTH_USER_MODEL = "comptes.Utilisateur"   # BEFORE the first migrate

Everywhere else, you never reference User directly: use models.ForeignKey(settings.AUTH_USER_MODEL, ...) in models and get_user_model() in the rest of the code.

Settings come from the environment

The same code runs in development, testing and production: only the environment variables change. Secrets are never committed.

# config/settings.py
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured

BASE_DIR = Path(__file__).resolve().parent.parent

DEBUG = os.environ.get("DJANGO_DEBUG", "False").lower() in {"1", "true", "yes", "on"}

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
if not SECRET_KEY:
    if not DEBUG:
        raise ImproperlyConfigured("DJANGO_SECRET_KEY missing")   # fail early
    SECRET_KEY = "dev-only-not-for-production"

ALLOWED_HOSTS = [h for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h]
if DEBUG and not ALLOWED_HOSTS:
    ALLOWED_HOSTS = ["localhost", "127.0.0.1"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("POSTGRES_DB", "atelier"),
        "USER": os.environ.get("POSTGRES_USER", "atelier"),
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""),
        "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
        "PORT": os.environ.get("POSTGRES_PORT", "5432"),
    }
}

DEBUG is False by default: forgetting a variable must never expose the detailed error pages. Packages like django-environ or dj-database-url simplify reading these values (in particular from a DATABASE_URL), but the principle stays the same. In development, an unversioned .env file can be loaded by your tooling (for example python-dotenv); a versioned .env.example documents the expected variables.

Templates, static files and media

TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"],          # shared templates (base.html)
    "APP_DIRS": True,                          # + app/templates/ of each app
    "OPTIONS": {"context_processors": [
        "django.template.context_processors.request",
        "django.contrib.auth.context_processors.auth",
        "django.contrib.messages.context_processors.messages",
    ]},
}]

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]       # sources
STATIC_ROOT = BASE_DIR / "staticfiles"         # collectstatic destination (production)
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"                # files uploaded by users

Static files: the project’s files (CSS, JS), versioned and collected at deployment. Media: files uploaded by users, never versioned, stored on a persistent disk or an object storage service.

Key takeaways

  • One app per domain; a custom AUTH_USER_MODEL before the first migration.
  • Configuration through environment variables, DEBUG=False by default, explicit failure if a secret is missing.
  • Static ≠ media.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 2Python in the browser2 hints

    Load the configuration from the environment

    Write charger_reglages(env) (env is a dict) that returns {"DEBUG", "SECRET_KEY", "ALLOWED_HOSTS", "DATABASES"}:

    • DEBUG: true if DJANGO_DEBUG is 1, true, yes or on (case-insensitive), false by default;
    • SECRET_KEY: DJANGO_SECRET_KEY; if missing, raise ImproperlyConfigured when not in DEBUG, otherwise use a development value;
    • ALLOWED_HOSTS: DJANGO_ALLOWED_HOSTS split on commas (spaces stripped, empty items ignored); if empty and DEBUG, ["localhost", "127.0.0.1"];
    • DATABASES["default"]: SQLite (ENGINE django.db.backends.sqlite3, NAME db.sqlite3) if DATABASE_URL is missing, otherwise parse a postgres://user:mdp@hote:port/base URL into ENGINE (django.db.backends.postgresql), NAME, USER, PASSWORD, HOST, PORT (a string, 5432 by default). Any other scheme raises ImproperlyConfigured.
    from urllib.parse import urlsplit
    
    
    class ImproperlyConfigured(Exception):
        pass
    
    
    def charger_reglages(env):
        ...
  2. Multiple choiceDifficulty 21 hint

    User model

    When should you set AUTH_USER_MODEL to a custom model?

    • When you need a new field
    • Before the very first migration of the project
    • Just before going to production
    • Never, you should keep User
  3. TaskDifficulty 22 hints

    Atelier project skeleton

    In your working environment: create the config project, the comptes and projets apps, the Utilisateur(AbstractUser) model and AUTH_USER_MODEL, then run the first migration. Configure settings.py to read DJANGO_DEBUG, DJANGO_SECRET_KEY and DJANGO_ALLOWED_HOSTS from the environment, add a .env.example and a .gitignore (.env, db.sqlite3, media/, staticfiles/). Create templates/base.html with a responsive container.

    • python manage.py migrate creates the comptes_utilisateur table
    • Starting the server without DJANGO_SECRET_KEY and without DJANGO_DEBUG fails with ImproperlyConfigured
    • No secret appears in versioned files
    • base.html is used by a home page of the projets app

End-of-lesson quiz

  1. How do you reference the user in a ForeignKey?

    • models.ForeignKey(User, ...)
    • models.ForeignKey(settings.AUTH_USER_MODEL, ...)
    • models.ForeignKey("auth.User", ...)
  2. What default value should DEBUG have?

    • True
    • False
    • None
  3. Where do files uploaded by users go?

    • STATIC_ROOT
    • MEDIA_ROOT
    • In the Git repository

The rest of the course

  1. Authentication and Authorization20 min
  2. ORM Relationships and Performance20 min
  3. Generic Views, JSON API and Responsive Interface20 min
  4. Tests, Security and Going to Production20 min

Complete Application with Django · Advanced · 5 h 38 min