Python Django Fullstack expliqué simplement (avec schémas et vrai code)

Python Django Fullstack : l'essentiel en un article — vrai code, schémas et étapes concrètes, extraits d'un cours de 24 leçons.

Python Django Fullstack expliqué simplement (avec schémas et vrai code)

Un guide qui va droit au but : Python Django Fullstack décortiqué avec des schémas, des exemples concrets et des commandes testées. Tout vient d'un cours structuré de 8 chapitres — en voici le meilleur.

tl;dr
  • Pourquoi Django
  • Models et migrations
  • Views et URLs
  • Templates et forms
  • Admin
~$ cat ./parcours.md # Python Django Fullstack — 8 chapitres
01
Pourquoi Django
→ Django vs Flask vs FastAPI→ Installation et premier projet+ 1 autres leçons
02
Models et migrations
→ Models et fields→ Relations+ 1 autres leçons
03
Views et URLs
→ Function-based views→ Class-based views (CBV)+ 1 autres leçons
04
Templates et forms
→ Template language→ Forms et ModelForms+ 1 autres leçons
05
Admin
→ Customizer ModelAdmin→ Actions et filters custom+ 1 autres leçons
06
Authentification
→ User model et auth views→ Custom User et Profile+ 1 autres leçons
07
Déploiement
→ Settings prod/dev→ Gunicorn + Nginx+ 1 autres leçons
08
Projet final blog ecom
→ Chapitre 07 – Projet final : Modeles et admin→ Chapitre 07 – Projet final : Views et templates+ 1 autres leçons
🏁
Projet final
→ Tu repars avec un projet concret et démontrable

Migrations

Cycle de base

output
# 1. Modifier un model
class Article(models.Model):
    title = models.CharField(max_length=200)
    summary = models.TextField(blank=True)         # NOUVEAU

# 2. Generer la migration
python manage.py makemigrations
# Migrations for 'blog':
#   blog/migrations/0002_article_summary.py

# 3. Appliquer
python manage.py migrate

# 4. Verifier
python manage.py showmigrations

Voir le SQL genere

output
python manage.py sqlmigrate blog 0002

# Output :
# BEGIN;
# ALTER TABLE blog_article ADD COLUMN summary TEXT NOT NULL DEFAULT '';
# COMMIT;

Reverter une migration

output
python manage.py migrate blog 0001
# Revert vers 0001 (annule 0002)

python manage.py migrate blog zero
# Tout annuler pour l'app blog

Migration de donnees

output
python manage.py makemigrations --empty --name backfill_slugs blog

# Editer le fichier genere :
from django.db import migrations
from django.utils.text import slugify

def backfill_slugs(apps, schema_editor):
    Article = apps.get_model("blog", "Article")
    for a in Article.objects.all():
        if not a.slug:
            a.slug = slugify(a.title)
            a.save()

def reverse_func(apps, schema_editor):
    pass

class Migration(migrations.Migration):
    dependencies = [("blog", "0001_initial")]
    operations = [migrations.RunPython(backfill_slugs, reverse_func)]

Squashing migrations

output
# Apres beaucoup de migrations, les compacter :
python manage.py squashmigrations blog 0001 0020

# Cree 0001_squashed_0020 qui remplace les 20 fichiers
# Les nouveaux developpeurs evitent de jouer 20 migrations

Migrations sans downtime

NOTEPour ajouter une colonne NOT NULL sans casser la prod :
  1. Deploy 1 : Ajouter colonne nullable
  2. Deploy 2 : Backfill via migration data
  3. Deploy 3 : Modifier le code pour utiliser/setter
  4. Deploy 4 : Migration ALTER NOT NULL

Renommer un model

output
# Si tu renommes Article -> Post, Django va proposer
# de creer un nouveau model + supprimer l'ancien (perd les donnees !)

# Solution : editer la migration generee
operations = [
    migrations.RenameModel("Article", "Post")
]

# Idem pour les champs
operations = [
    migrations.RenameField("Post", "summary", "excerpt")
]

Django vs Flask vs FastAPI

NOTEObjectif — Comprendre la philosophie Django et quand le choisir.

Comparaison rapide

CritereDjangoFlaskFastAPI
PhilosophieBatteries includedMicro frameworkAPI first, async
ORMIntegreSQLAlchemy externeSQLAlchemy externe
AdminAuto-genereA coderA coder
AuthCompleteExtensionsA coder JWT
TemplatesDjango templatesJinja2Jinja2 optionnel
Async natifPartiel (4.x)NonOui
ApprentissagePlus longTres simpleMoyen

Quand choisir Django

Quand NE PAS choisir Django

Architecture MTV

output
+----------+   request    +------+   query   +-------+
|  Browser | -----------> | View | --------> | Model |
+----------+              +------+           +-------+
     ^                       |                   |
     |                       v                   v
     |                  +----------+         +-------+
     +----------------- | Template |<--------|  DB   |
                        +----------+         +-------+

# M = Model (ORM, base de donnees)
# T = Template (HTML)
# V = View (logique controle, en MVC c'est le Controller)

Ecosysteme Django

Versions et LTS

NOTEDjango releases :
  • Django 5.0+ : actuelle
  • Django 4.2 LTS : supportee jusqu'a 2026
  • Cycle : nouvelle version tous les 8 mois
  • LTS tous les 2 ans, supportees 3 ans

Companies qui utilisent Django

Resume

NOTEA retenir
  • Django = batteries included pour apps web complexes
  • Architecture MTV (Model, Template, View)
  • Ideal : SaaS, CMS, e-commerce, dashboards
  • API pure : prefere DRF ou FastAPI

Chapitre 07 – Projet final : Modeles et admin

Cahier des charges

NOTEObjectif — Construire une boutique en ligne minimaliste avec : catalogue, panier, commandes, admin metier.

Structure projet

output
myshop/
  manage.py
  myshop/
    settings/
    urls.py
    wsgi.py
  accounts/
    models.py            # User custom
    views.py
    urls.py
  catalog/
    models.py            # Category, Product
    views.py
    admin.py
  cart/
    cart.py              # classe Cart (session)
    views.py
  orders/
    models.py            # Order, OrderItem
    views.py
    admin.py
  templates/
  static/
  media/

accounts/models.py

output
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    phone = models.CharField(max_length=20, blank=True)
    address = models.TextField(blank=True)
    city = models.CharField(max_length=100, blank=True)
    postal_code = models.CharField(max_length=10, blank=True)
    country = models.CharField(max_length=2, default="CA")
    
    class Meta:
        ordering = ["-date_joined"]
    
    def __str__(self):
        return self.username

catalog/models.py

output
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=120, unique=True)
    description = models.TextField(blank=True)
    parent = models.ForeignKey("self", null=True, blank=True,
                                on_delete=models.CASCADE, related_name="children")
    
    class Meta:
        verbose_name_plural = "Categories"
        ordering = ["name"]
    
    def __str__(self): return self.name
    
    def get_absolute_url(self):
        return reverse("catalog:category", kwargs={"slug": self.slug})

class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products")
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    image = models.ImageField(upload_to="products/%Y/%m/")
    stock = models.PositiveIntegerField(default=0)
    available = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["slug"]),
            models.Index(fields=["available", "-created_at"]),
        ]
    
    def __str__(self): return self.name
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)
    
    def get_absolute_url(self):
        return reverse("catalog:product", kwargs={"slug": self.slug})
    
    @property
    def in_stock(self):
        return self.available and self.stock > 0

orders/models.py

output
class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "En attente"
        PAID = "paid", "Paye"
        SHIPPED = "shipped", "Expedie"
        DELIVERED = "delivered", "Livre"
        CANCELED = "canceled", "Annule"
    
    user = models.ForeignKey("accounts.User", on_delete=models.PROTECT, related_name="orders")
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    address = models.TextField()
    city = models.CharField(max_length=100)
    postal_code = models.CharField(max_length=10)
    country = models.CharField(max_length=2)
    total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    paid_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ["-created_at"]
    
    def __str__(self): return f"Order #{self.id}"

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
    product = models.ForeignKey("catalog.Product", on_delete=models.PROTECT)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)
    
    @property
    def subtotal(self): return self.price * self.quantity

catalog/admin.py

output
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ["name", "parent", "product_count"]
    prepopulated_fields = {"slug": ("name",)}
    search_fields = ["name"]
    
    def product_count(self, obj):
        return obj.products.count()

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ["name", "category", "price", "stock", "available"]
    list_filter = ["available", "category", "created_at"]
    list_editable = ["price", "stock", "available"]
    search_fields = ["name", "description"]
    prepopulated_fields = {"slug": ("name",)}
    list_per_page = 50

orders/admin.py

output
class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    readonly_fields = ["product", "price", "quantity", "subtotal"]
    can_delete = False

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["id", "user", "status", "total", "created_at", "paid_at"]
    list_filter = ["status", "created_at"]
    search_fields = ["id", "user__username", "user__email"]
    inlines = [OrderItemInline]
    actions = ["mark_shipped"]
    date_hierarchy = "created_at"
    
    @admin.action(description="Marquer comme expedie")
    def mark_shipped(self, request, queryset):
        queryset.update(status=Order.Status.SHIPPED)
va-plus-loin

Cet article couvre les extraits les plus utiles — le cours complet Python Django Fullstack (8 chapitres, 24 leçons, exercices corrigés et projet final) t'emmène jusqu'au bout.

./acceder-au-cours-complet cours gratuit : Vibe Coding

FAQ

Combien de temps pour apprendre Python Django Fullstack ?
Avec une progression structurée (8 chapitres, 24 leçons courtes et pratiques), on atteint un niveau opérationnel en quelques semaines à raison de 30 à 60 minutes par jour. L'important est de pratiquer chaque notion immédiatement.
Faut-il des prérequis ?
Des bases en informatique suffisent. Si tu sais utiliser un terminal et lire du code simple, tu es prêt.
Par où commencer concrètement ?
Reproduis les commandes de cet article, puis suis le cours complet Python Django Fullstack : il enchaîne les 24 leçons dans l'ordre, avec exercices et projet final.

📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.