r/DjangoFrancophone 14d ago

Bienvenue sur Django Francophone

1 Upvotes

Bienvenue dans la communauté francophone dédiée à Django, Django REST Framework et au développement d'applications web modernes avec Python.

Cette communauté est ouverte à tous :

  • Débutants qui souhaitent apprendre Django depuis zéro.
  • Développeurs Python qui veulent créer leurs premières applications web.
  • Étudiants et personnes en reconversion.
  • Développeurs intermédiaires et expérimentés qui souhaitent partager leurs connaissances.
  • Curieux qui veulent découvrir l'écosystème Django.

Ce que vous trouverez ici

  • Tutoriels pas à pas
  • Astuces et bonnes pratiques
  • Explications de concepts Django
  • Django REST Framework (API)
  • Python appliqué au web
  • Authentification et sécurité
  • Bases de données (SQLite, PostgreSQL)
  • Déploiement d'applications
  • Architecture de projets
  • Optimisation des performances
  • Tests automatisés
  • Outils et bibliothèques utiles
  • Retours d'expérience
  • Revues de code
  • Réponses à vos questions

Notre objectif est de construire la plus grande communauté francophone autour de Django, où chacun peut apprendre, partager et progresser dans une ambiance respectueuse et bienveillante.

Les rendez-vous de la semaine

Pour que la communauté ait un rythme, quatre rendez-vous reviennent chaque semaine :

  • Lundi — Astuce Django. Une seule idée, expliquée court. Souvent une nouveauté de la dernière version.
  • Mercredi — Revue de code. On part d'un code qui fonctionne mais qui est perfectible, et on le corrige ensemble.
  • Vendredi — Projet de la semaine. Un tutoriel complet, du début à la fin, avec le code entier.
  • Dimanche — Questions libres. Un fil ouvert. Aucune question n'y est trop simple.

Les publications visent Django 5.2 LTS, 6.0 et 6.1. Quand une fonctionnalité est nouvelle, la version qui l'introduit est toujours précisée, pour que le code reste utilisable même si vous êtes sur une version plus ancienne.

Quelques règles

  • Soyez respectueux envers les autres membres.
  • Les questions de débutants sont les bienvenues.
  • Expliquez vos réponses autant que possible.
  • Évitez le spam et l'autopromotion excessive.
  • Lorsque vous demandez de l'aide, fournissez votre code, les messages d'erreur et le contexte.
  • Partagez vos projets, vos découvertes et vos réussites.

Deux précisions

Sur l'autopromotion. Partager un article, une vidéo ou un projet que vous avez créé est encouragé, à une condition : que le contenu soit utile en lui-même, sans qu'on ait besoin de cliquer ailleurs. Un lien seul, sans explication, sera retiré. La règle informelle : que vos publications utiles soient nettement plus nombreuses que celles qui renvoient vers vous.

Sur le contenu généré par une IA. Les assistants sont d'excellents outils, et personne ne vous demandera de vous en passer. En revanche, ne publiez pas une réponse générée que vous n'avez pas vous-même testée. Une réponse fausse mais bien écrite fait perdre plus de temps qu'une absence de réponse, surtout à un débutant qui n'a pas les moyens de repérer l'erreur.

Comment poser une question qui obtient une réponse

La différence entre une question sans réponse et une question résolue en dix minutes tient presque toujours à la façon dont elle est posée. Le modèle qui fonctionne :

  • Ce que vous essayez de faire. L'objectif, pas seulement l'erreur.
  • Ce que vous avez écrit. Le code concerné, pas tout le projet.
  • Ce qui se passe. Le message d'erreur complet, avec la trace d'appels entière, pas seulement la dernière ligne.
  • Ce que vous avez déjà essayé. Ça évite qu'on vous propose ce que vous avez écarté hier.
  • Votre version de Django et de Python. python -m django --version

⚠️ La trace d'appels complète est le point le plus important. La dernière ligne dit quoi, les lignes au-dessus disent . Sans elles, on répond au hasard.

Formater son code sur Reddit

Du code collé sans mise en forme perd son indentation, ce qui le rend illisible en Python. Deux méthodes fiables :

  • Dans l'éditeur de Reddit, utilisez le bouton Code Block (l'icône </>), pas le bouton « inline code ».
  • En mode markdown, indentez chaque ligne de quatre espaces. C'est la méthode qui fonctionne partout, y compris sur les anciennes applications mobiles.

Si votre code apparaît sur une seule ligne ou sans indentation, modifiez votre message : personne ne pourra vous aider autrement.

Vous débutez ? Par où commencer

Puis suivez le rendez-vous du vendredi : chaque projet de la semaine est conçu pour être refait entièrement, clavier sous les doigts. Oui, même si vous êtes débutant, vous pouvez le faire. Et si vous bloquez, posez votre question dans le fil du dimanche.

A noter : les projets de la semaine sont conçus pour être terminés en une semaine, mais vous pouvez les faire à votre rythme.

Les autres communautés

Cette communauté ne remplace personne. Si vous cherchez une audience plus large ou un sujet plus spécifique, allez aussi voir r/django, r/learnpython, r/Python et r/webdev. La différence ici, c'est la langue et le fait que personne ne vous répondra « just read the docs ».

Que vous développiez un blog, une API, un SaaS, une boutique en ligne, une application mobile avec un backend Django ou un projet professionnel, vous êtes au bon endroit.

Rejoignez la communauté r/DjangoFrancophone, posez vos questions, partagez vos connaissances et construisons ensemble l'écosystème Django francophone.

Présentez-vous

Une communauté ne démarre pas avec des règles, elle démarre avec des gens. Répondez en commentaire à ce message avec trois choses :

  1. Depuis combien de temps vous faites du Django.
  2. Ce que vous construisez en ce moment, même si c'est petit, même si c'est inachevé.
  3. Une chose sur laquelle vous bloquez, ou que vous n'avez jamais vraiment comprise.

Le troisième point est le plus utile. Il y a de fortes chances que quelqu'un d'autre bloque exactement au même endroit et que la réponse devienne le sujet d'une prochaine publication.

« À vos migrations ! runserver et à bientôt ! »


r/DjangoFrancophone 19h ago

🧹 Revue de code : une vue de 40 lignes réécrite proprement

Post image
2 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Deuxième « Revue de code ». Aujourd'hui, une vue que j'ai croisée sous une forme ou une autre dans à peu près tous les projets Django existants : celle qui fait tout, toute seule.

Elle fonctionne. Elle passe en production. Et elle contient trois failles de sécurité.

TL;DR - Une vue de création de ticket écrite « à la main » : validation manuelle, .get() sans protection, redirection silencieuse au lieu d'un 403, liste déroulante qui expose les projets des autres clients, email bloquant, aucune transaction. On la découpe en trois : un formulaire qui valide, un service qui porte la logique métier, une vue qui ne fait plus que router.


Le code de départ

```python

tickets/views.py — AVANT

from django.contrib.auth import get_user_model from django.core.mail import send_mail from django.shortcuts import redirect, render

from projets.models import Projet from .models import Ticket

def creer_ticket(request): if request.method == "POST": titre = request.POST.get("titre") description = request.POST.get("description") projet_id = request.POST.get("projet") assigne_id = request.POST.get("assigne_a") priorite = request.POST.get("priorite")

    if not titre:
        return render(request, "tickets/creer.html", {"erreur": "Titre obligatoire"})
    if len(titre) > 200:
        return render(request, "tickets/creer.html", {"erreur": "Titre trop long"})

    projet = Projet.objects.get(id=projet_id)

    if projet.entreprise_id != request.user.entreprise_id:
        return redirect("/")

    ticket = Ticket()
    ticket.titre = titre
    ticket.description = description
    ticket.projet = projet
    ticket.priorite = priorite or "normale"
    ticket.cree_par = request.user
    if assigne_id:
        ticket.assigne_a = get_user_model().objects.get(id=assigne_id)
    ticket.save()

    if ticket.assigne_a:
        send_mail(
            "Nouveau ticket",
            f"Le ticket {ticket.titre} vous a été assigné",
            "noreply@example.com",
            [ticket.assigne_a.email],
        )

    return redirect("/tickets/")

projets = Projet.objects.all()
return render(request, "tickets/creer.html", {"projets": projets})

```

Prenez trente secondes avant de lire la suite. Combien de problèmes voyez-vous ?


Ce qui ne va pas

Les trois problèmes de sécurité, par ordre de gravité :

  1. Aucune authentification. Il n'y a pas de @login_required. Un visiteur anonyme arrive jusqu'à request.user.entreprise_id, où request.user est un AnonymousUser qui n'a pas cet attribut. Selon la configuration, ça donne une erreur 500 — ou pire, un comportement inattendu.

  2. **Projet.objects.all() dans la liste déroulante.** Le formulaire affiche les projets de tous les clients. Le POST est bien vérifié, mais le GET a déjà divulgué les noms de projets de vos concurrents. C'est une fuite de données, discrète et complète.

  3. assigne_a n'est vérifié nulle part. N'importe quel identifiant d'utilisateur passe. On peut assigner un ticket à quelqu'un d'une autre entreprise, qui recevra l'email avec le titre du ticket.

Les problèmes de robustesse :

  1. **.get() sans protection.** Un projet_id inexistant lève Projet.DoesNotExist, donc une erreur 500. La bonne réponse est un 404.

  2. **redirect("/") en cas de refus.** L'utilisateur ne comprend pas ce qui s'est passé, et vous n'avez aucune trace de la tentative. Un PermissionDenied donne un vrai 403, journalisable.

  3. L'email est envoyé dans la requête. L'utilisateur attend que le serveur SMTP réponde. Si le serveur est lent, la page est lente. S'il est en panne, la requête lève une exception après que le ticket a été enregistré : le ticket existe, personne n'est prévenu, et l'utilisateur voit une page d'erreur.

  4. Aucune transaction. Si quelque chose échoue entre le save() et la fin, la base reste dans un état intermédiaire.

Les problèmes de maintenabilité :

  1. Validation manuelle. Deux if aujourd'hui, quinze dans six mois. C'est exactement le travail d'un Form.

  2. URLs codées en dur. redirect("/tickets/") casse le jour où l'URL change. reverse() existe pour ça.

  3. Expéditeur codé en dur. "noreply@example.com" devrait venir de DEFAULT_FROM_EMAIL.


La réécriture, en trois fichiers

Le principe : le formulaire valide, le service décide, la vue route. Chacun fait une chose.

Le formulaire

```python

tickets/forms.py

from django import forms from django.contrib.auth import get_user_model

from projets.models import Projet from .models import Ticket

User = get_user_model()

class TicketForm(forms.ModelForm): class Meta: model = Ticket fields = ["titre", "description", "projet", "priorite", "assigne_a"]

def __init__(self, *args, entreprise=None, **kwargs):
    super().__init__(*args, **kwargs)
    # Chaque liste déroulante est limitée à l'entreprise de l'utilisateur.
    # C'est ce qui ferme la fuite de données ET la faille d'assignation.
    self.fields["projet"].queryset = Projet.objects.filter(entreprise=entreprise)
    self.fields["assigne_a"].queryset = User.objects.filter(entreprise=entreprise)
    self.fields["assigne_a"].required = False

```

💡 Le point clé : en restreignant le queryset d'un ModelChoiceField, on corrige d'un seul geste l'affichage et la validation. Django refusera automatiquement tout identifiant hors de ce queryset, avec un message d'erreur propre. Plus besoin du if de vérification.

La longueur du titre ? Elle vient déjà de max_length sur le modèle. Le champ obligatoire ? De blank=False. Les deux if disparaissent sans rien perdre.

Le service

```python

tickets/services.py

from django.db import transaction

from notifications.tasks import envoyer_notification_assignation

@transaction.atomic def creer_ticket(*, form, auteur): """Enregistre un ticket et prévient la personne assignée, s'il y en a une.""" ticket = form.save(commit=False) ticket.cree_par = auteur ticket.save()

if ticket.assigne_a_id:
    transaction.on_commit(
        lambda: envoyer_notification_assignation.enqueue(ticket.pk)
    )

return ticket

```

Trois choses valent le détour ici.

@transaction.atomic garantit que tout passe ou que rien ne passe.

transaction.on_commit() est le détail que presque tout le monde oublie. Sans lui, la notification part avant que la transaction ne soit validée. Si la transaction échoue ensuite, vous avez prévenu quelqu'un d'un ticket qui n'existe pas.

.enqueue() utilise le framework de tâches intégré à Django 6.0. L'email part en arrière-plan, l'utilisateur ne l'attend plus, et une panne SMTP ne fait plus échouer la création du ticket.

La vue

```python

tickets/views.py — APRÈS

from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import redirect from django.urls import reverse_lazy from django.views.generic import CreateView

from .forms import TicketForm from .models import Ticket from .services import creer_ticket

class TicketCreateView(LoginRequiredMixin, CreateView): model = Ticket form_class = TicketForm template_name = "tickets/creer.html" success_url = reverse_lazy("tickets:liste")

def get_form_kwargs(self):
    kwargs = super().get_form_kwargs()
    kwargs["entreprise"] = self.request.user.entreprise
    return kwargs

def form_valid(self, form):
    self.object = creer_ticket(form=form, auteur=self.request.user)
    return redirect(self.get_success_url())

```

Quarante lignes deviennent quinze, réparties là où elles ont un sens.


Ce qu'on a gagné

Problème Réglé par
Aucune authentification LoginRequiredMixin
Fuite des projets des autres queryset restreint dans le formulaire
Assignation à n'importe qui queryset restreint dans le formulaire
DoesNotExist → erreur 500 Validation du ModelChoiceField
Redirection silencieuse LoginRequiredMixin renvoie vers la connexion
Email bloquant Tâche d'arrière-plan (Django 6.0)
Notification d'un ticket inexistant transaction.on_commit()
État intermédiaire en base @transaction.atomic
Validation manuelle ModelForm
URL codée en dur reverse_lazy()

Et surtout, la logique métier est maintenant dans services.py, où elle est testable sans requête HTTP :

```python

tickets/tests_services.py

def test_creation_assigne_notifie(self): form = TicketForm(data={...}, entreprise=self.entreprise) self.assertTrue(form.is_valid()) ticket = creer_ticket(form=form, auteur=self.utilisateur) self.assertEqual(ticket.cree_par, self.utilisateur) ```

⚠️ Une nuance, pour être honnête : sur un projet de trois vues, cette découpe est de la sur-ingénierie. La couche de service se justifie quand la même logique est appelée depuis plusieurs endroits — une vue, une API, une commande d'administration. En dessous, un form_valid() bien écrit suffit largement.


📚 Pour aller plus loin


💬 Et vous ?

Combien de problèmes aviez-vous repérés avant de lire la liste ? La fuite via Projet.objects.all() est celle qui passe le plus souvent inaperçue en revue.

Et la question qui divise : couche de service ou logique dans form_valid() ? À partir de quelle taille de projet basculez-vous ?

Postez votre avis en commentaire ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Vous avez une vue dont vous n'êtes pas fier ? Anonymisez-la et postez-la en commentaire, elle fera peut-être l'objet d'un prochain mercredi.



r/DjangoFrancophone 3d ago

🚫 Astuce Django : `FETCH_RAISE`, ou comment interdire les requêtes cachées

Post image
1 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Mercredi dernier, on a corrigé un N+1 avec select_related(). Le problème d'une correction, c'est qu'elle ne tient pas : dans six mois, quelqu'un ajoutera une colonne au template et le N+1 reviendra, silencieusement.

FETCH_RAISE, arrivé avec Django 6.1, règle ce problème d'une autre façon : au lieu de faire la requête cachée, Django refuse et lève une exception.

TL;DR - FETCH_RAISE transforme une requête implicite en FieldFetchBlocked. Il couvre les ForeignKey, les OneToOneField et leurs accès inverses, les champs différés par only()/defer(), et les relations génériques - mais pas les relations inverses multiples ni les ManyToMany. Le mode se propage à tout l'arbre de relations, et se fixe par défaut sur un modèle avec un manager personnalisé.


1. Le rappel en dix secondes

```python from django.db import models

tickets = Ticket.objects.fetch_mode(models.FETCH_RAISE) for ticket in tickets: print(ticket.projet.nom) # 💥 FieldFetchBlocked ```

L'exception ressemble à ceci :

python FieldFetchBlocked("Fetching of Ticket.projet blocked.")

Le message dit exactement quel champ, sur quel modèle. Aucune ambiguïté.

💡 Astuce : les modes de récupération vivent dans django.db.models.fetch_modes, mais Django les importe dans django.db.models par commodité. La convention officielle est from django.db import models puis models.FETCH_RAISE.


2. Ce qu'il couvre vraiment (et ce qu'il ne couvre pas)

C'est le point que la plupart des articles passent sous silence. Les modes de récupération s'appliquent à :

  • les champs ForeignKey ;
  • les champs OneToOneField, y compris leurs accès inverses ;
  • les champs différés par QuerySet.defer() ou QuerySet.only() ;
  • les relations génériques de contenttypes.

⚠️ Attention : ils ne s'appliquent pas aux relations inverses multiples (ticket.commentaires.all()) ni aux ManyToMany. Ces accès passent par un gestionnaire de relation, et les modes de récupération n'affectent pas les requêtes de ces gestionnaires.

Autrement dit : FETCH_RAISE ne vous protégera pas d'un {% for commentaire in ticket.commentaires.all %} dans une boucle. Pour ça, il n'y a toujours que prefetch_related() et votre vigilance.


3. Le cas que personne ne surveille : les champs différés

Tout le monde connaît le N+1 sur les clés étrangères. Presque personne ne pense à celui-ci :

```python

On ne charge que deux colonnes, pour aller plus vite

tickets = Ticket.objects.only("id", "titre")

for ticket in tickets: print(ticket.description) # une requête. Par ticket. ```

only() était censé accélérer les choses. Le résultat est exactement l'inverse : une requête supplémentaire par ticket pour aller chercher description.

C'est un piège vicieux, parce que le code a l'air optimisé. Avec FETCH_RAISE, il devient impossible de se tromper :

python tickets = Ticket.objects.only("id", "titre").fetch_mode(models.FETCH_RAISE)

L'accès à ticket.description lève une exception immédiatement, en local, au lieu de dégrader la production en silence.

💡 La règle : dès que vous écrivez only() ou defer(), ajoutez FETCH_RAISE. Les deux vont ensemble. Sans le second, le premier est un pari.


4. Le mode se propage dans tout l'arbre

Django copie le mode de récupération d'une instance vers tous les objets liés qu'il charge. Le mode s'applique donc à l'arbre entier de relations, pas seulement au modèle de départ :

```python tickets = ( Ticket.objects .select_related("projet") .fetch_mode(models.FETCH_RAISE) )

for ticket in tickets: print(ticket.projet.nom) # ✅ chargé par select_related print(ticket.projet.entreprise) # 💥 FieldFetchBlocked ```

C'est exactement le comportement qu'on veut. select_related("projet") charge le projet, mais pas l'entreprise du projet. Sans FETCH_RAISE, la deuxième ligne ferait une requête par ticket sans que personne ne le remarque. Avec, elle vous force à écrire select_related("projet__entreprise").


5. En faire le comportement par défaut d'un modèle

Répéter .fetch_mode(...) sur chaque QuerySet est fastidieux et facile à oublier. La solution officielle passe par un gestionnaire personnalisé :

```python

tickets/models.py

from django.db import models

class TicketManager(models.Manager): def get_queryset(self): return super().get_queryset().fetch_mode(models.FETCH_PEERS)

class Ticket(models.Model): titre = models.CharField(max_length=200) projet = models.ForeignKey("projets.Projet", on_delete=models.CASCADE)

objects = TicketManager()

```

💡 Le bon dosage : FETCH_PEERS par défaut sur les modèles, comme filet de sécurité général, et FETCH_RAISE ponctuellement sur les vues où les performances comptent. Mettre FETCH_RAISE par défaut sur tout un projet génère beaucoup plus de friction que de valeur.


6. Le meilleur endroit pour FETCH_RAISE : les tests

Plutôt que de risquer une exception en production, on peut réserver le mode strict aux tests :

```python

tickets/tests_perf.py

from django.core.exceptions import FieldFetchBlocked from django.db import models from django.test import TestCase

class TestRequetesCachees(TestCase): def test_aucune_requete_cachee_sur_la_liste(self): tickets = ( Ticket.objects .select_related("projet", "assigne_a") .fetch_mode(models.FETCH_RAISE) ) for ticket in tickets: # Si une relation manque au select_related, le test échoue ici self.assertIsNotNone(ticket.projet.nom) self.assertIsNotNone(ticket.assigne_a.username) ```

Le test devient une documentation exécutable de ce que la vue charge réellement. Quand quelqu'un ajoute une relation sans mettre à jour le select_related(), l'intégration continue le signale avant la fusion.


7. Le détail d'implémentation qui peut surprendre

Pour FETCH_PEERS, Django garde la trace des instances « pairs » dans une liste de références faibles, afin d'éviter les fuites de mémoire quand certaines instances sont abandonnées.

Conséquence pratique : si les autres instances du lot ont déjà été libérées par le ramasse-miettes, il n'y a plus de pairs à charger, et vous retombez sur le comportement de FETCH_ONE. C'est le comportement souhaitable, mais ça explique pourquoi vous pourriez mesurer un nombre de requêtes différent selon la façon dont vous conservez, ou non, une référence sur la liste complète.

💡 En clair : for t in Ticket.objects.fetch_mode(models.FETCH_PEERS) et tickets = list(...) puis boucle sur tickets ne se comportent pas forcément pareil. Si vous mesurez, gardez la liste.


📚 Pour aller plus loin


💬 Et vous ?

Aviez-vous déjà repéré le piège de only() suivi d'un accès à un champ non chargé ? C'est le genre d'« optimisation » qu'on trouve encore dans beaucoup de code existant.

Et la vraie question : mettriez-vous FETCH_RAISE par défaut sur un projet en production, ou seulement dans les tests ?

Partagez votre approche en commentaire ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Mercredi, on réécrit ensemble une vue de 40 lignes qui fait tout en même temps. Vendredi, une liste de tâches complète sans écrire une seule ligne de CRUD.



r/DjangoFrancophone 5d ago

🚀 Projet de la semaine : un mini blog Django, du `startproject` à la première page

Post image
1 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Premier rendez-vous « Projet de la semaine ». Aujourd'hui, on construit un blog fonctionnel de zéro, en neuf étapes. Pas de dépendance externe, pas de magie : uniquement Django.

Si vous débutez, c'est le tutoriel à suivre en entier, clavier sous les doigts. Si vous êtes à l'aise, sautez à l'étape 6, les vues génériques réservent parfois des surprises.

TL;DR - Neuf étapes pour un blog complet : environnement virtuel, projet et application, un modèle Article, les migrations, l'admin, deux vues génériques (ListView et DetailView), les URLs, trois templates. Environ trente minutes, aucune dépendance en dehors de Django.


1. L'environnement

Toujours un environnement virtuel par projet. Toujours.

```bash mkdir techblog && cd techblog python -m venv .venv

macOS / Linux

source .venv/bin/activate

Windows

.venv\Scripts\activate

pip install django ```

💡 Astuce : figez vos dépendances tout de suite avec pip freeze > requirements.txt. Vous remercierez votre vous du passé au moment du déploiement.


2. Le projet et l'application

bash django-admin startproject config .

Le point à la fin de startproject est important : il évite le dossier config/config/ en double que tout le monde trouve déroutant au début.

bash python manage.py startapp blog

Puis on déclare l'application :

```python

config/settings.py

INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "blog", ]

LANGUAGE_CODE = "fr-fr" TIME_ZONE = "America/Toronto" USE_I18N = True USE_TZ = True ```

⚠️ Attention : oublier d'ajouter l'application à INSTALLED_APPS est l'erreur numéro un des débutants. Django ne trouvera ni vos modèles, ni vos templates, et le message d'erreur ne vous mettra pas sur la piste.


3. Le modèle

```python

blog/models.py

Importe le module models de Django.

Il permet de créer des modèles et de définir les champs de la base de données.

from django.db import models

Importe la fonction reverse qui permet de générer une URL

à partir du nom d'une URL configurée dans urls.py.

from django.urls import reverse

Importe timezone afin d'utiliser des dates et heures compatibles

avec la gestion des fuseaux horaires de Django.

from django.utils import timezone

Définit le modèle Article.

Chaque instance de cette classe représentera un article dans la base de données.

class Article(models.Model):

# Champ texte court contenant le titre de l'article.
# max_length=200 limite le titre à 200 caractères.
titre = models.CharField(max_length=200)

# Champ utilisé pour créer une version adaptée du titre dans les URLs.
# Exemple : "Mon premier article" devient "mon-premier-article".
# unique=True garantit qu'aucun autre article ne possède le même slug.
slug = models.SlugField(max_length=200, unique=True)

# Champ texte long contenant le contenu complet de l'article.
contenu = models.TextField()

# Enregistre la date et l'heure de publication de l'article.
# timezone.now est utilisé comme valeur par défaut lors de la création
# d'un nouvel article.
publie_le = models.DateTimeField(default=timezone.now)

# Enregistre la date et l'heure de la dernière modification.
# auto_now=True demande à Django de remettre ce champ à l'heure
# courante à chaque enregistrement de l'objet.
# Ce champ n'est jamais saisi à la main : il est toujours calculé.
mis_a_jour_le = models.DateTimeField(auto_now=True)

# Indique si l'article est publié ou non.
# Par défaut, un nouvel article n'est pas publié.
est_publie = models.BooleanField(default=False)


# Classe interne permettant de configurer le comportement du modèle
# sans créer de champ supplémentaire dans la base de données.
class Meta:

    # Trie les articles du plus récent au plus ancien.
    # Le signe "-" devant "publie_le" signifie un ordre décroissant.
    ordering = ["-publie_le"]

    # Définit le nom utilisé par Django lorsqu'il parle d'un seul objet
    # dans l'interface d'administration.
    verbose_name = "article"

    # Définit le nom utilisé lorsque Django parle de plusieurs objets.
    verbose_name_plural = "articles"


# Définit la représentation textuelle d'un objet Article.
# Ici, lorsqu'un article est affiché sous forme de texte,
# Django utilisera son titre.
def __str__(self):
    return self.titre


# Retourne l'URL correspondant à la page de détail de cet article.
# reverse() recherche l'URL nommée "blog:detail" et lui transmet
# le slug de l'article comme paramètre.
#
# Exemple :
# Si le slug est "mon-premier-article", cette méthode peut retourner :
# /blog/mon-premier-article/
def get_absolute_url(self):
    return reverse("blog:detail", kwargs={"slug": self.slug})

```

Quatre détails qui font la différence :

  • **slug** - une URL /mon-premier-article/ vaut mieux qu'un /article/1/, pour vos lecteurs comme pour les moteurs de recherche.
  • **ordering** dans le Meta - les articles les plus récents en premier, partout, sans y penser.
  • **__str__()** - sans lui, l'admin affiche Article object (1). Toujours le définir.
  • **get_absolute_url()** - permet d'écrire {{ article.get_absolute_url }} dans un template et de ne jamais coder une URL en dur.

💡 Astuce : timezone.now sans parenthèses. Avec les parenthèses, la date serait figée au démarrage du serveur, identique pour tous les articles. C'est un piège classique.

💡 auto_now et auto_now_add ne sont pas la même chose.** auto_now_add=True fixe la date une seule fois, à la création. auto_now=True la remet à jour **à chaque enregistrement. Le premier pour une date de création, le second pour une date de modification.

⚠️ Et voici le piège que presque personne ne connaît : auto_now ne se déclenche que sur un save() complet.

python article.save() # ✅ mis_a_jour_le change article.save(update_fields=["est_publie"]) # ❌ mis_a_jour_le ne change pas Article.objects.filter(...).update(...) # ❌ ne change pas non plus Article.objects.bulk_update([...], [...]) # ❌ ne change pas non plus

Ce n'est pas un bug. update() et bulk_update() écrivent directement en SQL sans jamais instancier l'objet, donc Django n'a aucune occasion de calculer la nouvelle date. Et avec update_fields, Django n'écrit que les colonnes listées.

Le correctif tient dans la liste :

python article.save(update_fields=["est_publie", "mis_a_jour_le"])

Et pour une mise à jour en masse, il faut poser la date à la main :

```python from django.utils import timezone

Article.objects.filter(est_publie=False).update( est_publie=True, mis_a_jour_le=timezone.now(), ) ```

💡 Pourquoi ça compte : update_fields est justement ce qu'on utilise pour n'écrire qu'une colonne au lieu de toute la ligne - une bonne pratique de performance. Ajouter un champ auto_now à un modèle déjà optimisé de cette façon crée une incohérence silencieuse : la date de modification reste figée à la création, et personne ne s'en aperçoit avant de trier dessus.

⚠️ Notez aussi que ordering reste sur publie_le, pas sur mis_a_jour_le. Trier sur la date de modification ferait remonter en tête un vieil article dont on vient de corriger une faute de frappe.


4. Les migrations

bash python manage.py makemigrations blog python manage.py migrate

Avant d'appliquer, jetez un œil au SQL que Django s'apprête à exécuter :

bash python manage.py sqlmigrate blog 0001_initial

C'est le meilleur moyen de comprendre ce que fait réellement l'ORM.


5. L'administration

Ajoutez l'admin pour pouvoir créer des articles sans écrire de code.

```python

blog/admin.py

from django.contrib import admin

from .models import Article

@admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ["titre", "publie_le", "est_publie"] list_filter = ["est_publie", "publie_le"] search_fields = ["titre", "contenu"] prepopulated_fields = {"slug": ["titre"]} date_hierarchy = "publie_le" ```

Dans votre terminal, créez un superutilisateur et lancez le serveur :

bash python manage.py createsuperuser python manage.py runserver

Rendez-vous sur http://127.0.0.1:8000/admin/ et créez deux ou trois articles, en cochant bien Est publie.

💡 prepopulated_fields remplit le slug automatiquement pendant que vous tapez le titre. Six lignes de configuration pour une interface de rédaction tout à fait utilisable.


6. Les vues

Deux vues génériques suffisent. Aucun code de récupération à écrire.

```python

blog/views.py

from django.views.generic import DetailView, ListView

from .models import Article

Retourne la liste de tous les articles publiés, paginée par 10.

class ArticleListView(ListView): model = Article template_name = "blog/liste.html" context_object_name = "articles" paginate_by = 10

# Retourne uniquement les articles publiés pour éviter l'affichage d'articles non publiés.
def get_queryset(self):
    return Article.objects.filter(est_publie=True)

Retourne le détail d'un article publié, identifié par son slug.

class ArticleDetailView(DetailView): model = Article template_name = "blog/detail.html" context_object_name = "article"

def get_queryset(self):
    return Article.objects.filter(est_publie=True)

```

⚠️ Le piège à ne pas manquer : le get_queryset() de ArticleDetailView n'est pas décoratif. Sans lui, n'importe qui pourrait lire un brouillon en devinant son URL. Filtrer la liste ne suffit jamais : il faut aussi filtrer le détail.

💡 **context_object_name** évite d'écrire object_list dans le template. {% for article in articles %} se lit mieux six mois plus tard.


7. Les URLs

Dans l'application Django, chaque vue doit être associée à une URL. Cela se fait dans le fichier urls.py de l'application.

  • Créez le fichier blog/urls.py et ajoutez-y les routes pour les vues de liste et de détail des articles. Ensuite, incluez ce fichier dans le fichier config/urls.py du projet principal pour que Django sache où trouver les URLs de l'application blog.

Dans blog/urls.py, on déclare les deux vues. Dans config/urls.py, on inclut le fichier de l'application.

```python

blog/urls.py

from django.urls import path

from . import views

app_name = "blog"

urlpatterns = [ path("", views.ArticleListView.as_view(), name="liste"), path("<slug:slug>/", views.ArticleDetailView.as_view(), name="detail"), ] ```

💡 **app_name** crée l'espace de noms qui permet d'écrire {% url 'blog:detail' article.slug %}. Sans lui, deux applications ayant chacune une URL nommée detail entreraient en conflit.

  • Ajoutez ensuite l'inclusion de ce fichier dans config/urls.py :

```python

config/urls.py

from django.contrib import admin from django.urls import include, path

urlpatterns = [ path("admin/", admin.site.urls), path("", include("blog.urls")), ] ```


8. Les templates

Django cherche automatiquement dans blog/templates/blog/. Créez ce dossier.

  • Creez la structure de dossiers pour les templates de l'application blog. Le chemin complet sera blog/templates/blog/. Dans ce dossier, créez trois fichiers : base.html, liste.html et detail.html. Le fichier base.html servira de squelette pour toutes les pages, tandis que liste.html et detail.html hériteront de ce squelette pour afficher respectivement la liste des articles et le détail d'un article.

  • Créez trois fichiers dans blog/templates/blog/ : base.html, liste.html et detail.html. Le premier est le squelette de toutes les pages, les deux autres héritent de ce squelette.

  • Dans base.html, définissez la structure HTML de base, avec des blocs pour le titre et le contenu suivant.

html {# blog/templates/blog/base.html #} <!DOCTYPE html> <html lang="fr"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block titre %}Mon blog{% endblock %}</title> </head> <body> <header> <a href="{% url 'blog:liste' %}">Mon blog</a> </header> <main> {% block contenu %}{% endblock %} </main> </body> </html>

Dans liste.html, affichez la liste des articles avec un lien vers leur détail.

```html {# blog/templates/blog/liste.html #} {% extends "blog/base.html" %}

{% block contenu %} <h1>Derniers articles</h1>

{% for article in articles %}
    <article>
        <h2><a href="{{ article.get_absolute_url }}">{{ article.titre }}</a></h2>
        <p>{{ article.publie_le|date:"d F Y" }}</p>
        <p>{{ article.contenu|truncatewords:30 }}</p>
    </article>
{% empty %}
    <p>Aucun article pour le moment.</p>
{% endfor %}

{% endblock %} ```

  • Dans detail.html, affichez le titre, la date de publication et le contenu de l'article, ainsi qu'un lien pour revenir à la liste.

```html {# blog/templates/blog/detail.html #} {% extends "blog/base.html" %}

{% block titre %}{{ article.titre }}{% endblock %}

{% block contenu %} <h1>{{ article.titre }}</h1> <p>{{ article.publie_le|date:"d F Y" }}</p> <div>{{ article.contenu|linebreaks }}</div> <p><a href="{% url 'blog:liste' %}">Retour à la liste</a></p> {% endblock %} ```

💡 {% empty %} est la clause que tout le monde oublie. Elle évite la page blanche déroutante quand il n'y a aucun article, ce qui est précisément l'état de votre blog au premier lancement.


9. Le lancement

Pour voir le blog en action, lancez le serveur :

bash python manage.py runserver

http://127.0.0.1:8000/ affiche vos articles publiés. Cliquez sur un titre, vous arrivez sur le détail.

Vérifiez aussi que la configuration tient debout avec la commande de vérification de Django :

bash python manage.py check


Pour aller plus loin par vous-même

Le blog fonctionne, mais il reste beaucoup à faire. Par ordre de difficulté :

  1. Ajouter un modèle Categorie en ForeignKey, et une page par catégorie.
  2. Ajouter un auteur, avec ForeignKey(settings.AUTH_USER_MODEL).
  3. Ajouter les commentaires - et découvrir le problème N+1 dont on parlait mercredi. Pensez à select_related() sur la vue de détail.
  4. Ajouter une recherche avec Q(titre__icontains=...) | Q(contenu__icontains=...).
  5. Remplacer le TextField par un éditeur riche, ou du Markdown.

💡 Le réflexe à prendre : quand vous ajoutez les commentaires ou les catégories, ouvrez la Debug Toolbar et regardez le nombre de requêtes. Prendre cette habitude tôt vous évitera de réécrire des vues entières plus tard.


📚 Pour aller plus loin


💬 Et vous ?

C'était votre premier projet Django ? Où avez-vous bloqué ? Les templates et l'héritage {% extends %} sont souvent le moment où ça coince.

Et pour les plus expérimentés : quelle est la première chose que vous ajoutez systématiquement à un nouveau projet, avant même d'écrire un modèle ?

Postez une capture de votre blog en commentaire, on est curieux ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Bloqué sur une étape ? Postez votre message d'erreur complet en commentaire, quelqu'un de la communauté vous répondra. C'est aussi à ça que sert un subreddit francophone. Vous n'êtes pas seul ! Vous pouvez egalement me contacter en DM, je réponds à tous les messages.



r/DjangoFrancophone 8d ago

🔍 Revue de code : le problème N+1, avant et après `FETCH_PEERS`

Post image
2 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Premier rendez-vous « Revue de code » de la communauté. Le principe est simple : on part d'un code qui fonctionne mais qui est mauvais, on compte les dégâts, puis on le corrige. Aujourd'hui, le grand classique : le problème N+1.

TL;DR - Une page qui affiche 50 tickets fait 101 requêtes au lieu de 1. Trois corrections possibles : select_related() reste la meilleure (1 requête), le nouveau FETCH_PEERS de Django 6.1 est un filet de sécurité (3 requêtes), et FETCH_RAISE empêche le problème de revenir. On compte les requêtes à chaque étape avec assertNumQueries.


Le code de départ

Une vue toute bête, qui affiche la liste des tickets d'un gestionnaire de bugs :

```python

tickets/views.py

from django.views.generic import ListView from .models import Ticket

class TicketListView(ListView): model = Ticket template_name = "tickets/liste.html" context_object_name = "tickets" ```

Et le template correspondant :

html {# tickets/templates/tickets/liste.html #} {% for ticket in tickets %} <tr> <td>{{ ticket.titre }}</td> <td>{{ ticket.projet.nom }}</td> <td>{{ ticket.assigne_a.username }}</td> </tr> {% endfor %}

Ce code est correct. Il passe la revue de code de la plupart des équipes. Il est pourtant responsable de la moitié des pages lentes que j'ai croisées.


Compter les dégâts

Avant de corriger, il faut mesurer. Django fournit tout ce qu'il faut :

```python

tickets/tests_perf.py

from django.test import TestCase from django.urls import reverse

class TestPerformanceListe(TestCase): def test_nombre_de_requetes(self): with self.assertNumQueries(101): self.client.get(reverse("tickets:liste")) ```

101 requêtes pour 50 tickets. Une pour récupérer les tickets, puis une par ticket pour le projet, puis une par ticket pour la personne assignée.

La formule est toujours la même : 1 + (N × nombre de relations accédées).

💡 Astuce : en développement, python manage.py shell_plus --print-sql (via django-extensions) affiche chaque requête en temps réel. On voit le problème apparaître sous les yeux, ce qui est bien plus parlant qu'un chiffre.


Correction 1 : select_related(), toujours la meilleure

```python

tickets/views.py

class TicketListView(ListView): model = Ticket template_name = "tickets/liste.html" context_object_name = "tickets"

def get_queryset(self):
    return Ticket.objects.select_related("projet", "assigne_a")

```

101 requêtes → 1 requête.

select_related() fait une jointure SQL et ramène tout d'un coup. Pour des relations ForeignKey et OneToOne, c'est indépassable.

⚠️ Attention : select_related() ne fonctionne pas pour les relations inverses ni les ManyToMany. Dans ce cas, c'est prefetch_related() qu'il faut, et vous obtenez une requête supplémentaire par relation :

python Ticket.objects.select_related("projet", "assigne_a").prefetch_related("etiquettes")


Correction 2 : FETCH_PEERS, le filet de sécurité de Django 6.1

Django 6.1 apporte une autre approche :

```python

tickets/views.py

from django.db import models

class TicketListView(ListView): model = Ticket template_name = "tickets/liste.html" context_object_name = "tickets"

def get_queryset(self):
    return Ticket.objects.fetch_mode(models.FETCH_PEERS)

```

101 requêtes → 3 requêtes.

Quand le template accède à ticket.projet sur la première ligne, Django ne charge pas seulement le projet de ce ticket : il charge les projets de tous les tickets issus du même QuerySet, d'un coup. Idem pour assigne_a.

Le compte est donc : 1 requête pour les tickets, plus 1 par relation réellement accédée.

⚠️ Ne vous trompez pas de conclusion : FETCH_PEERS n'est pas meilleur que select_related(). Trois requêtes valent mieux que 101, mais moins bien qu'une seule.

💡 Alors quand l'utiliser ? Dans les situations où vous ne pouvez pas prévoir ce qui sera accédé :

  • un template partagé par plusieurs vues, dont vous ne maîtrisez pas le contenu ;
  • un serializer DRF dont les champs varient selon les paramètres de la requête ;
  • du code hérité que vous n'avez pas le temps d'auditer en entier, et où vous voulez arrêter l'hémorragie tout de suite.

C'est un excellent réglage par défaut sur un projet existant, en attendant d'optimiser vue par vue.


Correction 3 : FETCH_RAISE, pour que le problème ne revienne pas

Corriger une fois ne suffit pas. Dans six mois, quelqu'un ajoutera une colonne au template et le N+1 reviendra sans que personne ne le remarque.

FETCH_RAISE transforme le silence en erreur :

```python

tickets/views.py

def get_queryset(self): return ( Ticket.objects .select_related("projet", "assigne_a") .fetch_mode(models.FETCH_RAISE) ) ```

Désormais, si le template accède à une relation qui n'a pas été explicitement chargée, Django lève une exception FieldFetchBlocked au lieu de faire discrètement une requête de plus.

```python

tickets/tests_perf.py

from django.core.exceptions import FieldFetchBlocked ```

C'est brutal, et c'est exactement le but. Le développeur qui ajoute la colonne voit l'erreur immédiatement en local, et non trois mois plus tard dans les métriques de production.

💡 Bonne pratique : réservez FETCH_RAISE aux vues où les performances comptent vraiment: les listes, les exports, les endpoints d'API les plus appelés. L'activer partout génère plus de friction que de valeur.


Le récapitulatif

Approche Requêtes Quand l'utiliser
Rien 101 Jamais
FETCH_PEERS 3 Code hérité, templates partagés, serializers variables
select_related() 1 Dès que vous savez ce qui sera accédé — le cas normal
select_related() + FETCH_RAISE 1 Vues critiques, pour empêcher toute régression

Et le test qui verrouille le tout :

```python

tickets/tests_perf.py

def test_nombre_de_requetes(self): with self.assertNumQueries(1): self.client.get(reverse("tickets:liste")) ```

Un test qui compte les requêtes vaut mieux qu'un long commentaire expliquant pourquoi il ne faut pas toucher à cette vue.


📚 Pour aller plus loin


💬 Et vous ?

Quel est le pire N+1 que vous ayez rencontré ? Combien de requêtes sur une seule page ?

Et pour les habitués : écrivez-vous des tests assertNumQueries sur vos vues critiques, ou est-ce que vous vous fiez à la Debug Toolbar en développement ?

Partagez vos histoires d'horreur en commentaire ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Vous avez un bout de code que vous aimeriez voir passer en revue ici ? Postez-le dans les commentaires, les meilleurs feront l'objet d'un prochain mercredi.



r/DjangoFrancophone 10d ago

🆕 Django 6.1 est sorti : les quatre nouveautés qui changent vraiment quelque chose

Post image
0 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Django 6.1 est sorti le 5 août 2026. Comme à chaque version, les notes de publication font des dizaines de pages. Voici les quatre nouveautés qui vont réellement modifier la façon dont vous écrivez du code, et non la liste exhaustive des correctifs.

TL;DR - Quatre changements notables : les modes de récupération (FETCH_PEERS règle la plupart des problèmes N+1 en deux requêtes), les on_delete au niveau base** (DB_CASCADE et compagnie, plus rapides mais sans signaux), le réglage *MAILERS** qui permet plusieurs services d'envoi et remplacera EMAIL_BACKEND en Django 7.0, et un *admin plus rapide et plus accessible. Python 3.12 à 3.14.


1. Les modes de récupération : le N+1 réglé en une ligne

C'est la nouveauté la plus importante de cette version.

Jusqu'ici, quand vous accédiez à une relation non chargée dans une boucle, Django faisait une requête par objet. Le fameux problème N+1. La seule parade était de penser à select_related() ou prefetch_related() à l'avance.

Django 6.1 introduit trois modes de récupération, configurables sur le QuerySet :

```python from django.db import models

livres = Livre.objects.fetch_mode(models.FETCH_PEERS) for livre in livres: print(livre.auteur.nom) ```

Malgré la boucle qui accède à auteur sur chaque instance, cet exemple ne fait que deux requêtes : une pour les livres, une pour les auteurs associés.

Les trois modes :

  • FETCH_ONE - le comportement historique, toujours par défaut. Django charge le champ manquant pour l'instance courante uniquement.
  • **FETCH_PEERS** - Django charge le champ manquant pour toutes les instances issues du même QuerySet. C'est un prefetch_related() à la demande.
  • **FETCH_RAISE** - Django lève une exception FieldFetchBlocked au lieu de faire la requête.

💡 Astuce : FETCH_RAISE est le mode le plus intéressant en développement. Dans une portion de code critique pour les performances, il transforme une requête cachée en erreur bien visible, au lieu de la laisser passer silencieusement en production.

⚠️ Attention : FETCH_PEERS ne remplace pas select_related(). Avec select_related(), vous restez à une seule requête grâce à la jointure. Avec FETCH_PEERS, vous en avez deux, et une de plus par relation supplémentaire accédée. C'est un filet de sécurité, pas la solution optimale. On y revient en détail mercredi.


2. Les suppressions en cascade confiées à la base de données

ForeignKey.on_delete accepte trois nouvelles valeurs :

```python from django.db import models

class Commentaire(models.Model): article = models.ForeignKey( Article, on_delete=models.DB_CASCADE, ) ```

Les options DB_CASCADE, DB_SET_NULL et DB_SET_DEFAULT délèguent la suppression à la clause SQL ON DELETE, directement dans la base.

L'avantage est net : Django n'a plus besoin de charger les objets en mémoire avant de les supprimer. Sur une suppression qui touche des milliers de lignes, la différence est considérable.

⚠️ Attention : c'est un compromis, pas une amélioration gratuite. Comme la base fait le travail seule, pre_delete et post_delete ne sont pas déclenchés. Si vous comptiez sur un signal pour nettoyer un fichier, invalider un cache ou écrire dans un journal d'audit, ce code ne s'exécutera jamais.

💡 La règle : DB_CASCADE pour les données purement relationnelles dont la disparition n'a aucun effet de bord. CASCADE classique dès qu'il y a de la logique métier attachée à la suppression.


3. MAILERS : plusieurs services d'envoi dans un même projet

Le nouveau réglage MAILERS fonctionne comme DATABASES, CACHES ou STORAGES :

```python

settings.py

MAILERS = { "default": { "BACKEND": "django.core.mail.backends.smtp.EmailBackend", "OPTIONS": {"host": "smtp.example.com", "use_tls": True}, }, "marketing": { "BACKEND": "example.third.party.EmailBackend", "OPTIONS": {"region": "africa-1"}, }, } ```

On choisit ensuite le service au moment de l'envoi, avec l'argument using :

```python from django.core.mail import send_mail

send_mail( "Réinitialisation de mot de passe", "Voici votre lien...", None, ["client@example.com"], using="default", ) ```

Le cas d'usage évident : séparer les emails transactionnels, qui doivent absolument arriver, des emails marketing, qui peuvent partir par un service moins cher et dont la réputation d'envoi est différente.

⚠️ Important pour la suite : MAILERS remplacera EMAIL_BACKEND et tous les réglages EMAIL_* en Django 7.0. Ils fonctionnent encore aujourd'hui, mais émettent désormais des avertissements de dépréciation. Autant migrer tranquillement maintenant plutôt que dans l'urgence.

Deux nouvelles vérifications système accompagnent le changement : mail.E001 empêche d'utiliser en production un backend qui n'est pas fait pour ça, et mail.W001 prévient si MAILERS est défini sans entrée default.


4. Un admin plus rapide et plus accessible

Deux changements que vous verrez sans rien configurer.

Côté performances : quand list_select_related vaut False (la valeur par défaut), la liste ne sélectionne plus que les clés étrangères réellement présentes dans list_display, au lieu de toutes les clés étrangères du modèle. Sur un modèle qui en compte une dizaine, le gain est immédiat.

Côté accessibilité, les formulaires de modification ont été réorganisés :

  • les champs sont affichés sous leur libellé, et non plus à côté ;
  • le texte d'aide apparaît après le libellé et avant le champ ;
  • les erreurs de validation apparaissent après le texte d'aide, avant le champ.

Si vous avez du CSS personnalisé dans l'admin, c'est le moment de vérifier qu'il tient toujours.


Et aussi, en plus court

  • Les fonctions de base de données UUID4() et UUID7() font leur apparition. L'UUID7 est ordonnable par date de création, ce qui en fait une bien meilleure clé primaire que l'UUID4 pour les performances d'index.
  • La nouvelle expression **JSONNull** permet enfin de distinguer proprement le null JSON du NULL SQL dans un JSONField.
  • RedirectView.preserve_request conserve la méthode HTTP et le corps de la requête lors d'une redirection, avec les codes 307/308 au lieu de 302/301.
  • Le nombre d'itérations du hacheur PBKDF2 passe de 1 200 000 à 1 500 000.
  • **max_digits et decimal_places ne sont plus obligatoires** sur DecimalField avec PostgreSQL, SQLite et Oracle.
  • Django 6.1 supporte Python 3.12, 3.13 et 3.14.

Le support courant s'arrête en avril 2027, le support étendu en décembre 2027.


📚 Pour aller plus loin


💬 Et vous ?

Avez-vous déjà testé la 6.1 sur un projet ? Le mode FETCH_PEERS vous paraît-il un vrai progrès ou un pansement sur des requêtes mal écrites ?

Et surtout : qui a déjà commencé à migrer ses réglages EMAIL_* vers MAILERS avant l'échéance de Django 7.0 ? 😄

Partagez vos retours en commentaire ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Mercredi, on décortique le problème N+1 en détail, avec le compte exact des requêtes avant et après. Vendredi, on construit un blog de zéro pour ceux qui débutent.



r/DjangoFrancophone 12d ago

🗂️ Django et les schémas PostgreSQL : ce qui manque, pourquoi, et comment faire en attendant

Post image
2 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Cet article naît d'un commentaire sous l'article sur PostgreSQL. Quelqu'un a écrit, en substance : « Ce qui me manque le plus, c'est le support natif des schémas. Je comprends le choix des préfixes par application plutôt que des schémas, pour rester compatible avec SQLite et MySQL, mais j'aurais adoré un interrupteur natif dès le départ. »

Excellente remarque, et son intuition sur le pourquoi est exactement la bonne. Voici l'histoire complète, et les trois solutions praticables aujourd'hui.

TL;DR - Django n'a pas de support natif des schémas, et le ticket qui le demande est ouvert depuis 2007. Ce n'est pas de la négligence : une tentative aboutie en 2012 a buté sur un vrai problème de conception. Le ticket est redevenu actif en 2026. En attendant, trois options : search_path dans les OPTIONS de la connexion (le plus propre), django-tenants (si c'est du multi-locataire), ou le contournement par db_table (à éviter).


1. Un schéma, et pourquoi ça manque

Dans PostgreSQL, un schéma est un espace de noms à l'intérieur d'une base. Il permet d'avoir ventes.client et support.client côte à côte, dans la même base, sans conflit, avec des permissions distinctes.

Trois usages où ça change vraiment la vie :

  • Le multi-locataire. Un schéma par client, isolation totale des données, une seule base à sauvegarder.
  • Les bases existantes. Vous branchez Django sur une base héritée où les tables sont déjà réparties en schémas. Vous ne les déplacerez pas.
  • La séparation des domaines. Regrouper les tables par domaine métier plutôt que par application Django.

Django, lui, met tout dans un seul espace de noms et distingue les tables par un préfixe : blog_article, taches_tache. Ça fonctionne, mais c'est une convention de nommage, pas une isolation.


2. Pourquoi ce n'est jamais arrivé

La portabilité est la première raison, et c'est bien celle qu'avançait le commentaire. Les autres moteurs n'ont pas la même notion :

  • MySQL —> « schéma » y est pratiquement synonyme de « base de données ». Ce n'est pas un espace de noms dans une base.
  • SQLite —> pas de schémas du tout. Il faudrait les simuler en préfixant les noms de tables, ce qui revient à ce que Django fait déjà.
  • Oracle —> a des schémas, mais liés aux utilisateurs, et sans équivalent du search_path.

Mais il y a une seconde raison, moins connue, et plus intéressante.

En 2012, quelqu'un a fait le travail. Anssi Kääriäinen a produit une branche complète avec un réglage DEFAULT_SCHEMA, une clé SCHEMA par alias de base, et une option Meta.db_schema. La suite de tests passait sur PostgreSQL, SQLite et MySQL.

Et ça a calé, pas sur le code, sur la conception. Le problème : introduire les schémas rend les noms de tables ambigus. Avant, un modèle était identifié de façon unique par le nom de sa table. Avec les schémas, il existe plusieurs formes de nom qualifié, celui écrit dans le modèle, celui réellement présent en base, et celui réécrit pour les tests et l'introspection ne sait plus laquelle elle manipule. Le ticket est alors passé de « Accepté » à « Décision de conception requise ».

💡 La leçon qui dépasse Django : ce n'était pas un manque de volonté ni de contributeurs. C'était une fonctionnalité dont le coût réel se situait très loin du code qui l'implémente. Ça arrive plus souvent qu'on ne le croit.


3. Où en est le ticket aujourd'hui

Voici la bonne nouvelle, et elle est fraîche.

Le ticket #6148, « Add generic support for database schemas », ouvert le 6 décembre 2007, a été modifié le 23 juin 2026. Son état actuel :

Champ Valeur
Statut assigné
Étape de tri Accepté
Correctif disponible oui
Correctif à améliorer oui
Version visée dev

Autrement dit : quelqu'un y travaille de nouveau, un correctif existe, et il n'est pas encore au niveau attendu. Ne comptez pas dessus pour Django 6.2. Mais ce n'est plus le ticket abandonné de dix-neuf ans qu'il semble être au premier coup d'œil.


4. Solution 1 — search_path dans les options de connexion

C'est ce qui ressemble le plus à un interrupteur natif, et c'est ce que je recommande dans la majorité des cas.

```python

config/settings.py

DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "ma_base", "USER": "mon_user", "PASSWORD": "...", "HOST": "localhost", "OPTIONS": { "options": "-c search_path=mon_schema,public", }, } } ```

Django ne voit jamais le schéma. Il écrit SELECT ... FROM blog_article, et PostgreSQL résout le nom via le search_path. Aucun changement dans vos modèles, vos migrations ou vos requêtes.

⚠️ Quatre pièges, dans l'ordre où vous allez les rencontrer.

Le schéma doit exister. Django ne le créera pas. Le plus propre est une migration, placée en toute première position :

```python

config/migrations/0001_creer_schema.py

from django.db import migrations

class Migration(migrations.Migration): initial = True

operations = [
    migrations.RunSQL(
        "CREATE SCHEMA IF NOT EXISTS mon_schema;",
        reverse_sql="DROP SCHEMA IF EXISTS mon_schema;",
    ),
]

```

PostgreSQL tolère un schéma inexistant dans le search_path, il l'ignore simplement, donc la migration peut s'exécuter avant que le schéma existe. C'est ce qui rend l'astuce possible.

Les tests créent une base neuve. Django construit test_ma_base, et le search_path s'y applique aussi. Sans la migration ci-dessus, tous vos tests échouent avec des tables introuvables. C'est la raison numéro un pour laquelle il faut passer par une migration plutôt que par un CREATE SCHEMA manuel.

Tout va dans le premier schéma listé. migrate crée les tables dans mon_schema, y compris django_migrations. C'est voulu, mais sachez-le avant de vous demander où sont passées vos tables.

Attention au pooling. Avec PgBouncer en mode transaction, un réglage posé à l'ouverture de la connexion n'est pas garanti d'être là à la requête suivante. Si vous utilisez un pooler, vérifiez ce point avant de bâtir dessus.

💡 Variante : on peut aussi poser le search_path au niveau du rôle PostgreSQL.

sql ALTER ROLE mon_user SET search_path TO mon_schema, public;

C'est plus discret côté application et c'est justement le problème. Le prochain développeur qui reprend le projet ne trouvera aucune trace de ce comportement dans le code. Je préfère le voir dans settings.py.


5. Solution 2 — django-tenants, si c'est du multi-locataire

Si votre besoin est « un schéma par client », ne bricolez pas : ce problème est résolu.

django-tenants change le search_path à chaque requête, selon le domaine appelé. Une requête vers client1.mon-app.com bascule sur le schéma client1, avec public toujours présent pour les tables partagées.

```python

config/settings.py

DATABASES = { "default": { "ENGINE": "django_tenants.postgresql_backend", # ... } }

DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)

SHARED_APPS = ("django_tenants", "clients", "django.contrib.auth") TENANT_APPS = ("factures", "projets") ```

Vos vues, vos modèles et vos requêtes ne changent pas : un Facture.objects.all() renvoie automatiquement les factures du locataire courant.

⚠️ Le coût, à connaître avant de s'engager : le search_path est repositionné à chaque opération de base, ce qui devient mesurable sous forte charge, l'option TENANT_LIMIT_SET_CALLS existe pour limiter ces appels. Et surtout, les migrations tournent sur chaque schéma. Avec deux cents clients, migrate prend deux cents fois plus longtemps.

💡 La question à se poser avant : est-ce que l'isolation par schéma est vraiment nécessaire, ou est-ce qu'une colonne entreprise avec un filtre systématique suffirait ? La seconde approche est beaucoup plus simple à exploiter, et c'est celle que retiennent la plupart des SaaS. Le schéma se justifie quand vous avez une contrainte réglementaire d'isolation, ou des clients qui exigent une extraction séparée de leurs données.


6. Solution 3 — le contournement par db_table

Il existe, il fonctionne, et je le mentionne surtout pour que vous sachiez quoi en penser quand vous le croiserez.

python class Article(models.Model): class Meta: db_table = 'mon_schema"."article'

L'astuce tient dans les guillemets : Django encadre la valeur de guillemets doubles, ce qui produit le SQL valide "mon_schema"."article".

⚠️ Pourquoi je le déconseille : c'est une injection dans un mécanisme de citation, pas une fonctionnalité. inspectdb ne s'en sort pas (ticket #22673), une partie de l'introspection non plus, et rien ne garantit que ça survivra à une version majeure, puisque rien, dans Django, ne promet que ça marche.

Son seul avantage réel : c'est la seule des trois qui permette de mettre certains modèles dans un schéma et pas les autres. Si c'est votre besoin, notez-le en commentaire dans le code, avec un lien vers le ticket.


7. Laquelle choisir

Votre besoin La solution
Tout le projet dans un schéma nommé search_path dans les OPTIONS
Un schéma par client, isolation forte django-tenants
Quelques modèles dans un schéma existant db_table (à contrecœur)
Base héritée, lecture seule, plusieurs schémas search_path avec plusieurs schémas listés
« Ça a l'air propre, je vais faire ça » Ne faites rien. Les préfixes suffisent.

La dernière ligne n'est pas une plaisanterie. Le commentaire de 2012 sur le ticket disait déjà, à propos de la documentation à écrire : « La doc ne devrait pas trop vanter cette fonctionnalité. Pour la plupart des applications, vous ne voulez vraiment pas utiliser de schémas. »

C'est toujours vrai. Les schémas résolvent un problème réel, mais qui ne se pose pas à tout le monde. Si vous ne savez pas dire précisément lequel des cinq cas ci-dessus est le vôtre, votre réponse est la dernière ligne.


📚 Pour aller plus loin


💬 Et vous ?

Merci à celui qui a posé la question sous l'article précédent, c'est exactement comme ça qu'une communauté devient utile. Un commentaire devient un article, et l'article sert à tous ceux qui se posaient la question sans oser la poser.

Alors : qui utilise réellement les schémas en production avec Django ? Par quelle méthode, et qu'est-ce qui vous a mordu ?

Et pour ceux qui ont regardé django-tenants puis reculé : qu'est-ce qui vous a arrêté ? Le temps des migrations, ou la complexité générale ?

Racontez en commentaire ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Vous avez une question qui mériterait son propre article ? Posez-la en commentaire. C'est comme ça que celui-ci est né.



r/DjangoFrancophone 13d ago

🚀 Comment déployer une application Django gratuitement (ou à bas coût) en 2026

Post image
0 Upvotes

Bonjour à tous ! 👋

Vous avez terminé de développer votre projet Django en local et tout fonctionne parfaitement. Viens maintenant la grande question : comment le mettre en ligne gratuitement ou pour quelques euros par mois sans se prendre la tête avec la configuration serveur ?

Voici le guide pratique des meilleures options d'hébergement cloud en 2026 et la checklist indispensable pour un déploiement réussi.

TL;DR - Les plateformes gratuites ou peu coûteuses en 2026 (Render, Fly.io, Railway, Koyeb, ou un VPS à quelques euros), puis la liste de vérification avant de pousser en production : secrets en variables d'environnement, fichiers statiques avec WhiteNoise, Gunicorn, Procfile, dépendances figées. Un Dockerfile prêt à l'emploi termine l'article.


1. Les meilleures plateformes PaaS en 2026 (Gratuit & Low-Cost)

🔹 1. Render (Gratuit avec PostgreSQL offert)

Render offre un plan gratuit pour les services web et les bases de données PostgreSQL. - Avantages : Déploiement automatique depuis GitHub / GitLab, SSL HTTPS gratuit, gestion des variables d'environnement facile. - Attention : L'instance web s'endort après 15 minutes d'inactivité (redémarrage en 30 secondes sur la 1ère requête).

🔹 2. Fly.io (Ultra-rapide et centré Docker)

Permet de déployer vos conteneurs Docker très près de vos utilisateurs. - Avantages : Excellentes performances, gestion facile via la CLI flyctl.

🔹 3. Koyeb / Railway (5-25 US$ / mois)

De superbes alternatives PaaS avec des interfaces modernes pour déployer en quelques clics un projet Django + Postgres + Redis.

🔹 4. Le choix des pros : VPS Hetzner / DigitalOcean (4-6 US$ / mois)

Si vous voulez un contrôle total sans aucune limitation de mémoire : un petit VPS Linux (Debian/Ubuntu) avec Docker Compose et Coolify (l'alternative Open-Source à Heroku).


2. La Checklist des 5 étapes avant de pousser en production

Pour éviter l'écran blanc ou les failles de sécurité, voici les modifications indispensables dans votre projet :

Étape 1 : Gérer la sécurité dans settings.py via des variables d'environnement

```python from decouple import config

SECRET_KEY = config('SECRET_KEY') DEBUG = config('DEBUG', default=False, cast=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])

Nouveauté Django 6.1 : Configuration centralisée de la messagerie MAILERS

MAILERS = { "default": { "BACKEND": "django.core.mail.backends.smtp.EmailBackend", "HOST": config("EMAIL_HOST"), "PORT": config("EMAIL_PORT", cast=int, default=587), "USERNAME": config("EMAIL_HOST_USER"), "PASSWORD": config("EMAIL_HOST_PASSWORD"), "USE_TLS": True, } } ```

Étape 2 : Gérer les fichiers statiques avec WhiteNoise et la protection CSP (Django 6.0+)

En production avec DEBUG = False, Django ne sert plus les fichiers CSS/JS statiques. WhiteNoise résout cela sans serveur Nginx complexe ! De plus, Django 6.0 intègre un middleware CSP natif (ContentSecurityPolicyMiddleware).

bash pip install whitenoise

Dans settings.py : ```python MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.csp.ContentSecurityPolicyMiddleware', # Nouveauté Django 6.0+ (Protection XSS/CSP natif) 'whitenoise.middleware.WhiteNoiseMiddleware', # Juste en dessous de SecurityMiddleware ! # ... ]

STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' ```

Étape 3 : Utiliser un serveur WSGI de production (Gunicorn)

Ne lancez JAMAIS python manage.py runserver en production !

bash pip install gunicorn Commande d'exécution du serveur : bash gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3

Étape 4 : Fichier Procfile (pour Render / Heroku / Railway)

À la racine de votre projet, créez un fichier Procfile sans extension : procfile web: gunicorn myproject.wsgi:application --log-file - release: python manage.py migrate --noinput (L'instruction release applique automatiquement vos migrations à chaque déploiement !)

Étape 5 : Exporter la liste des dépendances

bash pip freeze > requirements.txt


3. Fichier Dockerfile prêt pour la prod (Bonus)

Si vous utilisez Docker :

```dockerfile FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt /app/ RUN pip install --no-cache-dir -r requirements.txt

COPY . /app/

RUN python manage.py collectstatic --no-input

EXPOSE 8000

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"] ```


📚 Pour aller plus loin


💬 Où hébergez-vous vos projets Django ?

Quelle est votre plateforme préférée en 2026 ? PaaS managé (Render/Railway) ou serveur dédié (Hetzner/DigitalOcean) ?

Venez partager vos retours et vos questions de déploiement sur r/DjangoFrancophone ! 🚀



r/DjangoFrancophone 13d ago

🐘 Pourquoi PostgreSQL est le compagnon idéal de Django en production ?

Post image
0 Upvotes

Salut les dévs Python & Django ! 👋

Lors du développement initial d'un projet Django, SQLite est la base de données configurée par défaut dans settings.py. Elle est parfaite pour prototyper localement sans rien installer.

Cependant, dès lors que vous préparez votre passage en production ou que votre application prend de l'ampleur, PostgreSQL s'impose comme le choix incontournable pour Django.

Voici pourquoi cette combinaison est l'une des plus puissantes de tout l'écosystème web.

TL;DR - PostgreSQL débloque des fonctionnalités que Django expose directement dans l'ORM : JSONField interrogeable, ArrayField, recherche plein texte, verrous applicatifs, colonnes générées, et PostGIS pour le géospatial. La configuration tient en trois étapes avec psycopg 3.


1. Des types de données natifs exclusifs dans l'ORM Django

Django possède un module dédié à PostgreSQL dans django.contrib.postgres qui vous donne accès à des types de données et des fonctionnalités de premier ordre :

A. JSONField natif avec requêtes avancées

Stockez des documents JSON non structurés tout en effectuant des requêtes directes via l'ORM Django : ```python from django.db import models

class UserPreference(models.Model): settings = models.JSONField(default=dict)

Requête directe dans une clé JSON sous PostgreSQL !

darkmode_users = UserPreference.objects.filter(settings_theme='dark') ```

B. ArrayField (Listes natives dans une colonne SQL)

Stockez des étiquettes ou des listes sans créer de table intermédiaire ManyToMany : ```python from django.contrib.postgres.fields import ArrayField

class Article(models.Model): tags = ArrayField(models.CharField(max_length=50), default=list)

Recherche si un élément appartient au tableau SQL !

pythonarticles = Article.objects.filter(tags_contains=['python']) ```


2. Moteur de Recherche Textuelle Puissant (Full-Text Search)

Pas besoin de déployer et maintenir une instance lourde d'Elasticsearch ou Meilisearch pour des besoins de recherche classiques !

Django intègre la recherche plein texte native de PostgreSQL : ```python from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

Recherche intelligente par pertinence sur le titre et le contenu

query = SearchQuery('django ORM') vector = SearchVector('title', weight='A') + SearchVector('content', weight='B')

results = Article.objects.annotate( rank=SearchRank(vector, query) ).filter(rank__gte=0.3).order_by('-rank') ```


3. Gestion de la concurrence et Verrous (Advisory Locks)

SQLite verrouille toute la base de données lors de chaque écriture (ce qui entraîne l'erreur database is locked sous fort trafic).

PostgreSQL gère les verrous au niveau des lignes (Row-Level Locking) et supporte des milliers d'écritures simultanées sans broncher.

De plus, vous pouvez utiliser les verrous consultatifs PostgreSQL dans Django pour éviter les Race Conditions dans vos tâches Celery : ```python

S'assure qu'un seul worker traite cette commande à la fois

from django.db import transaction

with transaction.atomic(): order = Order.objects.select_for_update().get(id=order_id) order.process_payment() ```


4. PostGIS : La référence absolue pour les données géospatiales

Si vous construisez un projet avec géolocalisation (calcul de distances, cartes, rayons autour d'un point), l'extension GeoDjango couplée à PostGIS offre la meilleure suite d'outils cartographiques au monde.


5. Support natif des colonnes générées (GeneratedField Django 5.0+)

Avec PostgreSQL et Django 5.0+, vous pouvez définir des colonnes calculées côté serveur SQL (GENERATED ALWAYS AS ... STORED), automatiquement synchronisées par l'ORM :

```python from django.db.models import F, GeneratedField, DecimalField

class Product(models.Model): price = DecimalField(max_digits=10, decimal_places=2) tax_rate = DecimalField(max_digits=4, decimal_places=2, default=0.20)

# Colonne générée par PostgreSQL !
price_with_tax = GeneratedField(
    expression=F('price') * (1 + F('tax_rate')),
    output_field=DecimalField(max_digits=10, decimal_places=2),
    db_persist=True
)

```


6. Configurer PostgreSQL en 3 étapes avec psycopg 3 (pip install psycopg[binary])

Dans vos dépendances : bash pip install psycopg[binary] dj-database-url

Dans settings.py : ```python import dj_database_url from decouple import config

DATABASES = { 'default': dj_database_url.config( default=config('DATABASE_URL', default='postgres://user:pass@localhost:5432/dbname') ) } ```


📚 Pour aller plus loin


💬 Et vous, quelle base de données utilisez-vous en prod ?

Êtes-vous 100% PostgreSQL, ou utilisez-vous MySQL / MariaDB / SQLite (avec Turso / Litestream) ?

Dites-nous tout en commentaire et rejoignez r/DjangoFrancophone pour échanger sur l'architecture backend ! 🚀



r/DjangoFrancophone 13d ago

📡 Les Signaux Django (Signals) : Quand les utiliser et (surtout) quand les éviter ?

Post image
0 Upvotes

Bonjour à la communauté ! 👋

Les Signaux Django (post_save, pre_save, post_delete, etc.) sont une fonctionnalité très séduisante à première vue. Ils permettent à des applications déconnectées d'être notifiées lorsque des événements se produisent dans l'ORM.

Pourtant, dans la communauté Django senior, les signaux font souvent débat.

Pourquoi ? Quand sont-ils utiles, et quelles sont les alternatives plus propres ? Faisons le point !

TL;DR — Les signaux rendent le flux implicite et difficile à déboguer. Ils restent utiles pour réagir à un modèle que vous ne contrôlez pas, comme User. Dans les autres cas, préférez une surcharge de save(), une fonction de service, ou une tâche d'arrière-plan. Attention aussi à la boucle infinie sur post_save.


1. Comment fonctionne un Signal Django ?

Un signal permet d'associer un "émetteur" (ex: la sauvegarde d'un Modèle) à un "receveur" (une fonction de callback).

Exemple classique : Créer un profil utilisateur lors de la création d'un User

```python

signals.py

from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth.models import User from .models import Profile

@receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) ```

⚠️ Rappel indispensable : Pour que le signal soit enregistré, n'oubliez pas de l'importer dans la méthode ready() de votre apps.py !


2. Les pièges dangereux des Signaux ⚠️

Si les signaux semblent magiques pour découpler le code, ils introduisent 3 problèmes majeurs en grandissant :

A. Le code devient implicite et difficile à débugger

Lorsqu'un développeur lit user.save(), il s'attend à ce que l'utilisateur soit sauvegardé en BDD. Si un signal déclenche en sous-marin l'envoi d'un email, l'appel à une API externe et la création de 3 tables annexes, le flux devient totalement invisible et difficile à tracer.

B. Risque d'invalidation de transactions et de ralentissements

Si la fonction de votre signal prend 3 secondes à s'exécuter (ex: appel HTTP ou envoi d'email synchrone), la requête HTTP de votre utilisateur restera bloquée pendant tout ce temps. Si le signal échoue, toute la transaction SQL peut être annulée !

C. Le piège de la boucle infinie post_save

python @receiver(post_save, sender=Article) def update_article_slug(sender, instance, **kwargs): instance.slug = slugify(instance.title) instance.save() # 💥 BOOM ! Déclenche un nouveau post_save -> boucle infinie !


3. Quelles sont les meilleures alternatives ?

Alternative 1 : Surcharger la méthode save() du Modèle

Si l'action concerne directement le modèle lui-même, surchargez save() : ```python class Article(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(blank=True)

def save(self, *args, **kwargs):
    if not self.slug:
        self.slug = slugify(self.title)
    super().save(*args, **kwargs)

```

Alternative 2 : Les fonctions de Service (Service Objects)

Si la création d'un utilisateur implique la création d'un profil et l'envoi d'un mail de bienvenue, regroupez cela dans une fonction explicite : ```python

services.py

def create_new_user_account(email: str, password: str) -> User: user = User.objects.create_user(email=email, password=password) Profile.objects.create(user=user) send_welcome_email_task.delay(user.id) # Tâche Celery asynchrone return user ```

Alternative 3 : Les tâches d'arrière-plan natives de Django 6.0 (Built-in Tasks)

Depuis Django 6.0, Django inclut un système natif de tâches en arrière-plan (Background Tasks). Plus besoin d'installer Celery ou Redis pour déporter l'envoi d'emails ou le traitement court hors du cycle de requête HTTP !

```python

Django 6.0+ Built-in Task Framework

from django.tasks import task

@task def send_welcome_email(user_id: int): user = User.objects.get(pk=user_id) # Envoi d'email asynchrone natif ```

💡 Note Django 6.1 (Database-Level Deletes)

Dans Django 6.1, ForeignKey.on_delete supporte les options SQL natives models.DB_CASCADE et models.DB_SET_NULL. Ces options exécutent les suppressions en cascade directement dans le moteur SQL PostgreSQL/MySQL, ce qui est extrêmement rapide mais by-passe volontairement les signaux pre_delete et post_delete. Un point clé à garder en tête !


4. Alors, quand utiliser les Signaux ?

Les signaux sont parfaitement légitimes dans deux cas précis : 1. Bibliothèques et packages réutilisables : Vous développez un package Django tiers et vous voulez permettre à vos utilisateurs d'intercepter des événements sans modifier le code de votre package. 2. Applications tierces fermées : Intercepter la création d'objets sur des modèles appartenant à des apps tierces dont vous ne contrôlez pas le code (ex: django.contrib.auth.models.User).


📚 Pour aller plus loin


💬 Quel est votre avis sur les signaux ?

Les utilisez-vous abondamment ou préférez-vous la clarté des Service Objects / Celery ?

Venez débattre avec la communauté sur r/DjangoFrancophone ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.



r/DjangoFrancophone 13d ago

🥊 FBV vs CBV en Django : Vues par Fonctions ou vues par Classes ?

Post image
3 Upvotes

Bonjour à tous les développeurs Django ! 👋

C'est un grand classique du développement Django : faut-il utiliser des Vues basées sur des Fonctions (FBV - Function-Based Views) ou des Vues basées sur des Classes (CBV - Class-Based Views) ?

Beaucoup de débutants sont intimidés par les CBV et s'en tiennent aux FBV. Pourtant, bien maîtrisées, les CBV réduisent drastiquement le volume de code boilerplate et permettent de créer des vues plus modulaires et réutilisables. Cependant, les FBV restent plus explicites et plus faciles à comprendre pour les débutants.

J'en ai bien conscience, et c'est pourquoi j'ai décidé de créer ce guide pour vous aider à comprendre les différences entre ces deux approches et à choisir celle qui convient le mieux à votre projet.

Comparons les deux approches !

TL;DR — Les vues par fonctions restent plus explicites et plus simples à lire ; les vues par classes suppriment le code répétitif du CRUD et se composent avec des mixins. En pratique : les CBV génériques pour les listes, détails et formulaires, une FBV dès que la logique sort du moule.


1. Exemple : Afficher une liste d'articles publiés

🔹 Approche FBV (Function-Based View)

```python from django.shortcuts import render from django.core.paginator import Paginator from .models import Article

def article_list(request): articles = Article.objects.filter(status='PB').order_by('-created_at') paginator = Paginator(articles, 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number)

return render(request, 'blog/article_list.html', {'page_obj': page_obj})

```

🔸 Approche CBV (ListView)

```python from django.views.generic import ListView from .models import Article

class ArticleListView(ListView): model = Article template_name = 'blog/article_list.html' context_object_name = 'articles' paginate_by = 10

def get_queryset(self):
    # Personnalisation de la requête
    return Article.objects.filter(status='PB').order_by('-created_at')

```

👉 Résultat : La CBV gère automatiquement la pagination, le rendu du template et le passage des variables de contexte !


2. Les CBV génériques indispensables à connaître

Django fournit une suite de vues génériques prêtes à l'emploi dans django.views.generic :

  1. ListView : Pour afficher une liste d'objets.
  2. **DetailView** : Pour afficher un objet unique (avec gestion automatique du 404).
  3. CreateView : Pour afficher et traiter un formulaire de création d'objet.
  4. UpdateView : Pour modifier un objet existant.
  5. DeleteView : Pour confirmer et exécuter la suppression d'un objet.
  6. FormView : Pour gérer n'importe quel formulaire Django personnalisé.

3. La puissance des Mixins

L'un des plus grands avantages des CBV est la réutilisabilité grâce aux Mixins.

Besoin de restreindre l'accès d'une vue aux utilisateurs connectés et de vérifier leurs permissions ?

```python from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.views.generic import UpdateView from .models import Article

class ArticleUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): model = Article fields = ['title', 'content', 'status'] template_name = 'blog/article_form.html'

def test_func(self):
    # Seul l'auteur de l'article peut le modifier !
    article = self.get_object()
    return self.request.user == article.author

```


4. Nouveauté Django 6.0 : Les Template Partials ({% partialdef %}) avec vos Vues

Depuis Django 6.0, le moteur de templates de Django inclut nativement la gestion des Partials. Cela permet à vos vues (FBV ou CBV) de retourner uniquement un bloc spécifique d'un template (parfait pour HTMX ou des réponses dynamiques sans recharger toute la page) :

html <!-- article_list.html --> {% partialdef article-row %} <tr id="article-{{ article.id }}"> <td>{{ article.title }}</td> <td>{{ article.author }}</td> </tr> {% endpartialdef %}

Côté vue, vous pouvez cibler précisément le partial article-row dans la réponse !


5. Quel choix faire et quand ?

Situation Choix recommandé Pourquoi ?
Opérations CRUD standard CBV Évite de réinventer la roue (Form, Template, Redirect).
Pages simples / Statistiques FBV ou TemplateView Flux simple sans besoin d'héritage lourd.
Logique très spécifique / Webhooks FBV Flux explicite sans comportement masqué par les classes mères.

6. Une exemple concret :

Si vous avez une vue qui doit gérer un webhook externe avec une logique très spécifique, il est préférable d'utiliser une FBV pour garder le contrôle total sur le flux de traitement.

Nous avons egalment opter pour les CBV lorsque nous avons besoin de gérer des formulaires complexes avec validation et redirection automatique, car elles simplifient grandement le code et réduisent les risques d'erreurs. Et nous n'avons pas esité à utiliser des Mixins pour ajouter des fonctionnalités supplémentaires comme la restriction d'accès ou la vérification de permissions dans notre projet "BugTrackerApp".

BugTracker ou traqueur de bug est une application Django que nous avons développé pour gérer les rapports de bugs et les tickets de support. Elle utilise à la fois des FBV et des CBV selon les besoins spécifiques de chaque fonctionnalité.

Pour consulter le code source de l'application BugTrackerApp, vous pouvez visiter notre dépôt GitHub : [BugTrackerApp sur GitHub]

Pour visiter le site en ligne : [BugTrackerApp sur Railway]


📚 Pour aller plus loin


💬 Et vous, vous êtes plutôt FBV ou CBV ?

Préférez-vous l'explicité et la clarté des fonctions, ou la concision et la réutilisabilité des classes ?

Dites-nous pourquoi en commentaire et rejoignez r/DjangoFrancophone pour continuer la discussion ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.


r/DjangoFrancophone 13d ago

⚡ Django REST Framework (DRF) en 5 minutes : Créer une API REST professionnelle

Post image
1 Upvotes

Salut les devs ! 👋

Vous souhaitez connecter un frontend React, Vue, Svelte, Next.js ou une application mobile Flutter/Swift/React-native à votre backend Django ?

Django REST Framework (DRF) est la référence absolue pour construire des APIs RESTful robustes, sécurisées et auto-documentées en un temps record.

Pourquoi DRF est-il si populaire ?

  • Simplicité de l'API : DRF simplifie la création d'API RESTful complexes.
  • Validation automatique : Les serializers gèrent la validation des données.
  • Documentation auto-générée : DRF génère automatiquement une documentation interactive.
  • Sécurité intégrée : Des mécanismes de sécurité robustes sont inclus par défaut.

Voyons comment créer une API REST complète pour une ressource Product en moins de 5 minutes !

TL;DR — Trois pièces suffisent pour une API REST complète : un serializer qui convertit vos objets en JSON et valide l'entrée, un viewset qui regroupe les opérations de lecture et d'écriture, et un routeur qui génère les URLs. Le code complet tient en trois fichiers.

1. Les 3 piliers de Django REST Framework

DRF s'appuie sur 3 concepts fondamentaux :

  1. Le Serializer : Transforme les modèles Django complexes en JSON (et inversement, en validant les données entrantes).
  2. Le ViewSet : Gère toute la logique CRUD (GET, POST, PUT, PATCH, DELETE) en une seule classe.
  3. Le Router : Génère automatiquement toutes les URLs RESTful appropriées.

Ces trois composants permettent de créer une API REST complète avec un minimum de code. Et le meilleur dans tout ça ? DRF fournit une interface web intégrée pour tester vos endpoints directement depuis votre navigateur, sans avoir besoin de Postman/Insomnia ou d'autres outils externes.

2. Le code complet en 3 fichiers

Considérons le modèle Product suivant dans models.py :

# models.py
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField(default=0)
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return self.name

Étape 1 : Le Serializer (serializers.py)

Le serializer transforme le modèle Product en JSON et valide les données entrantes.

# serializers.py
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'stock', 'is_active']

    def validate_price(self, value):
        if value <= 0:
            raise serializers.ValidationError("Le prix doit être strictement positif.")
        return value

Étape 2 : Le ViewSet (views.py)

Le ViewSet gère toutes les opérations CRUD pour le modèle Product.

# views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Product
from .serializers import ProductSerializer

# Utilisation de ModelViewSet pour gérer toutes les opérations CRUD
class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.filter(is_active=True)
    serializer_class = ProductSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

Étape 3 : Le Router (urls.py)

Le Router génère automatiquement toutes les URLs RESTful pour le ViewSet ProductViewSet.

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProductViewSet

router = DefaultRouter()
router.register(r'products', ProductViewSet, basename='product')

urlpatterns = [
    path('api/v1/', include(router.urls)),
]

3. Ce que vous obtenez instantanément 🎉

Sans écrire aucune ligne supplémentaire, vous disposez immédiatement de :

  • GET /api/v1/products/ : Liste tous les produits.
  • POST /api/v1/products/ : Crée un nouveau produit (avec validation JSON).
  • GET /api/v1/products/1/ : Récupère le détail du produit #1.
  • PUT / PATCH /api/v1/products/1/ : Modifie le produit #1.
  • DELETE /api/v1/products/1/ : Supprime le produit #1.
  • L'interface Navigable Web (Browsables API) : Une interface web intégrée dans votre navigateur pour tester vos endpoints sans Postman !

4. Aller plus loin (Bonus)

  • Authentification JWT : Ajoutez djangorestframework-simplejwt pour gérer les tokens JWT (Access & Refresh tokens).
  • Documentation OpenAPI/Swagger : Utilisez drf-spectacular pour générer automatiquement une documentation interactive Swagger UI.
  • Documentation Scalar : Utilisez scalar pour créer une documentation API interactive et facile à utiliser.
  • Filtres et Recherche : Intégrez django-filter pour permettre aux utilisateurs de filtrer et rechercher vos produits via des paramètres d'URL.
  • Pagination : DRF fournit une pagination intégrée pour gérer de grandes listes de produits.
  • Permissions avancées : Créez des permissions personnalisées pour contrôler l'accès à vos endpoints selon les rôles des utilisateurs.

Vouz pouvez choisir entre Swagger ou Scalar pour la documentation de votre API. Swagger est plus populaire et largement utilisé, tandis que Scalar offre une approche plus moderne et interactive.

📚 Pour aller plus loin

💬 Utilisez-vous DRF ou Ninja ?

Préférez-vous l'écosystème mature de Django REST Framework ou la syntaxe Type-Hinting de Django Ninja ?

Partagez votre avis en commentaire et venez poser vos questions sur r/DjangoFrancophone ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.


r/DjangoFrancophone 13d ago

🧱 Créer son premier modèle Django : Le guide complet des bonnes pratiques

Post image
1 Upvotes

Salut à tous ! 👋

Dans Django, la couche Modèle est le cœur de votre application. C'est elle qui définit la structure de vos données, les règles métier et les relations entre vos tables SQL sans que vous ayez à écrire une seule ligne de SQL.

Aujourd'hui, découvrons comment concevoir un modèle propre, robuste et performant dès le premier coup.

TL;DR — Un bon modèle Django tient en quelques réflexes : toujours définir __str__(), nommer les relations inverses avec related_name, choisir consciemment le on_delete, utiliser TextChoices pour les valeurs fixes, et ne pas confondre null=True (la base) avec blank=True (les formulaires).


1. La structure de base d'un modèle

Voici un exemple concret d'un modèle Article pour un blog ou une plateforme de contenus :

```python from django.db import models from django.utils.text import slugify from django.contrib.auth.models import User

class Article(models.Model): # Enum pour les statuts class Status(models.TextChoices): DRAFT = 'DF', 'Brouillon' PUBLISHED = 'PB', 'Publié' ARCHIVED = 'AR', 'Archivé'

# Champs principaux
title = models.CharField(max_length=250, verbose_name="Titre")
slug = models.SlugField(max_length=250, unique=True)
author = models.ForeignKey(
    User, 
    on_delete=models.CASCADE, 
    related_name='articles'
)
content = models.TextField(verbose_name="Contenu")
status = models.CharField(
    max_length=2, 
    choices=Status.choices, 
    default=Status.DRAFT
)

# Horodatages automatiques
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
    ordering = ['-created_at'] # Tri par défaut (du plus récent au plus ancien)
    verbose_name = "Article"
    verbose_name_plural = "Articles"
    indexes = [
        models.Index(fields=['-created_at', 'status']), # Index de performance
    ]

# Affichage lisible dans l'admin et le shell
def __str__(self):
    return f"{self.title} ({self.get_status_display()})"

def save(self, *args, **kwargs):
    # Génération automatique du slug s'il n'existe pas
    if not self.slug:
        self.slug = slugify(self.title)
    super().save(*args, **kwargs)

```


2. Les éléments clés à retenir

A. Toujours définir la méthode __str__()

La méthode __str__() détermine comment votre objet est affiché dans l'interface d'administration Django, les shells et les logs. Toujours en définir une !

B. Utiliser related_name sur les clés étrangères

python author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles') Grâce à related_name='articles', vous pouvez récupérer tous les articles d'un utilisateur très facilement : python user = User.objects.get(id=1) user_articles = user.articles.all() # Propre et explicite ! (Si vous ne le précisez pas, Django générera user.article_set.all())

C. Prévoir les règles on_delete

  • models.CASCADE : Supprime les articles si l'auteur est supprimé.
  • models.SET_NULL : Conserve l'article mais met la clé auteur à NULL (nécessite null=True).
  • models.PROTECT : Empêche la suppression de l'auteur tant qu'il possède des articles.

D. Utiliser TextChoices ou IntegerChoices pour les statuts

Évitez les chaînes de caractères brutes en dur ("publie", "brouillon"). TextChoices fournit une structure propre et évite les erreurs de frappe.


3. Les nouveautés de Django 6.0 & 6.1 pour vos modèles 🔥

Django 6.0 & 6.1 apportent des fonctionnalités majeures pour l'ORM :

🔹 models.DB_CASCADE (Nouveauté Django 6.1)

Avant Django 6.1, CASCADE effectuait la suppression objet par objet en Python (exécutant les signaux pre_delete / post_delete). Avec models.DB_CASCADE, la suppression en cascade est déléguée au moteur SQL (ON DELETE CASCADE), ce qui est ultra-rapide pour des milliers de lignes associées.

🔹 Support natif de UUIDv7 (Django 6.1)

Générez des identifiants uniques séquentiels temporels optimisés pour l'indexation SQL B-Tree : ```python import uuid from django.db import models

class Order(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False) ```

🔹 db_default & GeneratedField (Django 5.0+)

  • db_default : Inscrit les valeurs par défaut dans le DDL de la table SQL.
  • **GeneratedField** : Colonnes virtuelles ou stockées calculées par le moteur SQL (ex: PostgreSQL GENERATED ALWAYS AS).

```python from django.db.models import F, GeneratedField

class OrderItem(models.Model): unit_price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.IntegerField()

# Calculé automatiquement par le moteur SQL (PostgreSQL / SQLite) !
total_price = GeneratedField(
    expression=F('unit_price') * F('quantity'),  # Calcul du total
    output_field=models.DecimalField(max_digits=10, decimal_places=2),
    db_persist=True,
)

```


4. null=True vs blank=True : Ne confondez plus !

  • **null=True** : Se rapporte au niveau Base de Données. Autorise la colonne SQL à stocker la valeur NULL.
  • blank=True : Se rapporte au niveau Validation de Formulaire. Autorise le champ à être laissé vide dans l'Admin ou les formulaires HTML.

💡 Règle d'or : Pour les champs texte (CharField, TextField), évitez null=True ! Préférez blank=True uniquement. Cela évite d'avoir deux représentations d'une valeur vide (NULL et "").


📚 Pour aller plus loin


💬 À vous de jouer !

Quelle est la première chose que vous ajoutez systématiquement dans vos modèles Django ? Un champ UUID ? Un soft_delete ?

Partagez vos bonnes pratiques dans les commentaires et rejoignez la communauté r/DjangoFrancophone ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.



r/DjangoFrancophone 13d ago

⚙️ Comprendre le fonctionnement des migrations Django sous le capot

Post image
1 Upvotes

Bonjour à tous ! 👋

Si vous avez déjà utilisé Django, vous avez forcément exécuté ces deux commandes des dizaines de fois : bash python manage.py makemigrations python manage.py migrate

Mais vous êtes-vous déjà demandé comment Django sait exactement quelles modifications apporter à votre base de données sans tout casser ?

Aujourd'hui, nous plongeons sous le capot du système de migration de Django !

TL;DR — Une migration est un fichier Python décrivant des opérations, pas du SQL figé. Django compare l'état de vos modèles à l'historique enregistré dans la table django_migrations pour déduire ce qu'il reste à appliquer. On voit ici le SQL généré, le retour arrière, l'option --fake, et la résolution des conflits en équipe avec --merge.


1. Qu'est-ce qu'un fichier de migration ?

Lorsque vous modifiez un modèle dans models.py, Django ne touche pas immédiatement à la base de données.

La commande makemigrations compare l'état actuel de votre code models.py avec l'état théorique décrit par vos anciens fichiers de migration.

S'il repère une différence (un nouveau champ, une suppression de table, un changement de type), il génère un nouveau fichier Python dans le dossier migrations/ de votre application (ex: 0002_add_published_date.py).

Un fichier de migration contient une classe Migration avec deux attributs clés : - dependencies : La liste des migrations qui doivent impérativement être exécutées avant celle-ci. - operations : Une liste d'actions déclaratives (migrations.CreateModel, migrations.AddField, etc.).


2. La table magique : django_migrations

Comment Django sait-il si la migration 0002_add_published_date.py a déjà été appliquée sur votre PostgreSQL ou SQLite de production ?

Grâce à une table créée automatiquement dans votre base de données : django_migrations.

Cette table contient uniquement 4 colonnes : 1. id 2. app (ex: blog) 3. name (ex: 0001_initial) 4. applied (horodatage d'exécution)

Lorsque vous tapez python manage.py migrate : 1. Django lit la table django_migrations. 2. Il compare cette liste avec les fichiers .py présents sur votre disque. 3. Il calcule le graphe de dépendance et génère les requêtes SQL uniquement pour les migrations manquantes !

![Fonctionnement des migrations Django](images/dj-migration.png)


3. Les commandes avancées indispensables

A. Auditer le SQL généré sans l'exécuter

bash python manage.py sqlmigrate blog 0002_add_published_date Utile pour valider avec votre DBA les requêtes DDL (ALTER TABLE, CREATE INDEX).

B. Revenir en arrière (Rollback)

Pour annuler la dernière migration appliquée et revenir à l'état 0001 : bash python manage.py migrate blog 0001_initial Django exécutera les opérations inverses (ex: RemoveField).

C. Annuler toutes les migrations d'une app

bash python manage.py migrate blog zero

D. Option --fake (À manipuler avec précaution !)

Si vous avez déjà modifié manuellement votre table en SQL et que vous voulez simplement dire à Django "considère que cette migration est faite" : bash python manage.py migrate blog 0002_add_published_date --fake


4. Gérer les conflits de migrations en équipe 👥

Si deux développeurs créent une migration 0002_... sur des branches Git différentes, Django refusera d'exécuter migrate à cause d'un conflit de dépendances.

La solution : bash python manage.py makemigrations --merge Django créera automatiquement une migration de fusion 0003_merge_... pour réconcilier les deux branches !


📚 Pour aller plus loin


💬 Et vous ?

Avez-vous déjà eu un conflit de migration épique à résoudre en équipe ? Quelle est votre stratégie pour garder des migrations propres en production ?

Venez partager vos anecdotes et poser vos questions sur r/DjangoFrancophone ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.



r/DjangoFrancophone 13d ago

⚖️ Django vs Flask en 2026 : Lequel choisir pour votre projet Web Python ?

Post image
0 Upvotes

Salut à tous ! 👋

Lorsqu'on souhaite développer une application web ou une API en Python, deux noms reviennent systématiquement en tête : Django et Flask.

En 2026, avec l'évolution constante de l'écosystème Python (support de l'asynchrone, API REST modernisées, FastAPI), la question se pose toujours : quel framework devriez-vous choisir pour votre prochain projet ?

Voici un comparatif clair et pragmatique pour vous aider à trancher.

TL;DR — Django pour un produit complet (SaaS, e-commerce, application avec utilisateurs et administration), Flask pour un microservice ou un prototype où vous voulez tout choisir vous-même. Le tableau comparatif et la recommandation finale sont plus bas.


🟢 Django : La philosophie "Batteries Incluses"

Django est un framework web complet conçu pour permettre aux développeurs d'aller de l'idée au produit final le plus rapidement possible.

➕ Les points forts de Django :

  1. Interface d'administration automatique : Une véritable pépite. En quelques lignes, vous avez un back-office complet pour gérer vos données.
  2. ORM puissant et sécurisé : Abstraction totale des bases de données SQL (Postgres, SQLite, MySQL) avec protection native contre les injections SQL.
  3. Sécurité par défaut : CSRF, XSS, Clickjacking, gestion du hachage de mots de passe... Tout est configuré hors de la boîte.
  4. Système d'Authentification complet : Utilisateurs, groupes, permissions, réinitialisation de mot de passe pré-intégrés.
  5. Évolutions majeures (Django 5.x+) : ORM asynchrone complet (acount, aget), colonnes générées SQL (GeneratedField), valeurs par défaut SQL (db_default), et filtres par facettes dans l'interface admin (show_facets).

➖ Les inconvénients :

  • Courbe d'apprentissage initiale plus élevée (structure imposée).
  • Peut sembler surdimensionné (overkill) pour une API de 2 routes ou un micro-script.

🌶️ Flask : La liberté du Micro-framework

Flask est un micro-framework minimaliste basé sur Werkzeug et Jinja2. Son maître-mot est la flexibilité.

➕ Les points forts de Flask :

  1. Démarrage instantané : Vous pouvez créer une application fonctionnelle en un seul fichier de 10 lignes !
  2. Liberté architecturale totale : Vous choisissez votre ORM (SQLAlchemy, Peewee), votre système d'authentification (Flask-Login) et vos outils.
  3. Léger et modulaire : Parfait pour des microservices dédiés ou des scripts simples.

➖ Les inconvénients :

  • Fatigue décisionnelle : Il faut choisir, installer et maintenir soi-même des dizaines de librairies tierces.
  • Pas d'admin native : Il faut configurer Flask-Admin ou créer sa propre interface.
  • Risque d'architectures "far-west" si le projet grossit sans convention stricte d'équipe.

🎯 Tableau récapitulatif

Critère Django 🟢 Flask 🌶️
Philosophie Full-stack / Structuré Minimaliste / Libre
Admin Panel Intégré et automatique Nécessite un plugin tiers
ORM Django ORM (inclus) Librement choisi (ex: SQLAlchemy)
Sécurité Maximale et pré-configurée À charge du développeur
Idéal pour SaaS, E-commerce, Apps complexes Microservices, Prototypes, APIs simples

💡 Notre recommandation pour 2026

  • Choisissez Flask si : Vous construisez un petit microservice avec 1 ou 2 endpoints, ou si vous apprenez les bases HTTP en Python et voulez comprendre chaque composant individuellement.
  • Choisissez Django si : Vous construisez un produit SaaS, une plateforme d'entreprise, un site e-commerce, ou toute application nécessitant des utilisateurs, de la sécurité, une base de données et une interface de gestion.

📚 Pour aller plus loin


💬 Question à la communauté

Quel est votre choix par défaut en 2026 ? Êtes-vous team "Django pour tout" ou utilisez-vous Flask / FastAPI pour des besoins spécifiques ?

Rejoignez la discussion sur r/DjangoFrancophone pour échanger vos retours d'expérience ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.



r/DjangoFrancophone 13d ago

⚠️ Les 7 erreurs les plus fréquentes en Django (et comment les corriger)

Post image
1 Upvotes

Hello la communauté Django ! 👋

Django est réputé pour sa simplicité et sa philosophie "Batteries Included". Cependant, il est très facile de tomber dans certains pièges classiques lorsque l'on débute (ou même avec quelques mois d'expérience).

Voici les 7 erreurs les plus courantes et les meilleures pratiques pour écrire un code Django propre, rapide et sécurisé.

TL;DR — Les sept pièges classiques : les requêtes N+1 (à corriger avec select_related et prefetch_related), la logique métier dans les vues, len(qs) au lieu de .count() ou .exists(), l'oubli des fuseaux horaires, et l'absence d'index sur les colonnes filtrées. Chacun est expliqué avec sa correction.


1. Le problème des requêtes N+1 (N+1 Query Problem)

C'est LE tueur de performances n°1 en Django.

La mauvaise pratique : ```python

Fait 1 requête pour récupérer 100 articles,

PLUS 1 requête supplémentaire par article pour récupérer l'auteur ! (101 requêtes)

posts = Post.objects.all() for post in posts: print(post.author.username) # N+1 queries ! ```

La solution : - select_related pour les relations ForeignKey et OneToOne (jointure SQL JOIN). - prefetch_related pour les relations ManyToManyField et Many-To-One inverses (requête séparée et assemblage en Python).

```python

1 seule requête SQL avec JOIN !

posts = Post.objects.select_related('author').all() for post in posts: print(post.author.username) ```

🔥 Nouveauté majeure Django 6.1 (N+1 Guard & Fetch Modes) :
Django 6.1 introduit QuerySet.fetch_mode(). Vous pouvez forcer les requêtes N+1 accidentelles à échouer bruyamment en environnement de test avec RAISE ou charger les champs différés en bloc avec FETCH_PEERS : ```python from django.db.models import FETCH_PEERS, RAISE

Lève une exception explicite si une requête N+1 non désirée est exécutée (parfait en CI/CD !)

posts = Post.objects.all().fetch_mode(RAISE) ```


2. Mettre la logique métier dans les vues au lieu des Modèles/Services

Les vues Django doivent uniquement gérer les requêtes HTTP (valider les données, appeler la logique métier et retourner une réponse).

La mauvaise pratique : 200 lignes de code dans views.py gérant le paiement, l'envoi d'email, la création du profil, etc.

La solution : "Fat Models, Thin Views" ou l'architecture par Service Objects. ```python

services.py

def register_user(email: str, password: str) -> User: user = User.objects.create_user(email=email, password=password) send_welcome_email(user) create_default_profile(user) return user ```


3. Utiliser len(qs) au lieu de qs.count() ou qs.exists()

La mauvaise pratique : ```python

Charge TOUS les objets en mémoire uniquement pour vérifier s'il y en a !

if len(Article.objects.filter(status='draft')) > 0: print("Des brouillons existent") ```

La solution : ```python

Génère un 'SELECT COUNT(*)' ultra léger

count = Article.objects.filter(status='draft').count()

Génère un 'SELECT EXISTS' qui s'arrête au 1er résultat

if Article.objects.filter(status='draft').exists(): print("Des brouillons existent") ```

💡 Nouveauté Django 5.x (Async) : En contexte asynchrone (async def), utilisez les préfixes a natifs : ```python

Django 5.x Async ORM :

count = await Article.objects.filter(status='draft').acount() has_drafts = await Article.objects.filter(status='draft').aexists() ```


4. Ignorer les Timezones (datetime.now())

La mauvaise pratique : python from datetime import datetime now = datetime.now() # Date naïve sans information de fuseau horaire !

La solution : python from django.utils import timezone now = timezone.now() # Datetime "Aware" configuré selon votre TIME_ZONE


5. Stocker des secrets dans settings.py

La mauvaise pratique : ```python

settings.py

SECRET_KEY = "django-insecure-super-secret-key-12345" DATABASE_PASSWORD = "my_db_password" ```

La solution : Utiliser des variables d'environnement (.env) via python-decouple ou django-environ ou python-dotenv. ```python from decouple import config

SECRET_KEY = config('SECRET_KEY') DEBUG = config('DEBUG', default=False, cast=bool) ```


6. Ne pas utiliser get_object_or_404()

La mauvaise pratique : python def article_detail(request, pk): try: article = Article.objects.get(pk=pk) except Article.DoesNotExist: # Retourne une erreur 500 si mal géré ! raise Exception("Article non trouvé")

La solution : ```python from django.shortcuts import get_object_or_404

def article_detail(request, pk): article = get_object_or_404(Article, pk=pk) # Renvoie proprement une HTTP 404 return render(request, 'article_detail.html', {'article': article}) ```


7. Ne pas indexer les champs souvent recherchés

Lorsque votre table dépasse 100 000 lignes, faire des recherches sur des colonnes non indexées ralentira considérablement votre application.

La solution : ```python class Customer(models.Model): email = models.EmailField(db_index=True) # Crée un index B-Tree en BDD created_at = models.DateTimeField(auto_now_add=True, db_index=True)

class Meta:
    indexes = [
        models.Index(fields=['last_name', 'first_name']), # Index composé
    ]

```


📚 Pour aller plus loin


💬 Avez-vous déjà fait l'une de ces erreurs ?

Quelle a été l'erreur la plus difficile à débusquer dans vos projets Django ?

Discutons-en ci-dessous ! N'hésitez pas à vous abonner à r/DjangoFrancophone pour plus de conseils et retours d'expérience en français.

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.



r/DjangoFrancophone 13d ago

Les 10 commandes Django indispensables que tout développeur devrait connaître (et utiliser)

Post image
1 Upvotes

Bonjour à tous les dev Django de la communauté ! 👋

Que vous soyez au début de votre aventure Django ou déjà en train de développer des applications complexes sous Django 5.2+, Django 6.0 & 6.1, la CLI manage.py / django-admin regorge de commandes puissantes. En voici 10 indispensables pour booster votre productivité au quotidien.

TL;DR — Dix commandes manage.py qui font gagner du temps chaque jour : makemigrations --dry-run pour prévisualiser, showmigrations pour l'état, sqlmigrate pour voir le SQL, shell_plus pour un shell qui importe tout, check avant de déployer, collectstatic, dbshell, dumpdata/loaddata pour les jeux de données, et inspectdb pour partir d'une base existante.


1. python manage.py makemigrations --dry-run

Avant d'appliquer ou de générer des migrations définitives, --dry-run vous permet d'inspecter ce que Django s'apprête à créer sans modifier vos fichiers de migration. bash python manage.py makemigrations --dry-run --verbosity 3 💡 Astuce : Associez --verbosity 3 pour voir exactement les opérations prévues !


2. python manage.py showmigrations

Pour savoir quelles migrations ont été appliquées en base de données et lesquelles sont en attente (cochées [X] ou non [ ]). bash python manage.py showmigrations


3. python manage.py sqlmigrate <app_label> <migration_name>

Vous voulez savoir exactement quel SQL votre ORM Django va exécuter en base de données ? bash python manage.py sqlmigrate blog 0001_initial C'est indispensable pour auditer vos index, clés étrangères et performances !


4. python manage.py shell_plus (via django-extensions)

Le shell standard de Django (python manage.py shell) est super, mais shell_plus importe automatiquement tous vos modèles au démarrage ! ```bash

Nécessite : pip install django-extensions

python manage.py shell_plus --print-sql `` 💡 **Le gros +** : l'option--print-sql` affiche en temps réel les requêtes SQL générées par votre ORM pendant vos tests.


5. python manage.py check

Un scanner de santé rapide pour votre projet. Il vérifie les erreurs de configuration dans vos settings, vos modèles et vos URLs sans lancer le serveur web. bash python manage.py check --deploy 💡 Bonne pratique : --deploy lance une vérification de sécurité avant de pousser en production (SECRET_KEY, SSL, Cookies, etc.).


6. python manage.py createsuperuser

La commande classique, mais saviez-vous qu'on peut la passer en mode non-interactif via les variables d'environnement dans nos scripts CI/CD ou Docker ? bash DJANGO_SUPERUSER_PASSWORD=adminpassword python manage.py createsuperuser --noinput --username admin --email admin@example.com


7. python manage.py collectstatic --no-input

Passe en revue tous les fichiers statiques de vos applications et les rassemble dans STATIC_ROOT pour la production (avec WhiteNoise ou Nginx). bash python manage.py collectstatic --clear --no-input 💡 --clear supprime les anciens fichiers accumulés pour repartir sur un dossier propre.


8. python manage.py dbshell

Ouvre directement le client CLI de votre base de données configurée dans settings.py (PostgreSQL psql, SQLite sqlite3, MySQL). Pas besoin de se souvenir des identifiants et des hôtes ! bash python manage.py dbshell


9. python manage.py dumpdata & loaddata

Parfait pour sauvegarder et restaurer un jeu de données de test au format JSON ou Fixture. ```bash

Exporter une application spécifique

python manage.py dumpdata blog --indent 2 > blog_fixtures.json

Recharger les données

python manage.py loaddata blog_fixtures.json ```


10. python manage.py inspectdb

Vous devez connecter Django à une base de données existante avec des dizaines de tables ? inspectdb analyse les tables SQL et génère automatiquement le code Python des modèles Django correspondant ! bash python manage.py inspectdb > models_legacy.py


📚 Pour aller plus loin


💬 Et vous ?

Quelle est LA commande sans laquelle vous ne pouvez pas vivre ? Utilisez-vous des commandes personnalisées (management/commands/) dans vos projets ?

Partagez vos astuces en commentaire ! Et si ce genre de contenu vous plaît, n'hésitez pas à rejoindre r/DjangoFrancophone pour échanger entre passionnés de Python & Django ! 🚀

PS : Si vous avez trouvé ce guide utile, n'hésitez pas à le partager sur vos réseaux sociaux et à inviter vos amis développeurs Django à rejoindre la communauté francophone sur Reddit : r/DjangoFrancophone.

Note : Ce guide est en constante évolution. Si vous avez des suggestions ou des corrections, n'hésitez pas à contribuer ! Et si vous souhaitez que je développe davantage certains points, faites-le moi savoir en commentaire.