Modèles de Développement Django
Modèles d'architecture Django de qualité production pour des applications évolutives et maintenables.
Quand Activer
- Construire des applications web Django
- Concevoir des API Django REST Framework
- Travailler avec l'ORM et les modèles Django
- Mettre en place la structure de projet Django
- Implémenter la mise en cache, les signaux, le middleware
Structure de Projet
Disposition Recommandée
monprojet/
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # Paramètres de base
│ │ ├── development.py # Paramètres de développement
│ │ ├── production.py # Paramètres de production
│ │ └── test.py # Paramètres de test
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── manage.py
└── apps/
├── __init__.py
├── utilisateurs/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ ├── permissions.py
│ ├── filters.py
│ ├── services.py
│ └── tests/
└── produits/
└── ...
Modèle de Paramètres Éclatés
# config/settings/base.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
# Applications locales
'apps.utilisateurs',
'apps.produits',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST'),
'PORT': env('DB_PORT', default='5432'),
}
}
# config/settings/development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
DATABASES['default']['NAME'] = 'monprojet_dev'
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# config/settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Journalisation
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'fichier': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/django.log',
},
},
'loggers': {
'django': {
'handlers': ['fichier'],
'level': 'WARNING',
'propagate': True,
},
},
}
Modèles de Conception de Modèles
Meilleures Pratiques pour les Modèles
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator, MaxValueValidator
class Utilisateur(AbstractUser):
"""Modèle utilisateur personnalisé étendant AbstractUser."""
email = models.EmailField(unique=True)
telephone = models.CharField(max_length=20, blank=True)
date_naissance = models.DateField(null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'utilisateurs'
verbose_name = 'utilisateur'
verbose_name_plural = 'utilisateurs'
ordering = ['-date_joined']
def __str__(self):
return self.email
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
class Produit(models.Model):
"""Modèle de produit avec configuration de champ appropriée."""
nom = models.CharField(max_length=200)
slug = models.SlugField(unique=True, max_length=250)
description = models.TextField(blank=True)
prix = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0)]
)
stock = models.PositiveIntegerField(default=0)
est_actif = models.BooleanField(default=True)
categorie = models.ForeignKey(
'Categorie',
on_delete=models.CASCADE,
related_name='produits'
)
tags = models.ManyToManyField('Tag', blank=True, related_name='produits')
cree_le = models.DateTimeField(auto_now_add=True)
mis_a_jour_le = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'produits'
ordering = ['-cree_le']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['-cree_le']),
models.Index(fields=['categorie', 'est_actif']),
]
constraints = [
models.CheckConstraint(
check=models.Q(prix__gte=0),
name='prix_non_negatif'
)
]
def __str__(self):
return self.nom
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.nom)
super().save(*args, **kwargs)
Meilleures Pratiques pour les QuerySets
from django.db import models
class ProduitQuerySet(models.QuerySet):
"""QuerySet personnalisé pour le modèle Produit."""
def actifs(self):
"""Retourne uniquement les produits actifs."""
return self.filter(est_actif=True)
def avec_categorie(self):
"""Sélectionne la catégorie liée pour éviter les requêtes N+1."""
return self.select_related('categorie')
def avec_tags(self):
"""Précharge les tags pour les relations plusieurs-à-plusieurs."""
return self.prefetch_related('tags')
def en_stock(self):
"""Retourne les produits avec un stock > 0."""
return self.filter(stock__gt=0)
def rechercher(self, requete):
"""Recherche des produits par nom ou description."""
return self.filter(
models.Q(nom__icontains=requete) |
models.Q(description__icontains=requete)
)
class Produit(models.Model):
# ... champs ...
objects = ProduitQuerySet.as_manager() # Utilise le QuerySet personnalisé
# Utilisation
Produit.objects.actifs().avec_categorie().en_stock()
Méthodes de Manager
class ProduitManager(models.Manager):
"""Manager personnalisé pour les requêtes complexes."""
def obtenir_ou_none(self, **kwargs):
"""Retourne l'objet ou None au lieu de DoesNotExist."""
try:
return self.get(**kwargs)
except self.model.DoesNotExist:
return None
def creer_avec_tags(self, nom, prix, noms_tags):
"""Crée un produit avec des tags associés."""
produit = self.create(nom=nom, prix=prix)
tags = [Tag.objects.get_or_create(nom=nom)[0] for nom in noms_tags]
produit.tags.set(tags)
return produit
def mise_a_jour_stock_en_masse(self, ids_produits, quantite):
"""Met à jour le stock en masse pour plusieurs produits."""
return self.filter(id__in=ids_produits).update(stock=quantite)
# Dans le modèle
class Produit(models.Model):
# ... champs ...
custom = ProduitManager()
Modèles Django REST Framework
Modèles de Sérialiseur
from rest_framework import serializers
from django.contrib.auth.password_validation import validate_password
from .models import Produit, Utilisateur
class ProduitSerializer(serializers.ModelSerializer):
"""Sérialiseur pour le modèle Produit."""
nom_categorie = serializers.CharField(source='categorie.nom', read_only=True)
note_moyenne = serializers.FloatField(read_only=True)
prix_remise = serializers.SerializerMethodField()
class Meta:
model = Produit
fields = [
'id', 'nom', 'slug', 'description', 'prix',
'prix_remise', 'stock', 'nom_categorie',
'note_moyenne', 'cree_le'
]
read_only_fields = ['id', 'slug', 'cree_le']
def get_prix_remise(self, obj):
"""Calcule le prix de remise si applicable."""
if hasattr(obj, 'remise') and obj.remise:
return obj.prix * (1 - obj.remise.pourcentage / 100)
return obj.prix
def validate_prix(self, value):
"""Garantit que le prix est non négatif."""
if value < 0:
raise serializers.ValidationError("Le prix ne peut pas être négatif.")
return value
class ProduitCreateSerializer(serializers.ModelSerializer):
"""Sérialiseur pour la création de produits."""
class Meta:
model = Produit
fields = ['nom', 'description', 'prix', 'stock', 'categorie']
def validate(self, data):
"""Validation personnalisée pour plusieurs champs."""
if data['prix'] > 10000 and data['stock'] > 100:
raise serializers.ValidationError(
"Impossible d'avoir des produits de grande valeur avec un stock important."
)
return data
class UtilisateurInscriptionSerializer(serializers.ModelSerializer):
"""Sérialiseur pour l'inscription d'utilisateur."""
mot_de_passe = serializers.CharField(
write_only=True,
required=True,
validators=[validate_password],
style={'input_type': 'password'}
)
confirmation_mot_de_passe = serializers.CharField(write_only=True, style={'input_type': 'password'})
class Meta:
model = Utilisateur
fields = ['email', 'username', 'mot_de_passe', 'confirmation_mot_de_passe']
def validate(self, data):
"""Valide que les mots de passe correspondent."""
if data['mot_de_passe'] != data['confirmation_mot_de_passe']:
raise serializers.ValidationError({
"confirmation_mot_de_passe": "Les mots de passe ne correspondent pas."
})
return data
def create(self, validated_data):
"""Crée l'utilisateur avec un mot de passe haché."""
validated_data.pop('confirmation_mot_de_passe')
mot_de_passe = validated_data.pop('mot_de_passe')
utilisateur = Utilisateur.objects.create(**validated_data)
utilisateur.set_password(mot_de_passe)
utilisateur.save()
return utilisateur
Modèles de ViewSet
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from django_filters.rest_framework import DjangoFilterBackend
from .models import Produit
from .serializers import ProduitSerializer, ProduitCreateSerializer
from .permissions import EstProprietaireOuLectureSeule
from .filters import ProduitFilter
from .services import ProduitService
class ProduitViewSet(viewsets.ModelViewSet):
"""ViewSet pour le modèle Produit."""
queryset = Produit.objects.select_related('categorie').prefetch_related('tags')
permission_classes = [IsAuthenticated, EstProprietaireOuLectureSeule]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = ProduitFilter
search_fields = ['nom', 'description']
ordering_fields = ['prix', 'cree_le', 'nom']
ordering = ['-cree_le']
def get_serializer_class(self):
"""Retourne le sérialiseur approprié en fonction de l'action."""
if self.action == 'create':
return ProduitCreateSerializer
return ProduitSerializer
def perform_create(self, serializer):
"""Enregistre avec le contexte utilisateur."""
serializer.save(created_by=self.request.user)
@action(detail=False, methods=['get'])
def en_vedette(self, request):
"""Retourne les produits en vedette."""
en_vedette = self.queryset.filter(est_en_vedette=True)[:10]
serializer = self.get_serializer(en_vedette, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def acheter(self, request, pk=None):
"""Achète un produit."""
produit = self.get_object()
service = ProduitService()
resultat = service.acheter(produit, request.user)
return Response(resultat, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def mes_produits(self, request):
"""Retourne les produits créés par l'utilisateur actuel."""
produits = self.queryset.filter(created_by=request.user)
page = self.paginate_queryset(produits)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
Actions Personnalisées
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def ajouter_au_panier(request):
"""Ajoute un produit au panier de l'utilisateur."""
id_produit = request.data.get('id_produit')
quantite = request.data.get('quantite', 1)
try:
produit = Produit.objects.get(id=id_produit)
except Produit.DoesNotExist:
return Response(
{'erreur': 'Produit non trouvé'},
status=status.HTTP_404_NOT_FOUND
)
panier, _ = Panier.objects.get_or_create(utilisateur=request.user)
ArticlePanier.objects.create(
panier=panier,
produit=produit,
quantite=quantite
)
return Response({'message': 'Ajouté au panier'}, status=status.HTTP_201_CREATED)
Modèle de Couche de Service
# apps/commandes/services.py
from typing import Optional
from django.db import transaction
from .models import Commande, ArticleCommande
class CommandeService:
"""Couche de service pour la logique métier liée aux commandes."""
@staticmethod
@transaction.atomic