🐍 Django – Top 25 Addons Populaires
Guide IDEO‑Lab des 25 packages les plus utiles pour vos projets Django.
Django REST Framework
Powerful toolkit to build Web APIs.
APIRESTSerializersDjango Compressor
Powerful toolkit to Compress JS and CSS.
APICompressDevOpsDjango Environ
Manage your Django Settings.
APISettingsDevOpsDjango Debug Toolbar
Debug panel for development.
DebugSQLDevDjango Redis
Redis Server Access for Django.
CacheStorageDevOpsDjango Allauth
Complete authentication management (local, social).
AuthSocialUsersDjango Crispy Forms
Elegant form rendering.
FormsTemplatesCelery
Async tasks and queue management.
AsyncTasksWorkersDjango Channels
WebSockets and async protocols support.
WebSocketsAsyncDjango-filter
QuerySet filtering based on URL queries.
FilterQuerySetPillow
Image processing library (ImageField).
ImagesUploadsDjango-Taggit
Tag management for any model.
TagsModelsDjango-Storages
Storage backends for S3, Azure, GCS, etc.
StorageS3MediaSentry-SDK
Error monitoring integration.
ErrorsLoggingDjango-CORS-Headers
CORS headers management for cross-domain requests.
CORSAPISecurityWhitenoise
Simplified static files serving in production.
StaticDeployDjango-Simple-History
Automatic model history tracking.
AuditHistoryDjango-Extensions
Management commands and enhanced shell.
manage.pyShellDjango-Import-Export
Import/export data via admin.
AdminDataDjango-Guardian
Object-level permissions management.
PermissionsAuthDjango-Two-Factor-Auth
Two-factor authentication integration.
2FASecurityDjango-HTMX
HTMX integration for dynamic pages without heavy JS.
HTMXFrontendDjango-Select2
Select2 widgets integration (autocomplete).
FormsWidgetsDjango-Jazzmin
Modern responsive theme for Django admin.
AdminThemeDjango-Waffle
Feature flags management.
FeaturesFlagsDjango-Silk
Profiling and inspection of API/SQL requests.
ProfilingDebugDjango-Model-Utils
Useful model fields and mixins.
ModelsUtilsDjango-BAT
Track and block failed login attempts.
SecurityLogin12-factor settings: read config from environment (and optional .env)
django-environ is a small library that makes Django settings cleaner by reading values from environment variables (and optionally a .env file). It helps separate configuration from code, improves deployment portability, and reduces “it works on my machine” problems.
- Different settings per environment (dev/staging/prod) without branching code.
- Typed environment parsing (bool/int/list/url/db).
- Centralized config: database, caches, email, debug flags.
- Cleaner secrets handling (no secrets committed to git).
- Consistent deployments on servers and containers.
- Env vars override defaults.
.envis for local/dev only.- Production secrets come from the platform (systemd, Docker, CI/CD, vault).
- Fail fast if critical vars are missing.
.env with secrets to version control. Put it in .gitignore.Install
pip install django-environ
settings.py.# .gitignore
.env
.env.*
*.envSettings pattern
base.py + dev.py + prod.py while keeping the same env parsing approach.Minimal baseline (single settings.py)
from pathlib import Path
import environ
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env(
DEBUG=(bool, False),
SECRET_KEY=(str, ""),
ALLOWED_HOSTS=(list, []),
)
# Optional: load .env in local/dev only
# env.read_env(BASE_DIR / ".env")
DEBUG = env("DEBUG")
SECRET_KEY = env("SECRET_KEY")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY is missing")Database parsing (DATABASE_URL)
DATABASES = {
"default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3")
}Common environment variables
| Variable | Purpose | Example |
|---|---|---|
DJANGO_DEBUG or DEBUG | Enable debug mode (dev only) | DEBUG=true |
SECRET_KEY | Django cryptographic secret | SECRET_KEY=long-random-string |
ALLOWED_HOSTS | Host allowlist | ALLOWED_HOSTS=example.com,api.example.com |
DATABASE_URL | Database DSN | postgres://user:pass@host:5432/dbname |
CACHE_URL (optional) | Cache DSN | redis://localhost:6379/1 |
CSRF_TRUSTED_ORIGINS | CSRF trusted origins | https://example.com,https://*.example.com |
SECURE_PROXY_SSL_HEADER (optional) | Reverse proxy SSL header | HTTP_X_FORWARDED_PROTO,https |
Example .env (development only)
DEBUG=true
SECRET_KEY=dev-only-secret
ALLOWED_HOSTS=127.0.0.1,localhost
DATABASE_URL=sqlite:///db.sqlite3Secrets & safety
| Risk | Impact | Mitigation |
|---|---|---|
| Secrets committed to git | Permanent leak | Never commit .env, use a vault/CI secrets |
| Secrets in logs | Leak via monitoring | Never print env vars; sanitize debug output |
| Weak SECRET_KEY | Session/signing compromise | Generate strong random secret; rotate with care |
| Mis-set DEBUG in production | Information leak | Hard fail if DEBUG is true in prod environment |
ENVIRONMENT = env("ENVIRONMENT", default="dev") # dev|staging|prod
if ENVIRONMENT == "prod" and DEBUG:
raise RuntimeError("DEBUG must be false in production")Deploy playbook
- Define environment variables in the platform (systemd/Docker/CI secrets).
- Verify required vars exist (SECRET_KEY, DATABASE_URL, ALLOWED_HOSTS).
- Run migrations.
- Run collectstatic.
- Restart the service.
- Smoke test critical endpoints.
systemd example (concept)
# /etc/systemd/system/myapp.service (concept)
[Service]
Environment="ENVIRONMENT=prod"
Environment="DEBUG=false"
Environment="SECRET_KEY=..."
Environment="ALLOWED_HOSTS=example.com"
Environment="DATABASE_URL=postgres://user:pass@host:5432/dbname"Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| App crashes: SECRET_KEY is missing | Env var not injected | Set SECRET_KEY in platform env, restart service |
| DEBUG unexpectedly true | .env loaded in production | Do not call read_env in prod; guard by ENVIRONMENT |
| Database connection fails | Bad DATABASE_URL format | Validate DSN; test connectivity; check credentials |
| Allowed hosts error | ALLOWED_HOSTS not set correctly | Set hostnames; in dev use localhost/127.0.0.1 |
| Different behavior per machine | Hidden local env vars | Normalize with .env in dev; document required vars |
Links
High-performance Redis cache backend for Django
django-redis connects Django’s cache framework to Redis. It gives you fast in-memory caching, session storage, distributed locks, and a central cache shared across workers. It is a critical building block for scaling read-heavy applications.
- Very fast memory-based reads.
- Shared cache across multiple app instances.
- Reduces DB load dramatically.
- Supports TTL, atomic ops, locks.
- Useful for rate limits, computed dashboards, API caching.
- Page or fragment caching.
- Query result caching.
- Session backend.
- Celery broker / result backend.
- Feature flags & runtime config.
Install
pip install django-redis
redis-server --version
redis-cli ping
# should return: PONGCache configuration
Replace Django default cache with Redis backend. This is the most common baseline.
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"TIMEOUT": 300,
}
}- Use separate DB indexes for cache vs sessions.
- Set a default TIMEOUT to prevent unbounded growth.
- Enable compression if values are large.
Environment-based config (recommended)
REDIS_URL = env("REDIS_URL", default="redis://127.0.0.1:6379/1")
CACHES["default"]["LOCATION"] = REDIS_URLUsage patterns
Basic cache API
from django.core.cache import cache
cache.set("dashboard:stats", {"users": 120}, timeout=300)
data = cache.get("dashboard:stats")
cache.delete("dashboard:stats")| Pattern | Why it works | Example |
|---|---|---|
| Cache expensive query | Avoid repeated DB hits | Dashboard aggregates / analytics |
| Template fragment cache | Faster rendering | Menus, widgets, summary cards |
| Per-user cache | Personalized speedups | User profile summaries |
| Soft TTL pattern | Prevent cache stampede | Refresh in background before expiry |
Sessions & async ecosystem
Use Redis for sessions
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"- Celery broker and result backend.
- Rate limiting (DRF throttling backends).
- Distributed locks for cron jobs.
- Feature flags and runtime toggles.
Ops & performance strategy
| Concern | Symptom | Mitigation |
|---|---|---|
| Memory exhaustion | Evictions increase | Set maxmemory policy and monitor usage |
| Hot keys | CPU spikes | Spread keys, avoid huge single objects |
| Network latency | Slow cache calls | Keep Redis close to app servers |
| Cache stampede | Many recomputations | Use locks or soft TTL strategy |
Useful monitoring commands
redis-cli info memory
redis-cli info stats
redis-cli monitorTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Connection refused | Redis not running or wrong host | Check service and connection URL |
| Cache never hits | Keys expire too quickly | Increase TIMEOUT or inspect key logic |
| Random session logout | Eviction policy removes sessions | Use dedicated DB or persistence settings |
| High memory usage | Large cached objects | Compress data or cache smaller payloads |
| Slow responses despite cache | Network latency or serialization overhead | Co-locate Redis and optimize object size |
cache.get_or_set() for expensive computations to reduce race conditions and simplify cache logic.Links
Bundle and minify CSS/JS in Django templates
django-compressor compresses and bundles CSS/JS referenced in your templates. It can: concatenate assets, minify output, generate hashed filenames, and cache results. This improves load time and reduces the number of requests in production.
- Reduces HTTP requests by bundling assets.
- Minifies CSS/JS for smaller payloads.
- Supports caching to avoid recomputing bundles.
- Works at template level (easy incremental adoption).
- Helps when you keep a “Django template + vanilla JS” stack.
- If you already use Webpack/Vite/ESBuild pipeline fully.
- If your JS is mostly module-bundled externally.
- If you cannot run build steps on deploy (unless offline compression is used).
Install
pip install django-compressor
INSTALLED_APPS += ["compressor"]
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"compressor.finders.CompressorFinder",
]- Enable compressor in settings (next tab).
- Add one
compressblock in a template. - Load the page and verify bundled output is served.
Configuration
Baseline settings
COMPRESS_ENABLED = True
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
COMPRESS_OUTPUT_DIR = "CACHE"
COMPRESS_OFFLINE = FalseMinifier backends (concept)
| Asset | How minification happens | Notes |
|---|---|---|
| CSS | Filter pipeline / external tool | Verify your chosen filter is installed |
| JS | Filter pipeline / external tool | Be careful with modern syntax if tools are old |
| SCSS/LESS (optional) | Preprocessing filters | Usually handled by Vite/Webpack instead |
Usage
compress template tag. Use templatetag here to display snippets without executing them.Template example (CSS)
{% load compress %}
{% compress css %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">
<link rel="stylesheet" href="{% static 'css/theme.css' %}">
{% endcompress %}Template example (JS)
{% load compress %}
{% compress js %}
<script src="{% static 'js/app.js' %}"></script>
<script src="{% static 'js/charts.js' %}"></script>
{% endcompress %}<pre> without escaping, Django will try to execute them and your page may crash if the library is not installed.Pipeline choices
| Approach | Best for | Tradeoffs |
|---|---|---|
| Compressor only | Django template projects, simple assets | Minifiers may be limited vs modern JS bundlers |
| Vite/Webpack only | Modern SPA or heavy JS | More build complexity, but best output |
| Hybrid | Mostly Django + some bundles | Requires discipline to avoid double-processing |
Production strategy
| Mode | Behavior | Recommendation |
|---|---|---|
| On-the-fly | Compress at runtime | OK for staging; can add latency in production |
| Offline | Compress during deploy | Preferred for production: predictable and fast |
Offline compression command
python manage.py compress
collectstatic then (optionally) compress. If you skip these steps, assets may be missing or uncompressed.Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Template tag not found | Library not installed / app missing | Install django-compressor, add compressor to INSTALLED_APPS |
| Output files not served | Static settings mismatch | Verify STATIC_ROOT/STATIC_URL and collectstatic |
| Files not found during compress | Missing finder or wrong paths | Add CompressorFinder and fix static paths |
| Minification errors | External tool missing or incompatible | Adjust filters or use offline mode with proper tools |
COMPRESS_ENABLED and verify output is generated. Then move to offline mode for production deployments.Links
Le Standard Industriel des API Django
Django REST Framework (DRF) est une boîte à outils puissante et flexible pour construire des API Web. Il est conçu pour simplifier la sérialisation des données et gérer les complexités du protocole HTTP (négociation de contenu, authentification, permissions).
Historique & Impact
Créé par Tom Christie (Encode.io), DRF a débuté en 2011. Devant le manque de solutions robustes, le projet a pris son envol grâce à une campagne Kickstarter célèbre pour la version 3, prouvant que les développeurs étaient prêts à payer pour des outils Open Source de qualité.
Aujourd'hui, c'est le package Django le plus utilisé après le framework lui-même.
La "Killer Feature" : Browsable API
Contrairement à d'autres frameworks qui ne renvoient que du JSON brut, DRF génère automatiquement une interface Web HTML navigable pour chaque endpoint. Cela permet aux développeurs de tester, debugger et explorer l'API directement dans le navigateur sans outils tiers comme Postman.
28k+
GitHub Stars
Utilisé par :
- Mozilla
- Red Hat
- Heroku
- Eventbrite
Installation & Configuration Avancée
Matrice de Compatibilité
| DRF Version | Django | Python | Statut |
|---|---|---|---|
| 3.15+ | 4.2, 5.0+ | 3.8 - 3.12 | Actuel |
| 3.14 | 3.2, 4.0, 4.1 | 3.6 - 3.10 | Stable |
1. Installation des paquets
pip install djangorestframework
pip install markdown # Indispensable pour la Browsable API
pip install django-filter # Requis pour les filtres (?price__gt=100)2. settings.py (Configuration Pro)
Voici une configuration recommandée pour un projet en production.
INSTALLED_APPS = [
...
'rest_framework',
'django_filters',
]
REST_FRAMEWORK = {
# Sécurité : Par défaut, lecture seule pour anonymes, écriture pour auth
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
# Auth : Session (Web) et Token (Apps/Scripts)
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
# Pagination : Obligatoire pour éviter de tuer la DB
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
# Format date/heure ISO 8601
'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S%z',
}Architecture : La Trinité DRF
Contrairement à Django classique (MVT), DRF repose sur trois piliers distincts.
1. Serializers
Le rôle : Traducteur.
Django manipule des objets Python complexes (QuerySets). Le web parle JSON. Le Serializer fait la conversion dans les deux sens.
- Valide les données entrantes (POST).
- Gère les relations (ForeignKeys).
- Similaire aux
FormsDjango.
2. ViewSets
Le rôle : Contrôleur.
Au lieu d'écrire une vue pour la liste et une vue pour le détail, un ViewSet regroupe toute la logique CRUD.
ModelViewSet: Fournit list, create, retrieve, update, destroy.ReadOnlyModelViewSet: Lecture seule.
3. Routers
Le rôle : Cartographe.
Fini les regex complexes dans urls.py. Le Router prend un ViewSet et génère automatiquement toutes les URLs standard.
- Gère
/api/users/(GET, POST) - Gère
/api/users/1/(GET, PUT, DELETE)
Implémentation CRUD Complète
Création d'une API de gestion de produits. Copiez ces 3 fichiers et vous avez une API fonctionnelle.
1. serializers.py
from rest_framework import serializers from .models import Product, Category class ProductSerializer(serializers.ModelSerializer): # Champ calculé (ne vient pas de la DB) discount_price = serializers.SerializerMethodField() # Lien hypertexte vers le détail de la catégorie category = serializers.HyperlinkedRelatedField( view_name='category-detail', read_only=True ) class Meta: model = Product fields = ['id', 'name', 'price', 'discount_price', 'category'] read_only_fields = ['created_at'] def get_discount_price(self, obj): # Logique métier simple return obj.price * 0.9 if obj.on_sale else None
2. views.py
from rest_framework import viewsets, filters from rest_framework.permissions import IsAuthenticatedOrReadOnly from django_filters.rest_framework import DjangoFilterBackend from .models import Product from .serializers import ProductSerializer class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all().order_by('-created_at') serializer_class = ProductSerializer # Configuration Permissions permission_classes = [IsAuthenticatedOrReadOnly] # Configuration Filtres et Recherche filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ['category', 'on_sale'] # ?category=1 search_fields = ['name', 'description'] # ?search=iphone ordering_fields = ['price', 'created_at'] # ?ordering=-price
3. urls.py
from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import ProductViewSet # Création du routeur router = DefaultRouter() router.register(r'products', ProductViewSet) urlpatterns = [ path('api/v1/', include(router.urls)), # Routes pour l'auth browsable API (login/logout) path('api-auth/', include('rest_framework.urls')), ]
Sécurité & Authentification
DRF offre plusieurs mécanismes pour sécuriser votre API.
Méthodes d'Auth
1. Session Authentication
Utilise le système de session de Django (cookies). Parfait pour les appels AJAX depuis le même domaine ou pour l'interface d'admin. C'est le défaut.
2. Token Authentication
Génère un token permanent stocké en base de données. Idéal pour des clients simples (scripts python, apps mobiles basiques).
Header: Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
3. JWT (JSON Web Token)
Le standard moderne (via simplejwt). Stateless, expire, refresh token. Indispensable pour React/Vue/Angular.
Throttling (Limitation)
Pour éviter les attaques DDoS ou le scraping abusif, configurez des limites de débit.
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day', # Max 100 req/jour si anonyme
'user': '1000/day' # Max 1000 req/jour si connecté
}
}Écosystème & Extensions
DRF est la base, mais ces packages sont quasi-obligatoires en production.
drf-spectacular
Génération automatique de documentation OpenAPI 3.0 (Swagger UI / Redoc). Indispensable pour documenter votre API pour les front-ends.
SimpleJWT
La librairie standard pour gérer l'authentification JWT (access token + refresh token) avec DRF.
django-cors-headers
Ce n'est pas du DRF pur, mais si votre React est sur `localhost:3000` et Django sur `8000`, vous en aurez besoin pour autoriser les requêtes Cross-Origin.
Djoser
Fournit des endpoints REST prêts à l'emploi pour : Login, Register, Reset Password, Activation Email.
Le “radar” des problèmes Django en dev
Django Debug Toolbar ajoute une barre latérale dans tes pages Django (mode dev) pour inspecter instantanément les requêtes SQL, les templates rendus, le cache, les signaux, les headers, le timing, etc.
- Identifier les N+1 queries et les pages “lentes” en 2 clics.
- Comprendre quelle vue et quels templates ont été utilisés.
- Voir les hits/miss du cache et les headers HTTP.
- Accélérer les audits perf : timings, count SQL, duplication, etc.
Cas d’usage typiques
Pré-requis
- À activer uniquement en développement.
- Peut ralentir les pages (logique : il instrumente beaucoup de choses).
- À verrouiller côté réseau (allowed hosts / internal IPs) si besoin.
Installation & Configuration
pip install django-debug-toolbar
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware",
*MIDDLEWARE,
]
INTERNAL_IPS = ["127.0.0.1"] # ou IP LAN (Docker etc.)INTERNAL_IPS ou utilise un helper.from django.conf import settings
from django.urls import include, path
urlpatterns = [
# ... tes urls ...
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatternsConfiguration utile
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": "debug_toolbar.middleware.show_toolbar",
}Activer / désactiver proprement
- Garde le package en dépendance dev :
requirements-dev.txt - Conditionne l’activation au flag env :
DEBUG=True - Ne l’active jamais sur staging/prod (ou alors derrière VPN strict)
Panels : ce que tu vois (et comment l’exploiter)
| Panel | Ce qu’il donne | À surveiller |
|---|---|---|
| SQL | liste des requêtes + durée + stack trace | N+1, duplication, requêtes sans index, requêtes “longues” |
| Templates | templates rendus + context keys | includes multiples, templates lourds |
| Cache | hits/miss + backend | mauvais cache key, cache non utilisé |
| Signals | signaux déclenchés | signaux trop bavards / chaines d’effets |
| Headers | req/resp headers | cache-control, cookies, sécurité |
| Timer | profiling global page | latence view/DB/templates |
Limiter les panels (perf)
DEBUG_TOOLBAR_PANELS = [
"debug_toolbar.panels.sql.SQLPanel",
"debug_toolbar.panels.templates.TemplatesPanel",
"debug_toolbar.panels.cache.CachePanel",
# ... ajoute au besoin
]Usage “pro” : diagnostiquer vite
- Ouvre la page → regarde Timer + nombre de requêtes SQL.
- Dans SQL → trie par durée, détecte les requêtes répétées.
- Si répétition → corrige via
select_related/prefetch_related/ agrégations. - Vérifie Templates → repère un include coûteux / boucle lourde.
- Regarde Cache → valide que les zones “chaudes” tapent le cache.
Exemple anti N+1 (ORM)
# Avant (N+1)
orders = Order.objects.all()
for o in orders:
print(o.customer.name)
# Après (1 requête + join)
orders = Order.objects.select_related("customer").all()Debug Toolbar + DRF ?
- Oui : très utile sur endpoints HTML / pages admin / views server-side.
- Sur API JSON pure : le rendu toolbar dépend du client (mais le profiling côté serveur reste utile).
- Pour API : combine avec logging SQL, APM, ou un middleware de timing.
Sécurité & Bonnes pratiques
La toolbar peut révéler des infos sensibles (SQL, chemins, settings, headers…). Elle est faite pour le DEV.
Checklist rapide
- Conditionner l’activation à
DEBUGou variable d’environnement. - Restreindre l’accès via
INTERNAL_IPS. - Éviter de la charger sur des pages très sollicitées (même en dev partagé).
- Désactiver certains panels si ça “pollue” (perf / bruit).
Mode dev partagé (team)
def show_toolbar(request):
return request.user.is_staff and request.META.get("REMOTE_ADDR") in INTERNAL_IPS
DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": "config.debug.show_toolbar"}Écosystème & compléments
Compléments utiles
- Django Silk : profiling plus “APM-like” en dev.
- django-extensions : outils dev (shell_plus, runscript).
- django-querycount : alerte N+1 / seuil SQL.
- APM (Sentry/Datadog/NewRelic) : vision prod (mais autre sujet).
- Audits perf
- Chasse N+1
- Debug templates
- Validation cache
L’auth “tout-en-un” de Django (local + social)
Django Allauth fournit une solution complète pour : inscription, connexion, gestion de compte, vérification d’email, et authentification sociale (Google, GitHub, Microsoft, etc.). C’est l’addon le plus répandu quand tu veux une auth “production-ready” sans tout réécrire.
- Flows complets : login/logout/signup/password reset + email confirmation.
- Providers sociaux : OAuth2 prêts à brancher (Google/GitHub/…)
- Découplage propre : override templates, adaptateurs, signaux, settings.
- Solide en prod : gestion des comptes, sécurité, UX “raisonnable” par défaut.
Quand l’utiliser ?
- Projet SaaS / produit web avec comptes utilisateurs.
- Besoin de social login sans réinventer OAuth.
- Besoin de “vérif email” / anti-fraude de base.
Quand éviter ?
- Si tu veux une auth 100% custom (UI/flows) dès le départ.
- Si tu es full-API (SPA/mobile) et tu préfères JWT/OIDC externe (Keycloak/Auth0…).
Installation (rapide + safe)
pip install django-allauth
INSTALLED_APPS = [
# Django
"django.contrib.sites", # requis
# Allauth
"allauth",
"allauth.account",
"allauth.socialaccount",
# providers (exemples)
# "allauth.socialaccount.providers.google",
# "allauth.socialaccount.providers.github",
# ...
]
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]django.contrib.sites + SITE_ID sont obligatoires.from django.urls import include, path
urlpatterns = [
path("accounts/", include("allauth.urls")),
]python manage.py migrate
Configuration essentielle (prod)
Email & vérification
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory" # ou "optional"
ACCOUNT_AUTHENTICATION_METHOD = "username_email" # "username" / "email"
ACCOUNT_USERNAME_REQUIRED = False
LOGIN_REDIRECT_URL = "/"Templates context
Allauth a besoin de django.template.context_processors.request dans TEMPLATES.
TEMPLATES = [
{
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
],
},
},
]Envoi d’emails (dev vs prod)
# DEV: console
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# PROD: SMTP / SES / Mailgun / Sendgrid (ex)
# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# EMAIL_HOST = "smtp.mailgun.org"
# EMAIL_HOST_USER = "..."
# EMAIL_HOST_PASSWORD = "..."
# EMAIL_PORT = 587
# EMAIL_USE_TLS = TrueSocial Login (OAuth2)
Tu déclares un provider (Google/GitHub/…) dans INSTALLED_APPS, puis tu crées une SocialApp dans l’admin Django avec Client ID/Secret.
1) Activer un provider (ex : GitHub)
INSTALLED_APPS += [
"allauth.socialaccount.providers.github",
]2) Admin → Social Applications
- Admin Django → Social applications
- Créer : Provider, Name, Client ID, Secret
- Associer au bon Site (django.contrib.sites)
3) Redirection OAuth
# Exemple d’URL callback typique (provider dépendant)
# https://your-domain.com/accounts/github/login/callback/Templates & personnalisation
Allauth fournit des templates par défaut. Tu peux les surcharger en copiant ceux que tu veux dans ton projet, par exemple :
templates/
account/
login.html
signup.html
email_confirm.html
socialaccount/
connections.htmlExemple simple (navbar)
{% if user.is_authenticated %}
Bonjour, {{ user.username }}
Logout
{% else %}
Login
Signup
{% endif %}Custom logic (adapters)
Pour personnaliser le flow (validation username/email, redirections, création user…), tu peux utiliser AccountAdapter / SocialAccountAdapter.
Sécurité & bonnes pratiques
- Email verification : mandatory si produit public.
- HTTPS obligatoire + cookies sécurisés (
SESSION_COOKIE_SECURE). - Rate limiting sur login (nginx/Cloudflare/app-level).
- 2FA si besoin (via package dédié, ou SSO externe).
- Auditer redirect URLs / callback OAuth (provider settings).
Liens utiles
Le rendu de formulaires “propre” et élégant
Django Crispy Forms est l’addon “historique” pour rendre tes formulaires Django avec une mise en page nette, cohérente et contrôlable, sans écrire des tonnes de HTML. Il s’appuie sur des templates packs (Bootstrap, Tailwind via packs externes, etc.) et te permet de construire des formulaires complexes (grilles, fieldsets, colonnes) via des objets “Layout”.
- Consistance UI : tous tes formulaires respectent le même style.
- Moins de HTML : tu gardes la logique côté Python.
- Layouts avancés : colonnes, rows, accordéons, fieldsets…
- Meilleure DX : rapide à mettre en place et à maintenir.
Cas d’usage
- Back-office / admin custom avec beaucoup de formulaires.
- Onboarding, profils utilisateur, settings pages, workflows.
- Formulaires longs à structurer proprement (multi-colonnes).
Limites
- Si tu fais du front full React/Vue, Crispy est moins utile.
- Les template packs dépendent de ton design system (Bootstrap/Tailwind, etc.).
Installation & configuration
pip install django-crispy-forms crispy-bootstrap5
INSTALLED_APPS += [
"crispy_forms",
"crispy_bootstrap5",
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"{% load crispy_forms_tags %}
Compatibilité
- Crispy core + template pack (ex:
crispy-bootstrap5) - Fonctionne bien avec Django Forms et ModelForms.
- Peut cohabiter avec HTMX (forms serveur).
Usage quotidien : rendre vite, rendre bien
1) Rendu simple (filtre crispy)
{{ form|crispy }}2) Helper (contrôle du form)
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class ProfileForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.add_input(Submit("submit", "Enregistrer"))3) Contrôle des labels/field order
Tu peux ajuster l’ordre des champs dans le Form Python, et laisser Crispy gérer le rendu.
Layouts avancés : grilles, colonnes, Fieldsets
Le vrai “super-pouvoir” de Crispy : construire des layouts complexes en Python.
Exemple : 2 colonnes + fieldset
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column, Fieldset, Submit
class BillingForm(forms.Form):
first_name = forms.CharField()
last_name = forms.CharField()
email = forms.EmailField()
city = forms.CharField()
zip_code = forms.CharField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(
"Informations",
Row(
Column("first_name", css_class="col-md-6"),
Column("last_name", css_class="col-md-6"),
),
"email",
),
Fieldset(
"Adresse",
Row(
Column("city", css_class="col-md-8"),
Column("zip_code", css_class="col-md-4"),
),
),
Submit("submit", "Valider")
)Le rendu final dépend du template pack (Bootstrap 5 ici). Tu peux appliquer tes classes CSS (col-md-*) directement dans les Column(...).
Tips & bonnes pratiques
- Standardise : un BaseFormHelper réutilisable (submit, classes, method).
- Évite les surcharges “magiques” : garde les layouts lisibles.
- Test visuel : pages “forms showcase” (composants de formulaire).
- Si tu as des formulaires très dynamiques : combine avec HTMX.
Rendu custom (template override)
# Tu peux override des templates crispy via templates/crispy_forms/...
Écosystème & packs
Packs populaires
- crispy-bootstrap5 : le plus courant aujourd’hui.
- crispy-bootstrap4 : legacy / projets existants.
- Tailwind : via packs tiers (selon stratégie front).
Liens utiles
Le standard des tâches asynchrones (workers + broker)
Celery est la solution “de facto” pour exécuter des tâches en arrière-plan : envoi d’emails, génération PDF, traitements lourds, intégrations API, pipelines, etc. Le principe : ton app Django publie des messages vers un broker (Redis / RabbitMQ), et des workers consomment ces messages pour exécuter les tâches.
- Découplage : le web ne bloque plus sur des jobs longs.
- Scalabilité : tu ajoutes des workers à l’infini.
- Fiabilité : retries, acks, dead letters (selon broker).
- Écosystème : celery beat (schedule), monitoring, patterns.
Architecture (vue mentale)
Django (producer) --(message)--> Broker (Redis/RabbitMQ) --(consume)--> Celery Worker(s)
\
+--> Result backend (optionnel)Cas d’usage typiques
- Emails transactionnels (confirmation, reset password, notifications)
- Traitements lourds : import/export, parsing, IA, génération rapports
- Webhooks / intégrations (retry si API down)
- Batchs récurrents (cleanup, recompute KPIs) via celery beat
Installation Django + Celery (recipe standard)
pip install celery redis
proj/celery.pyimport os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proj.settings")
app = Celery("proj")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()proj/__init__.pyfrom .celery import app as celery_app
__all__ = ("celery_app",)CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/1" # optionnel
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_TIMEZONE = "Europe/Paris"Lancer en dev
# Terminal A: redis
redis-server
# Terminal B: worker
celery -A proj worker -l info
# Terminal C: Django
python manage.py runserverTasks : écrire, appeler, tracer
1) Créer une task
# app/tasks.py
from celery import shared_task
@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_jitter=True, max_retries=5)
def send_welcome_email(self, user_id):
# ... fetch user, send email ...
return {"user_id": user_id, "status": "ok"}2) Appeler une task
from app.tasks import send_welcome_email
# async
send_welcome_email.delay(user.id)
# avec options
send_welcome_email.apply_async(args=[user.id], countdown=10, expires=60)3) Idempotence (très important)
Une task peut être exécutée plus d’une fois (retry, crash, redelivery). Conçois tes tasks pour être idempotentes : mêmes inputs → mêmes effets, sans double action.
Pattern : verrou / déduplication (ex)
# pseudo-code
lock_key = f"task:welcome:{user_id}"
if redis.set(lock_key, "1", nx=True, ex=60):
# do work
passQueues, routing, priorité, concurrency
1) Séparer les queues
# settings.py
from kombu import Queue
CELERY_TASK_QUEUES = (
Queue("default"),
Queue("emails"),
Queue("heavy"),
)
CELERY_TASK_ROUTES = {
"app.tasks.send_welcome_email": {"queue": "emails"},
"app.tasks.*heavy*": {"queue": "heavy"},
}2) Workers dédiés
# worker emails
celery -A proj worker -Q emails -l info --concurrency=4
# worker heavy
celery -A proj worker -Q heavy -l info --concurrency=23) Concurrency : choix du pool
- prefork (défaut) : robuste pour CPU-bound / plupart des cas.
- gevent/eventlet : I/O bound (mais impact sur libs, monkey patch).
- threads : selon workload.
La séparation des queues + workers dédiés est LE levier #1 pour éviter que des jobs lourds bloquent des jobs “temps réel” (emails, webhooks, notifications).
Retry, backoff & schedule (Beat)
Retries (pattern recommandé)
@shared_task(bind=True, max_retries=5)
def call_partner_api(self, payload):
try:
# ... call API ...
return "ok"
except Exception as exc:
raise self.retry(exc=exc, countdown=2 ** self.request.retries)Celery Beat (cron-like)
# settings.py
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"cleanup-every-night": {
"task": "app.tasks.cleanup",
"schedule": crontab(hour=3, minute=0),
},
}Lancer Beat
celery -A proj beat -l info
Beat doit être unique (1 instance active) sinon tu risques des jobs doublés. En prod, utilise un mécanisme “single leader” (k8s leader election, systemd, etc.).
Production & observabilité
Run patterns (systemd / supervisor / docker)
# worker
celery -A proj worker -l info --concurrency=4
# beat
celery -A proj beat -l infoMonitoring
- Flower : UI simple pour voir tasks/workers/queues.
- Logs structurés + corrélation (task_id).
- Metrics : rate, duration, failures, backlog queue.
Hardening (checklist)
- Limiter taille des messages (éviter payloads énormes).
- Time limits :
CELERY_TASK_TIME_LIMIT/SOFT_TIME_LIMIT - Prefetch adapté :
worker_prefetch_multiplier - Dead-letter / retries maîtrisés (éviter tempêtes de retry).
Liens utiles
WebSockets & temps réel dans Django (sans quitter l’écosystème)
Django Channels étend Django au-delà de HTTP : WebSockets, long-polling, et plus généralement tout ce qui est asynchrone. Il introduit une couche ASGI, des Consumers (équivalent “views” pour WS), et souvent un channel layer (Redis) pour faire communiquer plusieurs workers.
- Chat / messagerie / notifications live.
- Dashboards temps réel (metrics, trading, monitoring).
- Collaboration (édition live, présence, “typing…”).
- Push d’événements serveur vers clients (sans polling agressif).
Architecture mental model
Client (WebSocket)
|
v
ASGI server (Daphne/Uvicorn) ---> Django Channels (Consumers)
| |
| v
| Channel Layer (Redis)
v |
DB / Cache / Services Group Send / Fan-outQuand préférer autre chose ?
- Simple notification “rare” → parfois SSE suffit (plus simple).
- Très gros scale realtime → broker dédié (NATS/Kafka) + gateway WS.
Installation & configuration (ASGI + Redis layer)
pip install channels channels_redis
INSTALLED_APPS += ["channels"]
ASGI_APPLICATION = "proj.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [("127.0.0.1", 6379)]},
}
}import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import app.routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proj.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(app.routing.websocket_urlpatterns)
),
})Run server (dev)
# Redis
redis-server
# ASGI (Uvicorn)
uvicorn proj.asgi:application --reload --port 8000Concepts clés (à connaître pour ne pas souffrir)
- ASGI : l’équivalent async de WSGI (Django “realtime-capable”).
- Consumer : “view” pour WebSocket (connect/receive/disconnect).
- Channel Layer : bus interne (souvent Redis) pour messages entre workers.
- Groups : fan-out (“broadcast” à un groupe de clients).
- Sync vs Async : éviter de bloquer l’event loop (DB calls sync…).
SyncToAsync / AsyncToSync
from asgiref.sync import sync_to_async
@sync_to_async
def get_user(user_id):
return User.objects.get(pk=user_id)Auth (cookies/sessions)
AuthMiddlewareStack te permet d’utiliser les sessions Django côté WebSocket, pratique pour les apps “login required”.
Code complet : Echo + Group broadcast
routing.py
# app/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/echo/$", consumers.EchoConsumer.as_asgi()),
re_path(r"ws/room/(?P\\w+)/$", consumers.RoomConsumer.as_asgi()),
] consumers.py (Echo)
# app/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class EchoConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
async def receive(self, text_data=None, bytes_data=None):
await self.send(text_data=text_data)
class RoomConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room = self.scope["url_route"]["kwargs"]["room"]
self.group_name = f"room_{self.room}"
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group_name, self.channel_name)
async def receive(self, text_data=None, bytes_data=None):
payload = json.loads(text_data)
await self.channel_layer.group_send(self.group_name, {"type": "room.message", "payload": payload})
async def room_message(self, event):
await self.send(text_data=json.dumps(event["payload"]))Client JS (test)
const ws = new WebSocket("ws://localhost:8000/ws/room/dev/");
ws.onmessage = (e) => console.log("msg", e.data);
ws.onopen = () => ws.send(JSON.stringify({text:"hello"}));Production : scaling, proxy, sécurité
ASGI server
- Uvicorn (souvent) ou Daphne (Channels).
- Garder Django WSGI possible pour HTTP “classique”, mais le plus simple : tout en ASGI.
Nginx (WebSocket proxy)
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}Hardening
- Limiter origins / hosts (CORS/CSRF ne s’applique pas pareil à WS).
- Authentifier côté consumer (scope user) + permissions par room.
- Mettre des timeouts et ping/pong selon infra (LB).
- Eviter les gros messages (préférer events compacts).
Écosystème & patterns
Compléments utiles
- Redis : channel layer quasi standard.
- Celery : jobs lourds (ne pas faire CPU-bound dans le consumer).
- SSE : alternative pour “push serveur” sans WebSocket.
- Observabilité : logs, metrics, backlog groups (selon usage).
Liens utiles
Filtrer des QuerySets proprement, via l’URL
Django-filter est l’addon “standard” pour exposer un filtrage simple et robuste sur des QuerySets à partir des paramètres URL : ?status=active&created_after=2025-01-01&price_min=10. Il s’intègre parfaitement avec Django (CBV) et Django REST Framework.
- Simple : une classe FilterSet et tu as des filtres automatiques.
- Safe : tu contrôles quels champs sont filtrables.
- Expressif : lookup
exact,icontains,gte,lte,in, etc. - DRF-ready : filtrage standardisé sur les endpoints API.
Cas d’usage
- Listes admin / dashboards : filtrer par date, statut, type, owner, etc.
- APIs list endpoints : filtres “query param” propres et documentables.
- Recherche avancée (sans full-text) : combinatoire de filtres métiers.
Installation & setup
pip install django-filter
INSTALLED_APPS += ["django_filters"]
Philosophie
- Whitelist : tu exposes explicitement les champs filtrables.
- Lisibilité : tes règles de filtrage sont centralisées (FilterSet).
- Prévisibilité : tu évites les query params “magiques” non documentés.
Basics : FilterSet + lookup expressions
Exemple simple (Model + FilterSet)
# models.py
class Ticket(models.Model):
status = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)
priority = models.IntegerField(default=3)
# filters.py
import django_filters
class TicketFilter(django_filters.FilterSet):
created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte")
created_before = django_filters.DateFilter(field_name="created_at", lookup_expr="lte")
status = django_filters.CharFilter(lookup_expr="iexact")
priority_min = django_filters.NumberFilter(field_name="priority", lookup_expr="gte")
class Meta:
model = Ticket
fields = ["status", "created_after", "created_before", "priority_min"]CBV (ListView) : appliquer le filtre
# views.py
from django.views.generic import ListView
from .models import Ticket
from .filters import TicketFilter
class TicketListView(ListView):
model = Ticket
def get_queryset(self):
qs = super().get_queryset()
self.filterset = TicketFilter(self.request.GET, queryset=qs)
return self.filterset.qsURL examples
/tickets/?status=open&created_after=2025-01-01&priority_min=2
Intégration DRF (FilterBackend)
Activer le backend globalement
# settings.py
REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
],
}1) Filtrage simple : filterset_fields
# views.py
from rest_framework.viewsets import ModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
class TicketViewSet(ModelViewSet):
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ["status", "priority"]2) Filtrage avancé : FilterSet custom
from .filters import TicketFilter
class TicketViewSet(ModelViewSet):
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
filterset_class = TicketFilterDjango-filter + Pagination DRF + Ordering → API “list” clean et scalable.
Avancé : méthodes custom, relations, “in”, ranges
Filtre custom via méthode
class TicketFilter(django_filters.FilterSet):
q = django_filters.CharFilter(method="filter_q")
def filter_q(self, queryset, name, value):
return queryset.filter(
Q(status__icontains=value) |
Q(description__icontains=value)
)Filtrer sur relations
class OrderFilter(django_filters.FilterSet):
customer_email = django_filters.CharFilter(field_name="customer__email", lookup_expr="iexact")
min_total = django_filters.NumberFilter(field_name="total", lookup_expr="gte")“in” (listes)
status__in=open,closed,pending
Pour les relations, pense à select_related/prefetch_related dans le queryset DRF, sinon tu crées un N+1 même si le filtrage est “bon”.
Pièges classiques (et comment les éviter)
1) Exposer trop de champs
- Ne “filter” pas tout par défaut : whitelist.
- Certains champs peuvent faire des scans (text, relations non indexées).
2) Filtrage + ordering = index
Si tu proposes ?created_after=... + tri ?ordering=-created_at, assure-toi d’avoir les bons index (sinon la DB souffre).
3) Validation des paramètres
- Django-filter valide (types dates/nombres) mais tes filtres custom doivent être stricts.
- Ne fais pas de parsing libre sans garde-fous (regex/allowed values).
Liens utiles
La base “images” en Python (et le meilleur ami de ImageField)
Pillow (PIL fork) est la bibliothèque Python la plus utilisée pour lire, écrire et transformer des images : redimensionnement, conversion de formats, thumbnails, compression, EXIF, etc. Dans Django, elle est souvent requise dès que tu utilises ImageField et que tu veux valider/inspecter correctement les uploads.
- Support formats : JPEG/PNG/GIF/WebP/TIFF… (selon libs système).
- Transformations : resize/crop/rotate/thumbnail.
- Validation : vérifier qu’un upload est une “vraie” image.
- Optimisation : compression, conversion WebP/AVIF (partiel via plugins/outils).
Cas d’usage
- Avatar utilisateur, photos, thumbnails, galleries.
- Validation anti-fichiers corrompus / mauvais formats.
- Pré-processing avant stockage (S3) : resize + recompress.
À retenir
- Pillow = traitement CPU. En prod : préférer async (Celery) si c’est lourd.
- Pour des variantes d’images : souvent mieux d’utiliser une lib dédiée (voir “Écosystème”).
Installation (Python + dépendances natives)
pip install pillow
sudo apt-get update
sudo apt-get install -y libjpeg-dev zlib1g-dev libpng-dev libwebp-devVérifier le build (formats supportés)
python -c "from PIL import features; print('JPEG', features.check('jpg')); print('WEBP', features.check('webp'))"Intégration Django : ImageField, validation, storage
Model : ImageField
from django.db import models
class Profile(models.Model):
avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)Validation (type + dimensions)
from django.core.exceptions import ValidationError
from django.core.files.images import get_image_dimensions
def validate_avatar(image):
w, h = get_image_dimensions(image)
if w < 128 or h < 128:
raise ValidationError("Image trop petite (min 128x128).")Forms : nettoyage & check simple
from PIL import Image
def is_valid_image(uploaded_file):
try:
img = Image.open(uploaded_file)
img.verify() # check structure
return True
except Exception:
return FalsePillow ne “stocke” rien : Django gère le stockage (local / S3 via storages). Pillow sert à valider / transformer avant d’écrire.
Traitements : resize, crop, thumbnails, WebP
Resize + conversion (ex : WebP)
from io import BytesIO
from PIL import Image
from django.core.files.base import ContentFile
def make_webp(uploaded, max_side=1024, quality=82):
img = Image.open(uploaded)
img = img.convert("RGB")
# resize (conserve ratio)
w, h = img.size
scale = min(max_side / max(w, h), 1.0)
img = img.resize((int(w*scale), int(h*scale)), Image.LANCZOS)
out = BytesIO()
img.save(out, format="WEBP", quality=quality, method=6)
return ContentFile(out.getvalue(), name="image.webp")Thumbnail (simple)
img = Image.open(uploaded)
img.thumbnail((320, 320), Image.LANCZOS)EXIF (rotation auto)
from PIL import ImageOps
img = ImageOps.exif_transpose(Image.open(uploaded))En prod : fais le traitement (resize/convert) en asynchrone (Celery), surtout si tu génères plusieurs variantes.
Perf & sécurité (upload d’images = zone à risque)
1) Limiter taille / dimensions
- Max file size (ex 5–10MB) au niveau form + reverse proxy.
- Max dimensions (ex 6000×6000) pour éviter les images “bombes”.
2) Décompression bomb / DOS
Des images peuvent “exploser” en mémoire lors de la décompression. Pillow a des garde-fous, mais tu dois aussi limiter côté app (dimensions) et côté infra.
3) Offload CPU
- Traitements lourds → Celery workers dédiés.
- Génération variantes → batch asynchrone (et cache/CDN).
4) Hygiène de stockage
- Utiliser des noms non devinables (UUID) pour éviter enumeration.
- Servir via CDN (CloudFront) plutôt que Django.
Écosystème (quand Pillow ne suffit plus)
Libs / patterns utiles
- django-storages : S3/GCS/Azure Storage.
- sorl-thumbnail / easy-thumbnails : gestion de thumbnails côté Django.
- Services images (Cloudinary, Imgix) : variants on-the-fly + CDN.
- Celery : pipeline de traitement (resize, recompress, watermark).
Liens utiles
Ajouter des tags partout (sans réinventer la roue)
Django-Taggit est l’addon “standard” pour ajouter un système de tags à n’importe quel modèle Django : articles, projets, tickets, assets, documents, etc. Tu déclares un champ TaggableManager, et tu obtiens directement : ajout/suppression de tags, relation Many-to-Many, recherche par tags, et admin intégré.
- Ultra simple : 1 champ et ça marche.
- Générique : réutilisable sur tous tes modèles.
- QuerySet friendly : filtrer par tags est naturel.
- Admin ready : édition des tags directement dans l’admin.
Cas d’usage
- Portfolio / PostFolio / docs : classification légère “à la carte”.
- Catalogue d’assets (images, templates, scripts) : tags multi-axes.
- Tickets / incidents : “db”, “nginx”, “prod”, “urgent”.
À retenir
- Les tags ne remplacent pas une taxonomie “forte” (catégories/relations).
- Pour des volumes énormes, pense index + stratégie de recherche (onglet “Queries”).
Installation & setup
pip install django-taggit
INSTALLED_APPS += ["taggit"]
python manage.py migrate
Option : case-insensitive (déduplication)
Tu peux vouloir normaliser “DevOps” et “devops”. Taggit a des options / patterns de normalisation (souvent via custom Tag model / slug). Pour IDEO-Lab, tu peux aussi dédupliquer côté code.
Modèles : TaggableManager (1 champ)
Exemple : Article taggable
from django.db import models
from taggit.managers import TaggableManager
class Article(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
tags = TaggableManager(blank=True)Ajouter / retirer des tags
a = Article.objects.first()
a.tags.add("django", "api", "devops")
a.tags.remove("api")
a.tags.set(["django", "orm"])
a.tags.clear()Accès tags (lecture)
a.tags.names() # ["django", "orm"]
a.tags.all() # queryset Tag
list(a.tags.slugs()) # slugs si activésQueries : filtrer, combiner, optimiser
Filtrer par tag
Article.objects.filter(tags__name__in=["django"]) # contient "django"
Contient plusieurs tags (AND)
qs = Article.objects.all()
for t in ["django", "orm"]:
qs = qs.filter(tags__name=t)
qs = qs.distinct()Exclure un tag
Article.objects.exclude(tags__name="draft")
Perf : éviter N+1 tags
Article.objects.prefetch_related("tags")Les combinaisons de tags peuvent générer beaucoup de JOINs. Pour des gros volumes, pense à limiter les filtres exposés, paginer, et monitorer le plan SQL.
Admin : ergonomie & UX “tagging”
Admin intégré
Taggit s’intègre bien à l’admin Django : tu peux éditer les tags directement depuis le modèle (field “tags” en texte).
Améliorer l’UX
- Auto-complétion tags (widget custom) si tu as beaucoup de tags.
- Normaliser casing/slug (éviter doublons).
- Mettre des tags “recommandés” (liste de tags populaires).
Tag cloud (simple)
from taggit.models import Tag
Tag.objects.all().order_by("name")API / DRF : exposer les tags proprement
Serializer : tags en liste de strings
from rest_framework import serializers
class ArticleSerializer(serializers.ModelSerializer):
tags = serializers.ListField(child=serializers.CharField(), required=False)
class Meta:
model = Article
fields = ["id", "title", "body", "tags"]
def create(self, validated_data):
tags = validated_data.pop("tags", [])
obj = super().create(validated_data)
if tags:
obj.tags.set(tags)
return obj
def update(self, instance, validated_data):
tags = validated_data.pop("tags", None)
obj = super().update(instance, validated_data)
if tags is not None:
obj.tags.set(tags)
return objFilter par tag (GET)
/api/articles/?tag=django
Ajoute un endpoint /tags/ (liste + popularité) pour aider le front à proposer de l’auto-complétion et éviter les tags “fantômes”.
Liens utiles
Stockage “cloud-ready” (sans réécrire ton code fichiers)
Django-Storages fournit des backends de stockage pour brancher Django sur des fournisseurs (S3 / GCS / Azure Blob…) via l’API de stockage, tout en gardant la même interface côté app : upload, lecture, gestion des URLs, stockage statique et media, etc.
- Scale : objets dans un bucket, pas sur le disque de ton serveur.
- HA : stockage durable (replication / zone / région selon provider).
- CDN-friendly : URLs publiques ou signées, cache, distribution.
- Dev/Prod : local en dev, cloud en prod (switch propre).
- Separation : static vs media, ou buckets distincts.
- Uploads “toolbox” (whl, pdf, assets).
- Images portfolio / CV / bannières.
- Exports (JSON, reports, logs).
- Backups applicatifs (objets).
- Multi-sites sur un même socle.
Architecture typique
Browser/User
|
| upload/read
v
Django (Storage API)
|
| backend driver (S3/GCS/Azure)
v
Object Storage Bucket <-----> CDN (optional)
|
| lifecycle / versioning / retention
v
Durable StorageChecklist de décision (rapidement)
- Public : images publiques, assets “marketing”.
- Private : documents, exports, logs, pièces sensibles.
- Private + signed URLs : pattern “clean” (accès contrôlé).
- Static : collectstatic + cache agressif.
- Media : accès direct, permissions, quotas.
- Souvent 2 buckets + 2 domaines/CDN.
Installation & configuration (base)
pip install django-storages
INSTALLED_APPS += ["storages"]
STORAGES = {
"default": {"BACKEND": "path.to.MediaStorage"},
"staticfiles": {"BACKEND": "path.to.StaticStorage"},
}- Mettre les clés provider directement dans le code (mauvaise idée) → utilise env vars / IAM / Workload Identity.
- Confondre STATIC et MEDIA.
- Oublier CORS (uploads depuis navigateur) ou headers cache.
- Donner un bucket public “par facilité” alors que le contenu est sensible.
Organisation recommandée (sane defaults)
ideolab/prod/, ideolab/dev/)AWS S3 (boto3) : setup propre + options utiles
pip install boto3
from storages.backends.s3 import S3Storage
Example: MediaStorage / StaticStorage (S3)
# storages_backends.py
from storages.backends.s3 import S3Storage
class MediaStorage(S3Storage):
bucket_name = "myapp-media"
location = "media"
default_acl = None
file_overwrite = False
class StaticStorage(S3Storage):
bucket_name = "myapp-static"
location = "static"
default_acl = None
file_overwrite = TrueSTORAGES + URLs (Django)
STORAGES = {
"default": {"BACKEND": "project.storages_backends.MediaStorage"},
"staticfiles": {"BACKEND": "project.storages_backends.StaticStorage"},
}
MEDIA_URL = "/media/"
STATIC_URL = "/static/"S3: tableau “option → impact”
| Option | Impact | À utiliser quand |
|---|---|---|
file_overwrite | Empêche d’écraser un fichier existant (crée un nom unique si collision). | Uploads utilisateurs / versioning |
location | Prefix dans le bucket (ex: media/, static/). | Organisation multi-projets |
default_acl = None | Évite des ACL legacy ; privilégie bucket policy/IAM. | Bonne pratique moderne |
querystring_auth | URLs signées (temporaire) pour objets privés. | Docs sensibles, downloads contrôlés |
Google Cloud Storage (GCS) : simple, propre, scalable
Example: Storage classes (GCS)
# storages_backends.py
from storages.backends.gcloud import GoogleCloudStorage
class MediaStorage(GoogleCloudStorage):
bucket_name = "myapp-media"
location = "media"
file_overwrite = False
class StaticStorage(GoogleCloudStorage):
bucket_name = "myapp-static"
location = "static"
file_overwrite = TrueTypical settings
STORAGES = {
"default": {"BACKEND": "project.storages_backends.MediaStorage"},
"staticfiles": {"BACKEND": "project.storages_backends.StaticStorage"},
}
GS_BUCKET_NAME = "myapp-media"- CORS si upload direct depuis le navigateur.
- Cache headers si static derrière CDN.
- Private media : signed URLs (pattern identique S3).
- Lifecycle rules : purge auto (temp files, exports).
Azure Blob Storage : storage enterprise + CDN
- Stack Azure déjà en place (Entra ID, policies, governance).
- Besoin de patterns enterprise (RBAC, audit, compliance).
- CDN Azure / Front Door pour distribution.
Example: Azure storage class
# storages_backends.py
from storages.backends.azure_storage import AzureStorage
class MediaStorage(AzureStorage):
azure_container = "media"
expiration_secs = None
class StaticStorage(AzureStorage):
azure_container = "static"
expiration_secs = NonePatterns solides (prod) : multi-env, multi-buckets, URLs signées
Pattern A — Local dev / Cloud prod
if DEBUG:
# Use local filesystem for speed and simplicity
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}
else:
# Use cloud storages
STORAGES = {
"default": {"BACKEND": "project.storages_backends.MediaStorage"},
"staticfiles": {"BACKEND": "project.storages_backends.StaticStorage"},
}Pattern B — URLs signées (private downloads)
from django.core.files.storage import default_storage
def get_private_download_url(name: str) -> str:
# The storage backend may return signed URLs depending on configuration
return default_storage.url(name)Pattern C — Upload direct navigateur (pré-signé)
project-env-static / project-env-mediaapp_name/ + yyyy/mm/ (si tu veux structurer)Sécurité & performance : les règles qui évitent les catastrophes
- Bucket public par défaut → fuite de données.
- Credentials statiques sur disque → compromission.
- URLs permanentes sur contenu sensible → partage non contrôlé.
- Pas d’audit logs → impossible d’investiguer.
- CDN devant static → latence très basse.
- Cache headers corrects → charge serveur quasi nulle.
- Upload direct → serveur Django épargné.
- Lifecycles → coûts stables (purge auto).
Table “action → bénéfice” (prod)
| Action | Bénéfice | Remarque |
|---|---|---|
| Separate buckets (static/media) | Policies et cache adaptés, maintenance plus simple | Recommandé presque toujours |
| Signed URLs for private media | Accès contrôlé et traçable | Expire rapidement |
| CDN for static | Time-to-first-byte très bas | Cache long + invalidation |
| Lifecycle rules | Réduction coûts, nettoyage automatique | Temp/export/log objects |
| Role-based auth (no local keys) | Évite la fuite de secrets | IAM/Identity-first |
Troubleshooting : symptômes → causes → actions
| Symptôme | Cause probable | Action |
|---|---|---|
| 403 Forbidden sur URL media | Bucket privé sans signed URLs / policy trop stricte | Configurer signed URLs ou ajuster policy/IAM |
| Uploads OK, mais fichiers introuvables | Mauvais prefix (location) ou bucket | Vérifier bucket_name/location + logs |
| Static not updating after deploy | Cache CDN trop agressif / pas de versionnement | Hash filenames + invalidation CDN |
| Timeout sur gros upload | Upload via Django (proxy) + limites reverse proxy | Passer en upload direct pré-signé |
| Coûts qui explosent | Pas de lifecycle + objets temporaires infinis | Lifecycle rules + monitoring + quotas |
- Le fichier est-il bien dans le bucket ? (console provider)
- Le path correspond-il à
location+ filename ? - La policy autorise-t-elle GET/PUT ?
- Les URLs sont-elles publiques ou signées ?
- Le CDN cache-t-il trop ? (headers + purge)
Liens & docs utiles
Production error monitoring that actually reduces downtime
Sentry collects exceptions, stack traces, breadcrumbs, user context, release info, and (optionally) performance traces. The goal is not “more logs”, but faster diagnosis: what broke, where, for whom, and what changed.
- Error grouping: same root cause aggregated.
- Context: request, headers, user id/email (if configured), tags.
- Breadcrumbs: events timeline before crash.
- Releases: correlate spikes with deploys.
- Alerts: notify on regressions and new issues.
- Tracing (optional): slow endpoints, DB spans, external calls.
- Track 500s per app/module (toolbox, dashboards, parsers).
- Pinpoint template/runtime errors in production.
- Detect spikes after migrations or deploy changes.
- Measure API latency (Django + DB spans).
- Link errors to release version and git sha.
Signal path
Django exception / log error
-> Sentry SDK captures
-> Event envelope (context, tags, release)
-> Sentry project
-> Grouping + Alerts + Dashboards- Keep DSN in env vars, never in code.
- Tag everything: env, app, host, release.
- Sample traces, but do not sample errors blindly.
- Scrub PII (tokens, passwords, cookies).
- Ship source maps for JS if you have front assets.
Install
pip install sentry-sdk
export SENTRY_DSN="https://public_key@o0.ingest.sentry.io/0"
- SDK installed but not initialized (no events).
- DSN missing in production environment.
- Network egress blocked (firewall) to Sentry ingest.
- PII sent unintentionally (cookies, auth headers).
Core configuration
Recommended init (baseline)
import os
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
SENTRY_DSN = os.environ.get("SENTRY_DSN", "")
if SENTRY_DSN:
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration()],
environment=os.environ.get("APP_ENV", "prod"),
release=os.environ.get("APP_RELEASE", "unknown"),
send_default_pii=False,
traces_sample_rate=float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.0")),
)environment: prod/staging/dev separation.release: deploy correlation (git sha, version).send_default_pii: keep false by default.traces_sample_rate: start low (0.05–0.2) and adjust.
sentry_sdk.set_tag("app", "ideolab")
sentry_sdk.set_tag("host", os.environ.get("HOSTNAME", "unknown"))
sentry_sdk.set_tag("cluster", os.environ.get("CLUSTER", "default"))Noise control
| Problem | Cause | Fix |
|---|---|---|
| Too many events | Expected exceptions, bots, bad endpoints | Filter via before_send, ignore specific exceptions, adjust routes |
| Grouping is messy | Dynamic messages, different stack shapes | Use fingerprints/tags, normalize error messages |
| Tracing too expensive | High sample rate | Lower traces_sample_rate and use sampling rules |
Django patterns (prod-grade)
Capture handled exceptions (when you decide)
import sentry_sdk
def safe_call():
try:
risky_operation()
except Exception as exc:
sentry_sdk.capture_exception(exc)
raiseCapture messages with context
import sentry_sdk
sentry_sdk.capture_message(
"Unexpected state in parser pipeline",
level="warning",
)Attach request/user context safely
import sentry_sdk
def set_actor(user_id: str):
sentry_sdk.set_user({"id": user_id})Integrate with logging
import logging
logger = logging.getLogger("app")
def run_job():
logger.info("job_start", extra={"job": "nightly_parser"})
# ...
logger.error("job_failed", extra={"job": "nightly_parser"})- Set
APP_RELEASEon deploy (git sha or semver). - Use the same value across web + workers.
- Tag environment explicitly (prod/staging).
Alerts & operations
- New issue in production
- Error spike (regression)
- High rate for a specific endpoint
- Slow transactions (p95/p99) if tracing enabled
- Identify release correlation
- Check tags (app/module/host)
- Read stack + breadcrumbs
- Fix or rollback
- Mark resolved + add note
Operational checklist
| Item | Why | Default |
|---|---|---|
| Set environment | Separate prod/staging data | APP_ENV=prod |
| Set release | Deploy correlation | APP_RELEASE=<git_sha> |
| Traces sampling | Cost vs insight | Start at 0.05 |
| PII scrubbing | Compliance and security | On (default) + extra filters |
| Alert routing | Actionable notifications | Ops channel + email fallback |
Security & privacy
- Passwords, tokens, API keys
- Authorization headers
- Full cookies content
- Raw request bodies with PII
- Use tags instead of raw payloads
- Scrub headers/cookies
- Set retention policy in Sentry
- Limit access with SSO/RBAC
Filtering events (before_send)
def before_send(event, hint):
# Example: drop known noisy errors by message substring
exc = event.get("exception", {})
# Keep it simple here; implement your own rules carefully
return eventTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No events in Sentry | SDK not initialized or DSN missing | Verify env vars + init path + startup logs |
| Events from dev mixed with prod | Environment not set | Set APP_ENV and enforce it in init |
| Too many issues | No filtering, bots, expected exceptions | Add ignore rules, rate limit noisy endpoints, use before_send |
| Grouping splits | Dynamic error messages | Normalize messages, use tags, adjust fingerprint if needed |
| High CPU/latency | Tracing sample too high | Reduce sampling, exclude endpoints, tune integrations |
- Confirm DSN is set on web + workers.
- Trigger a controlled exception on a protected route.
- Check “environment” and “release” tags appear.
- Verify outbound network access to ingest endpoint.
Links
Cross-domain requests without breaking security
Django-CORS-Headers adds the right CORS response headers so browsers can call your API from another origin (domain/port/protocol). It solves “blocked by CORS policy” while keeping strict security rules.
- Frontend on different domain than Django API.
- Admin tools calling endpoints from a separate origin.
- Local dev: localhost:3000 → API localhost:8000.
- Using cookies/session auth across origins.
- Using Authorization header (Bearer token) from browser.
- Never use
CORS_ALLOW_ALL_ORIGINSin production. - Whitelist exact origins (scheme + host + port).
- Enable credentials only if required.
- Know preflight: OPTIONS must return headers.
- Keep allowed headers/methods minimal.
CORS 101 (quick but accurate)
Origin = scheme + host + port
Examples:
https://ideolab.io (443 implied)
https://api.ideolab.io (different host)
http://localhost:3000 (different port)
http://localhost:8000 (API)
Same-origin policy blocks JS calls across origins unless CORS allows it.Simple vs preflighted requests
Access-Control-Allow-Origin- Non-simple methods (PUT/PATCH/DELETE)
- Custom headers (Authorization)
- Credentials + constraints
Core response headers (what they do)
| Header | Meaning | Common pitfall |
|---|---|---|
Access-Control-Allow-Origin | Which origin is allowed to read the response | Using * with credentials (not allowed) |
Access-Control-Allow-Credentials | Allow cookies / auth credentials | Forgetting it when using session cookies |
Access-Control-Allow-Headers | Which request headers are permitted | Missing Authorization causes blocked calls |
Access-Control-Allow-Methods | Which HTTP methods are permitted | Missing PATCH/DELETE triggers preflight failure |
Access-Control-Expose-Headers | Which response headers JS can read | Frontend cannot read custom headers without it |
Access-Control-Max-Age | Cache preflight response | Too low = too many preflights |
Install (Django)
pip install django-cors-headers
INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
# ...
]Configuration (safe defaults)
Whitelist exact origins
CORS_ALLOWED_ORIGINS = [
"https://ideolab.io",
"https://app.ideolab.io",
"http://localhost:3000",
]CORS_ALLOW_HEADERS = [
"accept",
"authorization",
"content-type",
"x-csrftoken",
"x-requested-with",
]authorization must be allowed.CORS_PREFLIGHT_MAX_AGE = 86400
Common patterns (choose one)
| Pattern | Best for | Key settings |
|---|---|---|
| Strict allowlist | Production websites and APIs | CORS_ALLOWED_ORIGINS only |
| Regex allowlist | Multiple subdomains | CORS_ALLOWED_ORIGIN_REGEXES |
| Dev permissive | Local testing only | Use allowlist for localhost, never wildcard in prod |
CORS_ALLOW_ALL_ORIGINS = Truein prod.- Allowing credentials + wildcard origin.
- Letting any custom header through “just to make it work”.
Cookies, sessions, and auth
If you use session cookies across origins
CORS_ALLOW_CREDENTIALS = True
Token auth (Authorization header)
Authorization: Bearer ..., the browser will preflight. Ensure Authorization is allowed, and that OPTIONS returns correct allow headers.Nginx / reverse proxy notes
Proxy pitfalls checklist
- OPTIONS requests returning 405/404 at the proxy layer.
- Proxy removing
Access-Control-*headers. - Caching preflight incorrectly.
- Different CORS policy for /api vs /media endpoints.
Troubleshooting: symptom → cause → fix
| Symptom | Likely cause | Fix |
|---|---|---|
| Blocked by CORS policy (no allow origin) | Origin not in allowlist, middleware not active | Whitelist exact origin, ensure CorsMiddleware is high in MIDDLEWARE |
| Preflight OPTIONS fails (405/404) | Proxy blocks OPTIONS or route missing | Allow OPTIONS through proxy, ensure Django responds with headers |
| Credentials mode fails | Wildcard origin, missing allow credentials | Set explicit origins + CORS_ALLOW_CREDENTIALS=True |
| Authorization header blocked | Allowed headers missing authorization | Add authorization to allowed headers |
| Works in curl, fails in browser | CORS is enforced only by browsers | Fix CORS headers; curl bypasses browser policy |
| Some endpoints work, others fail | Different middleware stack or path rules | Ensure consistent settings and routing for /api/** |
- Open browser devtools → Network → find OPTIONS request.
- Check response headers include Access-Control-Allow-Origin.
- Confirm allowed headers include Authorization (if used).
- Confirm origin matches exactly (scheme/host/port).
- Check proxy logs for 405/404 on OPTIONS.
Links
Serve static files in production — without extra infrastructure
WhiteNoise lets your Django app serve its own static files efficiently, with proper caching, gzip/brotli compression, and versioned filenames. It’s ideal when you want a clean deploy without having to maintain a separate static server or object storage setup.
- Static delivery with immutable caching (hashed filenames).
- Compression (gzip/brotli) with correct headers.
- One-process deploys: no need for separate static service.
- Simple scaling: every web instance serves the same collected assets.
- Great default for “classic” Django deployments (VMs/containers).
- Huge static traffic at scale → prefer CDN + object storage.
- Multi-region static distribution → CDN becomes mandatory.
- Strict separation of concerns enforced by platform.
- Ultra heavy assets (video, large downloads) → storage/CDN.
Mental model
collectstatic
-> STATIC_ROOT (a folder with all assets)
-> WhiteNoise serves /static/* directly from that folder
-> adds cache headers + compression + immutable versioningInstall
pip install whitenoise
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# ...
]python manage.py collectstatic --noinput
STATIC_URLset (usually/static/)STATIC_ROOTset (a real directory)- collectstatic executed during deploy/build
- web process can read STATIC_ROOT
Configuration (recommended)
Storage backend for hashed filenames
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"
}
}- Build produces unique filenames (content hash).
- Browsers can cache “forever”.
- Deploying a new version does not break cache.
- Compression variants (gzip/brotli) handled cleanly.
Static root baseline
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"Caching & compression strategy
| Asset type | Cache policy | How to achieve |
|---|---|---|
| Hashed (manifest) | Very long (immutable) | Use CompressedManifestStaticFilesStorage |
| Non-hashed | Short cache | Ensure you do not rely on non-hashed for core assets |
| Large JS/CSS bundles | Long + compression | Gzip/brotli variants served automatically |
Nginx / CDN integration
Deploy playbook
| # | Step | Goal |
|---|---|---|
| 1 | Install requirements | WhiteNoise available at runtime |
| 2 | Run collectstatic | STATIC_ROOT contains correct assets |
| 3 | Restart web process | New code serves new static build |
| 4 | Verify /static load | Confirm cache headers and correct paths |
| 5 | Watch for manifest errors | Catch missing assets early |
- STATIC_ROOT readable by app user.
- Correct file permissions after deploy.
- Manifest storage enabled for long cache.
- Static URLs resolve with correct host/proxy settings.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 404 on /static/* | collectstatic not run or STATIC_ROOT wrong | Run collectstatic, verify STATIC_ROOT path and permissions |
| Static loads but is outdated | No hashed filenames or aggressive proxy cache | Enable manifest storage, use immutable caching, purge proxy if needed |
| Manifest error at runtime | Referenced asset missing from build | Fix build pipeline, ensure assets are included and collected |
| High CPU serving static | Huge static traffic hitting app | Move static to Nginx/CDN, keep WhiteNoise for build features |
- Open a static file URL directly in browser.
- Check response headers: cache-control, content-encoding.
- Confirm hashed filenames exist in STATIC_ROOT.
- Verify deploy ran collectstatic after code update.
Links
Automatic model audit trail (who changed what, when, and from where)
Django-Simple-History creates “historical” rows for your models automatically. Each change (create/update/delete) generates a snapshot with metadata (timestamp, user, change type), so you can audit, rollback, compare versions, and build compliance-grade traces without custom tables.
- Immutable audit trail: historical snapshots per change.
- Who: history_user (if configured).
- When: history_date.
- What: history_type (+ create, ~ update, - delete).
- Rollback: revert to previous version.
- Diff: compare two versions (basic or custom).
- Admin changes (critical configs, routing, templates).
- Security ops tables (allow/deny lists, rules engines).
- Content changes (CMS-like, portfolio, docs).
- Client data modifications (traceability / incidents).
- “Who edited this?” answers in seconds.
Mental model
Model table: app_model
History table: app_historicalmodel
Save/delete on model
-> historical row created
-> includes all tracked fields + metadata (date/type/user)Install
pip install django-simple-history
INSTALLED_APPS += ["simple_history"]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
# ...
"simple_history.middleware.HistoryRequestMiddleware",
]python manage.py migrate
- Middleware missing → history_user not populated consistently.
- Forgetting to run migrations after adding HistoricalRecords.
- Tracking too many large fields → storage bloat.
Model integration
Basic: track a model
from django.db import models
from simple_history.models import HistoricalRecords
class Project(models.Model):
name = models.CharField(max_length=200)
status = models.CharField(max_length=50, default="active")
updated_at = models.DateTimeField(auto_now=True)
history = HistoricalRecords()- All model fields (unless excluded)
history_datehistory_typehistory_user(if middleware)history_change_reason(optional)
obj.history_change_reason = "Bulk status update"
obj.save()Selective tracking (avoid bloat)
| Data type | Recommendation | Reason |
|---|---|---|
| Large text fields | Exclude or track only when required | History table grows fast |
| Binary / file-like | Do not store raw content in history | Explodes storage and backup times |
| Computed/cache fields | Exclude | Noise, no audit value |
| Critical config fields | Always track | Incident traceability |
Admin UX (history visibility)
Admin integration baseline
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import Project
@admin.register(Project)
class ProjectAdmin(SimpleHistoryAdmin):
list_display = ("id", "name", "status", "updated_at")
search_fields = ("name",)- Expose history for critical models only (signal > noise).
- Require admin permissions; audit tables can contain sensitive data.
- Encourage change reasons for bulk actions.
Query, diff, rollback
Read history for an object
obj = Project.objects.get(pk=1)
# Latest first
for h in obj.history.all()[:5]:
print(h.history_date, h.history_type, h.history_user_id)Restore previous version (rollback)
obj = Project.objects.get(pk=1)
previous = obj.history.all()[1] # example: previous snapshot
previous.instance.save()Diff idea (practical)
def diff_history(a, b, fields):
changes = []
for f in fields:
va = getattr(a, f)
vb = getattr(b, f)
if va != vb:
changes.append((f, va, vb))
return changesPatterns (production)
Audit-friendly tags
# Example: tag changes by execution context
obj.history_change_reason = "cron: nightly sync"
obj.save()Performance & storage
| Risk | Impact | Mitigation |
|---|---|---|
| History bloat | DB size & backups increase | Track only critical models, avoid big fields |
| Slow history queries | Admin history pages become slow | Index by (object_id, history_date) if needed |
| Retention unclear | Infinite growth | Define retention policy + purge jobs |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| history_user is always null | HistoryRequestMiddleware missing | Add middleware and ensure authentication is active |
| History not created on save | HistoricalRecords not added or migrations missing | Add field + run migrate |
| History tables huge | Tracking too many models/fields | Reduce scope, avoid large fields, define retention policy |
| Rollback causes side effects | Signals/constraints triggered on save | Validate in staging, implement controlled rollback procedures |
- Modify an object in admin.
- Open the object “History” view.
- Confirm history_type and history_date are set.
- Confirm history_user appears (if logged in).
Links
The “power tools” pack for Django development
Django-Extensions adds a set of high-impact development utilities: an enhanced shell with auto-imports, a better dev server with debugging helpers, model graph generation, and a bunch of workflow commands that reduce “manual repetitive operations” in daily Django work.
- shell_plus: immediate productivity (auto-import models).
- runserver_plus: better errors + debug improvements.
- graph_models: generate diagrams for data model review.
- show_urls: quickly inspect URL routing (big projects!).
- shell_plus --print-sql: visibility for ORM query patterns.
- Enable it in dev/staging, not necessarily in prod.
- Keep it out of “minimal runtime” containers if required.
- Use it for diagnosis, documentation, and dev speed.
Command mindset
"I need to inspect something quickly"
-> django-extensions usually has a command for it
-> faster than writing a one-off scriptInstall
pip install django-extensions
INSTALLED_APPS += ["django_extensions"]
- IPython for an excellent shell experience.
- Werkzeug for runserver_plus enhancements.
- Graphviz for graph_models (diagrams export).
shell_plus
Most used commands
python manage.py shell_plus
python manage.py shell_plus --print-sql
Practical use cases
| Need | What to do | Value |
|---|---|---|
| Inspect a model quickly | shell_plus + query a few objects | Fast diagnosis without writing scripts |
| Understand ORM performance | --print-sql and check generated queries | Instant visibility on joins/scans |
| Run a one-off cleanup | Small controlled loop (careful!) | Safe, interactive operations |
runserver_plus
Start dev server
python manage.py runserver_plus
Common enhancements
- Richer debug output during development.
- Helpful when debugging template issues, middleware stacks, and request flow.
- Better dev ergonomics without changing production server (gunicorn/uwsgi).
graph_models & architecture visibility
Example: generate a PNG diagram
python manage.py graph_models myapp -o myapp_models.png
Top commands (high value)
| Command | What it does | Best for |
|---|---|---|
shell_plus | Auto-imports models/utilities into interactive shell | Daily inspection and quick ops |
runserver_plus | Enhanced dev server experience | Debugging complex request flow |
show_urls | List URL patterns for your project/apps | Large projects routing sanity checks |
graph_models | Generate model diagrams (Graphviz) | Documentation + architecture reviews |
show_template_tags | Inspect available template tags/filters | Template debugging |
print_settings | Print effective settings values | Debug config differences per env |
show_urls and print_settings when you debug “works in staging but not in prod” issues. Those two commands save hours.IDEO-Lab patterns (real productivity)
shell_plus --print-sqlto understand ORM behaviorprint_settingsto validate environment configshow_urlsto confirm routing at scale
- Generate per-app model diagrams
- Store diagrams as static assets
- Link them from your IDEO-Lab dashboards
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Command not found | App not in INSTALLED_APPS / wrong venv | Add django_extensions, verify environment |
| graph_models fails | Graphviz missing | Install graphviz system package and retry |
| shell_plus not using IPython | IPython not installed | Install IPython for best experience |
| runserver_plus issues | Missing dev dependency | Install related optional packages; keep dev-only |
- Run
python manage.py shell_plusand ensure models import. - Run
python manage.py show_urlsand check output is sane. - Try
graph_modelsonly if Graphviz is installed.
Links
Admin-grade import/export for CSV and Excel
Django-Import-Export adds robust import/export actions in Django Admin using “resources”. You can export selected rows, import files with preview/dry-run, map fields, handle FK relations, and enforce validation rules before committing changes.
- Export data from Admin to CSV/XLSX for reporting.
- Import client lists, catalogs, configuration tables.
- Preview changes before writing to the database.
- Handle updates (match on id or business key).
- Reduce custom “one-off scripts” for data ops.
- Large datasets (performance + locks).
- High-risk models (security configs, billing data).
- Imports that can delete or overwrite silently.
- PII columns (compliance and retention).
- Always enable preview/dry-run first.
- Require explicit permissions for import.
- Use “business keys” for updates, not only IDs.
- Validate and normalize columns before saving.
- Log who imported what and when (audit).
Install
pip install django-import-export
INSTALLED_APPS += ["import_export"]
- Use XLSX export/import only if your stack includes the required spreadsheet libraries.
- Prefer CSV for massive imports (simpler, faster, more predictable).
Admin setup (fast path)
Minimal integration
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import Customer
@admin.register(Customer)
class CustomerAdmin(ImportExportModelAdmin):
list_display = ("id", "email", "status")
search_fields = ("email",)Resources (control columns & updates)
Define a Resource
from import_export import resources
from .models import Customer
class CustomerResource(resources.ModelResource):
class Meta:
model = Customer
fields = ("id", "email", "status", "created_at")
export_order = ("id", "email", "status", "created_at")Attach Resource to Admin
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .resources import CustomerResource
from .models import Customer
@admin.register(Customer)
class CustomerAdmin(ImportExportModelAdmin):
resource_class = CustomerResourceUpdate strategy (business key)
class CustomerResource(resources.ModelResource):
class Meta:
model = Customer
import_id_fields = ("email",)
fields = ("email", "status")Validation & normalization
| Risk | Typical cause | Mitigation |
|---|---|---|
| Wrong column mapping | Headers changed by users | Require exact headers, provide a template export |
| Bad types (dates, numbers) | Locale formats, Excel conversions | Normalize formats and validate strictly |
| Duplicates | No stable import key | Use business key in import_id_fields |
| Silent overwrites | Ambiguous matching | Reject if multiple matches are found |
- Export a template from Admin.
- Fill it with data (no extra columns).
- Import with preview/dry-run.
- Fix errors, re-import.
- Commit only when validation passes.
Workflows (real ops)
- Only superusers can import.
- Imports require a standard template.
- Dry-run mandatory.
- Audit record created (who/when/file hash).
- Match on business keys.
- Reject unknown keys (fail fast).
- Log changes count per field.
- Optional: run in off-peak windows.
Security & governance
| Control | Why | Implementation idea |
|---|---|---|
| Restrict import permission | Prevents data corruption | Admin permissions + group restriction |
| Restrict exported fields | PII leak prevention | Resource fields whitelist |
| Audit who imported | Traceability | Record user + timestamp + file metadata |
| Validate headers and types | Stops garbage input | Strict mapping + normalization |
- Export allowed for staff (limited fields).
- Import restricted to superusers (or dedicated ops group).
- Templates provided by export to reduce “format drift”.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Import button not visible | Admin class not using ImportExportModelAdmin | Inherit from ImportExportModelAdmin or add mixin |
| Wrong columns exported | No resource configured | Define Resource fields and attach via resource_class |
| Updates create duplicates | No stable import key defined | Set import_id_fields to business key |
| Encoding issues | CSV encoding mismatch | Use UTF-8 and standardize export templates |
| Import is slow / times out | Huge dataset in Admin request | Move to async job or batch imports off-peak |
- Export a small dataset to confirm columns and encoding.
- Re-import the same file as dry-run.
- Confirm update matching works (business key).
- Test permissions with a non-privileged staff user.
Links
Object-level permissions for Django
Django’s built-in permissions are model-level (global): “can view any Project”, “can change any Report”. Django-Guardian adds object-level permissions: “user A can view Project #17, but not Project #18”. This is essential for multi-tenant apps, per-client access control, and fine-grained authorization.
- Multi-tenant dashboards: each customer sees only their objects.
- Sharing objects: allow a user/team to access a specific record.
- Per-project roles (owner/editor/viewer) inside the same model.
- Private documents, notes, incident reports, audit items.
- Admin delegation: let staff manage only assigned objects.
- Define a clear permission strategy per model.
- Prefer role-based mapping (owner/editor/viewer).
- Keep object-perm checks consistent in code paths.
- Watch query performance for large datasets.
Mental model
Model-level: "can view Project"
Object-level: "can view Project #17"
Guardian stores per-object permissions and helps query
"objects user can access".Install
pip install django-guardian
INSTALLED_APPS += ["guardian"]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
]python manage.py migrate
Permissions model strategy
| Approach | Best for | Notes |
|---|---|---|
| Per-object perms (Guardian) | Sharing, delegated access, fine-grained rules | Powerful, but can create many rows. Use role mapping. |
| Tenant FK filtering | Strict multi-tenant partition | Simple and fast; combine with Guardian only for sharing exceptions. |
| Role table (owner/editor/viewer) | Most SaaS projects | Maps roles to permissions; can drive Guardian assignments. |
API examples
Assign permissions
from guardian.shortcuts import assign_perm, remove_perm
assign_perm("view_project", user, project)
assign_perm("change_project", user, project)
remove_perm("change_project", user, project)Check permissions
user.has_perm("view_project", project)
user.has_perm("change_project", project)Query objects user can access
from guardian.shortcuts import get_objects_for_user
qs = get_objects_for_user(user, "app.view_project")
# qs is a queryset of Project objects the user can viewDRF view pattern
from rest_framework.viewsets import ModelViewSet
from guardian.shortcuts import get_objects_for_user
class ProjectViewSet(ModelViewSet):
def get_queryset(self):
return get_objects_for_user(self.request.user, "app.view_project")Query & performance
get_objects_for_user) instead of doing “N checks per object” in Python loops.| Anti-pattern | Why it hurts | Better approach |
|---|---|---|
| Loop objects and call has_perm repeatedly | N+1 permission queries | Use get_objects_for_user to filter at DB level |
| Create perms for every object for every user | Permission table explodes | Use tenant filtering + perms for exceptions |
| Complex dynamic rules in code | Hard to maintain, slow | Define stable roles + explicit assignments |
Admin patterns
Filter queryset by perms
from django.contrib import admin
from guardian.shortcuts import get_objects_for_user
from .models import Project
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return get_objects_for_user(request.user, "app.view_project", klass=qs)Security posture
| Control | Why | Implementation idea |
|---|---|---|
| Enforce perms at queryset level | Avoid data leakage | Use get_objects_for_user in list endpoints |
| Enforce perms on detail endpoints | Protect object reads | Check has_perm on the object |
| Centralize permission names | Reduce mistakes | Constants or helper functions |
| Audit assignments | Trace who granted access | Track via admin logs or history on mapping models |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| has_perm always returns false | Guardian backend not configured | Add ObjectPermissionBackend to AUTHENTICATION_BACKENDS |
| Users see too many objects | Queryset not filtered | Use get_objects_for_user for list endpoints/admin queryset |
| Permissions table grows too fast | Assigning perms for every object | Use tenant filtering; assign perms only for sharing exceptions |
| Slow permission checks | N+1 checks in Python loops | Filter querysets at DB level instead |
- Assign a perm to a user on a single object.
- Confirm
user.has_perm(..., obj)returns true. - Confirm list endpoint uses
get_objects_for_userfiltering. - Test with a non-privileged user account.
Links
Strong authentication for Django accounts
django-two-factor-auth adds a second factor to the login flow (most commonly TOTP), plus optional methods like hardware-backed authentication (WebAuthn) and backup tokens. It reduces account takeover risk dramatically, especially for admin and privileged users.
- TOTP setup (QR provisioning, authenticator apps).
- Second-step challenge during login.
- Backup tokens for recovery.
- Optional WebAuthn (security keys / passkeys) integration.
- Per-user device management and enrollment UI.
- Admins and staff accounts first.
- Users with elevated permissions next.
- Optionally enforce for all accounts in sensitive apps.
Mental model
Password step
-> if user has 2FA enabled:
challenge step (TOTP/WebAuthn)
-> session established
-> else:
optional enrollment enforcement policyInstall
pip install django-two-factor-auth
INSTALLED_APPS += [
"django_otp",
"django_otp.plugins.otp_totp",
"django_otp.plugins.otp_static",
"two_factor",
]MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_otp.middleware.OTPMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]from django.urls import include, path
urlpatterns = [
path("", include("two_factor.urls", "two_factor")),
]python manage.py migrate
Auth flow (what changes)
| Step | What happens | Failure mode |
|---|---|---|
| Username + password | Primary authentication (Django auth) | Brute force risk without rate limits |
| 2FA challenge | TOTP code or WebAuthn assertion | Device lost, clock drift, mis-enrollment |
| Session established | User is authenticated and OTP verified | Session fixation if cookies not hardened |
| Manage devices | Add/remove devices, generate backup tokens | Recovery abuse if not controlled |
- Enrollment required for staff/admin accounts.
- Offer TOTP as default; offer WebAuthn if available.
- Always generate and display backup tokens once.
- Provide clear “lost device” recovery procedure.
Configuration patterns
Login URL alignment
LOGIN_URL = "two_factor:login"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"Enforcement strategy
| Policy | Best for | Notes |
|---|---|---|
| Optional 2FA | Public apps, low-risk | Encourage adoption with prompts |
| Required for staff | Most production systems | Enforce for admin and privileged roles |
| Required for everyone | High-risk environments | Ensure strong recovery and support processes |
Recovery and backup tokens
- Show backup tokens only once at generation time.
- Require re-authentication for device changes.
- Notify users on device add/remove.
- Log all recovery actions.
- Define who can reset 2FA for a user.
- Define what evidence is required for a reset.
- Log the reset request and the approver.
- Force re-enrollment after reset.
Hardening
| Control | Goal | Notes |
|---|---|---|
| Rate limit login and OTP endpoints | Stop brute force | Apply IP + account rate limits |
| Harden session cookies | Reduce session theft | Secure, HttpOnly, SameSite, short TTL for admin |
| Enforce HTTPS everywhere | Prevent interception | HSTS recommended |
| Device management protections | Prevent takeover | Re-auth required for device changes |
| Alerts and audit logs | Detection and response | Notify on new device, failed OTP attempts |
- 2FA required for all staff/superusers.
- Short session lifetime for admin sessions.
- Rate limiting on password and OTP entry.
- Audit logs for enrollment and resets.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| OTP always rejected | Clock drift or wrong device enrollment | Sync server time (NTP), re-enroll device |
| OTP middleware not working | OTPMiddleware missing or wrong order | Place OTPMiddleware after AuthenticationMiddleware |
| Login loop after successful OTP | Session/cookie misconfiguration | Check cookie domain, secure flags, proxy headers |
| Users locked out too easily | No recovery path or strict policy without support | Enable backup tokens and document reset process |
- Confirm server time sync (NTP).
- Enroll one test user and complete login.
- Verify OTP middleware activation.
- Test backup token usage.
Links
Dynamic pages with server-rendered HTML (no heavy frontend framework)
HTMX lets you build dynamic user interfaces by sending AJAX-like requests directly from HTML attributes (hx-get, hx-post, hx-target) and swapping HTML fragments into the page. With Django, this is a powerful “SSR-first” approach: fast iteration, simple deployments, and clean templates without a large JS bundle.
- SSR with small partial updates (no full page reload).
- Less JavaScript complexity (no SPA state machine).
- Templates remain the source of truth.
- Works extremely well with Django forms and validation.
- Great for dashboards, admin-like UIs, tools pages.
- Highly interactive canvases (complex client rendering).
- Offline-first apps with heavy client storage.
- Real-time multiplayer UIs (specialized websockets).
- Massive client-side data exploration (maybe SPA).
Mental model
User clicks a button
-> HTMX sends request (GET/POST)
-> Django returns HTML fragment
-> HTMX swaps fragment into target elementInstall
pip install django-htmx
INSTALLED_APPS += ["django_htmx"]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
# ...
"django_htmx.middleware.HtmxMiddleware",
]<script src="https://unpkg.com/htmx.org@1.9.12"></script>
request.htmx. HTMX still works, but your server-side branching becomes more manual.Core concepts
| Attribute | Meaning | Practical use |
|---|---|---|
hx-get | Send GET request | Live search, refresh panels, pagination |
hx-post | Send POST request | Form submit, toggle switches, create/update |
hx-target | Where to swap HTML | Update a table body, a card, a modal body |
hx-swap | How to swap | Replace content, append rows, swap outerHTML |
hx-trigger | When to send | keyup, change, load, custom events |
hx-indicator | Loading indicator | Show spinner on async update |
hx-vals | Extra params | Send additional context without hidden fields |
request.htmx to branch safely.Patterns that scale
- Keep a base page template.
- Extract replaceable blocks as partials.
- Return partial on HTMX requests.
- Page works without JS.
- HTMX adds faster updates.
- Fallback links/forms still function.
Examples (copy-ready)
Example 1 — Live search
<input
type="search"
name="q"
placeholder="Search..."
hx-get="/tools/search/"
hx-trigger="keyup changed delay:250ms"
hx-target="#results"
hx-swap="innerHTML"
>
<div id="results"></div>Example 2 — Button action + partial refresh
<button
hx-post="/netguard/ip/block/"
hx-vals='{"ip":"203.0.113.10"}'
hx-target="#block-status"
hx-swap="outerHTML"
>Block IP</button>
<div id="block-status"></div>Example 3 — Django view branching
from django.shortcuts import render
from django_htmx.http import HttpResponseClientRedirect
def tool_list(request):
items = [] # fetch items
template = "tools/_list.html" if getattr(request, "htmx", False) else "tools/index.html"
return render(request, template, {"items": items})Security
| Risk | Impact | Mitigation |
|---|---|---|
| CSRF on hx-post | Unauthorized writes | Use Django CSRF protection and include CSRF token |
| Auth bypass via partial endpoints | Data leakage | Same auth checks as full views |
| Overexposed actions | Privilege escalation | Permission checks on every endpoint |
Performance
| Issue | Symptom | Fix |
|---|---|---|
| N+1 queries in partial views | Slow fragment refresh | Use select_related/prefetch_related |
| Too frequent triggers | Server load spikes | Use delay/debounce in hx-trigger |
| Large HTML fragments | Bandwidth still high | Return only needed markup |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No request sent | HTMX script not loaded | Check network tab and script tag location |
| Partial returns full page | No branching on request.htmx | Return partial template when request is HTMX |
| 403 on hx-post | Missing CSRF token | Ensure CSRF token is included in requests |
| Swap updates wrong area | hx-target selector mismatch | Verify target id/class and swap mode |
Links
Better select fields: search, tagging, multi-select, remote data
Django-Select2 integrates Select2 widgets into Django forms and admin: searchable dropdowns, multi-select, async loading for large datasets, and a UX that scales when your choices list becomes too large for standard HTML selects.
- Search inside select fields (instant usability boost).
- Supports huge datasets using AJAX rather than loading all options.
- Cleaner UX for foreign keys and many-to-many relations.
- Reduces user errors by guiding selection.
- Works well for admin tools and internal dashboards.
- Large user list selectors (assignees, owners).
- Tags, categories, labels (multi-select).
- Country/city selectors with search.
- Admin FK fields for big tables.
- “Attach existing object” in modals.
Mental model
Small dataset:
-> classic Select2 with local options
Large dataset:
-> Select2 sends AJAX query (search term)
-> Django returns JSON results
-> Select2 renders dropdownInstall
pip install django-select2
INSTALLED_APPS += ["django_select2"]
from django.urls import include, path
urlpatterns = [
# ...
path("select2/", include("django_select2.urls")),
]Widgets
| Widget type | Best for | Notes |
|---|---|---|
| Local select widget | Small fixed choices | Simple, fast, no AJAX |
| Model select widget | Foreign keys / M2M | Can be AJAX-backed for scale |
| Heavy select widget | Huge datasets | AJAX search, requires server-side filtering |
| Tagging | Labels/categories | Decide whether tags are free-form or constrained |
AJAX autocomplete
Example: Heavy select widget (ModelChoiceField)
from django import forms
from django_select2.forms import HeavySelect2Widget
from .models import Project
class TaskForm(forms.Form):
project = forms.ModelChoiceField(
queryset=Project.objects.all(),
widget=HeavySelect2Widget(
data_url="/select2/fields/auto.json"
),
)Operational checklist
- Limit results per request (pagination).
- Add a minimum input length (e.g., 2-3 characters).
- Index search fields (name, code, email).
- Scope results by tenant and permissions.
- Cache frequent queries if needed.
Forms patterns
Performance
| Issue | Symptom | Fix |
|---|---|---|
| No pagination | Slow responses, huge payloads | Paginate results server-side |
| Unindexed search field | DB CPU spikes on search | Add index to searched columns |
| Too broad query | Users see irrelevant results | Scope by tenant and add filters |
| Requests too frequent | Load spikes | Minimum input length + debounce |
- Minimum input length: 2-3 characters.
- Results per request: 20-50.
- Index the search column(s).
- Scope by permissions and tenant.
Security
| Risk | Impact | Mitigation |
|---|---|---|
| Autocomplete data leakage | Users infer hidden records | Permission-check and tenant-scope results |
| Enumeration via search | Attackers brute list names/emails | Rate limit, require auth, minimum input length |
| PII exposure in labels | Privacy risk | Return safe labels only (avoid full emails/phones) |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Widget renders as plain select | JS/CSS assets not loaded | Fix static pipeline and ensure scripts load |
| AJAX calls return 404 | URLs not included | Add path("select2/", include(...)) |
| AJAX returns 403 | Auth/CSRF issues | Require auth, ensure correct middleware and headers |
| Search is slow | Unindexed query | Add indexes, restrict search scope |
| No results | Filtering too strict or wrong field mapping | Inspect query, verify search fields and tenant scope |
Links
Modern responsive theme for Django Admin
Jazzmin is a drop-in admin theme that modernizes Django Admin with a cleaner UI, better navigation, responsive layouts, and configurable branding. It is a high ROI addon for internal tools where Admin is used as a real “back office”.
- Improves admin usability immediately (less “legacy admin feel”).
- Better menus and grouping for multi-app projects.
- Cleaner list pages and forms (less visual clutter).
- Branding: logos, titles, links, icons.
- Good fit for mobile and narrow screens.
- If you rely on heavy admin CSS overrides.
- If you have multiple custom admin themes.
- If you ship a “minimal static assets” policy.
- After major Django upgrades (verify theme compatibility).
django-import-export, simple-history, and strict permissions for a strong “ops admin”.Install
pip install django-jazzmin
INSTALLED_APPS = [
"jazzmin",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# ...
]python manage.py collectstatic
- Restart server.
- Open
/admin/. - Confirm visual theme is applied.
- Confirm static files load (no 404 in devtools).
Configuration
Minimal settings
JAZZMIN_SETTINGS = {
"site_title": "Admin",
"site_header": "Admin",
"site_brand": "Back Office",
"welcome_sign": "Welcome",
"copyright": "Company",
"topmenu_links": [
{"name": "Home", "url": "/", "permissions": ["auth.view_user"]},
],
"show_sidebar": True,
"navigation_expanded": True,
}Logo and icons
JAZZMIN_SETTINGS.update({
"site_logo": "img/brand/logo.svg",
"site_logo_classes": "img-circle",
"site_icon": "img/brand/favicon.ico",
})UI & navigation design
Menu structuring (recommended)
| Section | What belongs there | Example |
|---|---|---|
| Operations | Incident, monitoring, security tables | NetGuard events, blocks, telemetry |
| Content | CMS-like models | Pages, templates, translations |
| Accounts | Users, groups, permissions | Auth, Guardian, 2FA devices |
| Data Tools | Bulk imports/exports, audit | ImportExport, History, job logs |
Ops & hardening
| Control | Why | Implementation idea |
|---|---|---|
| Require 2FA for staff | Prevent admin takeover | Enforce 2FA enrollment for staff users |
| IP allowlist / VPN | Reduce exposure | Restrict /admin/ to trusted networks |
| Least privilege groups | Limit blast radius | Granular group permissions per app |
| Audit admin actions | Trace changes | Use history/audit logs and backups |
| Short admin sessions | Reduce session theft impact | Separate session policies for staff if needed |
Compatibility & upgrade strategy
- Pin Jazzmin version.
- Upgrade in staging first.
- Verify /admin/ pages and static assets.
- Validate custom admin overrides.
- Roll out with a rollback plan.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Theme not applied | INSTALLED_APPS order wrong | Put jazzmin before django.contrib.admin |
| Broken CSS or missing icons | Static files not collected or served | Run collectstatic and verify static routing |
| Custom admin pages look wrong | Template overrides not compatible | Review overrides and reduce custom CSS |
| After upgrade, admin layout breaks | Theme version mismatch | Pin versions; test compatibility in staging |
/admin/, confirm CSS/JS requests succeed (no 404/500), then validate INSTALLED_APPS order.Links
Feature flags for safe releases and experiments
Django-Waffle enables feature flagging: you can turn features on/off without redeploying, roll out gradually, target specific users/groups, and quickly disable risky changes in production. Feature flags are one of the best “safety valves” for fast-moving platforms.
- Kill switch for risky features in production.
- Gradual rollouts (limited audience first).
- A/B testing and experiments (basic form).
- Staging features behind flags until ready.
- Tenant/customer-specific feature enablement.
- Flag: boolean gate for a feature.
- Switch: global on/off toggle.
- Sample: percentage-based rollout.
Install
pip install django-waffle
INSTALLED_APPS += ["waffle"]
python manage.py migrate
Flags model
| Type | Behavior | Use case |
|---|---|---|
| Switch | Global on/off | Emergency kill switch for a subsystem |
| Flag | Boolean gate, can target users/groups | Enable a feature for staff first, then for all |
| Sample | Percentage-based enablement | Roll out to 5% then 25% then 100% |
dashboard_v2_enabled, parser_new_dedup, admin_bulk_export. Avoid ambiguous names like test1 or new_feature.Usage
Python check
from waffle import flag_is_active
def view(request):
if flag_is_active(request, "dashboard_v2_enabled"):
# new path
pass
else:
# stable path
passTemplate check
{% load waffle_tags %}
{% flag "dashboard_v2_enabled" %}
<div>New UI</div>
{% else %}
<div>Old UI</div>
{% endflag %}Switch and sample
from waffle import switch_is_active, sample_is_active
if switch_is_active("emergency_kill_switch"):
# disable subsystem behavior
pass
if sample_is_active("rollout_25_percent"):
# experiment path
passRollout playbook
- Create the flag in admin, default OFF.
- Deploy code with both paths (new + stable).
- Enable for internal users only (staff group).
- Monitor errors and performance for 24h.
- Expand audience (sample 5% → 25% → 100%).
- Remove legacy path and delete the flag.
| Phase | Audience | What to watch |
|---|---|---|
| Phase 0 | OFF | No behavior change; confirm stability |
| Phase 1 | Staff | Errors, UX feedback, edge cases |
| Phase 2 | Sample 5–25% | Latency, DB load, conversion, incidents |
| Phase 3 | 100% | Long-tail bugs, ops load |
Ops & governance
| Practice | Why | How |
|---|---|---|
| Flag ownership | Accountability | Assign an owner per flag (team or person) |
| Expiry policy | Prevent flag sprawl | Each flag has a removal date and ticket |
| Least privilege | Prevent abuse | Restrict edit rights in admin |
| Audit and logging | Trace changes | Log flag toggles and who changed them |
| Emergency procedure | Faster incident response | Document the kill switch list |
Performance
| Anti-pattern | Why it hurts | Fix |
|---|---|---|
| Check flag inside a loop | Repeated evaluations | Compute once per request and reuse |
| Many flags per template render | Complexity and overhead | Consolidate by feature groups |
| No caching | Extra DB reads | Enable cache backend and configure Waffle caching |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Admin models not visible | App not installed or migrations missing | Add waffle to INSTALLED_APPS and migrate |
| Flag check always false | Wrong flag name | Verify spelling and naming convention |
| Flag works in template but not in Python | Different contexts or request missing | Use request-based helpers consistently |
| Performance regression | Too many checks without caching | Enable cache; reduce repeated checks |
- Create a test flag in admin:
waffle_smoke_test. - Wrap a visible UI element in a template flag block.
- Toggle flag ON/OFF and confirm behavior changes instantly.
- Confirm permissions prevent unauthorized edits.
Links
Request and SQL profiling inside your Django app
Django-Silk is an in-app profiler that records HTTP requests, response timings, SQL queries (including timing), and helps identify performance regressions quickly. It is ideal for diagnosing slow pages, N+1 query issues, and unexpected backend work during requests.
- See slow endpoints and the exact SQL queries executed.
- Spot N+1 patterns and missing
select_related/prefetch_related. - Measure time spent in DB vs application logic.
- Compare before/after changes when tuning views.
- Quickly validate caching effectiveness.
- Staging or local profiling sessions.
- Reproducing a “slow page” incident.
- Measuring ORM query changes.
- Validating pagination and filters.
Mental model
Request comes in
-> Silk middleware records timing + metadata
-> DB queries are recorded with durations
-> Silk UI shows slow requests and query breakdownInstall
pip install django-silk
INSTALLED_APPS += ["silk"]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"silk.middleware.SilkyMiddleware",
]from django.urls import include, path
urlpatterns = [
# ...
path("silk/", include("silk.urls", namespace="silk")),
]python manage.py migrate
- Restart server.
- Browse a few pages to generate requests.
- Open
/silk/and confirm requests appear. - Open one request and inspect SQL query timings.
What it captures
| Signal | What you learn | Typical action |
|---|---|---|
| Request timing | Which endpoints are slow | Focus tuning on top offenders |
| SQL queries + durations | ORM work and DB cost | Fix N+1, add indexes, reduce joins |
| Headers / status / path | Traffic shape and errors | Find abusive endpoints or unexpected callers |
| SQL count per request | Chatty views | Prefetch, cache, restructure view |
| Request body (careful) | Inputs that trigger slow paths | Reproduce and test fixes |
Workflow (how to use it well)
- Reproduce the slow request (same filters, same dataset).
- Open the request in Silk and note total time.
- Check SQL section: count, slowest query, duplicates.
- Decide: ORM fix vs index vs caching vs pagination.
- Apply fix and re-run the exact request.
- Compare before/after timings and query count.
| Finding | Interpretation | Fix examples |
|---|---|---|
| High SQL count | N+1 or chatty ORM | select_related / prefetch_related / annotate |
| One very slow SQL query | Missing index or bad plan | Add index, reduce scope, add pagination |
| Slow but few queries | Python work / serialization | Reduce payload, cache, optimize loops |
| Time spikes on filters | Unindexed filter columns | Index filters used in list pages |
Common cases
select_related for FK and prefetch_related for M2M/reverse relations.Safety (do it right)
| Risk | Impact | Mitigation |
|---|---|---|
| Data exposure | Leak headers/POST bodies | Restrict access, sanitize capture, run on staging |
| Performance overhead | Slows down requests | Enable only temporarily, sample traffic if possible |
| Public endpoint exposure | Attack surface | Protect /silk/ by auth and IP rules |
Performance impact
| Concern | Symptom | Fix |
|---|---|---|
| DB growth | Silk tables grow fast | Limit window, cleanup old rows |
| Overhead too high | Requests slow down | Disable Silk when not profiling |
| Noise | Too many irrelevant requests | Use ignore paths or filters |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| /silk/ returns 404 | URLs not included | Add path("silk/", include(...)) |
| No requests recorded | Middleware missing | Add silk.middleware.SilkyMiddleware |
| Access denied | Auth restrictions | Ensure staff permissions and correct login |
| DB grows too quickly | Profiling left enabled | Disable Silk after profiling; purge old rows |
/silk/, the integration is correct.Links
Small utilities that remove a lot of boilerplate in Django models
django-model-utils provides lightweight model mixins and fields commonly used across projects: timestamp tracking, “status” patterns, handy fields, and helpers for readable choices. It improves consistency across apps and reduces repetitive model code.
- Standardize fields like
createdandmodifiedacross apps. - Implement status/state machines in a clean, consistent way.
- Reduce copy/paste in models and migrations.
- Make admin and APIs more uniform (same fields everywhere).
- Improve readability and maintainability of models.
- Multi-app platforms (shared conventions matter).
- Internal tools with lots of CRUD models.
- Projects where audit-like fields are common.
- Apps that need status transitions (jobs, tasks, workflows).
Install
pip install django-model-utils
# requirements.txt
django-model-utils==4.*Mixins
| Mixin | What it provides | Best use |
|---|---|---|
TimeStampedModel | created and modified fields | Most persistent business entities |
StatusModel | Unified “status” field pattern | Jobs, tasks, workflows, pipelines |
SoftDeletableModel | Soft delete behavior (mark deleted) | When deletion must be reversible |
UUIDModel (when used) | UUID primary key or id field pattern | Public identifiers, distributed systems |
Example: timestamps
from django.db import models
from model_utils.models import TimeStampedModel
class Project(TimeStampedModel):
name = models.CharField(max_length=200)Example: status model
from django.db import models
from model_utils.models import StatusModel
from model_utils import Choices
class Task(StatusModel):
STATUS = Choices("new", "running", "failed", "done")
name = models.CharField(max_length=200)Fields
| Field | Problem solved | Notes |
|---|---|---|
MonitorField | Update a timestamp when another field changes | Useful for “status_changed_at” tracking |
SplitField | One input, multiple stored values | Use carefully; can be surprising |
UUIDField (legacy usage) | UUID support in older Django versions | Modern Django has native UUIDField |
Example: monitor a field
from django.db import models
from model_utils.fields import MonitorField
class Job(models.Model):
status = models.CharField(max_length=32)
status_changed_at = MonitorField(monitor="status")Choices
Example: readable choices
from model_utils import Choices
JOB_STATUS = Choices(
("new", "new", "New"),
("running", "running", "Running"),
("failed", "failed", "Failed"),
("done", "done", "Done"),
)Design patterns
| Pattern | Why it helps | Example |
|---|---|---|
| Platform base model | Consistency across many apps | BaseEntity(TimeStampedModel) used everywhere |
| Status + timestamps | Better operational visibility | StatusModel + MonitorField for transitions |
| Soft delete + audit | Accidental deletion recovery | SoftDeletableModel + admin filters |
| Public ids | Do not expose sequential primary keys | UUID or short public token field |
Pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
| Soft-deleted rows still visible | Querysets not filtering deleted items | Use consistent managers and admin filters |
| Status transitions not tracked | No transition timestamp field | Add MonitorField and use it in dashboards |
| Choice values keep changing | DB values not stable | Use stable internal values; change labels only |
| Too many base classes | Mixin stacking without plan | Define one platform base model and reuse |
- Do all core models have
created/modified? - Do workflow models have a status + transition timestamps?
- Are choice values stable over time?
- Is soft delete used consistently (or not used at all)?
Links
Track and block failed login attempts (anti brute-force)
Django-BAT is the “Bad Actor Tracker” layer for authentication endpoints. It records failed login attempts and applies lockouts when a threshold is exceeded. In practice, most projects use django-axes as the underlying engine (failed login tracking + blocking, with DB or cache backends, multiple lockout keys, cooloff, allowlists/blocklists). :contentReference[oaicite:2]{index=2}
- Django admin login
- Public login form endpoints
- API token/session login endpoints
- Credential stuffing and bot-driven brute force
- Fewer successful credential guessing attacks
- Less noise in auth logs
- Faster incident response (clear lockout signals)
- Reduced load on auth endpoints
Install
pip install django-axes
INSTALLED_APPS += ["axes"]
MIDDLEWARE = [
# ...
"axes.middleware.AxesMiddleware",
]python manage.py migrate
- Open the login page.
- Fail authentication a few times.
- Confirm lockout behavior triggers (or logs show attempts recorded).
- Confirm admin/staff access is not accidentally blocked (allowlist policy).
Policy design
| Setting goal | Recommendation | Notes |
|---|---|---|
| Attempt limit | 5–10 failures | Lower for admin, slightly higher for public login |
| Cooloff time | 15–60 minutes | Use progressive cooloff for repeated abuse |
| Lockout keys | IP + username (or IP + user agent) | Balance safety and NAT/shared IP risk |
| Allowlisting | Staff/VPN subnets | Protect operations from accidental lockouts |
| Visibility | Log + admin view | Security controls must be observable |
Configuration (Axes-style)
# Security baseline (example)
AXES_FAILURE_LIMIT = 7
AXES_COOLOFF_TIME = 1 # hours (example)
AXES_LOCK_OUT_AT_FAILURE = True
# Lockout keys
AXES_ONLY_USER_FAILURES = False
AXES_LOCKOUT_PARAMETERS = ["ip_address", "username"]
# Safety valves
AXES_NEVER_LOCKOUT_WHITELIST = True
AXES_IP_WHITELIST = ["127.0.0.1"] # add VPN / admin subnets
AXES_USERNAME_WHITELIST = [] # service users if needed
# Observability
AXES_VERBOSE = True/admin/ and require 2FA + IP restrictions for staff. Treat admin authentication as a different security tier.Storage backends
Axes-style engines can persist attempts either in the database or in a cache backend for speed and resilience. :contentReference[oaicite:6]{index=6}
| Backend | Pros | Cons / notes |
|---|---|---|
| Database | Easy to audit and query | Can grow quickly; needs cleanup/retention |
| Cache (Redis/Memcached) | Fast, good under attack | Less durable; still needs policy for visibility |
Ops & integrations
| Layer | Goal | Example |
|---|---|---|
| NGINX rate limit | Drop bot bursts early | limit_req on /login and /admin |
| Firewall automation | Block abusive subnets | Feed repeated offenders into NetGuard rules |
| 2FA for staff | Prevent admin takeover | Enforce 2FA for staff accounts |
| Monitoring & alerting | Detect active attack | Alert on spike in failed logins |
- Confirm spike in failures (logs / dashboard).
- Enable stricter rate limiting on auth endpoints.
- Lower attempt limit temporarily for /admin/.
- Block top abusive IPs/subnets at firewall.
- Review allowlist to protect staff access.
- After incident: restore policy and purge old noise data.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No lockouts happen | Middleware missing or misordered | Add axes.middleware.AxesMiddleware |
| Everyone gets locked out | Lockout key too broad (IP-only) | Use IP + username; add allowlist |
| Real IP not detected | Proxy headers not handled | Fix proxy config; validate client IP source |
| DB tables grow too fast | Retention not enforced | Add cleanup job / retention policy |
