Vue d'ensemble

Django 5.0, publié le 4 décembre 2023, ouvre un nouveau cycle majeur avec des fonctionnalités qui répondent à des demandes de longue date. Le GeneratedField permet de définir des colonnes calculées directement en base de données, les filtres à facettes dans l'admin affichent le nombre d'objets par valeur de filtre, et les valeurs par défaut des champs peuvent être calculées par la base de données.

Le rendu des formulaires est également simplifié grâce à Field.template_name, qui permet de personnaliser le template de chaque type de champ sans surcharger le widget entier.

Fonctionnalités principales

GeneratedField

Le nouveau GeneratedField permet de créer des colonnes dont la valeur est calculée automatiquement par la base de données à partir d'autres colonnes. Deux modes sont disponibles : STORED (la valeur est stockée physiquement) et VIRTUAL (calculée à chaque lecture). C'est idéal pour les dénormalisations qui doivent toujours rester cohérentes.

python
from django.db import models
from django.db.models import F, Value
from django.db.models.functions import Concat, Lower

class Employe(models.Model):
    prenom = models.CharField(max_length=50)
    nom = models.CharField(max_length=50)
    salaire_mensuel = models.DecimalField(max_digits=10, decimal_places=2)
    heures_semaine = models.PositiveIntegerField(default=35)

    # Nom complet calculé en base de données
    nom_complet = models.GeneratedField(
        expression=Concat('prenom', Value(' '), 'nom'),
        output_field=models.CharField(max_length=101),
        db_persist=True,  # STORED : stocké physiquement
    )

    # Salaire annuel (12 mois + 1 mois de prime)
    salaire_annuel = models.GeneratedField(
        expression=F('salaire_mensuel') * 13,
        output_field=models.DecimalField(max_digits=12, decimal_places=2),
        db_persist=True,
    )

    # Identifiant normalisé en minuscules
    identifiant = models.GeneratedField(
        expression=Concat(Lower('prenom'), Value('.'), Lower('nom')),
        output_field=models.CharField(max_length=101),
        db_persist=True,
    )

# Utilisation : les champs générés sont accessibles en lecture
emp = Employe.objects.create(
    prenom='Marie', nom='Dupont', salaire_mensuel=3500
)
emp.refresh_from_db()
print(emp.nom_complet)     # Marie Dupont
print(emp.salaire_annuel)  # 45500.00
print(emp.identifiant)     # marie.dupont

Filtres à facettes dans l'admin

L'interface d'administration affiche désormais le nombre d'objets correspondant à chaque valeur de filtre, directement dans la barre latérale. Cela permet de voir d'un coup d'œil la distribution des données sans cliquer sur chaque filtre. Cette fonctionnalité s'active simplement en définissant show_facets sur le ModelAdmin.

python
from django.contrib import admin

class CommandeAdmin(admin.ModelAdmin):
    list_display = ['reference', 'client', 'statut', 'montant', 'date']
    list_filter = ['statut', 'date', 'mode_paiement']

    # Activer les compteurs sur les filtres
    show_facets = admin.ShowFacets.ALWAYS

    # Trois options disponibles :
    # ShowFacets.ALWAYS   — toujours afficher les compteurs
    # ShowFacets.ALLOW    — afficher via un paramètre URL
    # ShowFacets.NEVER    — désactiver (par défaut)

# Résultat dans la barre latérale :
# Statut
#   En attente (23)
#   Validée (156)
#   Expédiée (89)
#   Livrée (412)
#   Annulée (7)

Valeurs par défaut calculées en base

Les champs de modèle acceptent désormais un paramètre db_default pour définir une valeur par défaut calculée par la base de données plutôt que par Python. Cela garantit que la valeur par défaut est appliquée même lors d'insertions directes en SQL, et permet d'utiliser des fonctions de base de données comme Now().

python
from django.db import models
from django.db.models.functions import Now, Pi

class JournalActivite(models.Model):
    action = models.CharField(max_length=200)
    utilisateur = models.CharField(max_length=100)

    # Horodatage géré par la base de données
    date_creation = models.DateTimeField(db_default=Now())

    # Compteur initialisé à zéro par la base
    nb_tentatives = models.IntegerField(db_default=0)

    # Priorité par défaut
    priorite = models.CharField(
        max_length=10,
        db_default=Value('normal'),
    )

# L'objet est créé sans spécifier les champs à défaut
entree = JournalActivite.objects.create(
    action='connexion', utilisateur='marie'
)
entree.refresh_from_db()
print(entree.date_creation)  # 2023-12-04 10:30:00+00:00
print(entree.nb_tentatives)  # 0
print(entree.priorite)       # normal

Rendu simplifié des formulaires

L'attribut Field.template_name permet de définir un template de rendu pour chaque type de champ de formulaire. Cela simplifie la personnalisation du rendu sans avoir à surcharger les widgets ou à écrire des templatetags complexes. Les templates par groupe de champs (div, table, ul) sont également personnalisables.

python
from django import forms

class InscriptionForm(forms.Form):
    # Utiliser un template personnalisé pour ce champ
    email = forms.EmailField(
        template_name='forms/champs/email_avec_aide.html',
    )
    mot_de_passe = forms.CharField(
        widget=forms.PasswordInput,
        template_name='forms/champs/mot_de_passe.html',
    )
    conditions = forms.BooleanField(
        template_name='forms/champs/case_a_cocher.html',
    )

    # Rendu global du formulaire avec un template personnalisé
    template_name_div = 'forms/inscription_div.html'

# Dans le template principal :
# {{ form }}  — utilise template_name_div par défaut
# {{ form.email }}  — utilise le template du champ

Sources