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_MODELbefore the first migration. - Configuration through environment variables,
DEBUG=Falseby 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.
Load the configuration from the environment
Write
charger_reglages(env)(env is a dict) that returns{"DEBUG", "SECRET_KEY", "ALLOWED_HOSTS", "DATABASES"}:DEBUG: true ifDJANGO_DEBUGis1,true,yesoron(case-insensitive), false by default;SECRET_KEY:DJANGO_SECRET_KEY; if missing, raiseImproperlyConfiguredwhen not in DEBUG, otherwise use a development value;ALLOWED_HOSTS:DJANGO_ALLOWED_HOSTSsplit on commas (spaces stripped, empty items ignored); if empty and DEBUG,["localhost", "127.0.0.1"];DATABASES["default"]: SQLite (ENGINEdjango.db.backends.sqlite3,NAMEdb.sqlite3) ifDATABASE_URLis missing, otherwise parse apostgres://user:mdp@hote:port/baseURL intoENGINE(django.db.backends.postgresql),NAME,USER,PASSWORD,HOST,PORT(a string, 5432 by default). Any other scheme raisesImproperlyConfigured.
from urllib.parse import urlsplit class ImproperlyConfigured(Exception): pass def charger_reglages(env): ...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
Atelier project skeleton
In your working environment: create the
configproject, thecomptesandprojetsapps, theUtilisateur(AbstractUser)model andAUTH_USER_MODEL, then run the first migration. Configuresettings.pyto readDJANGO_DEBUG,DJANGO_SECRET_KEYandDJANGO_ALLOWED_HOSTSfrom the environment, add a.env.exampleand a.gitignore(.env,db.sqlite3,media/,staticfiles/). Createtemplates/base.htmlwith 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
How do you reference the user in a ForeignKey?
- models.ForeignKey(User, ...)
- models.ForeignKey(settings.AUTH_USER_MODEL, ...)
- models.ForeignKey("auth.User", ...)
What default value should DEBUG have?
- True
- False
- None
Where do files uploaded by users go?
- STATIC_ROOT
- MEDIA_ROOT
- In the Git repository
The rest of the course
- Authentication and Authorization20 min
- ORM Relationships and Performance20 min
- Generic Views, JSON API and Responsive Interface20 min
- Tests, Security and Going to Production20 min