Modèles de développement Django
Modèles d'architecture Django de qualité production pour des applications évolutives et maintenables.
Quand activer
- Construction d'applications web Django
- Conception d'API Django REST Framework
- Travail avec l'ORM et les modèles Django
- Mise en place de la structure de projet Django
- Implémentation de la mise en cache, des signaux, du middleware
Structure du 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
├── users/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ ├── permissions.py
│ ├── filters.py
│ ├── services.py
│ └── tests/
└── products/
└── ...
Modèle de paramètres divisé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.users',
'apps.products',
]
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': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/django.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
},
}
Modèles de conception de modèles
Bonnes 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 User(AbstractUser):
"""Modèle utilisateur personnalisé étendant AbstractUser."""
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
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 Product(models.Model):
"""Modèle de produit avec configuration de champ appropriée."""
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, max_length=250)
description = models.TextField(blank=True)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0)]
)
stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
category = models.ForeignKey(
'Category',
on_delete=models.CASCADE,
related_name='products'
)
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'products'
ordering = ['-created_at']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['-created_at']),
models.Index(fields=['category', 'is_active']),
]
constraints = [
models.CheckConstraint(
check=models.Q(price__gte=0),
name='price_non_negative'
)
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
Bonnes pratiques pour les QuerySets
from django.db import models
class ProductQuerySet(models.QuerySet):
"""QuerySet personnalisé pour le modèle Product."""
def active(self):
"""Retourne uniquement les produits actifs."""
return self.filter(is_active=True)
def with_category(self):
"""Sélectionne la catégorie associée pour éviter les requêtes N+1."""
return self.select_related('category')
def with_tags(self):
"""Précharge les tags pour les relations many-to-many."""
return self.prefetch_related('tags')
def in_stock(self):
"""Retourne les produits avec un stock > 0."""
return self.filter(stock__gt=0)
def search(self, query):
"""Recherche des produits par nom ou description."""
return self.filter(
models.Q(name__icontains=query) |
models.Q(description__icontains=query)
)
class Product(models.Model):
# ... champs ...
objects = ProductQuerySet.as_manager() # Utilise le QuerySet personnalisé
# Utilisation
Product.objects.active().with_category().in_stock()
Méthodes du Manager
class ProductManager(models.Manager):
"""Manager personnalisé pour les requêtes complexes."""
def get_or_none(self, **kwargs):
"""Retourne l'objet ou None au lieu de DoesNotExist."""
try:
return self.get(**kwargs)
except self.model.DoesNotExist:
return None
def create_with_tags(self, name, price, tag_names):
"""Crée un produit avec des tags associés."""
product = self.create(name=name, price=price)
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
product.tags.set(tags)
return product
def bulk_update_stock(self, product_ids, quantity):
"""Met à jour le stock en masse pour plusieurs produits."""
return self.filter(id__in=product_ids).update(stock=quantity)
# Dans le modèle
class Product(models.Model):
# ... champs ...
custom = ProductManager()
Modèles Django REST Framework
Modèles de sérialiseurs
from rest_framework import serializers
from django.contrib.auth.password_validation import validate_password
from .models import Product, User
class ProductSerializer(serializers.ModelSerializer):
"""Sérialiseur pour le modèle Product."""
category_name = serializers.CharField(source='category.name', read_only=True)
average_rating = serializers.FloatField(read_only=True)
discount_price = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id', 'name', 'slug', 'description', 'price',
'discount_price', 'stock', 'category_name',
'average_rating', 'created_at'
]
read_only_fields = ['id', 'slug', 'created_at']
def get_discount_price(self, obj):
"""Calcule le prix réduit si applicable."""
if hasattr(obj, 'discount') and obj.discount:
return obj.price * (1 - obj.discount.percent / 100)
return obj.price
def validate_price(self, value):
"""Assure que le prix est non négatif."""
if value < 0:
raise serializers.ValidationError("Le prix ne peut pas être négatif.")
return value
class ProductCreateSerializer(serializers.ModelSerializer):
"""Sérialiseur pour la création de produits."""
class Meta:
model = Product
fields = ['name', 'description', 'price', 'stock', 'category']
def validate(self, data):
"""Validation personnalisée pour plusieurs champs."""
if data['price'] > 10000 and data['stock'] > 100:
raise serializers.ValidationError(
"Impossible d'avoir des produits de grande valeur avec un stock important."
)
return data
class UserRegistrationSerializer(serializers.ModelSerializer):
"""Sérialiseur pour l'inscription des utilisateurs."""
password = serializers.CharField(
write_only=True,
required=True,
validators=[validate_password],
style={'input_type': 'password'}
)
password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'})
class Meta:
model = User
fields = ['email', 'username', 'password', 'password_confirm']
def validate(self, data):
"""Valide que les mots de passe correspondent."""
if data['password'] != data['password_confirm']:
raise serializers.ValidationError({
"password_confirm": "Les mots de passe ne correspondent pas."
})
return data
def create(self, validated_data):
"""Crée un utilisateur avec un mot de passe haché."""
validated_data.pop('password_confirm')
password = validated_data.pop('password')
user = User.objects.create(**validated_data)
user.set_password(password)
user.save()
return user
Modèles de ViewSets
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 Product
from .serializers import ProductSerializer, ProductCreateSerializer
from .permissions import IsOwnerOrReadOnly
from .filters import ProductFilter
from .services import ProductService
class ProductViewSet(viewsets.ModelViewSet):
"""ViewSet pour le modèle Product."""
queryset = Product.objects.select_related('category').prefetch_related('tags')
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = ProductFilter
search_fields = ['name', 'description']
ordering_fields = ['price', 'created_at', 'name']
ordering = ['-created_at']
def get_serializer_class(self):
"""Retourne le sérialiseur approprié en fonction de l'action."""
if self.action == 'create':
return ProductCreateSerializer
return ProductSerializer
def perform_create(self, serializer):
"""Enregistre avec le contexte utilisateur."""
serializer.save(created_by=self.request.user)
@action(detail=False, methods=['get'])
def featured(self, request):
"""Retourne les produits en vedette."""
featured = self.queryset.filter(is_featured=True)[:10]
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def purchase(self, request, pk=None):
"""Achète un produit."""
product = self.get_object()
service = ProductService()
result = service.purchase(product, request.user)
return Response(result, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def my_products(self, request):
"""Retourne les produits créés par l'utilisateur courant."""
products = self.queryset.filter(created_by=request.user)
page = self.paginate_queryset(products)
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 add_to_cart(request):
"""Ajoute un produit au panier de l'utilisateur."""
product_id = request.data.get('product_id')
quantity = request.data.get('quantity', 1)
try:
product = Product.objects.get(id=product_id)
except Product.DoesNotExist:
return Response(
{'error': 'Produit non trouvé'},
status=status.HTTP_404_NOT_FOUND
)
cart, _ = Cart.objects.get_or_create(user=request.user)
CartItem.objects.create(
cart=cart,
product=product,
quantity=quantity
)
return Response({'message': 'Ajouté au panier'}, status=status.HTTP_201_CREATED)
Modèle de couche de services
# apps/orders/services.py
from typing import Optional
from django.db import transaction
from .models import Order, OrderItem
class OrderService:
"""Couche de services pour la logique métier liée aux commandes."""
@staticmethod
@transaction.atomic
def create_order(user, cart):
"""Crée une commande à partir d'un panier."""
order = Order.objects.create(user=user)
for item in cart.items.all():
OrderItem.objects.create(
order=order,
product=item.product,
quantity=item.quantity,
price=item.product.price
)
if item.product.stock < item.quantity:
raise ValueError(f"Stock insuffisant pour {item.product.name}")
item.product.stock -= item.quantity
item.product.save()
return order
Gestion de la mise en cache
# Cache avec le décorateur cache_page et cache API bas niveau
from django.views.decorators.cache import cache_page
from django.core.cache import cache
from django.utils.decorators import method_decorator
@method_decorator(cache_page(60 * 15), name='dispatch')
class ProductListView(generics.ListAPIView):
"""Vue de liste avec mise en cache."""
queryset = Product.objects.all()
serializer_class = ProductSerializer
# Utilisation du cache bas niveau
def get_product_stats():
stats = cache.get('product_stats')
if not stats:
stats = {
'total': Product.objects.all().count(),
'total_value': Product.objects.aggregate(
models.Sum(models.F('price') * models.F('stock'))
)['total_value']
}
cache.set('product_stats', stats, 60 * 30)
return stats
Gestion des signaux
# apps/products/signals.py
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.core.cache import cache
@receiver(post_save, sender=Product)
def clear_product_cache(sender, instance, **kwargs):
"""Vide le cache lors de l'enregistrement d'un produit."""
cache.delete('product_list')
cache.delete(f'product_{instance.pk}')
@receiver(pre_delete, sender=Product)
def archive_product(sender, instance, **kwargs):
"""Archive le produit avant suppression."""
# Enregistrer dans un modèle d'archives
ProductArchive.objects.create(
name=instance.name,
deleted_at=timezone.now()
)
Configuration des permissions
# apps/products/permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
"""Permission personnalisée : seuls les créateurs peuvent modifier."""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.created_by == request.user
class IsAdminOrReadOnly(BasePermission):
"""Permission personnalisée : seuls les administrateurs peuvent écrire."""
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return request.user and request.user.is_staff
Gestion des erreurs
# exceptions.py ou au niveau de la vue
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
def custom_exception_handler(exc, context):
"""Gestionnaire d'exceptions personnalisé pour les API DRF."""
response = exception_handler(exc, context)
if response is not None:
errors = response.data
custom_errors = {
'error': True,
'message': str(exc),
'details': errors,
'status_code': response.status_code
}
response.data = custom_errors
return response
# Exemple d'utilisation avec les ViewSets
class ProductViewSet(viewsets.ModelViewSet):
# ... code du ViewSet ...
def perform_create(self, serializer):
try:
serializer.save()
except Exception as e:
raise ValidationError({
'error': 'Échec de la création du produit',
'detail': str(e)
})
Configuration de l'environnement
# requirements/production.txt
django==4.2.1
djangorestframework==3.14.0
django-cors-headers==4.0.0
django-filter==23.1
django-debug-toolbar==4.1.0
psycopg2-binary==2.9.6
gunicorn==20.1.0
whitenoise==6.5.0
celery==5.3.0
redis==4.5.5
pytest-django==4.5.2
coverage==7.2.7
drf-spectacular==0.26.4
# requirements/development.txt
-r production.txt
pytest==7.3.2
pytest-cov==4.1.0
factory-boy==3.2.1
Faker==18.11.2
ipython==8.14.0
# requirements/test.txt
-r production.txt
pytest==7.3.2
pytest-cov==4.1.0
factory-boy==3.2.1
Faker==18.11.2
Ressources supplémentaires
- Documentation officielle de Django
- Django REST Framework
- Deux Scoops of Django 3.x
- Django ORM Cookbook
- Tests Django avec pytest
Utilisation recommandée de l'IA
- Démarrer un nouveau projet : Posez les bases avec la structure de dossiers et la configuration des paramètres divisés.
- Concevoir des modèles : Utilisez les conventions de dénomination, les index, les contraintes et les QuerySets personnalisés.
- Créer une API REST : Implémentez des ViewSets, sérialiseurs et permissions en suivant les exemples.
- Optimiser les performances : Utilisez
select_related,prefetch_relatedet la mise en cache. - Refactoriser du code existant : Appliquez la couche de services et les tests unitaires pour améliorer la maintenabilité.
Ce document est un guide de référence. Adaptez les exemples aux besoins spécifiques de votre projet.