# ═══════════════════════════════════════════════════════════════════
# DJANGO ULTRA-DÉTAILLÉ POUR GRANDS DÉBUTANTS
# Guide Complet avec Méthodologie POURQUOI / QUAND / COMMENT
# ═══════════════════════════════════════════════════════════════════

# Ce guide explore TOUTES les fonctionnalités de Django de manière exhaustive
# Chaque concept est expliqué avec:
# - POURQUOI: La raison d'être de cette fonctionnalité
# - QUAND: Les cas d'usage appropriés
# - COMMENT: L'implémentation pratique avec exemples
# - Analogies pour faciliter la compréhension
# - Erreurs courantes et comment les éviter


# ═══════════════════════════════════════════════════════════════════
# PARTIE 1: INTRODUCTION APPROFONDIE À DJANGO
# ═══════════════════════════════════════════════════════════════════


# ═══ 1.1 QU'EST-CE QUE DJANGO? ═══

# POURQUOI Django existe-t-il?
# ═════════════════════════════════

# Django a été créé en 2005 par Adrian Holovaty et Simon Willison, développeurs
# pour le journal Lawrence Journal-World. Ils devaient créer rapidement des
# sites web d'actualités avec des fonctionnalités communes:
# - Système de publication d'articles
# - Gestion des utilisateurs et permissions
# - Interface d'administration
# - Formulaires de contact et commentaires
# - Gestion d'images et médias

# Au lieu de recoder ces fonctionnalités pour chaque projet, ils ont créé
# un framework réutilisable: Django est né!

# Le nom "Django" vient du guitariste de jazz Django Reinhardt, reflétant
# la philosophie du framework: élégance, performance et créativité.


# POURQUOI choisir Django plutôt qu'un autre framework?
# ══════════════════════════════════════════════════════

# 1. BATTERIES INCLUDED (Tout est inclus)
#    Django fournit TOUT ce dont vous avez besoin dès l'installation:
#    
#    Sans Django, pour créer un site, vous devez:
#    [X] Coder système d'authentification (login/logout/permissions)
#    [X] Créer interface d'administration (CRUD)
#    [X] Gérer uploads de fichiers
#    [X] Protéger contre failles de sécurité (SQL injection, XSS, CSRF)
#    [X] Mettre en place système de formulaires avec validation
#    [X] Configurer ORM pour base de données
#    [X] Créer système de templates
#    [X] Gérer sessions utilisateur
#    -> Des CENTAINES d'heures de développement!
#    
#    Avec Django:
#    [OK] Tout est déjà codé, testé et sécurisé
#    [OK] Prêt à l'emploi en quelques minutes
#    [OK] Économie de MOIS de développement

# 2. SÉCURITÉ PAR DÉFAUT
#    Django vous protège automatiquement contre:
#    - SQL Injection (requêtes malveillantes base de données)
#    - Cross-Site Scripting (XSS) - injection de code JavaScript
#    - Cross-Site Request Forgery (CSRF) - actions non autorisées
#    - Clickjacking - piégeage de clics
#    - Et bien d'autres...
#    
#    Vous n'avez RIEN à faire, c'est automatique!

# 3. ÉVOLUTIVITÉ (Scalability)
#    Django est utilisé par des sites avec des MILLIONS d'utilisateurs:
#    - Instagram (400+ millions d'utilisateurs)
#    - Spotify (streaming pour millions d'utilisateurs simultanés)
#    - Pinterest (300+ millions d'utilisateurs mensuels)
#    - YouTube (au début)
#    - NASA (oui, l'agence spatiale!)
#    
#    Si c'est assez bon pour eux, c'est assez bon pour votre projet!

# 4. DOCUMENTATION EXCEPTIONNELLE
#    Django a la MEILLEURE documentation du monde Python:
#    - Tutoriel complet pour débutants
#    - Documentation technique exhaustive
#    - Guides de bonnes pratiques
#    - Exemples concrets
#    - Traduite en plusieurs langues
#    
#    Vous trouverez TOUJOURS une réponse à vos questions!

# 5. COMMUNAUTÉ GIGANTESQUE
#    - 70 000+ sites Django dans le monde
#    - 10 000+ packages réutilisables (djangopackages.org)
#    - Forums actifs, Discord, Reddit
#    - Conférences DjangoCon dans le monde entier
#    - Stack Overflow: 300 000+ questions/réponses
#    
#    Vous n'êtes JAMAIS seul!

# 6. PHILOSOPHIE DRY (Don't Repeat Yourself)
#    Django évite la duplication de code:
#    - Définissez votre structure de données UNE FOIS (models)
#    - Django génère automatiquement:
#      -> Tables SQL
#      -> Interface admin
#      -> Formulaires
#      -> API (avec Django REST Framework)
#    
#    Un seul code -> Multiple usages!

# 7. PYTHON PUR
#    Django est écrit en Python (pas en PHP, Ruby, etc.)
#    - Langage clair et lisible
#    - Écosystème riche (data science, ML, automation)
#    - Facile à apprendre
#    - Très demandé sur le marché du travail


# QUAND utiliser Django?
# ═══════════════════════

# [OK] UTILISEZ Django pour:
#   1. Sites web avec base de données (blogs, e-commerce, réseaux sociaux)
#   2. Plateformes de contenu (CMS, portails d'actualités)
#   3. APIs REST (backend pour applications mobiles/frontend JS)
#   4. Outils internes d'entreprise (dashboards, CRM, gestion)
#   5. SaaS (Software as a Service)
#   6. Sites nécessitant authentification et permissions
#   7. Applications nécessitant admin pour gérer données
#   8. Projets où la sécurité est critique
#   9. MVPs (Minimum Viable Product) - prototypes rapides
#   10. Projets devant évoluer (scalabilité)

# [X] N'utilisez PAS Django pour:
#   1. Sites statiques simples (HTML/CSS uniquement) -> Utilisez Jekyll, Hugo
#   2. Applications temps réel intensif (chat, jeux) -> Utilisez Node.js, WebSockets
#   3. Microservices ultra-légers -> Utilisez FastAPI, Flask
#   4. Applications hors-ligne (desktop) -> Utilisez PyQt, Tkinter
#   5. Scripts simples -> Python pur suffit


# ANALOGIE: Django vs construire une maison
# ══════════════════════════════════════════

# Sans Django (construire from scratch):
# - Vous devez fabriquer les briques
# - Couler les fondations
# - Monter les murs brique par brique
# - Installer plomberie et électricité
# - Créer les fenêtres
# - Construire le toit
# -> Des MOIS de travail!

# Avec Django (maison préfabriquée):
# - Fondations déjà coulées (ORM)
# - Murs déjà montés (Views/Templates)
# - Plomberie/électricité installée (Auth/Admin)
# - Fenêtres posées (Forms)
# - Toit en place (Sécurité)
# -> Vous décorez l'intérieur et emménagez!
# -> Quelques JOURS de travail!


# ═══ 1.2 ARCHITECTURE MTV (Model-Template-View) ═══

# POURQUOI Django utilise MTV et pas MVC?
# ════════════════════════════════════════

# MVC (Model-View-Controller) est un pattern classique:
# - Model: Données
# - View: Interface utilisateur (ce que l'utilisateur voit)
# - Controller: Logique métier (le cerveau)

# Django utilise MTV (Model-Template-View):
# - Model: Données (comme MVC)
# - Template: Interface utilisateur (équivalent View de MVC)
# - View: Logique métier (équivalent Controller de MVC)

# Pourquoi ce changement de noms?
# Les créateurs de Django trouvaient que "View" devrait représenter
# la logique (ce que le code "voit" des données) et non l'affichage.
# C'est juste une question de terminologie!

# Correspondance:
# MTV (Django)  <-->  MVC (Autres)
# Model              Model
# Template           View
# View               Controller


# COMMENT fonctionne MTV?
# ═══════════════════════

# Flux complet d'une requête Django:

# 1. UTILISATEUR fait une action
#    Exemple: Tape URL www.monblog.com/articles/
#    ou clique sur bouton "Se connecter"

# 2. URLS.PY (Routeur) attrape la requête
#    Django cherche dans urls.py:
#    path('articles/', views.article_list)
#    -> "Ah! /articles/ correspond à la fonction article_list()"

# 3. VIEW (Cerveau) s'exécute
#    La fonction article_list() dans views.py:
#    - Reçoit la requête HTTP
#    - Va chercher données dans la BDD (via Models)
#    - Prépare les données
#    - Choisit quel template utiliser
#    - Envoie les données au template

# 4. MODEL (Données) interrogé
#    Si la view a besoin de données:
#    articles = Article.objects.all()
#    -> Django va en BDD chercher tous les articles
#    -> Retourne des objets Python (pas du SQL brut!)

# 5. TEMPLATE (Affichage) génère HTML
#    Le template article_list.html:
#    - Reçoit les données (articles)
#    - Génère HTML avec ces données:
#      {% for article in articles %}
#        <h2>{{ article.titre }}</h2>
#      {% endfor %}

# 6. VIEW retourne la réponse
#    Le HTML généré est envoyé au navigateur

# 7. UTILISATEUR voit la page
#    Son navigateur affiche le HTML reçu


# ANALOGIE: MTV = Restaurant
# ═══════════════════════════

# MODEL = Cuisine et Stock
#   - Base de données = Frigo et stocks
#   - Chaque table = Type d'ingrédient (viandes, légumes, etc.)
#   - ORM = Système de gestion des stocks (vous demandez "2 steaks",
#           pas besoin de savoir où exactement dans le frigo)

# VIEW = Serveur
#   - Reçoit commande client (requête HTTP)
#   - Va en cuisine chercher ingrédients (interroge Models)
#   - Prépare l'assiette (organise les données)
#   - Choisit quelle assiette utiliser (template)
#   - Apporte au client (retourne réponse HTTP)

# TEMPLATE = Assiette et Présentation
#   - Disposition des aliments sur l'assiette
#   - Décoration (CSS)
#   - Présentation finale au client (HTML)

# URLS.PY = Maître d'hôtel
#   - Dirige clients vers bonnes tables (routes)
#   - "Table 5 pour commande de pizza" = "URL /pizza/ vers view pizza_list()"


# EXEMPLE CONCRET: Blog simple
# ══════════════════════════════

# Imaginons un blog avec articles.

# 1. MODEL (blog/models.py)
# ─────────────────────────
class Article(models.Model):
    titre = models.CharField(max_length=200)
    contenu = models.TextField()
    date_publication = models.DateTimeField(auto_now_add=True)
    auteur = models.ForeignKey(User, on_delete=models.CASCADE)

# Traduction SQL automatique par Django:
# CREATE TABLE blog_article (
#     id INTEGER PRIMARY KEY,
#     titre VARCHAR(200),
#     contenu TEXT,
#     date_publication DATETIME,
#     auteur_id INTEGER FOREIGN KEY
# );
# Vous n'écrivez JAMAIS ce SQL!

# 2. VIEW (blog/views.py)
# ───────────────────────
def article_list(request):
    # Récupérer tous les articles (Django fait SELECT * FROM blog_article)
    articles = Article.objects.all()
    
    # Préparer données pour template
    context = {
        'articles': articles,
        'titre_page': 'Mon Blog'
    }
    
    # Rendre template avec données
    return render(request, 'blog/article_list.html', context)

# 3. TEMPLATE (blog/templates/blog/article_list.html)
# ────────────────────────────────────────────────────
"""
<h1>{{ titre_page }}</h1>

{% for article in articles %}
    <div class="article">
        <h2>{{ article.titre }}</h2>
        <p>{{ article.contenu }}</p>
        <small>Par {{ article.auteur.username }} le {{ article.date_publication|date:"d/m/Y" }}</small>
    </div>
{% endfor %}
"""

# 4. URL (blog/urls.py)
# ─────────────────────
urlpatterns = [
    path('articles/', views.article_list, name='article_list'),
]

# Quand l'utilisateur va sur www.monblog.com/articles/
# -> Django appelle views.article_list()
# -> View récupère données (Article.objects.all())
# -> Template génère HTML
# -> Utilisateur voit la liste des articles!


# POURQUOI séparer en MTV?
# ═════════════════════════

# 1. MAINTENABILITÉ
#    Code organisé par responsabilité:
#    - Problème de données? -> Regardez Models
#    - Problème d'affichage? -> Regardez Templates
#    - Problème de logique? -> Regardez Views

# 2. RÉUTILISABILITÉ
#    - Même Model utilisé par plusieurs Views
#    - Même Template utilisé par plusieurs Views
#    - Même View peut retourner différents formats (HTML, JSON, XML)

# 3. TRAVAIL EN ÉQUIPE
#    - Designer travaille sur Templates (HTML/CSS)
#    - Backend dev travaille sur Models et Views (Python)
#    - Pas de conflit!

# 4. TESTABILITÉ
#    - Models testés indépendamment
#    - Views testées indépendamment
#    - Templates testés indépendamment


# ═══ 1.3 DJANGO VS AUTRES FRAMEWORKS ═══

# Comparaison détaillée des frameworks Python

# DJANGO (Batteries Included - Framework Complet)
# ════════════════════════════════════════════════

# POUR:
# [OK] Tout inclus (Auth, Admin, ORM, Forms, etc.)
# [OK] Sécurité par défaut
# [OK] Structure imposée (bonnes pratiques)
# [OK] Documentation exceptionnelle
# [OK] Grande communauté
# [OK] Scalable (gros projets)
# [OK] Admin gratuit et puissant

# CONTRE:
# [X] Courbe d'apprentissage (beaucoup à apprendre)
# [X] "Lourd" pour petits projets
# [X] Moins flexible (structure imposée)
# [X] Pas natif async (amélioration en cours)

# QUAND utiliser Django:
# - Projets complexes avec BDD
# - Sites nécessitant admin
# - Équipe qui préfère structure claire
# - Projets à long terme
# - Sécurité critique

# Exemples de sites Django:
# Instagram, Spotify, Pinterest, NASA


# FLASK (Micro-Framework - Minimaliste)
# ══════════════════════════════════════

# POUR:
# [OK] Très simple à apprendre
# [OK] Léger et rapide (démarrage)
# [OK] Très flexible
# [OK] Parfait pour microservices
# [OK] Choix des composants

# CONTRE:
# [X] Pas d'admin intégré
# [X] Pas d'ORM par défaut (SQLAlchemy séparé)
# [X] Pas d'auth par défaut
# [X] Structure libre (peut devenir le chaos)
# [X] Sécurité manuelle

# QUAND utiliser Flask:
# - APIs simples
# - Microservices
# - Prototypes rapides
# - Liberté architecturale
# - Petits projets

# Exemples de sites Flask:
# LinkedIn (certaines parties), Netflix (tooling)


# FASTAPI (Moderne - Async & APIs)
# ═════════════════════════════════

# POUR:
# [OK] Très rapide (async natif)
# [OK] Documentation auto (Swagger)
# [OK] Type hints (validation auto)
# [OK] Moderne (Python 3.7+)
# [OK] Parfait pour APIs

# CONTRE:
# [X] Pas d'admin
# [X] Pas de templates intégrés
# [X] Jeune (moins de ressources)
# [X] Focalisé APIs (pas sites web complets)

# QUAND utiliser FastAPI:
# - APIs REST modernes
# - Backends pour SPA (React, Vue)
# - Microservices performants
# - WebSockets

# Exemples: Uber, Microsoft (services internes)


# Comparaison tableau:
# ═══════════════════════

"""
Fonctionnalité      Django    Flask     FastAPI
─────────────────   ────────  ────────  ────────
Admin intégré       [OK][OK][OK]       [X]         [X]
ORM intégré         [OK][OK][OK]       [X]         [X]
Auth intégré        [OK][OK][OK]       [X]         [X]
Forms intégré       [OK][OK][OK]       [X]         [X]
Templates           [OK][OK][OK]       [OK][OK]        [X]
Sécurité auto       [OK][OK][OK]       [OK]         [OK][OK]
Documentation       [OK][OK][OK]       [OK][OK]        [OK][OK][OK]
Courbe appr.        Moyenne   Facile    Moyenne
Performance         [OK][OK]        [OK][OK]        [OK][OK][OK]
Async               [OK]         [X]         [OK][OK][OK]
Flexibilité         [OK]         [OK][OK][OK]       [OK][OK]
Sites complets      [OK][OK][OK]       [OK][OK]        [X]
APIs REST           [OK][OK]        [OK][OK]        [OK][OK][OK]
"""


# DÉCISION: Quel framework choisir?
# ══════════════════════════════════

# Choisissez DJANGO si:
# -> Vous créez un site web complet avec BDD
# -> Vous voulez un admin pour gérer contenu
# -> Vous préférez "tout inclus" plutôt que "à la carte"
# -> Vous voulez sécurité par défaut
# -> Votre projet va grandir
# -> Vous êtes débutant (structure claire)

# Choisissez FLASK si:
# -> Vous créez une API simple
# -> Vous voulez liberté architecturale
# -> Vous connaissez déjà bien Python web
# -> Vous créez un microservice
# -> Vous voulez quelque chose de léger

# Choisissez FASTAPI si:
# -> Vous créez une API REST moderne
# -> Performance critique (async)
# -> Vous créez backend pour frontend JS
# -> Vous aimez type hints Python
# -> Vous voulez documentation auto

# Si hésitation: COMMENCEZ PAR DJANGO
# Raison: Vous apprendrez concepts fondamentaux
# qui s'appliquent partout. Vous pourrez toujours
# passer à Flask/FastAPI après!


# ═══════════════════════════════════════════════════════════════════
# PARTIE 2: INSTALLATION ET CONFIGURATION DÉTAILLÉES
# ═══════════════════════════════════════════════════════════════════


# ═══ 2.1 PRÉREQUIS: COMPRENDRE PYTHON ET L'ENVIRONNEMENT ═══

# POURQUOI Python est-il nécessaire?
# ═══════════════════════════════════

# Django est un framework Python, donc vous avez BESOIN de Python.
# C'est comme avoir besoin d'un moteur pour conduire une voiture:
# Django = Carrosserie élégante
# Python = Moteur qui fait tourner le tout

# Versions Python supportées par Django:
# - Django 5.0+: Python 3.10, 3.11, 3.12
# - Django 4.2 LTS: Python 3.8, 3.9, 3.10, 3.11, 3.12
# - Django 4.1: Python 3.8+
# - Django 3.2 LTS: Python 3.6, 3.7, 3.8, 3.9, 3.10


# COMMENT vérifier si Python est installé?
# ═════════════════════════════════════════

# Ouvrir terminal/invite de commandes:
# - Windows: Win+R, taper "cmd", Entrée
# - Mac: Cmd+Espace, taper "terminal", Entrée
# - Linux: Ctrl+Alt+T

# Taper:
python --version
# ou
python3 --version

# Résultat attendu:
# Python 3.11.5 (ou version 3.10+)

# Si erreur "commande non trouvée":
# -> Python n'est PAS installé
# -> Installez Python d'abord!


# COMMENT installer Python?
# ══════════════════════════

# WINDOWS:
# --------
# 1. Aller sur python.org/downloads
# 2. Télécharger "Python 3.12.x" (dernière version)
# 3. Lancer l'installeur
# 4. [ATTENTION] IMPORTANT: Cocher "Add Python to PATH"
#    (Case en bas de la première fenêtre)
# 5. Cliquer "Install Now"
# 6. Attendre fin installation
# 7. Vérifier: python --version dans cmd

# MAC:
# ----
# Méthode 1 (Homebrew - recommandée):
# 1. Installer Homebrew (si pas déjà fait):
#    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. Installer Python:
#    brew install python3
# 3. Vérifier: python3 --version

# Méthode 2 (python.org):
# 1. Télécharger installeur sur python.org
# 2. Double-cliquer fichier .pkg
# 3. Suivre assistant installation

# LINUX (Ubuntu/Debian):
# ----------------------
sudo apt update
sudo apt install python3 python3-pip python3-venv

# Fedora:
sudo dnf install python3 python3-pip

# Vérifier:
python3 --version
pip3 --version


# ═══ 2.2 ENVIRONNEMENTS VIRTUELS: LE CONCEPT ESSENTIEL ═══

# POURQUOI les environnements virtuels sont OBLIGATOIRES?
# ════════════════════════════════════════════════════════

# Problème sans environnement virtuel:
# ────────────────────────────────────

# Vous avez 2 projets Django:
# Projet A: Django 3.2 (vieux projet, en production)
# Projet B: Django 5.0 (nouveau projet)

# Si vous installez tout globalement sur votre ordinateur:
pip install django  # Installe Django 5.0

# Problème:
# [X] Projet A casse (il a besoin de Django 3.2!)
# [X] Impossible d'avoir 2 versions Django en même temps
# [X] Packages s'écrasent mutuellement
# [X] Conflits de dépendances
# [X] Cauchemar de maintenance!

# C'est comme avoir UN SEUL garage pour TOUTES vos voitures:
# - Vous avez des pièces de Ferrari mélangées avec des pièces de Renault
# - Quand vous réparez une voiture, vous cassez l'autre
# - Impossible de savoir quelle pièce va avec quelle voiture!


# Solution: Environnement virtuel
# ════════════════════════════════

# Un environnement virtuel = UN garage par voiture
# Chaque projet a sa propre copie de Python et ses packages

# Projet A:
# /projet_a/.venv/
#   - Python 3.9
#   - Django 3.2
#   - Pillow 8.0
#   - etc.

# Projet B:
# /projet_b/.venv/
#   - Python 3.11
#   - Django 5.0
#   - Pillow 10.0
#   - etc.

# Avantages:
# [OK] Projets totalement indépendants
# [OK] Pas de conflit entre versions
# [OK] Facile à déployer (liste précise des packages)
# [OK] Facile à supprimer (effacer dossier .venv)
# [OK] Ne pollue pas Python système


# ANALOGIE: Bacs à sable
# ═══════════════════════

# Imaginez un parc avec plusieurs bacs à sable:
# - Bac A: Enfants jouent avec Lego
# - Bac B: Enfants jouent avec Playmobil
# - Bac C: Enfants jouent avec voitures

# Sans séparation:
# [X] Tous les jouets mélangés
# [X] Pièces Lego dans le château Playmobil
# [X] Roues de voiture sur bonhomme Lego
# [X] Le CHAOS!

# Avec séparation (bacs différents):
# [OK] Chaque jeu dans son bac
# [OK] Pas de mélange
# [OK] Facile de ranger
# [OK] Facile de nettoyer un bac sans affecter les autres

# Environnements virtuels = Bacs à sable pour projets!


# COMMENT créer un environnement virtuel?
# ════════════════════════════════════════

# Étape 1: Créer dossier projet
# ──────────────────────────────

# Windows:
mkdir C:\MesProjets\mon_blog
cd C:\MesProjets\mon_blog

# Linux/Mac:
mkdir -p ~/MesProjets/mon_blog
cd ~/MesProjets/mon_blog

# Explication:
# - mkdir = Make Directory (créer dossier)
# - cd = Change Directory (aller dans dossier)
# - ~/MesProjets = Dossier dans votre répertoire utilisateur


# Étape 2: Créer environnement virtuel
# ─────────────────────────────────────

python -m venv .venv

# Décortiquons cette commande:
# - python: Lance Python
# - -m venv: Module "venv" (Virtual ENVironment)
# - .venv: Nom du dossier (convention: .venv ou venv)

# Pourquoi ".venv" avec un point?
# Le point (.) rend le dossier caché sur Linux/Mac
# Convention pour indiquer "fichier système, ne pas toucher"

# Alternative (si python ne marche pas):
python3 -m venv .venv

# Sur Windows si erreur d'exécution:
python -m venv .venv --without-pip
# Puis installer pip séparément


# Que s'est-il passé?
# ═══════════════════

# Python a créé un dossier .venv/ contenant:
# .venv/
# ├── Scripts/          (Windows) ou bin/ (Linux/Mac)
# │   ├── python.exe    -> Copie de Python
# │   ├── pip.exe       -> Gestionnaire packages
# │   ├── activate.bat  -> Script d'activation (Windows)
# │   └── activate      -> Script d'activation (Linux/Mac)
# ├── Lib/              (Windows) ou lib/ (Linux/Mac)
# │   └── site-packages/ -> Packages installés (Django, etc.)
# └── pyvenv.cfg        -> Configuration

# C'est une copie COMPLÈTE et ISOLÉE de Python!


# Étape 3: Activer l'environnement virtuel
# ─────────────────────────────────────────

# TRÈS IMPORTANT: L'environnement virtuel DOIT être activé
# pour être utilisé!

# Windows (CMD):
.venv\Scripts\activate.bat

# Windows (PowerShell):
.venv\Scripts\Activate.ps1

# Linux/Mac (bash/zsh):
source .venv/bin/activate

# Si erreur PowerShell "scripts désactivés":
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
# Puis réessayez l'activation


# Comment savoir si c'est activé?
# ════════════════════════════════

# Votre terminal change:
# Avant:
# C:\MesProjets\mon_blog>

# Après:
# (.venv) C:\MesProjets\mon_blog>
#  ^ Ce (.venv) indique que l'environnement est ACTIF

# Linux/Mac:
# Avant: user@ordi:~/MesProjets/mon_blog$
# Après: (.venv) user@ordi:~/MesProjets/mon_blog$


# Étape 4: Vérifier que tout fonctionne
# ──────────────────────────────────────

# Vérifier Python utilisé:
which python      # Linux/Mac
where python      # Windows

# Résultat attendu:
# /path/to/mon_blog/.venv/bin/python
# ou
# C:\MesProjets\mon_blog\.venv\Scripts\python.exe

# Si vous voyez /usr/bin/python ou C:\Python39\python.exe
# -> L'environnement n'est PAS activé!
# -> Réactivez-le!

# Vérifier pip:
pip --version
# Résultat: pip 23.x.x from .../mon_blog/.venv/...


# ERREURS COURANTES et SOLUTIONS
# ═══════════════════════════════

# Erreur 1: "python n'est pas reconnu"
# ────────────────────────────────────
# Problème: Python pas dans PATH
# Solution: Réinstaller Python en cochant "Add to PATH"

# Erreur 2: Environnement pas activé (oubli fréquent!)
# ─────────────────────────────────────────────────────
# Symptôme: Pas de (.venv) dans terminal
# Solution: source .venv/bin/activate (Linux/Mac)
#          .venv\Scripts\activate (Windows)

# Erreur 3: "pip: command not found" après activation
# ────────────────────────────────────────────────────
# Solution: python -m pip install --upgrade pip

# Erreur 4: Permission denied (Linux/Mac)
# ────────────────────────────────────────
# Solution: Ne PAS utiliser sudo!
#          L'environnement doit appartenir à votre user

# Erreur 5: Scripts désactivés PowerShell
# ────────────────────────────────────────
# Solution: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser


# QUAND désactiver l'environnement?
# ══════════════════════════════════

# Pour désactiver (quitter l'environnement):
deactivate

# Terminal redevient normal:
# (.venv) C:\MesProjets\mon_blog>  ->  C:\MesProjets\mon_blog>

# Quand désactiver:
# - Vous avez fini de travailler sur le projet
# - Vous passez à un autre projet
# - Vous fermez le terminal

# IMPORTANT: À CHAQUE nouvelle session terminal,
# vous devez RÉACTIVER l'environnement!

# Workflow typique:
# 1. Ouvrir terminal
# 2. cd mon_blog
# 3. source .venv/bin/activate  (ou équivalent Windows)
# 4. Travailler sur projet
# 5. deactivate (optionnel à la fin)


# ═══ 2.3 INSTALLER DJANGO ═══

# POURQUOI installer Django dans environnement virtuel?
# ══════════════════════════════════════════════════════

# Django et ses dépendances seront installés dans:
# .venv/lib/site-packages/

# Avantages:
# [OK] Isolé du Python système
# [OK] Version Django spécifique au projet
# [OK] Facile à supprimer (effacer .venv/)
# [OK] Reproductible (requirements.txt)


# COMMENT installer Django?
# ══════════════════════════

# Prérequis: Environnement virtuel ACTIVÉ (voir (.venv) dans terminal)

# Installer dernière version:
pip install django

# Explication:
# - pip: Package Installer for Python (gestionnaire packages)
# - install: Commande pour installer
# - django: Nom du package à installer

# Django télécharge depuis PyPI (Python Package Index):
# - Django lui-même
# - Dépendances (sqlparse, asgiref, tzdata)

# Durée: 10-30 secondes selon connexion


# Installer version spécifique:
pip install django==5.0.0     # Version exacte
pip install django==4.2       # Dernière 4.2.x
pip install django>=4.2,<5.0  # Entre 4.2 et 5.0

# Pourquoi version spécifique?
# - Projet existant avec version précise
# - Éviter breaking changes
# - Production stable (LTS recommandée)


# Versions Django importantes:
# ════════════════════════════

# LTS (Long Term Support):
pip install django==4.2  # Support jusqu'à avril 2026

# Dernière stable:
pip install django  # 5.0+ (au moment d'écriture)

# Version de développement (UNSTABLE):
pip install django==5.1a1  # Alpha, bêta (pour tests uniquement)


# Vérifier installation:
# ══════════════════════

python -m django --version
# Résultat: 5.0.0 (ou votre version)

# Alternative:
django-admin --version

# Si erreur "django-admin not found":
# -> Environnement pas activé
# -> Django pas installé correctement


# Lister packages installés:
pip list

# Résultat:
"""
Package    Version
---------- -------
asgiref    3.7.2
Django     5.0.0
pip        23.3.1
setuptools 69.0.2
sqlparse   0.4.4
tzdata     2023.3
"""

# Packages installés avec Django:
# - asgiref: Support ASGI (serveur async)
# - sqlparse: Parse requêtes SQL
# - tzdata: Données fuseaux horaires


# ═══ 2.4 INSTALLER PACKAGES COMPLÉMENTAIRES ═══

# POURQUOI installer d'autres packages?
# ══════════════════════════════════════

# Django de base est puissant, mais vous aurez besoin de:
# - Pillow: Pour gérer images (avatars, photos, etc.)
# - psycopg2: Pour PostgreSQL (BDD production)
# - python-decouple: Pour variables d'environnement (secrets)
# - whitenoise: Pour servir fichiers statiques (production)
# - django-extensions: Commandes supplémentaires utiles


# COMMENT installer packages complémentaires?
# ════════════════════════════════════════════

# Un par un:
pip install pillow                    # Images
pip install psycopg2-binary           # PostgreSQL
pip install python-decouple           # Variables env
pip install whitenoise                # Static files
pip install gunicorn                  # Serveur production

# Ou tous en même temps:
pip install pillow psycopg2-binary python-decouple whitenoise gunicorn


# Packages expliqués en détail:
# ═══════════════════════════════

# PILLOW
# ──────
# Pourquoi: Django peut stocker chemins d'images, mais ne peut pas
#          les manipuler (redimensionner, convertir, etc.)
# Quand: Dès que vous avez ImageField dans vos models
# Exemple: Avatar utilisateur, photos produits e-commerce

pip install pillow

# Sans Pillow:
class UserProfile(models.Model):
    avatar = models.ImageField(upload_to='avatars/')
    # ^ Erreur si Pillow pas installé!

# Avec Pillow:
# Django peut valider que c'est bien une image, obtenir dimensions, etc.


# PSYCOPG2-BINARY
# ───────────────
# Pourquoi: SQLite (BDD par défaut) OK pour développement,
#          mais PostgreSQL recommandé pour production
# Quand: Quand vous utilisez PostgreSQL
# Note: psycopg2-binary (facile) vs psycopg2 (optimisé production)

pip install psycopg2-binary

# Configuration PostgreSQL dans settings.py:
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'ma_base',
        'USER': 'mon_user',
        'PASSWORD': 'mon_password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}


# PYTHON-DECOUPLE
# ───────────────
# Pourquoi: Ne JAMAIS mettre secrets dans code (GitHub!)
# Quand: Toujours (bonne pratique)
# Utilisation: Variables d'environnement

pip install python-decouple

# Créer fichier .env (à la racine du projet):
# SECRET_KEY=votre-cle-secrete-django
# DEBUG=True
# DATABASE_URL=postgresql://user:pass@localhost/db

# Dans settings.py:
from decouple import config

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

# Avantages:
# [OK] Secrets hors du code
# [OK] Différentes config par environnement (dev/prod)
# [OK] Sécurité (ne versionnez JAMAIS .env)


# WHITENOISE
# ──────────
# Pourquoi: Servir fichiers statiques (CSS/JS) en production
# Quand: Avant déploiement
# Alternative: CDN, Nginx (plus complexe)

pip install whitenoise

# Configuration dans settings.py:
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # Après SecurityMiddleware
    # ...
]

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'


# GUNICORN
# ────────
# Pourquoi: Serveur WSGI pour production (runserver uniquement pour dev!)
# Quand: Déploiement sur serveur (Heroku, DigitalOcean, etc.)

pip install gunicorn

# Lancer en production:
gunicorn monprojet.wsgi:application --bind 0.0.0.0:8000


# DJANGO-EXTENSIONS
# ─────────────────
# Pourquoi: Commandes utiles (shell_plus, runserver_plus, graph_models)
# Quand: Développement (pas obligatoire)

pip install django-extensions

# Dans settings.py:
INSTALLED_APPS = [
    # ...
    'django_extensions',
]

# Commandes ajoutées:
# python manage.py shell_plus     (shell avec imports auto)
# python manage.py graph_models   (graphe des models)
# python manage.py show_urls      (liste toutes les URLs)


# ═══ 2.5 REQUIREMENTS.TXT: GÉRER LES DÉPENDANCES ═══

# POURQUOI requirements.txt?
# ═══════════════════════════

# Problème: Comment partager votre projet?
# - Vous avez installé 15 packages
# - Votre collègue télécharge votre code
# - Il manque tous les packages!
# - Il doit deviner lesquels installer

# Solution: requirements.txt
# Fichier listant TOUS les packages avec versions


# COMMENT créer requirements.txt?
# ════════════════════════════════

# Automatique (génère liste de TOUS les packages):
pip freeze > requirements.txt

# Résultat (requirements.txt):
"""
asgiref==3.7.2
Django==5.0.0
Pillow==10.1.0
psycopg2-binary==2.9.9
python-decouple==3.8
gunicorn==21.2.0
whitenoise==6.6.0
sqlparse==0.4.4
tzdata==2023.3
"""

# Chaque ligne = package==version


# COMMENT utiliser requirements.txt?
# ═══════════════════════════════════

# Sur autre machine/environnement:
pip install -r requirements.txt

# -r = read (lire fichier)

# pip va installer TOUS les packages avec versions exactes!
# Durée: 30 secondes à 2 minutes selon nombre de packages


# Organiser requirements (projet complexe):
# ══════════════════════════════════════════

# Créer dossier requirements/:
requirements/
├── base.txt        # Packages communs (toujours nécessaires)
├── dev.txt         # Développement uniquement
├── prod.txt        # Production uniquement
└── test.txt        # Tests uniquement

# base.txt (commun):
"""
django==5.0.0
pillow==10.1.0
python-decouple==3.8
"""

# dev.txt (développement):
"""
-r base.txt  # Inclut base.txt
django-debug-toolbar==4.2.0
django-extensions==3.2.3
ipython==8.18.0
black==23.12.0
"""

# prod.txt (production):
"""
-r base.txt  # Inclut base.txt
gunicorn==21.2.0
whitenoise==6.6.0
psycopg2-binary==2.9.9
"""

# test.txt (tests):
"""
-r base.txt  # Inclut base.txt
pytest==7.4.3
pytest-django==4.7.0
coverage==7.3.2
"""

# Installation:
pip install -r requirements/dev.txt    # Développement
pip install -r requirements/prod.txt   # Production


# Mettre à jour un package:
# ══════════════════════════

# Mise à jour:
pip install --upgrade django

# Mettre à jour requirements.txt:
pip freeze > requirements.txt


# Versionner requirements.txt:
# ══════════════════════════════

# TOUJOURS versionner requirements.txt avec Git!
# C'est la "recette" pour reproduire votre environnement

# .gitignore (NE PAS versionner):
"""
.venv/           # Environnement virtuel (trop gros)
__pycache__/
*.pyc
db.sqlite3       # Base de données locale
.env             # Secrets!
media/           # Fichiers uploadés
"""

# À versionner:
"""
requirements.txt   [OK]
manage.py         [OK]
monprojet/        [OK]
apps/             [OK]
"""


# ═══════════════════════════════════════════════════════════════════
# PARTIE 3: CRÉER PREMIER PROJET DJANGO
# ═══════════════════════════════════════════════════════════════════


# ═══ 3.1 PROJET VS APPLICATION: COMPRENDRE LA DIFFÉRENCE ═══

# POURQUOI cette distinction Projet/Application?
# ═══════════════════════════════════════════════

# Confusion fréquente des débutants!
# Django sépare en 2 niveaux:
# 1. PROJET (Project) = Site web complet
# 2. APPLICATION (App) = Fonctionnalité spécifique


# ANALOGIE: Entreprise et Départements
# ═════════════════════════════════════

# PROJET = Entreprise entière
# - Nom: "MonEntreprise SA"
# - Siège social: config, settings
# - Infrastructure: base de données, serveurs
# - URLs principales: site.com/

# APPLICATIONS = Départements de l'entreprise
# - Département Marketing -> app "blog"
# - Département Ventes -> app "boutique"
# - Département RH -> app "employes"
# - Département Support -> app "tickets"

# Chaque département:
# [OK] Indépendant (peut fonctionner seul)
# [OK] Réutilisable (marketing peut servir dans autre entreprise)
# [OK] Responsabilité unique (marketing fait marketing, pas RH)


# PROJET: Site web complet
# ═════════════════════════

# Contient:
# - Configuration globale (settings.py)
# - URLs racine (urls.py)
# - Déploiement (wsgi.py, asgi.py)
# - Base de données commune
# - Templates globaux (optionnel)
# - Static files globaux (optionnel)

# Exemple de projets:
# - Site e-commerce complet
# - Plateforme de blogging
# - Réseau social
# - Portail d'actualités

# Un projet = UN site web


# APPLICATION: Fonctionnalité spécifique
# ═══════════════════════════════════════

# Contient:
# - Models (tables BDD spécifiques)
# - Views (logique métier)
# - URLs (routes spécifiques)
# - Templates (HTML spécifiques)
# - Forms (formulaires)
# - Tests

# Exemples d'applications:
# - blog: Articles, catégories, commentaires
# - boutique: Produits, panier, commandes
# - utilisateurs: Profils, paramètres
# - forum: Discussions, messages
# - api: Endpoints REST

# Une app = UNE fonctionnalité cohérente


# QUAND créer une nouvelle application?
# ══════════════════════════════════════

# Règle d'or: UNE app = UN concept métier

# [OK] Créez une app si:
# - Fonctionnalité indépendante
# - Peut être réutilisée ailleurs
# - Responsabilité unique et claire
# - <strong>Models</strong> cohérents entre eux

# [X] NE créez PAS d'app si:
# - Juste 1-2 views simples
# - Trop couplé avec autre app
# - Pas de models propres

# Exemples de découpage:
# ──────────────────────

# Site e-commerce:
# projet: monshop/
# apps:
#   - products/     (produits, catégories)
#   - cart/         (panier)
#   - orders/       (commandes, facturation)
#   - accounts/     (profils utilisateurs)
#   - reviews/      (avis produits)

# Plateforme de blogging:
# projet: monblog/
# apps:
#   - blog/         (articles, catégories)
#   - comments/     (commentaires)
#   - accounts/     (profils auteurs)
#   - newsletter/   (abonnements email)


# Mauvais découpage (trop granulaire):
# ─────────────────────────────────────
# [X] articles/      (juste Model Article)
# [X] categories/    (juste Model Category)
# [X] tags/          (juste Model Tag)
# -> Trop d'apps! Regroupez dans "blog/"

# Mauvais découpage (pas assez granulaire):
# ──────────────────────────────────────────
# [X] main/          (tout dedans!)
#   - Models: Article, Product, Order, User
#   - Views: 50 fonctions mélangées
# -> App monstre! Séparez par domaine métier


# ═══ 3.2 CRÉER LE PROJET ═══

# COMMENT créer un projet Django?
# ════════════════════════════════

# Prérequis:
# 1. Environnement virtuel activé (.venv)
# 2. Django installé (pip install django)
# 3. Terminal dans dossier de travail

# Commande:
django-admin startproject monprojet

# Décortiquons:
# - django-admin: Commande Django (installée avec Django)
# - startproject: Sous-commande pour créer projet
# - monprojet: Nom du projet (VOUS le choisissez)

# Conventions nommage projet:
# [OK] monblog, monshop, config, core
# [OK] snake_case ou lowercase
# [X] MonProjet (éviter majuscules)
# [X] django, test (noms réservés)
# [X] mon-projet (éviter tirets)


# Structure créée:
# ════════════════

# Sans point final:
django-admin startproject monprojet

# Crée:
monprojet/                    # Dossier racine
├── monprojet/               # Package configuration (nom identique!)
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
└── manage.py

# Problème: Double niveau "monprojet/monprojet/"
# Confusant pour débutants!


# RECOMMANDÉ: Avec point final
django-admin startproject monprojet .

# Le point "." = dossier actuel
# Crée directement dans dossier où vous êtes

# Structure propre:
./                           # Dossier actuel
├── monprojet/              # Package configuration
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
└── manage.py

# Plus clair! Un seul niveau


# Fichiers créés expliqués:
# ══════════════════════════

# manage.py
# ─────────
# *** FICHIER LE PLUS UTILISÉ ***

# C'est votre "couteau suisse" Django
# Permet de faire TOUT:
# - Lancer serveur: python manage.py runserver
# - Créer migrations: python manage.py makemigrations
# - Appliquer migrations: python manage.py migrate
# - Créer superuser: python manage.py createsuperuser
# - Lancer shell: python manage.py shell
# - Collecter static: python manage.py collectstatic
# - Et bien plus!

# NE JAMAIS MODIFIER manage.py!
# C'est un wrapper autour de django-admin
# avec configuration de votre projet


# monprojet/__init__.py
# ─────────────────────
# Fichier vide (ou presque)
# Indique à Python: "Ce dossier est un package Python"
# Permet: from monprojet import settings

# Normalement vous n'y touchez JAMAIS
# (Sauf usage avancé: Celery, etc.)


# monprojet/settings.py
# ─────────────────────
# *** FICHIER LE PLUS IMPORTANT ***

# TOUTE la configuration de votre projet:
# - Base de données (quelle BDD utiliser)
# - Applications installées (vos apps + apps Django)
# - Middleware (couches de traitement requêtes)
# - Templates (où sont les HTML)
# - Static files (CSS/JS/images)
# - Sécurité (SECRET_KEY, DEBUG, ALLOWED_HOSTS)
# - Internationalisation (langue, timezone)
# - Email (serveur SMTP)
# - Cache (Redis, Memcached)
# - Et TOUT le reste!

# Vous modifierez ce fichier CONSTAMMENT


# monprojet/urls.py
# ─────────────────
# ** FICHIER URLS PRINCIPAL **

# Définit les URLs racine de votre site:
# - /admin/ -> interface admin Django
# - /blog/ -> votre app blog
# - /api/ -> votre API REST
# - etc.

# C'est le "standard téléphonique" principal
# qui redirige vers les bonnes apps

# Exemple:
"""
urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),    # Redirige vers blog/urls.py
    path('shop/', include('shop.urls')),    # Redirige vers shop/urls.py
]
"""


# monprojet/wsgi.py
# ─────────────────
# Web Server Gateway Interface

# Utilisé pour déploiement en production
# Interface entre serveur web (Apache, Nginx)
# et votre application Django

# Vous n'y touchez JAMAIS (sauf config production avancée)
# Serveurs de production (Gunicorn, uWSGI) utilisent ce fichier


# monprojet/asgi.py
# ─────────────────
# Asynchronous Server Gateway Interface

# Version async de WSGI
# Pour features asynchrones:
# - WebSockets (chat en temps réel)
# - Long-polling
# - Server-Sent Events

# Utilisé avec Django Channels
# Vous n'y touchez qu'en cas d'usage avancé


# ═══ 3.3 STRUCTURE FINALE RECOMMANDÉE ═══

# Structure professionnelle d'un projet Django:
# ══════════════════════════════════════════════

mon_projet/                      # Dossier racine
├── .venv/                       # Environnement virtuel (ne PAS versionner)
├── .git/                        # Git (versionning)
├── .gitignore                   # Fichiers à ignorer par Git
├── README.md                    # Documentation projet
├── requirements.txt             # Dépendances Python
├── manage.py                    # Script Django
│
├── monprojet/                   # Configuration projet
│   ├── __init__.py
│   ├── settings.py              # Settings (ou settings/ si séparés)
│   ├── urls.py                  # URLs principales
│   ├── wsgi.py
│   └── asgi.py
│
├── apps/                        # Toutes vos applications (optionnel)
│   ├── blog/
│   │   ├── migrations/
│   │   ├── templates/blog/
│   │   ├── static/blog/
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── urls.py
│   │   ├── forms.py
│   │   └── tests.py
│   ├── shop/
│   └── accounts/
│
├── templates/                   # Templates globaux (base.html, etc.)
│   ├── base.html
│   ├── includes/
│   └── registration/
│
├── static/                      # Static files globaux
│   ├── css/
│   ├── js/
│   └── images/
│
├── media/                       # Fichiers uploadés (ne PAS versionner)
│   ├── avatars/
│   └── documents/
│
├── staticfiles/                 # Static collectés (ne PAS versionner)
│
└── locale/                      # Traductions (si multilingue)
    ├── fr/
    └── en/


# Alternatives structure apps:
# ════════════════════════════

# Option 1: Apps à la racine (simple, petits projets)
mon_projet/
├── monprojet/
├── blog/
├── shop/
└── manage.py

# Option 2: Apps dans dossier apps/ (propre, moyens projets)
mon_projet/
├── monprojet/
├── apps/
│   ├── blog/
│   └── shop/
└── manage.py

# Option 3: Apps séparées (gros projets, microservices)
mon_projet/
├── monprojet/
├── src/
│   ├── core/
│   ├── authentication/
│   ├── content/
│   └── commerce/
└── manage.py


# ═══ 3.4 PREMIER LANCEMENT: RUNSERVER ═══

# COMMENT lancer le serveur de développement?
# ════════════════════════════════════════════

# Prérequis:
# 1. Projet créé (django-admin startproject)
# 2. Terminal dans dossier projet (là où est manage.py)
# 3. Environnement virtuel activé

# Commande:
python manage.py runserver

# Décortiquons:
# - python: Lance Python de l'environnement virtuel
# - manage.py: Script Django du projet
# - runserver: Commande pour lancer serveur développement

# Alternative si python ne marche pas:
python3 manage.py runserver


# Que se passe-t-il?
# ══════════════════

# Terminal affiche:
"""
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly 
until you apply the migrations for app(s): admin, auth, contenttypes, 
sessions.
Run 'python manage.py migrate' to apply them.

December 15, 2024 - 10:30:25
Django version 5.0.0, using settings 'monprojet.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
"""

# Décryptage ligne par ligne:

# "Watching for file changes with StatReloader"
# -> Django surveille changements de fichiers
# -> Si vous modifiez code, serveur redémarre auto (HOT RELOAD!)

# "System check identified no issues"
# -> Django vérifie configuration (OK)
# -> S'il y a erreurs, elles apparaissent ici

# "You have 18 unapplied migration(s)"
# -> [ATTENTION] AVERTISSEMENT (pas erreur)
# -> Migrations = changements base de données
# -> On va les appliquer juste après

# "Django version 5.0.0, using settings 'monprojet.settings'"
# -> Version Django utilisée
# -> Fichier settings utilisé

# "Starting development server at http://127.0.0.1:8000/"
# -> * SERVEUR DÉMARRÉ!
# -> Accessible sur http://127.0.0.1:8000/
# -> 127.0.0.1 = localhost = votre ordinateur
# -> 8000 = port (numéro de porte)

# "Quit the server with CTRL-BREAK"
# -> Comment arrêter serveur
# -> Windows: CTRL+C ou CTRL+BREAK
# -> Linux/Mac: CTRL+C


# COMMENT accéder au site?
# ═════════════════════════

# Ouvrir navigateur (Chrome, Firefox, Safari, Edge)
# Aller sur: http://127.0.0.1:8000/
# ou: http://localhost:8000/

# Vous devriez voir:
# ┌────────────────────────────────────────┐
# │  [RAPIDE] Django                             │
# │                                         │
# │  The install worked successfully!       │
# │  Congratulations!                       │
# │                                         │
# │  You are seeing this page because...   │
# └────────────────────────────────────────┘

# * FÉLICITATIONS! Django fonctionne!


# Options runserver:
# ══════════════════

# Changer port (si 8000 occupé):
python manage.py runserver 8080
python manage.py runserver 9000

# Résultat: http://127.0.0.1:8080/

# Rendre accessible depuis réseau local:
python manage.py runserver 0.0.0.0:8000

# Maintenant accessible depuis:
# - http://127.0.0.1:8000/ (vous)
# - http://192.168.1.x:8000/ (autres appareils réseau local)
# Utile pour tester sur mobile/tablette

# IP et port personnalisés:
python manage.py runserver 192.168.1.100:8080

# Sans rechargement auto (rare):
python manage.py runserver --noreload


# Arrêter le serveur:
# ═══════════════════

# Dans terminal où serveur tourne:
# CTRL+C

# Le serveur s'arrête:
"""
^C
Shutting down server...
"""

# Pour relancer:
python manage.py runserver


# POURQUOI runserver n'est PAS pour production?
# ═══════════════════════════════════════════════

# runserver = Serveur de DÉVELOPPEMENT

# Problèmes en production:
# [X] Pas performant (1 requête à la fois)
# [X] Pas sécurisé (affiche erreurs complètes)
# [X] Instable (crashs non gérés)
# [X] Pas de load balancing
# [X] Pas de cache
# [X] Pas de compression

# Pour production, utilisez:
# [OK] Gunicorn + Nginx (Linux)
# [OK] uWSGI + Nginx
# [OK] Apache + mod_wsgi
# [OK] Platforms comme Heroku, Railway, DigitalOcean


# ═══ 3.5 MIGRATIONS INITIALES ═══

# POURQUOI le message "18 unapplied migrations"?
# ═══════════════════════════════════════════════

# Django inclut apps intégrées qui nécessitent tables BDD:
# - admin: Interface administration
# - auth: Utilisateurs, groupes, permissions
# - contenttypes: Suivi des models
# - sessions: Sessions utilisateur

# Ces apps ont besoin de tables SQL:
# - auth_user (utilisateurs)
# - auth_group (groupes)
# - auth_permission (permissions)
# - django_session (sessions)
# - etc.

# Migrations = Instructions pour créer ces tables
# Django les a préparées, mais pas encore appliquées à votre BDD


# COMMENT appliquer les migrations?
# ══════════════════════════════════

# Commande:
python manage.py migrate

# Décortiquons:
# - migrate: Commande pour appliquer migrations
# - Django lit fichiers migrations (migrations/0001_initial.py, etc.)
# - Exécute SQL pour créer tables
# - Enregistre quelles migrations sont appliquées

# Terminal affiche:
"""
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
"""

# Chaque "OK" = une migration appliquée = SQL exécuté


# Que s'est-il passé?
# ═══════════════════

# 1. Django a créé fichier db.sqlite3
#    (Base de données SQLite à la racine du projet)

# 2. Dans db.sqlite3, Django a créé tables:
#    - auth_user
#    - auth_group
#    - auth_permission
#    - django_admin_log
#    - django_content_type
#    - django_session
#    - django_migrations (suivi migrations appliquées)

# 3. Vous pouvez vérifier avec:
#    - DB Browser for SQLite (logiciel gratuit)
#    - python manage.py dbshell

# 4. Maintenant vous pouvez:
#    - Créer utilisateurs
#    - Utiliser admin Django
#    - Gérer permissions
#    - Stocker sessions


# Relancer serveur:
python manage.py runserver

# Plus de message "unapplied migrations"! [OK]


# ═══ 3.6 CRÉER SUPERUTILISATEUR (ADMIN) ═══

# POURQUOI créer un superutilisateur?
# ════════════════════════════════════

# Django inclut interface d'administration gratuite!
# Accessible sur http://localhost:8000/admin/

# Mais pour y accéder, vous avez besoin:
# - Username
# - Password

# Le superutilisateur = Administrateur avec TOUS les droits:
# [OK] Accès admin Django
# [OK] Créer/modifier/supprimer utilisateurs
# [OK] Créer/modifier/supprimer données
# [OK] Tous les droits (staff + superuser)


# COMMENT créer superutilisateur?
# ════════════════════════════════

# Prérequis:
# - Migrations appliquées (python manage.py migrate)
# - Serveur peut être éteint (pas obligatoire qu'il tourne)

# Commande:
python manage.py createsuperuser

# Django pose questions interactives:
"""
Username (leave blank to use 'votre_nom_pc'): admin
Email address: admin@example.com
Password: ********
Password (again): ********
"""

# Détails:

# Username:
# - Nom d'utilisateur pour se connecter
# - Conseils: admin, adminuser, votre_prenom
# - Évitez: caractères spéciaux, espaces
# - Si vide: utilise nom de votre PC

# Email:
# - Adresse email (optionnel mais recommandé)
# - Utilisé pour reset password
# - Exemple: admin@monsite.com, admin@example.com

# Password:
# - Mot de passe (NE S'AFFICHE PAS pendant frappe!)
# - Django valide force du password:
#   [X] Trop court (min 8 caractères)
#   [X] Trop courant (password, 12345678)
#   [X] Trop similaire à username
#   [X] Entièrement numérique

# Exemples de passwords:
# [X] admin123        (trop faible)
# [X] password        (trop courant)
# [X] 12345678        (trop numérique)
# [OK] Admin2024!Xyz   (bon pour dev)
# [OK] D!jango#Secure2024 (bon pour prod)

# Si password faible, Django demande confirmation:
"""
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: 
"""

# En développement: Vous pouvez bypass (y)
# En production: JAMAIS! Choisissez password fort


# Résultat:
"""
Superuser created successfully.
"""


# Tester l'accès admin:
# ═════════════════════

# 1. Lancer serveur (si pas déjà lancé):
python manage.py runserver

# 2. Ouvrir navigateur: http://localhost:8000/admin/

# 3. Vous voyez page de connexion:
"""
┌────────────────────────────────────┐
│  Django administration             │
│                                     │
│  Username: [________]              │
│  Password: [________]              │
│                                     │
│  [ Log in ]                        │
└────────────────────────────────────┘
"""

# 4. Entrer credentials:
#    Username: admin
#    Password: (votre password)

# 5. Cliquer "Log in"

# 6. * VOUS ÊTES DANS L'ADMIN DJANGO!

# Vous voyez:
"""
┌────────────────────────────────────────────┐
│  Django administration                     │
│  Welcome, admin. View site / Change password / Log out │
│                                             │
│  Site administration                        │
│                                             │
│  AUTHENTICATION AND AUTHORIZATION          │
│  Groups     Add | Change                   │
│  Users      Add | Change                   │
│                                             │
│  Recent actions                            │
│  None available                            │
└────────────────────────────────────────────┘
"""

# Félicitations! Vous avez:
# [OK] Créé projet Django
# [OK] Lancé serveur
# [OK] Appliqué migrations
# [OK] Créé superutilisateur
# [OK] Accédé à l'admin

# Prochaine étape: Créer votre première application!


# Commandes supplémentaires utilisateur:
# ═══════════════════════════════════════

# Changer password d'un utilisateur:
python manage.py changepassword admin

# Créer utilisateur en shell:
python manage.py shell
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user('john', 'john@example.com', 'johnpassword')
>>> user.is_staff = True  # Accès admin
>>> user.save()

# Créer superuser en une ligne (production):
python manage.py createsuperuser --username=admin --email=admin@example.com --noinput
# Puis changer password:
python manage.py changepassword admin


# ═══════════════════════════════════════════════════════════════════
# PARTIE 4: APPLICATIONS DJANGO - CRÉER PREMIÈRE APP
# ═══════════════════════════════════════════════════════════════════


# ═══ 4.1 CRÉER UNE APPLICATION ═══

# QUAND créer une application?
# ════════════════════════════

# Vous créez une app pour chaque fonctionnalité distincte:
# - Blog -> app "blog"
# - Boutique e-commerce -> app "shop"
# - Forum -> app "forum"
# - API REST -> app "api"
# - Profils utilisateurs -> app "accounts" ou "profiles"

# Projet = assemblage d'applications


# COMMENT créer une application?
# ═══════════════════════════════

# Prérequis:
# - Projet Django créé
# - Terminal dans dossier projet (là où est manage.py)
# - Environnement virtuel activé

# Commande:
python manage.py startapp blog

# Décortiquons:
# - python manage.py: Utilise manage.py du projet
# - startapp: Sous-commande pour créer application
# - blog: Nom de l'application (VOUS le choisissez)

# Conventions nommage app:
# [OK] blog, shop, accounts, forum, api
# [OK] Singulier OU pluriel (cohérence dans projet)
# [OK] snake_case si multi-mots: user_profiles
# [X] Blog (éviter majuscules)
# [X] mon-blog (éviter tirets)
# [X] django, admin, auth (noms réservés)


# Structure créée:
# ════════════════

blog/
├── migrations/              # Dossier migrations (vide au début)
│   └── __init__.py
├── __init__.py              # Package Python
├── admin.py                 # Configuration admin Django
├── apps.py                  # Configuration app
├── models.py                # * Vos models (tables BDD)
├── tests.py                 # Tests automatisés
└── views.py                 # * Vos views (logique)


# Fichiers expliqués:
# ═══════════════════

# migrations/
# ───────────
# Dossier contenant historique des changements BDD
# Django génère automatiquement fichiers migration:
# - 0001_initial.py (première migration)
# - 0002_add_field_xyz.py (ajout champ)
# - 0003_delete_model_abc.py (suppression model)
# NE JAMAIS modifier ces fichiers manuellement!
# Versionner avec Git


# __init__.py
# ───────────
# Fichier vide indiquant: "blog est un package Python"
# Permet: from blog import models
# Vous n'y touchez presque jamais


# admin.py
# ────────
# Configuration pour interface admin Django
# Vous enregistrez vos models ici:
"""
from django.contrib import admin
from .models import Post

admin.site.register(Post)
"""
# Après ça, Post apparaît dans admin!


# apps.py
# ───────
# Configuration de l'application
# Django l'a généré automatiquement:
"""
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'blog'
"""
# Vous modifiez rarement (sauf usage avancé)


# models.py
# ─────────
# *** FICHIER PRINCIPAL DE L'APP ***
# Vous définissez vos models (tables BDD):
"""
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
"""
# Django génère SQL automatiquement!


# views.py
# ────────
# ** FICHIER LOGIQUE MÉTIER **
# Vous définissez vos views (fonctions/classes):
"""
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.all()
    return render(request, 'blog/post_list.html', {'posts': posts})
"""


# tests.py
# ────────
# Tests automatisés de votre app
# Django utilise framework de test intégré:
"""
from django.test import TestCase
from .models import Post

class PostTestCase(TestCase):
    def test_post_creation(self):
        post = Post.objects.create(title='Test')
        self.assertEqual(post.title, 'Test')
"""


# Fichiers/Dossiers à créer manuellement:
# ════════════════════════════════════════

# Django NE crée PAS automatiquement:

# urls.py
# ───────
# URLs spécifiques à l'app
# VOUS devez créer ce fichier:
"""
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('<int:pk>/', views.post_detail, name='post_detail'),
]
"""


# templates/
# ──────────
# Dossier contenant fichiers HTML
# Structure recommandée:
blog/
├── templates/
│   └── blog/                # Namespace (évite conflits)
│       ├── post_list.html
│       ├── post_detail.html
│       └── post_form.html

# POURQUOI blog/templates/blog/ (double "blog")?
# -> Namespacing!
# Si 2 apps ont fichier "index.html":
# shop/templates/index.html
# blog/templates/index.html
# -> Django ne sait pas lequel prendre!

# Avec namespacing:
# shop/templates/shop/index.html
# blog/templates/blog/index.html
# -> Clair: render(request, 'blog/index.html')


# static/
# ───────
# Fichiers statiques de l'app (CSS, JS, images)
blog/
├── static/
│   └── blog/                # Namespace (même raison)
│       ├── css/
│       │   └── blog.css
│       ├── js/
│       │   └── blog.js
│       └── images/
│           └── logo.png


# forms.py
# ────────
# Formulaires Django de l'app
# VOUS créez ce fichier:
"""
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']
"""


# ═══ 4.2 ENREGISTRER L'APPLICATION DANS LE PROJET ═══

# POURQUOI enregistrer l'app?
# ════════════════════════════

# Django ne "voit" pas automatiquement votre app!
# C'est comme créer un département dans entreprise
# sans l'ajouter à l'organigramme.

# Sans enregistrement:
# [X] Models pas détectés
# [X] Migrations ne se créent pas
# [X] Templates pas trouvés
# [X] Admin ne voit pas les models
# -> App complètement inutilisable!


# COMMENT enregistrer l'app?
# ═══════════════════════════

# Ouvrir monprojet/settings.py

# Trouver INSTALLED_APPS (ligne ~30):
INSTALLED_APPS = [
    'django.contrib.admin',           # Admin Django
    'django.contrib.auth',            # Authentification
    'django.contrib.contenttypes',    # Types de contenu
    'django.contrib.sessions',        # Sessions
    'django.contrib.messages',        # Messages
    'django.contrib.staticfiles',     # Fichiers statiques
]

# Ajouter votre app À LA FIN:
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Mes applications
    'blog',                           # Méthode simple
]

# OU (recommandé):
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Mes applications
    'blog.apps.BlogConfig',           # Méthode avec config
]


# Différence entre 'blog' et 'blog.apps.BlogConfig'?
# ═══════════════════════════════════════════════════

# Méthode 1: 'blog' (simple)
# ──────────────────────────
# [OK] Rapide
# [OK] Fonctionne
# [X] Moins de contrôle

INSTALLED_APPS = [
    # ...
    'blog',
]

# Méthode 2: 'blog.apps.BlogConfig' (recommandée)
# ────────────────────────────────────────────────
# [OK] Plus de contrôle (ready(), default_auto_field, etc.)
# [OK] Bonne pratique
# [OK] Recommandé par Django

INSTALLED_APPS = [
    # ...
    'blog.apps.BlogConfig',
]

# Django cherche dans blog/apps.py:
"""
class BlogConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'blog'
    verbose_name = 'Blog Articles'  # Nom affiché dans admin
"""


# Ordre des apps dans INSTALLED_APPS:
# ════════════════════════════════════

# IMPORTANT: L'ordre peut être important!

# Bonne pratique:
INSTALLED_APPS = [
    # 1. Apps Django de base
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # 2. Apps tierces (packages externes)
    'rest_framework',              # Django REST Framework
    'django_filters',              # Filtres
    'corsheaders',                 # CORS
    'crispy_forms',                # Formulaires Bootstrap
    
    # 3. Vos applications locales
    'blog.apps.BlogConfig',
    'shop.apps.ShopConfig',
    'accounts.apps.AccountsConfig',
]

# Pourquoi cet ordre?
# - Apps Django base -> Fondation (auth avant tout)
# - Apps tierces -> Peuvent étendre apps de base
# - Vos apps -> Utilisent tout ce qui précède


# Vérifier que l'app est bien enregistrée:
# ════════════════════════════════════════

python manage.py check

# Si OK:
"""
System check identified no issues (0 silenced).
"""

# Si erreur:
"""
ERRORS:
?: (admin.E403) 'blog.apps.BlogConfig' is not in INSTALLED_APPS.
"""
# -> Vérifiez INSTALLED_APPS dans settings.py


# ═══ 4.3 APPS DJANGO DE BASE (COMPRENDRE) ═══

# Django inclut apps intégrées dans INSTALLED_APPS:
# ═════════════════════════════════════════════════

# 'django.contrib.admin'
# ──────────────────────
# *** Interface d'administration

# Fournit:
# - /admin/ (URL admin)
# - Interface graphique pour gérer données
# - CRUD automatique sur vos models
# - Permissions et groupes
# - Historique des actions

# Sans cette app:
# [X] Pas d'accès /admin/
# [X] Vous devez coder interface vous-même

# Pourquoi incluse:
# [OK] Gain de temps ÉNORME
# [OK] Interface professionnelle gratuite
# [OK] Utilisée même en production (backoffice)


# 'django.contrib.auth'
# ─────────────────────
# *** Authentification et autorisation

# Fournit:
# - Model User (utilisateurs)
# - Model Group (groupes)
# - Model Permission (permissions)
# - Login/Logout
# - Hashage sécurisé passwords
# - Décorateurs @login_required
# - Système permissions (can_add, can_delete, etc.)

# Sans cette app:
# [X] Pas de gestion utilisateurs
# [X] Vous devez tout coder from scratch
# [X] Risques de failles sécurité

# Tables créées:
# - auth_user
# - auth_group
# - auth_permission
# - auth_user_groups
# - auth_user_user_permissions


# 'django.contrib.contenttypes'
# ─────────────────────────────
# Suivi des models dans projet

# Fournit:
# - Table django_content_type
# - Référence tous models projet
# - Utilisé par auth (permissions par model)
# - Utilisé par admin (affichage models)

# Usage avancé:
# - Relations génériques (GenericForeignKey)
# - Permissions dynamiques

# Sans cette app:
# [X] Permissions ne fonctionnent pas
# [X] Admin ne fonctionne pas

# Vous n'interagissez presque jamais directement


# 'django.contrib.sessions'
# ─────────────────────────
# Gestion sessions utilisateur

# Fournit:
# - Table django_session
# - Stockage données temporaires par visiteur
# - Panier e-commerce
# - Préférences utilisateur
# - Données entre requêtes

# Sans cette app:
# [X] request.session ne fonctionne pas
# [X] Login ne persiste pas

# Exemple usage:
"""
# Sauvegarder dans session:
request.session['panier'] = [1, 2, 3]

# Lire depuis session:
panier = request.session.get('panier', [])
"""


# 'django.contrib.messages'
# ─────────────────────────
# Messages flash (notifications)

# Fournit:
# - Messages temporaires après actions
# - "Article créé avec succès!"
# - "Erreur: Formulaire invalide"
# - Catégories (success, error, warning, info)

# Sans cette app:
# [X] django.contrib.messages ne fonctionne pas

# Exemple usage:
"""
from django.contrib import messages

def create_post(request):
    # ...
    messages.success(request, 'Article créé!')
    return redirect('post_list')

# Template:
{% if messages %}
    {% for message in messages %}
        <div class="alert alert-{{ message.tags }}">
            {{ message }}
        </div>
    {% endfor %}
{% endif %}
"""


# 'django.contrib.staticfiles'
# ────────────────────────────
# Gestion fichiers statiques (CSS/JS/images)

# Fournit:
# - Collecte fichiers statiques (collectstatic)
# - Sert fichiers en développement
# - Tag template {% static %}
# - Finder pour localiser fichiers

# Sans cette app:
# [X] {% static %} ne fonctionne pas
# [X] Fichiers statiques non trouvés

# Exemple usage:
"""
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="Logo">
"""


# Apps Django OPTIONNELLES (pas incluses par défaut):
# ═══════════════════════════════════════════════════

# 'django.contrib.sites'
# ──────────────────────
# Support multi-sites (un Django -> plusieurs domaines)
# Usage: blogs.monsite.com et shop.monsite.com

# 'django.contrib.sitemaps'
# ─────────────────────────
# Génération sitemaps XML (SEO)

# 'django.contrib.syndication'
# ────────────────────────────
# Génération flux RSS/Atom

# 'django.contrib.humanize'
# ─────────────────────────
# Filtres template "humanisés":
# - {{ number|intcomma }} -> "1,000,000"
# - {{ date|naturalday }} -> "aujourd'hui"

# 'django.contrib.flatpages'
# ──────────────────────────
# Pages statiques (À propos, CGU, etc.)

# 'django.contrib.redirects'
# ──────────────────────────
# Gestion redirections 301/302


# ═══ 4.4 CONFIGURER LANGUE ET TIMEZONE ═══

# POURQUOI configurer langue et timezone?
# ═══════════════════════════════════════

# Par défaut, Django utilise:
# - Langue: Anglais (US)
# - Timezone: UTC (Temps Universel Coordonné)

# Problèmes si vous êtes en France:
# [X] Interface admin en anglais
# [X] Dates affichées en anglais (January, February)
# [X] Heures en UTC (décalage avec heure locale)
# [X] Formats dates US (MM/DD/YYYY au lieu de DD/MM/YYYY)


# COMMENT configurer?
# ═══════════════════

# Ouvrir monprojet/settings.py

# Trouver (ligne ~106):
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'

# Modifier pour France:
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'

# Lignes suivantes (laisser True):
USE_I18N = True      # Internationalisation (traductions)
USE_TZ = True        # Timezone-aware datetimes (recommandé)


# Codes langue disponibles:
# ══════════════════════════

LANGUAGE_CODE = 'fr-fr'    # Français (France)
LANGUAGE_CODE = 'fr-ca'    # Français (Canada)
LANGUAGE_CODE = 'en-us'    # Anglais (US)
LANGUAGE_CODE = 'en-gb'    # Anglais (UK)
LANGUAGE_CODE = 'es-es'    # Espagnol (Espagne)
LANGUAGE_CODE = 'de-de'    # Allemand
LANGUAGE_CODE = 'it-it'    # Italien
LANGUAGE_CODE = 'pt-br'    # Portugais (Brésil)
LANGUAGE_CODE = 'ja-jp'    # Japonais
LANGUAGE_CODE = 'zh-hans'  # Chinois simplifié


# Timezones disponibles (liste complète):
# ═══════════════════════════════════════

# Europe:
TIME_ZONE = 'Europe/Paris'      # Paris, France
TIME_ZONE = 'Europe/London'     # Londres, UK
TIME_ZONE = 'Europe/Berlin'     # Berlin, Allemagne
TIME_ZONE = 'Europe/Madrid'     # Madrid, Espagne
TIME_ZONE = 'Europe/Rome'       # Rome, Italie

# Afrique:
TIME_ZONE = 'Africa/Dakar'      # Dakar, Sénégal
TIME_ZONE = 'Africa/Cairo'      # Le Caire, Égypte
TIME_ZONE = 'Africa/Lagos'      # Lagos, Nigeria

# Amériques:
TIME_ZONE = 'America/New_York'  # New York (EST)
TIME_ZONE = 'America/Chicago'   # Chicago (CST)
TIME_ZONE = 'America/Los_Angeles' # Los Angeles (PST)
TIME_ZONE = 'America/Toronto'   # Toronto, Canada
TIME_ZONE = 'America/Sao_Paulo' # São Paulo, Brésil

# Asie:
TIME_ZONE = 'Asia/Tokyo'        # Tokyo, Japon
TIME_ZONE = 'Asia/Shanghai'     # Shanghai, Chine
TIME_ZONE = 'Asia/Dubai'        # Dubaï, EAU
TIME_ZONE = 'Asia/Kolkata'      # New Delhi, Inde

# Océanie:
TIME_ZONE = 'Australia/Sydney'  # Sydney, Australie
TIME_ZONE = 'Pacific/Auckland'  # Auckland, NZ

# UTC (Temps Universel):
TIME_ZONE = 'UTC'               # Pas de décalage

# Liste complète:
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones


# Effets de la configuration:
# ════════════════════════════

# Avec LANGUAGE_CODE = 'fr-fr':
# [OK] Admin Django en français
# [OK] Messages d'erreur en français
# [OK] Noms mois en français (janvier, février)
# [OK] Formats dates français (15/01/2024)

# Avec TIME_ZONE = 'Europe/Paris':
# [OK] Heures affichées en heure de Paris
# [OK] Timezone-aware datetimes en Paris
# [OK] auto_now_add utilise heure de Paris


# USE_I18N: Internationalisation
# ═══════════════════════════════

USE_I18N = True  # Recommandé: Toujours True

# Si True:
# [OK] Django traduit interface selon LANGUAGE_CODE
# [OK] Support traductions multiples langues
# [OK] Détection langue navigateur possible

# Si False:
# [X] Tout en anglais (peu importe LANGUAGE_CODE)
# [X] Pas de traductions

# Quand mettre False?
# Uniquement si site 100% anglais ET vous voulez
# économiser quelques millisecondes (micro-optimisation)


# USE_TZ: Timezone-aware datetimes
# ═════════════════════════════════

USE_TZ = True  # Recommandé: Toujours True

# Pourquoi c'est important?
# Problème sans USE_TZ (naive datetimes):
# - User 1 à Paris crée article: 14h00
# - User 2 à New York voit: 14h00 (FAUX! Devrait être 8h00)
# -> Confusion totale sur les heures

# Avec USE_TZ = True (aware datetimes):
# - Django stocke TOUTES les dates en UTC en BDD
# - Affichage converti en timezone de l'utilisateur
# - User 1 à Paris: 14h00
# - User 2 à New York: 8h00
# -> Correct!

# En BDD (UTC):
# 2024-01-15 13:00:00+00:00

# Affiché Paris (UTC+1):
# 2024-01-15 14:00:00

# Affiché New York (UTC-5):
# 2024-01-15 08:00:00


# Changer timezone par utilisateur (avancé):
# ══════════════════════════════════════════

# settings.py reste TIME_ZONE = 'UTC'

# Dans view:
from django.utils import timezone
import zoneinfo

def my_view(request):
    # Timezone utilisateur (ex: profil user)
    user_timezone = request.user.profile.timezone  # 'Europe/Paris'
    
    # Activer timezone
    timezone.activate(zoneinfo.ZoneInfo(user_timezone))
    
    # Maintenant toutes les dates en Europe/Paris
    now = timezone.now()  # En heure de Paris
    
    # ...


# Formats de date personnalisés:
# ═══════════════════════════════

# settings.py
DATE_FORMAT = 'd/m/Y'           # 15/01/2024
DATETIME_FORMAT = 'd/m/Y H:i'   # 15/01/2024 14:30
SHORT_DATE_FORMAT = 'd/m/y'     # 15/01/24

# Template:
{{ date_obj|date:"d/m/Y" }}      # 15/01/2024
{{ date_obj|date:"d F Y" }}      # 15 janvier 2024
{{ date_obj|date:"l d F Y" }}    # lundi 15 janvier 2024


# ═══════════════════════════════════════════════════════════════════
# PARTIE 5: MODELS - BASE DE DONNÉES AVEC DJANGO ORM
# ═══════════════════════════════════════════════════════════════════


# ═══ 5.1 QU'EST-CE QU'UN MODEL? CONCEPT FONDAMENTAL ═══

# POURQUOI les Models existent-ils?
# ══════════════════════════════════

# Problème sans Models:
# ─────────────────────

# Pour stocker données, il faut:
# 1. Écrire SQL pour créer tables:
"""
CREATE TABLE blog_post (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""

# 2. Écrire SQL pour insérer données:
"""
INSERT INTO blog_post (title, content) 
VALUES ('Mon article', 'Le contenu...');
"""

# 3. Écrire SQL pour lire données:
"""
SELECT * FROM blog_post WHERE id = 1;
"""

# 4. Écrire SQL pour mettre à jour:
"""
UPDATE blog_post 
SET title = 'Nouveau titre' 
WHERE id = 1;
"""

# 5. Écrire SQL pour supprimer:
"""
DELETE FROM blog_post WHERE id = 1;
"""

# Problèmes:
# [X] Beaucoup de SQL à écrire
# [X] Risque d'erreurs SQL
# [X] Risque SQL injection (sécurité!)
# [X] Code répétitif
# [X] Difficile à maintenir
# [X] Dépendant de la BDD (changer MySQL->PostgreSQL = réécrire tout)


# Solution: Django ORM (Object-Relational Mapping)
# ════════════════════════════════════════════════

# ORM = Pont entre Python et SQL
# Vous écrivez Python -> Django génère SQL

# Avec Models Django:
"""
# 1. Définir structure (Model)
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

# Django génère SQL automatiquement!

# 2. Créer (Create)
post = Post.objects.create(title='Mon article', content='Le contenu')

# 3. Lire (Read)
post = Post.objects.get(id=1)
posts = Post.objects.all()

# 4. Mettre à jour (Update)
post.title = 'Nouveau titre'
post.save()

# 5. Supprimer (Delete)
post.delete()
"""

# Avantages:
# [OK] Code Python lisible
# [OK] Pas de SQL à écrire
# [OK] Protection SQL injection automatique
# [OK] Code réutilisable
# [OK] Facile à tester
# [OK] Indépendant de la BDD (changer facilement)


# ANALOGIE: Model = Fiche papier
# ═══════════════════════════════

# Imaginez une bibliothèque avec fiches cartonnées pour livres:

# FICHE LIVRE (Model):
# ┌─────────────────────────────┐
# │ Titre: [_________________] │
# │ Auteur: [_________________]│
# │ ISBN: [___________________]│
# │ Date: [___________________]│
# │ Disponible: [ ] Oui [X] Non│
# └─────────────────────────────┘

# En Django:
class Livre(models.Model):
    titre = models.CharField(max_length=200)
    auteur = models.CharField(max_length=100)
    isbn = models.CharField(max_length=13)
    date_publication = models.DateField()
    disponible = models.BooleanField(default=True)

# Un Model = Template de fiche
# Une instance = Une fiche remplie

# Fiche 1: "Django pour débutants", "Jean Dupont", "978-2-1234", 2024-01-15, True
# Fiche 2: "Python avancé", "Marie Martin", "978-2-5678", 2023-12-01, False


# QU'EST-CE qu'un Model concrètement?
# ════════════════════════════════════

# Un Model est une classe Python qui:
# 1. Hérite de models.Model
# 2. Définit des champs (attributs de classe)
# 3. Représente une table en base de données

# Exemple simple:
class Post(models.Model):
    title = models.CharField(max_length=200)
    published = models.BooleanField(default=False)

# Django traduit en SQL:
"""
CREATE TABLE blog_post (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(200) NOT NULL,
    published BOOLEAN NOT NULL DEFAULT 0
);
"""

# Correspondances:
# Model Post        <-->  Table blog_post
# Attribut title    <-->  Colonne title
# Attribut published <--> Colonne published
# Instance post      <-->  Ligne dans table


# QUAND utiliser Models?
# ═══════════════════════

# [OK] TOUJOURS quand vous stockez données persistantes:
# - Articles blog
# - Produits e-commerce
# - Utilisateurs (déjà fourni par Django)
# - Commandes
# - Commentaires
# - Messages
# - Images
# - Fichiers
# - Tout ce qui doit survivre au redémarrage serveur

# [X] PAS de Model pour:
# - Données temporaires (session)
# - Calculs à la volée
# - APIs externes (pas votre BDD)
# - Cache (Redis, Memcached)


# ═══ 5.2 CRÉER PREMIER MODEL ═══

# COMMENT créer un Model?
# ═══════════════════════

# Étape 1: Ouvrir blog/models.py

# Contenu par défaut:
"""
from django.db import models

# Create your models here.
"""

# Étape 2: Définir votre Model

# Exemple: Model Post pour blog
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    """
    Model représentant un article de blog.
    
    Attributes:
        title: Titre de l'article (max 200 caractères)
        content: Contenu de l'article (texte long)
        created_at: Date de création (automatique)
        updated_at: Date de modification (automatique)
        published: Publié ou brouillon?
        author: Auteur de l'article (lien vers User)
    """
    
    # Champs du model
    title = models.CharField(max_length=200, verbose_name="Titre")
    content = models.TextField(verbose_name="Contenu")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="Créé le")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="Modifié le")
    published = models.BooleanField(default=False, verbose_name="Publié")
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='posts',
        verbose_name="Auteur"
    )
    
    # Méthodes du model
    def __str__(self):
        """Représentation en string"""
        return self.title
    
    def get_absolute_url(self):
        """URL de l'objet"""
        from django.urls import reverse
        return reverse('blog:post_detail', kwargs={'pk': self.pk})
    
    # Métadonnées du model
    class Meta:
        ordering = ['-created_at']  # Tri par date décroissante
        verbose_name = "Article"
        verbose_name_plural = "Articles"


# Décortiquons ligne par ligne:
# ══════════════════════════════

# class Post(models.Model):
# ─────────────────────────
# - class: Définition de classe Python
# - Post: Nom du model (TOUJOURS singulier, CamelCase)
#   [OK] Post, Article, Product, User, Comment
#   [X] Posts, articles, blog_post
# - (models.Model): Hérite de Model Django (OBLIGATOIRE!)
#   Sans ça: juste classe Python normale, pas un Model Django


# """Docstring"""
# ───────────────
# Documentation du model (optionnel mais recommandé)
# Explique ce que représente le model
# Visible dans documentation auto-générée


# title = models.CharField(max_length=200)
# ────────────────────────────────────────
# Définition d'un champ
# - title: Nom de l'attribut Python ET nom colonne SQL
# - models.CharField: Type de champ Django
#   -> Texte court (VARCHAR en SQL)
# - max_length=200: Longueur maximum (OBLIGATOIRE pour CharField)
# - verbose_name="Titre": Nom affiché (admin, forms)

# En SQL, Django crée:
# title VARCHAR(200) NOT NULL


# content = models.TextField()
# ────────────────────────────
# - TextField: Texte long (TEXT en SQL, pas de limite)
# Parfait pour: articles, descriptions, commentaires
# Pas de max_length (illimité)


# created_at = models.DateTimeField(auto_now_add=True)
# ─────────────────────────────────────────────────────
# - DateTimeField: Date + heure
# - auto_now_add=True: Remplit automatiquement à la CRÉATION
#   -> Valeur définie UNE SEULE FOIS
#   -> Vous NE pouvez PAS la modifier
# Exemple: 2024-01-15 14:30:45

# Usage: Date de création, inscription user, etc.


# updated_at = models.DateTimeField(auto_now=True)
# ─────────────────────────────────────────────────
# - auto_now=True: Met à jour automatiquement à CHAQUE save()
#   -> Valeur change à chaque modification
# Exemple: "Dernière modification: 15/01/2024 16:20"

# Usage: Date dernière modification


# published = models.BooleanField(default=False)
# ──────────────────────────────────────────────
# - BooleanField: True ou False (BOOLEAN en SQL)
# - default=False: Valeur par défaut
#   -> Nouveau post = brouillon (non publié)

# Usage: Statuts, flags, options activées/désactivées


# author = models.ForeignKey(User, on_delete=models.CASCADE)
# ──────────────────────────────────────────────────────────
# ** RELATION vers autre Model **
# - ForeignKey: Relation "Many-to-One" (Plusieurs Posts -> Un User)
# - User: Model cible (table liée)
# - on_delete=models.CASCADE: Que faire si User supprimé?
#   CASCADE = Supprimer aussi tous ses Posts
#   (On verra autres options après)
# - related_name='posts': Nom relation inverse
#   user.posts.all() au lieu de user.post_set.all()

# En SQL:
# author_id INTEGER FOREIGN KEY REFERENCES auth_user(id)


# def __str__(self):
# ──────────────────
# Méthode magique Python
# Définit comment afficher l'objet en string

# Sans __str__:
# >>> post = Post.objects.get(id=1)
# >>> print(post)
# Post object (1)  # Pas informatif!

# Avec __str__:
# >>> print(post)
# Mon premier article Django  # Lisible!

# Utilisé partout:
# - Admin Django
# - Shell
# - Templates
# - Logs


# def get_absolute_url(self):
# ────────────────────────────
# Convention Django pour obtenir URL d'un objet
# Utilisé par: admin, templates, redirections

# Exemple:
# post = Post.objects.get(id=5)
# url = post.get_absolute_url()
# # Résultat: '/blog/post/5/'

# Dans template:
# <a href="{{ post.get_absolute_url }}">Voir l'article</a>


# class Meta:
# ───────────
# Classe interne pour métadonnées du Model
# Options NON liées aux champs

# ordering = ['-created_at']
# - Ordre par défaut des requêtes
# - '-created_at': Tri décroissant (plus récent d'abord)
#   Préfixe '-' = DESC, sans '-' = ASC
# Post.objects.all() -> Automatiquement trié!

# verbose_name = "Article"
# - Nom singulier affiché (admin, messages)
# Par défaut: "post" (nom classe en lowercase)

# verbose_name_plural = "Articles"
# - Nom pluriel affiché
# Par défaut: verbose_name + "s" = "articles"
# Mais pour français: "Articless" (moche!)
# Donc on précise "Articles"


# ═══ 5.3 TYPES DE CHAMPS DJANGO ═══

# Django fournit BEAUCOUP de types de champs
# Chaque type = SQL différent + validation différente

# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS TEXTE                                           │
# └────────────────────────────────────────────────────────┘

# CharField
# ─────────
# Texte COURT avec longueur maximum
# SQL: VARCHAR(max_length)

name = models.CharField(max_length=100)
title = models.CharField(max_length=200)

# Options importantes:
name = models.CharField(
    max_length=100,           # OBLIGATOIRE
    blank=False,              # Requis dans forms (défaut: False)
    null=False,               # NULL autorisé en BDD (défaut: False)
    default='',               # Valeur par défaut
    unique=False,             # Valeur unique (défaut: False)
    choices=CHOICES,          # Liste de choix (optionnel)
    help_text='Aide',         # Texte d'aide
    verbose_name='Nom',       # Label
    db_index=False,           # Index BDD (défaut: False)
)

# Quand utiliser:
# [OK] Noms, titres, adresses email
# [OK] URLs, slugs
# [OK] Codes, identifiants
# [X] Texte long (utilisez TextField)


# TextField
# ─────────
# Texte LONG sans limite
# SQL: TEXT

description = models.TextField()
content = models.TextField()
bio = models.TextField(blank=True)

# Différence avec CharField:
# - TextField: Pas de max_length, illimité
# - CharField: max_length obligatoire, limité

# Quand utiliser:
# [OK] Articles, descriptions
# [OK] Commentaires, messages
# [OK] Biographies
# [OK] Contenu HTML


# SlugField
# ─────────
# Texte pour URLs (lettres, nombres, tirets, underscores)
# SQL: VARCHAR(max_length)

slug = models.SlugField(max_length=200, unique=True)

# Exemple slug: "mon-premier-article-django"
# À partir de titre: "Mon Premier Article Django!"

# Validation automatique:
# [OK] mon-article, article_123, django-tutorial
# [X] Mon Article (espaces), article! (caractères spéciaux)

# Souvent avec prepopulated_fields dans admin:
# class PostAdmin(admin.ModelAdmin):
#     prepopulated_fields = {'slug': ('title',)}

# Quand utiliser:
# [OK] URLs SEO-friendly
# [OK] Identifiants uniques lisibles


# EmailField
# ──────────
# Email avec validation
# SQL: VARCHAR(254)

email = models.EmailField()
contact_email = models.EmailField(blank=True)

# Validation automatique:
# [OK] user@example.com
# [X] user@
# [X] user.example.com

# max_length par défaut: 254 (norme RFC)


# URLField
# ────────
# URL avec validation
# SQL: VARCHAR(200) par défaut

website = models.URLField(blank=True)
github = models.URLField(max_length=500)

# Validation automatique:
# [OK] https://example.com
# [OK] http://example.com/path/
# [X] example.com (manque protocole)
# [X] htp://example (protocole invalide)


# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS NUMÉRIQUES                                      │
# └────────────────────────────────────────────────────────┘

# IntegerField
# ────────────
# Nombre entier (-2147483648 à 2147483647)
# SQL: INTEGER

age = models.IntegerField()
quantity = models.IntegerField(default=0)
score = models.IntegerField(
    validators=[MinValueValidator(0), MaxValueValidator(100)]
)

# Quand utiliser:
# [OK] Âge, quantités
# [OK] Scores, notes
# [X] Prix (utilisez DecimalField)


# PositiveIntegerField
# ────────────────────
# Nombre entier POSITIF (0 à 2147483647)
# SQL: INTEGER avec CHECK (valeur >= 0)

quantity = models.PositiveIntegerField(default=0)
views = models.PositiveIntegerField(default=0)

# Quand utiliser:
# [OK] Quantités (jamais négatif)
# [OK] Compteurs (vues, likes)


# SmallIntegerField / BigIntegerField
# ───────────────────────────────────
# Entiers avec plages différentes

# SmallIntegerField: -32768 à 32767
# SQL: SMALLINT
age = models.SmallIntegerField()

# BigIntegerField: -9223372036854775808 à 9223372036854775807
# SQL: BIGINT
national_debt = models.BigIntegerField()


# DecimalField
# ────────────
# * Nombre décimal PRÉCIS (pour argent!)
# SQL: NUMERIC(max_digits, decimal_places)

price = models.DecimalField(max_digits=10, decimal_places=2)
# max_digits=10: Total de chiffres (avant + après virgule)
# decimal_places=2: Chiffres après virgule

# Exemples valides:
# 99.99 (2 avant, 2 après = 4 total)
# 12345.67 (5 avant, 2 après = 7 total)
# 12345678.99 (8 avant, 2 après = 10 total = max)

# [X] 123456789.99 (11 total > 10)

# POURQUOI DecimalField pour prix et pas FloatField?
# FloatField a erreurs d'arrondi:
# >>> 0.1 + 0.2
# 0.30000000000000004  # PROBLÈME!

# DecimalField est EXACT:
# >>> Decimal('0.1') + Decimal('0.2')
# Decimal('0.3')  # PARFAIT!

# Quand utiliser:
# [OK] Prix, montants d'argent
# [OK] Pourcentages précis
# [OK] Calculs financiers


# FloatField
# ──────────
# Nombre décimal avec approximations
# SQL: REAL ou DOUBLE PRECISION

latitude = models.FloatField()
longitude = models.FloatField()
temperature = models.FloatField()

# Quand utiliser:
# [OK] Coordonnées GPS
# [OK] Mesures scientifiques (où approximation OK)
# [X] Argent (JAMAIS! Utilisez DecimalField)


# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS DATES & HEURES                                  │
# └────────────────────────────────────────────────────────┘

# DateField
# ─────────
# Date UNIQUEMENT (pas d'heure)
# SQL: DATE

birth_date = models.DateField()
event_date = models.DateField(blank=True, null=True)

# Format: 2024-01-15

# Options auto:
publication_date = models.DateField(auto_now_add=True)  # À la création
last_update = models.DateField(auto_now=True)  # À chaque save

# Quand utiliser:
# [OK] Date de naissance
# [OK] Date d'événement
# [OK] Date d'expiration


# DateTimeField
# ─────────────
# Date + Heure
# SQL: DATETIME ou TIMESTAMP

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
meeting_time = models.DateTimeField()

# Format: 2024-01-15 14:30:45.123456

# Avec timezone (USE_TZ=True):
# 2024-01-15 14:30:45.123456+01:00

# Quand utiliser:
# [OK] Timestamps (création, modification)
# [OK] Rendez-vous, réunions
# [OK] Logs, historiques


# TimeField
# ─────────
# Heure UNIQUEMENT (pas de date)
# SQL: TIME

opening_time = models.TimeField()
closing_time = models.TimeField()

# Format: 14:30:00

# Quand utiliser:
# [OK] Horaires d'ouverture
# [OK] Alarmes, rappels récurrents
# [OK] Durées quotidiennes


# DurationField
# ─────────────
# Durée (intervalle de temps)
# SQL: BIGINT (stocke microsecondes)

video_duration = models.DurationField()
processing_time = models.DurationField()

# Python: datetime.timedelta
from datetime import timedelta
video_duration = timedelta(hours=2, minutes=30, seconds=15)

# Format affiché: 2:30:15

# Quand utiliser:
# [OK] Durées vidéos, audios
# [OK] Temps de traitement
# [OK] Délais


# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS BOOLÉENS                                        │
# └────────────────────────────────────────────────────────┘

# BooleanField
# ────────────
# True ou False
# SQL: BOOLEAN ou TINYINT(1)

published = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
accept_terms = models.BooleanField()

# Attention:
# - Pas de blank=True pour BooleanField
# - Si omis dans form: Django considère False
# - default recommandé

# Quand utiliser:
# [OK] Statuts (publié/brouillon)
# [OK] Flags (actif/inactif)
# [OK] Cases à cocher


# NullBooleanField (DÉPRÉCIÉ depuis Django 4.0)
# ──────────────────────────────────────────────
# Remplacé par:
field = models.BooleanField(null=True, blank=True)

# Permet: True, False, None (NULL)


# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS FICHIERS                                        │
# └────────────────────────────────────────────────────────┘

# FileField
# ─────────
# Fichier quelconque
# SQL: VARCHAR (chemin fichier)

document = models.FileField(upload_to='documents/')
pdf = models.FileField(upload_to='pdfs/%Y/%m/', blank=True)

# upload_to: Sous-dossier dans MEDIA_ROOT
# Exemple: MEDIA_ROOT/documents/file.pdf

# upload_to dynamique:
def user_directory_path(instance, filename):
    # Fichier uploadé dans MEDIA_ROOT/users/user_<id>/<filename>
    return f'users/user_{instance.user.id}/{filename}'

document = models.FileField(upload_to=user_directory_path)

# Quand utiliser:
# [OK] Documents (PDF, DOC, etc.)
# [OK] Archives (ZIP, RAR)
# [X] Images (utilisez ImageField)


# ImageField
# ──────────
# Image avec validation
# SQL: VARCHAR (chemin fichier)
# Nécessite: Pillow (pip install pillow)

avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
photo = models.ImageField(upload_to='photos/%Y/%m/%d/')

# Validation automatique:
# [OK] JPG, PNG, GIF, WEBP
# [X] PDF, TXT, etc.

# Accès propriétés:
# avatar.url -> '/media/avatars/user123.jpg'
# avatar.path -> '/path/to/media/avatars/user123.jpg'
# avatar.width -> 800
# avatar.height -> 600

# Quand utiliser:
# [OK] Avatars utilisateurs
# [OK] Photos produits
# [OK] Images articles


# FilePathField
# ─────────────
# Fichier du système de fichiers serveur
# (Pas upload, fichier déjà existant)

log_file = models.FilePathField(path='/var/log/')


# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS AVEC CHOIX (LISTES DÉROULANTES)                │
# └────────────────────────────────────────────────────────┘

# Choices avec CharField ou IntegerField
# ──────────────────────────────────────

class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Brouillon'),
        ('published', 'Publié'),
        ('archived', 'Archivé'),
    ]
    # ('valeur_stockée_bdd', 'Label_affiché')
    
    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default='draft'
    )

# Usage:
post = Post(status='draft')
post.save()
print(post.status)  # 'draft'
print(post.get_status_display())  # 'Brouillon' (label)

# Admin affiche liste déroulante avec labels!

# Avec Enum (Django 3.0+):
from django.db import models

class Status(models.TextChoices):
    DRAFT = 'draft', 'Brouillon'
    PUBLISHED = 'published', 'Publié'
    ARCHIVED = 'archived', 'Archivé'

class Post(models.Model):
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.DRAFT
    )

# Avantage Enum:
# [OK] Autocomplétion IDE
# [OK] Protection contre typos
# post.status = Status.PUBLISHED  # Sûr
# post.status = 'publised'  # IDE détecte erreur


# ┌────────────────────────────────────────────────────────┐
# │ CHAMPS SPÉCIAUX                                        │
# └────────────────────────────────────────────────────────┘

# UUIDField
# ─────────
# UUID (Universally Unique Identifier)
# SQL: CHAR(32) ou UUID (PostgreSQL)

import uuid

class MyModel(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False
    )

# Génère: 123e4567-e89b-12d3-a456-426614174000

# Quand utiliser:
# [OK] IDs publiques (APIs)
# [OK] Éviter séquences prévisibles
# [OK] Systèmes distribués


# JSONField
# ─────────
# Données JSON
# SQL: JSON (PostgreSQL), TEXT (autres)

metadata = models.JSONField(default=dict, blank=True)
settings = models.JSONField(default=dict)

# Stocke dict Python:
obj.metadata = {
    'tags': ['python', 'django'],
    'likes': 42,
    'verified': True
}
obj.save()

# Queries sur JSON (PostgreSQL):
Post.objects.filter(metadata__tags__contains=['python'])

# Quand utiliser:
# [OK] Données semi-structurées
# [OK] Configuration dynamique
# [OK] Métadonnées variables


# BinaryField
# ───────────
# Données binaires brutes
# SQL: BLOB

data = models.BinaryField()

# Stocke bytes Python
# Rarement utilisé (préférez FileField)


# GenericIPAddressField
# ─────────────────────
# Adresse IP (v4 et/ou v6)
# SQL: CHAR(39)

ip_address = models.GenericIPAddressField()
# Accepte: 192.168.1.1, ::1, 2001:0db8:85a3::8a2e:0370:7334

ip_v4_only = models.GenericIPAddressField(protocol='IPv4')
# Accepte seulement: 192.168.1.1


# ═══ 5.4 OPTIONS DES CHAMPS (PARAMÈTRES) ═══

# Chaque champ accepte options (paramètres)
# Certaines sont communes, d'autres spécifiques

# ┌────────────────────────────────────────────────────────┐
# │ OPTIONS VALIDATION                                     │
# └────────────────────────────────────────────────────────┘

# null (Base de données)
# ──────────────────────
# Autoriser NULL en BDD?

# null=False (défaut)
name = models.CharField(max_length=100)
# SQL: name VARCHAR(100) NOT NULL

# null=True
birth_date = models.DateField(null=True)
# SQL: birth_date DATE NULL

# Quand utiliser null=True:
# [OK] Champs optionnels (dates, nombres, relations)
# [X] CharField/TextField (utilisez blank=True + default='')

# ATTENTION:
# Pour CharField/TextField, évitez null=True
# Raison: 2 façons de dire "vide" ('' et NULL) = confusion

# [X] MAUVAIS (CharField):
name = models.CharField(max_length=100, null=True, blank=True)
# Peut être: '' OU NULL (2 états pour "vide")

# [OK] BON (CharField):
name = models.CharField(max_length=100, blank=True, default='')
# Peut être: '' seulement (1 état pour "vide")


# blank (Formulaires)
# ───────────────────
# Autoriser vide dans formulaires Django?

# blank=False (défaut)
name = models.CharField(max_length=100)
# Champ OBLIGATOIRE dans forms

# blank=True
bio = models.TextField(blank=True)
# Champ OPTIONNEL dans forms

# Distinction null vs blank:
# - null -> Base de données (peut être NULL?)
# - blank -> Formulaires (peut être vide?)

# Exemples:

# Texte optionnel:
bio = models.TextField(blank=True, default='')
# BDD: '' (pas NULL)
# Forms: Peut être vide

# Date optionnelle:
birth_date = models.DateField(null=True, blank=True)
# BDD: NULL
# Forms: Peut être vide

# Image optionnelle:
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
# BDD: NULL si pas d'image
# Forms: Peut être vide


# default
# ───────
# Valeur par défaut

published = models.BooleanField(default=False)
created_at = models.DateTimeField(default=timezone.now)
# ATTENTION: timezone.now (sans parenthèses!)
# timezone.now() -> Valeur au moment définition (fixe)
# timezone.now -> Appelle fonction à chaque création (dynamique)

# Avec callable:
def get_default_expiry():
    from datetime import datetime, timedelta
    return datetime.now() + timedelta(days=30)

expiry_date = models.DateTimeField(default=get_default_expiry)


# unique
# ──────
# Valeur UNIQUE dans toute la table

email = models.EmailField(unique=True)
slug = models.SlugField(max_length=200, unique=True)

# SQL: UNIQUE INDEX
# Tentative de dupliquer -> IntegrityError

# Quand utiliser:
# [OK] Emails (un par utilisateur)
# [OK] Slugs (un par article)
# [OK] Codes, identifiants uniques


# unique_together (Déprécié, utilisez constraints)
# ────────────────────────────────────────────────
class Meta:
    unique_together = [['user', 'product']]
# Un user peut "liker" un produit UNE SEULE FOIS

# Nouveau (Django 2.2+):
class Meta:
    constraints = [
        models.UniqueConstraint(
            fields=['user', 'product'],
            name='unique_user_product'
        )
    ]


# ┌────────────────────────────────────────────────────────┐
# │ OPTIONS AFFICHAGE                                      │
# └────────────────────────────────────────────────────────┘

# verbose_name
# ────────────
# Nom affiché (admin, forms)

title = models.CharField(max_length=200, verbose_name="Titre de l'article")
# Admin affiche: "Titre de l'article" (pas "Title")

# Par défaut: nom champ en minuscules avec espaces
# created_at -> "created at"
# user_name -> "user name"


# help_text
# ─────────
# Texte d'aide sous le champ

slug = models.SlugField(
    max_length=200,
    help_text="URL-friendly: lettres, nombres, tirets"
)
# Admin affiche aide sous le champ


# ┌────────────────────────────────────────────────────────┐
# │ OPTIONS BASE DE DONNÉES                                │
# └────────────────────────────────────────────────────────┘

# db_index
# ────────
# Créer index BDD (accélère recherches)

username = models.CharField(max_length=100, db_index=True)
# SQL: CREATE INDEX idx_username ON table(username)

# Quand utiliser:
# [OK] Champs souvent filtrés/recherchés
# [OK] Champs dans WHERE, ORDER BY
# [X] Champs rarement utilisés (index ralentit INSERT/UPDATE)


# db_column
# ─────────
# Nom colonne SQL (si différent de nom Python)

user_name = models.CharField(max_length=100, db_column='username')
# Python: user_name
# SQL: username


# primary_key
# ───────────
# Définir clé primaire (rare, Django crée automatiquement)

id = models.UUIDField(primary_key=True, default=uuid.uuid4)
# Remplace id INTEGER auto-incrémenté par défaut

# Par défaut, Django crée:
# id = models.BigAutoField(primary_key=True)


# editable
# ────────
# Champ éditable dans forms/admin?

created_at = models.DateTimeField(auto_now_add=True, editable=False)
# Champ caché dans forms/admin


# ═══ 5.5 RELATIONS ENTRE MODELS ═══

# Relations = Liens entre tables
# 3 types principaux:
# 1. ForeignKey (Many-to-One) - Plusieurs -> Un
# 2. ManyToManyField (Many-to-Many) - Plusieurs -> Plusieurs
# 3. OneToOneField (One-to-One) - Un -> Un


# ┌────────────────────────────────────────────────────────┐
# │ FOREIGNKEY (MANY-TO-ONE)                              │
# └────────────────────────────────────────────────────────┘

# POURQUOI ForeignKey?
# ════════════════════

# Problème sans relation:
# ──────────────────────

# Table Post:
# id | title              | author_name | author_email
# 1  | Article 1          | Jean        | jean@mail.com
# 2  | Article 2          | Jean        | jean@mail.com
# 3  | Article 3          | Marie       | marie@mail.com

# Problèmes:
# [X] Données dupliquées (Jean répété)
# [X] Si Jean change email -> Modifier TOUS ses articles
# [X] Incohérence possible (typo dans nom)
# [X] Pas de lien structuré

# Solution: ForeignKey
# ════════════════════

# Table User:
# id | name  | email
# 1  | Jean  | jean@mail.com
# 2  | Marie | marie@mail.com

# Table Post:
# id | title     | author_id (ForeignKey -> User)
# 1  | Article 1 | 1
# 2  | Article 2 | 1
# 3  | Article 3 | 2

# Avantages:
# [OK] Données normalisées (pas de duplication)
# [OK] Modifier user -> Tous ses posts mis à jour auto
# [OK] Intégrité référentielle
# [OK] Requêtes optimisées (JOIN)


# COMMENT définir ForeignKey?
# ═══════════════════════════

from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        User,                    # Model cible
        on_delete=models.CASCADE,  # Comportement suppression
        related_name='posts',    # Nom relation inverse
        verbose_name="Auteur"
    )

# Paramètres ForeignKey:

# 1er argument: Model cible
# ────────────────────────
# Quel Model est lié?
author = models.ForeignKey(User, ...)
category = models.ForeignKey('Category', ...)  # String si défini après

# Self-reference (arbre, hiérarchie):
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)


# on_delete (OBLIGATOIRE)
# ───────────────────────
# Que faire si objet lié est supprimé?

# CASCADE: Supprimer aussi cet objet
author = models.ForeignKey(User, on_delete=models.CASCADE)
# Si User supprimé -> Tous ses Posts supprimés

# PROTECT: Empêcher suppression
category = models.ForeignKey(Category, on_delete=models.PROTECT)
# Si Category a Posts -> Erreur, impossible supprimer Category

# SET_NULL: Mettre NULL
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
# Si User supprimé -> author=None

# SET_DEFAULT: Mettre valeur par défaut
author = models.ForeignKey(User, on_delete=models.SET_DEFAULT, default=1)
# Si User supprimé -> author=User(id=1)

# SET(...): Mettre valeur/fonction spécifique
def get_sentinel_user():
    return User.objects.get(username='deleted')

author = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
# Si User supprimé -> author=deleted user

# DO_NOTHING: Ne rien faire (DANGEREUX!)
author = models.ForeignKey(User, on_delete=models.DO_NOTHING)
# Si User supprimé -> author_id reste (référence cassée)
# IntegrityError si BDD a contrainte FK


# related_name
# ────────────
# Nom relation inverse (depuis objet lié)

# Sans related_name:
user = User.objects.get(username='jean')
posts = user.post_set.all()  # Généré automatiquement (moche)

# Avec related_name='posts':
user = User.objects.get(username='jean')
posts = user.posts.all()  # Plus lisible!

# Convention:
# - related_name au pluriel
# - Descriptif du Model source

author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
reviewer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reviewed_posts')


# Plusieurs ForeignKey vers même Model
# ─────────────────────────────────────

class Article(models.Model):
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='authored_articles'
    )
    editor = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        related_name='edited_articles'
    )

# Accès:
user.authored_articles.all()  # Articles écrits
user.edited_articles.all()    # Articles édités


# COMMENT utiliser ForeignKey?
# ════════════════════════════

# Créer avec relation:
user = User.objects.get(username='jean')
post = Post.objects.create(
    title='Mon article',
    author=user  # Passer objet User
)

# Ou avec ID:
post = Post.objects.create(
    title='Mon article',
    author_id=1  # Passer ID directement
)

# Accéder objet lié:
post = Post.objects.get(id=1)
author = post.author  # Objet User
print(author.username)  # 'jean'
print(author.email)     # 'jean@mail.com'

# Accéder ID sans requête:
author_id = post.author_id  # Pas de query BDD!
# vs
author_id = post.author.id  # Query BDD

# Relation inverse:
user = User.objects.get(username='jean')
posts = user.posts.all()  # Tous ses posts
posts = user.posts.filter(published=True)  # Posts publiés

# Compter:
nb_posts = user.posts.count()

# Existe?
has_posts = user.posts.exists()


# Filtrer via ForeignKey:
# ═══════════════════════

# Posts d'un auteur:
posts = Post.objects.filter(author__username='jean')
# __ = Suit la relation

# Posts d'auteurs actifs:
posts = Post.objects.filter(author__is_active=True)

# Posts d'auteurs avec email Gmail:
posts = Post.objects.filter(author__email__endswith='@gmail.com')


# Optimisation (éviter N+1 queries):
# ═══════════════════════════════════

# [X] MAUVAIS (N+1 queries):
posts = Post.objects.all()
for post in posts:
    print(post.author.username)  # Query pour CHAQUE post!

# [OK] BON (2 queries avec select_related):
posts = Post.objects.select_related('author').all()
for post in posts:
    print(post.author.username)  # Pas de query!


# ┌────────────────────────────────────────────────────────┐
# │ MANYTOMANYFIELD (MANY-TO-MANY)                        │
# └────────────────────────────────────────────────────────┘

# POURQUOI ManyToManyField?
# ═════════════════════════

# Problème:
# ─────────

# Un article peut avoir plusieurs tags
# Un tag peut être sur plusieurs articles

# Table Post:
# id | title     | tags
# 1  | Article 1 | python,django,web
# 2  | Article 2 | python,api

# Problèmes:
# [X] Données dupliquées ("python" répété)
# [X] Difficile de chercher (LIKE '%python%')
# [X] Pas de lien structuré

# Solution: ManyToManyField
# ═════════════════════════

# Table Post:
# id | title
# 1  | Article 1
# 2  | Article 2

# Table Tag:
# id | name
# 1  | python
# 2  | django
# 3  | web
# 4  | api

# Table intermédiaire (créée auto par Django):
# post_id | tag_id
# 1       | 1
# 1       | 2
# 1       | 3
# 2       | 1
# 2       | 4

# Avantages:
# [OK] Pas de duplication
# [OK] Requêtes faciles
# [OK] Intégrité référentielle


# COMMENT définir ManyToManyField?
# ════════════════════════════════

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(
        Tag,
        related_name='posts',
        blank=True
    )

# Django crée table intermédiaire automatiquement:
# blog_post_tags (post_id, tag_id)


# COMMENT utiliser ManyToManyField?
# ═════════════════════════════════

# Créer objets:
tag1 = Tag.objects.create(name='python')
tag2 = Tag.objects.create(name='django')
post = Post.objects.create(title='Mon article')

# Ajouter tags:
post.tags.add(tag1)          # Ajouter un tag
post.tags.add(tag1, tag2)    # Ajouter plusieurs
post.tags.set([tag1, tag2])  # Remplacer tous

# Retirer tags:
post.tags.remove(tag1)       # Retirer un
post.tags.clear()            # Retirer tous

# Lire tags:
tags = post.tags.all()       # Tous les tags
tags = post.tags.filter(name__startswith='py')

# Compter:
nb_tags = post.tags.count()

# Existe?
has_python = post.tags.filter(name='python').exists()

# Relation inverse:
tag = Tag.objects.get(name='python')
posts = tag.posts.all()      # Tous posts avec ce tag
posts = tag.posts.filter(published=True)


# ManyToManyField avec table intermédiaire personnalisée:
# ═══════════════════════════════════════════════════════

# Parfois vous voulez données supplémentaires dans relation

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(
        Tag,
        through='PostTag',     # Table intermédiaire custom
        related_name='posts'
    )

class PostTag(models.Model):
    """Table intermédiaire avec données supplémentaires"""
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_date = models.DateTimeField(auto_now_add=True)
    added_by = models.ForeignKey(User, on_delete=models.CASCADE)
    
    class Meta:
        unique_together = [['post', 'tag']]

# Utilisation:
post = Post.objects.create(title='Mon article')
tag = Tag.objects.create(name='python')
PostTag.objects.create(
    post=post,
    tag=tag,
    added_by=request.user
)

# Requêtes:
post.tags.all()  # Fonctionne toujours
# Mais add(), remove() ne fonctionnent plus!
# (Utiliser create() sur PostTag)


# ┌────────────────────────────────────────────────────────┐
# │ ONETOONEFIELD (ONE-TO-ONE)                            │
# └────────────────────────────────────────────────────────┘

# POURQUOI OneToOneField?
# ═══════════════════════

# Problème:
# ─────────

# User Django a champs limités (username, email, password)
# Vous voulez ajouter: bio, avatar, birth_date, website, etc.

# [X] Modifier Model User directement (compliqué)
# [OK] Créer Model UserProfile lié 1-à-1

# COMMENT définir OneToOneField?
# ══════════════════════════════

class UserProfile(models.Model):
    """Profil étendu d'un utilisateur"""
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='profile'
    )
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    website = models.URLField(blank=True)
    
    def __str__(self):
        return f"Profil de {self.user.username}"


# COMMENT utiliser OneToOneField?
# ═══════════════════════════════

# Créer profil:
user = User.objects.get(username='jean')
profile = UserProfile.objects.create(
    user=user,
    bio='Développeur Django passionné',
    website='https://jean-dev.com'
)

# Accès depuis User:
user = User.objects.get(username='jean')
bio = user.profile.bio         # Accès direct!
avatar = user.profile.avatar

# Accès depuis Profile:
profile = UserProfile.objects.get(id=1)
username = profile.user.username
email = profile.user.email


# Créer profil automatiquement (signals):
# ═══════════════════════════════════════

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    """Créer profil quand User créé"""
    if created:
        UserProfile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    """Sauvegarder profil quand User sauvé"""
    instance.profile.save()

# Maintenant:
user = User.objects.create_user('jean', 'jean@mail.com', 'password')
# Profil créé automatiquement!
print(user.profile)  # Existe!


# FIN DE LA PARTIE 5.5 - Relations entre Models

# Cette première partie du guide ultra-détaillé couvre:
# [OK] Introduction Django (pourquoi, quand, comment)
# [OK] Installation environnement virtuel
# [OK] Création projet et applications
# [OK] Models détaillés avec tous types de champs
# [OK] Relations ForeignKey, ManyToMany, OneToOne

# Dans la suite (parties 6-15), nous couvrirons:
# - Admin Django personnalisé
# - Views (FBV et CBV) exhaustives
# - Templates avec système complet
# - Formulaires avec validation
# - URLs et routing
# - Authentification avancée
# - Fichiers statiques et média
# - APIs REST
# - Tests
# - Sécurité
# - Déploiement
# - Et bien plus!


# ═══════════════════════════════════════════════════════════════════
# PARTIE 6: ADMIN DJANGO - INTERFACE D'ADMINISTRATION AUTOMATIQUE
# ═══════════════════════════════════════════════════════════════════


# ═══ 6.1 POURQUOI L'ADMIN DJANGO EST RÉVOLUTIONNAIRE ═══

# POURQUOI l'Admin existe?
# ════════════════════════

# Imaginez que vous créez un blog. Vous avez besoin d'une interface pour:
# - Créer/Modifier/Supprimer des articles
# - Gérer les utilisateurs et permissions
# - Modérer les commentaires
# - Upload d'images
# - Filtrer et rechercher du contenu

# SANS Django Admin, vous devez coder:
# ────────────────────────────────────
# 1. Page de liste avec pagination (100+ lignes)
# 2. Page de création avec formulaire (50+ lignes)
# 3. Page d'édition avec validation (50+ lignes)
# 4. Page de suppression avec confirmation (30+ lignes)
# 5. Système de filtres et recherche (100+ lignes)
# 6. Gestion permissions (qui peut voir/éditer quoi) (200+ lignes)
# 7. Upload fichiers sécurisé (50+ lignes)
# 8. Interface responsive (CSS/JS) (200+ lignes)
# 9. Actions en masse (supprimer plusieurs items) (100+ lignes)
# -> TOTAL: 880+ lignes de code MINIMUM par Model!
# -> Pour 5 Models: 4400+ lignes!
# -> Temps: 2-4 semaines de développement

# AVEC Django Admin:
# ─────────────────
# admin.py:
# from django.contrib import admin
# from .models import Post
# 
# admin.site.register(Post)
# 
# -> 3 LIGNES DE CODE!
# -> Temps: 30 secondes!
# -> Interface complète, sécurisée, responsive automatiquement générée!

# C'est ça la magie de Django: "batteries included"!


# QUAND utiliser l'Admin Django?
# ═══════════════════════════════

# [OK] PARFAIT POUR:

# 1. Gestion interne d'entreprise
#    • Employés modifiant produits, commandes, clients
#    • Staff modérant contenu utilisateur
#    • Support client consultant données
#    -> Interface rapide sans développement frontend

# 2. CMS (Content Management System)
#    • Rédacteurs publiant articles
#    • Éditeurs révisant contenu
#    • Admins gérant catégories/tags
#    -> Alternative gratuite à WordPress Admin

# 3. MVP (Minimum Viable Product)
#    • Startup testant concept rapidement
#    • Prototype avant interface custom
#    • Admin suffit pour premiers users
#    -> Économie de SEMAINES de développement

# 4. Outils internes
#    • Dashboards analytics internes
#    • Configuration système
#    • Gestion inventaire
#    -> Les employés n'ont pas besoin d'UI fancy

# 5. Data entry (saisie données)
#    • Import/export données
#    • Correction d'erreurs
#    • Validation manuelle
#    -> Interface CRUD efficace


# [X] NE PAS UTILISER POUR:

# 1. Interface public utilisateur final
#    [X] Blog où users publient directement
#    [X] Réseau social (posts, comments)
#    [X] Application mobile frontend
#    -> Admin = BACKEND uniquement!

# 2. UI hautement personnalisée
#    [X] Design unique avec animations
#    [X] UX complexe multi-étapes
#    [X] Interface responsive ultra-custom
#    -> Créer views custom à la place


# ═══ 6.2 ACTIVER ET ACCÉDER À L'ADMIN ═══

# COMMENT activer l'Admin?
# ════════════════════════

# ÉTAPE 1: Vérifier que django.contrib.admin est installé
# ────────────────────────────────────────────────────────
# settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',          # <- Admin app (déjà là par défaut)
    'django.contrib.auth',           # <- Système users (requis pour admin)
    'django.contrib.contenttypes',   # <- Types de contenu (requis)
    'django.contrib.sessions',       # <- Sessions (requis pour login)
    'django.contrib.messages',       # <- Messages flash
    'django.contrib.staticfiles',    # <- CSS/JS de l'admin
    # Vos apps...
]

# Ces apps sont déjà là par défaut quand vous créez un projet!


# ÉTAPE 2: Vérifier que l'URL admin est configurée
# ─────────────────────────────────────────────────
# urls.py (racine du projet):

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

urlpatterns = [
    path('admin/', admin.site.urls),  # <- Route admin (déjà là!)
    # Vos URLs...
]

# Maintenant l'admin est accessible à: http://127.0.0.1:8000/admin/


# ÉTAPE 3: Créer un superuser (compte admin)
# ───────────────────────────────────────────
# Terminal:

python manage.py createsuperuser

# Django demande:
# Username: admin                    # Votre choix
# Email: admin@example.com           # Optionnel
# Password: ********                 # Minimum 8 caractères
# Password (again): ********         # Confirmation

# [ATTENTION] ERREURS COURANTES:
# ─────────────────────
# Error: "no such table: auth_user"
# -> Solution: python manage.py migrate (créer tables BDD d'abord!)

# Error: "Password too short"
# -> Solution: Utilisez 8+ caractères

# Error: "Password too common"
# -> Solution: N'utilisez pas "password123", "admin123"
# -> Django a une liste de 20,000 mots de passe trop courants!


# ÉTAPE 4: Démarrer le serveur et se connecter
# ─────────────────────────────────────────────
python manage.py runserver

# Navigateur: http://127.0.0.1:8000/admin/

# Page de login apparaît:
# ┌─────────────────────────────────────┐
# │     Django administration           │
# │                                     │
# │  Username: [admin]                  │
# │  Password: [********]               │
# │                                     │
# │  [Log in]                           │
# └─────────────────────────────────────┘

# Après login, interface admin complète!


# ═══ 6.3 ENREGISTRER VOS MODELS DANS L'ADMIN ═══

# POURQUOI enregistrer des Models?
# ═════════════════════════════════

# Par défaut, l'admin Django montre SEULEMENT:
# - Users (utilisateurs)
# - Groups (groupes de permissions)

# Vos Models custom (Post, Product, Comment...) ne sont PAS visibles!
# -> Vous devez les "enregistrer" dans l'admin pour les gérer.


# COMMENT enregistrer un Model (méthode simple)?
# ═══════════════════════════════════════════════

# Exemple: Model Post

# models.py:
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.title  # Affichage dans l'admin


# admin.py (dans le même dossier que models.py):
from django.contrib import admin
from .models import Post

# Méthode 1: Enregistrement simple (1 ligne)
# ───────────────────────────────────────────
admin.site.register(Post)

# C'EST TOUT! Post apparaît maintenant dans l'admin!

# Interface générée automatiquement:
# ┌───────────────────────────────────────────────┐
# │ Django administration                         │
# ├───────────────────────────────────────────────┤
# │ Blog                                          │
# │   Posts                    [Add] [Change]     │
# │                                               │
# │ Authentication and Authorization              │
# │   Groups                   [Add] [Change]     │
# │   Users                    [Add] [Change]     │
# └───────────────────────────────────────────────┘

# Cliquer sur "Posts" -> Liste des posts
# Cliquer sur [Add] -> Formulaire création
# Cliquer sur un post -> Formulaire édition
# -> TOUT fonctionne automatiquement!


# ═══ 6.4 PERSONNALISER L'ADMIN AVEC ModelAdmin ═══

# POURQUOI personnaliser?
# ═══════════════════════

# L'enregistrement simple (admin.site.register(Post)) génère interface basique:
# [OK] Fonctionne
# [X] Liste montre juste "Post object (1)", "Post object (2)"...
# [X] Pas de filtres
# [X] Pas de recherche
# [X] Tous les champs modifiables (même ceux qu'on veut read-only)

# SOLUTION: Classe ModelAdmin custom pour contrôler EXACTEMENT l'interface!


# COMMENT créer un ModelAdmin personnalisé?
# ═════════════════════════════════════════

# admin.py:
from django.contrib import admin
from .models import Post

class PostAdmin(admin.ModelAdmin):
    """Configuration admin pour Post"""
    
    # 1. LISTE DES POSTS (page principale)
    # ────────────────────────────────────
    
    # list_display: Colonnes à afficher
    list_display = ['title', 'author', 'published', 'created_at']
    # Avant: "Post object (1)", "Post object (2)"
    # Après: Tableau avec colonnes Title | Author | Published | Created at
    
    # list_filter: Filtres dans sidebar droite
    list_filter = ['published', 'created_at', 'author']
    # Ajoute filtres:
    # "By published: All | Yes | No"
    # "By date: Any | Today | Past 7 days | This month | This year"
    # "By author: All | User1 | User2..."
    
    # search_fields: Champs recherchables
    search_fields = ['title', 'content']
    # Ajoute barre de recherche:
    # [Search: ________] [Go]
    # Cherche dans title ET content
    
    # date_hierarchy: Navigation par date
    date_hierarchy = 'created_at'
    # Ajoute navigation:
    # 2024 › January › 15
    # Permet naviguer par année › mois › jour
    
    # ordering: Tri par défaut
    ordering = ['-created_at']  # Plus récent en premier
    # Note: '-' = ordre descendant (DESC)
    #       sans '-' = ordre ascendant (ASC)
    
    # list_per_page: Items par page
    list_per_page = 25  # Défaut: 100
    # Évite surcharge si 1000s d'items
    
    # list_editable: Champs modifiables dans liste
    list_editable = ['published']
    # Permet cocher/décocher 'published' directement dans liste
    # Sans ouvrir page d'édition!
    
    
    # 2. PAGE D'ÉDITION (formulaire)
    # ──────────────────────────────
    
    # fields: Ordre des champs
    fields = ['title', 'content', 'author', 'published']
    # Contrôle ordre d'affichage
    
    # fieldsets: Regrouper champs en sections
    fieldsets = [
        ('Informations principales', {
            'fields': ['title', 'content']
        }),
        ('Métadonnées', {
            'fields': ['author', 'published', 'created_at'],
            'classes': ['collapse']  # Section repliable
        }),
    ]
    # Note: fields OU fieldsets, pas les deux!
    
    # readonly_fields: Champs non modifiables
    readonly_fields = ['created_at', 'updated_at']
    # Affichés mais grisés, impossibles à modifier
    
    # prepopulated_fields: Auto-remplissage
    prepopulated_fields = {'slug': ('title',)}
    # Quand vous tapez dans 'title',
    # 'slug' se remplit automatiquement:
    # title: "Mon Article" -> slug: "mon-article"
    
    # autocomplete_fields: Autocomplétion pour ForeignKey
    autocomplete_fields = ['author', 'category']
    # Au lieu de dropdown avec 1000s d'options,
    # champ de recherche avec suggestions!
    
    # raw_id_fields: ID brut pour ForeignKey
    raw_id_fields = ['author']
    # Au lieu de dropdown, juste ID numérique + loupe
    # Utile si 10,000+ users
    
    # filter_horizontal / filter_vertical: Pour ManyToMany
    filter_horizontal = ['tags']  # Ou filter_vertical
    # Interface double-liste pour sélectionner tags:
    # [Available tags] <--> [Chosen tags]
    
    
    # 3. ACTIONS PERSONNALISÉES
    # ─────────────────────────
    
    actions = ['publish_posts', 'unpublish_posts']
    
    def publish_posts(self, request, queryset):
        """Publier plusieurs posts d'un coup"""
        count = queryset.update(published=True)
        self.message_user(request, f'{count} posts publiés.')
    publish_posts.short_description = "Publier les posts sélectionnés"
    
    def unpublish_posts(self, request, queryset):
        """Dépublier plusieurs posts"""
        count = queryset.update(published=False)
        self.message_user(request, f'{count} posts dépubliés.')
    unpublish_posts.short_description = "Dépublier les posts sélectionnés"
    
    # Utilisation:
    # 1. Cocher plusieurs posts dans liste
    # 2. Choisir action dans dropdown
    # 3. Cliquer "Go"
    # -> Tous les posts cochés sont modifiés!


# Enregistrer avec personnalisation:
admin.site.register(Post, PostAdmin)

# Ou syntaxe decorator (équivalent):
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # ... configuration ...
    pass


# ═══ 6.5 OPTIONS AVANCÉES DE ModelAdmin ═══

# COMMENT créer colonnes calculées?
# ══════════════════════════════════

class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'word_count', 'is_recent']
    
    def word_count(self, obj):
        """Compter mots dans content"""
        return len(obj.content.split())
    word_count.short_description = 'Nombre de mots'
    
    def is_recent(self, obj):
        """Post créé il y a moins de 7 jours?"""
        from django.utils import timezone
        from datetime import timedelta
        recent = timezone.now() - timedelta(days=7)
        return obj.created_at >= recent
    is_recent.boolean = True  # Affiche [OK] ou [X]
    is_recent.short_description = 'Récent?'

# Ces colonnes n'existent PAS dans la base de données!
# Elles sont calculées à la volée pour l'affichage.


# COMMENT ajouter des couleurs/icônes?
# ═════════════════════════════════════

class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'status_colored']
    
    def status_colored(self, obj):
        """Afficher statut avec couleur"""
        if obj.published:
            color = 'green'
            icon = '[OK]'
            text = 'Publié'
        else:
            color = 'red'
            icon = '[X]'
            text = 'Brouillon'
        
        return format_html(
            '<span style="color: {};">{} {}</span>',
            color, icon, text
        )
    status_colored.short_description = 'Statut'

# format_html() permet HTML dans admin (sécurisé)


# COMMENT limiter queryset (filtrer objets)?
# ══════════════════════════════════════════

class PostAdmin(admin.ModelAdmin):
    
    def get_queryset(self, request):
        """Limiter posts visibles"""
        qs = super().get_queryset(request)
        
        # Exemple 1: User ne voit que SES posts
        if not request.user.is_superuser:
            return qs.filter(author=request.user)
        
        # Exemple 2: Cacher posts supprimés
        return qs.filter(deleted=False)
        
        # Superuser voit tout
        return qs


# COMMENT changer permissions?
# ════════════════════════════

class PostAdmin(admin.ModelAdmin):
    
    def has_delete_permission(self, request, obj=None):
        """Qui peut supprimer?"""
        # Seulement superusers
        return request.user.is_superuser
    
    def has_change_permission(self, request, obj=None):
        """Qui peut modifier?"""
        # Staff peut modifier ses propres posts
        if obj and obj.author == request.user:
            return True
        return request.user.is_superuser
    
    def has_add_permission(self, request):
        """Qui peut créer?"""
        # Tous les staff
        return request.user.is_staff


# COMMENT sauvegarder avec logique custom?
# ═════════════════════════════════════════

class PostAdmin(admin.ModelAdmin):
    
    def save_model(self, request, obj, form, change):
        """Logique avant sauvegarde"""
        # Si création (pas modification)
        if not change:
            # Assigner auteur automatiquement
            obj.author = request.user
        
        # Sauvegarder
        super().save_model(request, obj, form, change)
        
        # Logique après sauvegarde
        if obj.published:
            # Envoyer email notification
            send_publish_notification(obj)


# ═══ 6.6 INLINE MODELS (Relations dans l'admin) ═══

# POURQUOI les Inlines?
# ═════════════════════

# Exemple: Blog avec Posts et Comments
# 
# Sans Inline:
# ────────────
# 1. Ouvrir page Post
# 2. Voir titre/contenu
# 3. FERMER
# 4. Aller dans section Comments
# 5. Filtrer comments par post_id
# 6. Modifier comment
# -> 6 étapes pour gérer post + comments!

# Avec Inline:
# ───────────
# 1. Ouvrir page Post
# 2. Voir titre/contenu
# 3. DIRECTEMENT EN DESSOUS: Liste des comments
# 4. Ajouter/Modifier/Supprimer comments
# -> 1 page pour tout!


# COMMENT créer un Inline?
# ════════════════════════

# Models:
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    author = models.CharField(max_length=100)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)


# admin.py:
from django.contrib import admin
from .models import Post, Comment

# Créer Inline pour Comment
class CommentInline(admin.TabularInline):  # Ou StackedInline
    model = Comment
    extra = 1  # Nombre de formulaires vides à afficher
    fields = ['author', 'text']  # Champs à montrer
    readonly_fields = ['created_at']

class PostAdmin(admin.ModelAdmin):
    inlines = [CommentInline]  # Ajouter inline

admin.site.register(Post, PostAdmin)

# Maintenant quand vous éditez un Post:
# ┌────────────────────────────────────────┐
# │ Change post: "Mon Article"             │
# │                                        │
# │ Title: [Mon Article]                   │
# │ Content: [...........................]  │
# │                                        │
# │ COMMENTS                               │ <- Inline!
# │ ┌──────────────────────────────────┐   │
# │ │ Author  | Text         | Delete? │   │
# │ ├──────────────────────────────────┤   │
# │ │ Jean    | Super!       | [ ]       │   │
# │ │ Marie   | J'adore!     | [ ]       │   │
# │ │ [____]  | [_________]  |         │   │ <- Nouveau
# │ └──────────────────────────────────┘   │
# │                                        │
# │ [Save]  [Save and add another]         │
# └────────────────────────────────────────┘


# DIFFÉRENCE TabularInline vs StackedInline:
# ═══════════════════════════════════════════

# TabularInline (tableau):
# ───────────────────────
# Author    | Text              | Delete?
# ──────────|───────────────────|─────────
# Jean      | Super!            | [ ]
# Marie     | J'adore!          | [ ]
# 
# -> Compact, idéal pour plusieurs items avec peu de champs

# StackedInline (empilé):
# ──────────────────────
# ┌─ Comment #1 ─────────────┐
# │ Author: Jean             │
# │ Text: Super!             │
# │ Created: 2024-01-15      │
# │ Delete: [ ]                │
# └──────────────────────────┘
# 
# ┌─ Comment #2 ─────────────┐
# │ Author: Marie            │
# │ Text: J'adore!           │
# │ Created: 2024-01-15      │
# │ Delete: [ ]                │
# └──────────────────────────┘
# 
# -> Vertical, idéal pour peu d'items avec beaucoup de champs


# OPTIONS AVANCÉES des Inlines:
# ══════════════════════════════

class CommentInline(admin.TabularInline):
    model = Comment
    
    # Nombre de forms vides
    extra = 3              # Défaut: 3
    max_num = 10           # Maximum d'items
    min_num = 1            # Minimum requis
    
    # Contrôle permissions
    can_delete = True      # Autoriser suppression
    show_change_link = True  # Lien vers page dédiée
    
    # Champs
    fields = ['author', 'text', 'created_at']
    readonly_fields = ['created_at']
    
    # Classes CSS custom
    classes = ['collapse']  # Replié par défaut


# INLINE avec filtrage:
# ═════════════════════

class CommentInline(admin.TabularInline):
    model = Comment
    
    def get_queryset(self, request):
        """Afficher seulement comments non supprimés"""
        qs = super().get_queryset(request)
        return qs.filter(deleted=False)


# ═══ 6.7 PERSONNALISATION GLOBALE DE L'ADMIN ═══

# COMMENT changer titre/header admin?
# ════════════════════════════════════

# admin.py (ou urls.py):
from django.contrib import admin

# Titre dans <title>
admin.site.site_title = "Mon Site Admin"

# Header en haut de page
admin.site.site_header = "Administration Mon Site"

# Texte page index
admin.site.index_title = "Bienvenue dans l'admin"


# COMMENT réorganiser apps dans index?
# ═════════════════════════════════════

# Par défaut, ordre alphabétique: Auth, Blog, Shop...
# Pour contrôler ordre:

# admin.py:
class MyAdminSite(admin.AdminSite):
    def get_app_list(self, request):
        """Ordre custom des apps"""
        app_list = super().get_app_list(request)
        
        # Ordre désiré
        app_order = ['blog', 'shop', 'auth']
        
        # Trier selon ordre
        app_list.sort(key=lambda x: app_order.index(x['app_label']))
        return app_list

# Utiliser custom admin site:
admin_site = MyAdminSite(name='myadmin')

# urls.py:
urlpatterns = [
    path('admin/', admin_site.urls),  # Au lieu de admin.site.urls
]


# COMMENT créer dashboard custom?
# ════════════════════════════════

# templates/admin/index.html:
{% extends "admin/index.html" %}

{% block content %}
{{ block.super }}

<div class="dashboard-stats">
    <h2>Statistiques du jour</h2>
    <ul>
        <li>Nouveaux users: <strong>42</strong></li>
        <li>Posts publiés: <strong>15</strong></li>
        <li>Comments: <strong>127</strong></li>
    </ul>
</div>
{% endblock %}


# ═══ 6.8 ACTIONS EN MASSE (Bulk Actions) ═══

# POURQUOI des actions en masse?
# ═══════════════════════════════

# Sans actions: Modifier 100 posts un par un = 100 clics!
# Avec actions: Sélectionner 100, action "Publier" = 2 clics!


# COMMENT créer action custom?
# ════════════════════════════

class PostAdmin(admin.ModelAdmin):
    actions = ['make_published', 'make_draft', 'export_as_csv']
    
    def make_published(self, request, queryset):
        """Publier posts sélectionnés"""
        updated = queryset.update(published=True)
        self.message_user(
            request, 
            f'{updated} posts ont été publiés.'
        )
    make_published.short_description = "Publier les posts sélectionnés"
    
    def make_draft(self, request, queryset):
        """Mettre en brouillon"""
        updated = queryset.update(published=False)
        self.message_user(request, f'{updated} posts en brouillon.')
    make_draft.short_description = "Mettre en brouillon"
    
    def export_as_csv(self, request, queryset):
        """Exporter en CSV"""
        import csv
        from django.http import HttpResponse
        
        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="posts.csv"'
        
        writer = csv.writer(response)
        writer.writerow(['Title', 'Author', 'Published', 'Created'])
        
        for post in queryset:
            writer.writerow([
                post.title,
                post.author.username,
                post.published,
                post.created_at
            ])
        
        return response
    export_as_csv.short_description = "Exporter en CSV"


# ACTION avec confirmation:
# ═════════════════════════

from django.contrib import messages

class PostAdmin(admin.ModelAdmin):
    
    def delete_selected_posts(self, request, queryset):
        """Supprimer avec confirmation"""
        if 'confirm' not in request.POST:
            # Afficher page confirmation
            return render(request, 'admin/confirm_delete.html', {
                'queryset': queryset,
                'action': 'delete_selected_posts'
            })
        
        # Suppression confirmée
        count = queryset.count()
        queryset.delete()
        self.message_user(
            request, 
            f'{count} posts supprimés.', 
            messages.SUCCESS
        )


# DÉSACTIVER action "Delete selected":
# ═════════════════════════════════════

class PostAdmin(admin.ModelAdmin):
    
    def get_actions(self, request):
        """Enlever action supprimer"""
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions


# ═══ 6.9 ADMIN AVEC AUTOCOMPLETE ═══

# POURQUOI Autocomplete?
# ══════════════════════

# Problème avec ForeignKey standard:
# ──────────────────────────────────
# Post.author = ForeignKey(User)
# 
# Dans admin, dropdown avec TOUS les users:
# [Select user [BLACK_DOWN-POINTING_TRIANGLE]]
#   admin
#   user1
#   user2
#   ... (continue 10,000 users!)
# 
# -> Charge lente (10,000 éléments HTML)
# -> Difficile à trouver le bon user
# -> Mauvaise UX!

# Solution Autocomplete:
# ─────────────────────
# [Search user: jean__]
#   -> jean_dupont
#   -> jean_martin
#   -> jeanne_doe
# 
# -> Charge 3 résultats seulement
# -> Rapide et pratique!


# COMMENT activer autocomplete?
# ══════════════════════════════

# 1. Activer search dans Model cible (User)
# ──────────────────────────────────────────

class UserAdmin(admin.ModelAdmin):
    search_fields = ['username', 'email', 'first_name', 'last_name']
    # REQUIS pour autocomplete!

admin.site.unregister(User)  # Si déjà enregistré
admin.site.register(User, UserAdmin)


# 2. Utiliser autocomplete_fields dans Model référençant
# ───────────────────────────────────────────────────────

class PostAdmin(admin.ModelAdmin):
    autocomplete_fields = ['author', 'category']
    # author et category ont maintenant autocomplete!

# Ça marche avec:
# • ForeignKey
# • ManyToManyField


# ═══ 6.10 IMPORT/EXPORT DANS L'ADMIN ═══

# POURQUOI import/export?
# ═══════════════════════

# Cas d'usage courants:
# • Migrer données d'ancien système
# • Backup manuel de données
# • Édition en masse dans Excel puis réimport
# • Partage données avec partenaires


# COMMENT ajouter import/export?
# ═══════════════════════════════

# Installation:
pip install django-import-export

# settings.py:
INSTALLED_APPS = [
    'import_export',  # Ajouter
    # ...
]


# admin.py:
from import_export import resources
from import_export.admin import ImportExportModelAdmin

# Définir ressource
class PostResource(resources.ModelResource):
    class Meta:
        model = Post
        fields = ('id', 'title', 'content', 'author__username', 'published')
        # author__username = relation ForeignKey!
        export_order = fields  # Ordre colonnes export

# Admin avec import/export
class PostAdmin(ImportExportModelAdmin):
    resource_class = PostResource
    list_display = ['title', 'author', 'published']

admin.site.register(Post, PostAdmin)


# Interface admin affiche maintenant:
# ┌────────────────────────────────────┐
# │ [Import] [Export]                  │  <- Nouveaux boutons!
# │                                    │
# │ Posts                              │
# │ Title         | Author | Published │
# │ ───────────────────────────────────│
# │ Article 1     | Jean   | [OK]         │
# │ Article 2     | Marie  | [X]         │
# └────────────────────────────────────┘


# EXPORT: Cliquer [Export]
# ────────────────────────
# Formats disponibles:
# • CSV
# • XLS (Excel)
# • XLSX (Excel moderne)
# • TSV
# • JSON
# • YAML
# 
# Télécharge fichier avec données!


# IMPORT: Cliquer [Import]
# ────────────────────────
# 1. Upload fichier (CSV, XLS, XLSX...)
# 2. Django preview changements:
#    "5 nouveaux posts seront créés"
#    "2 posts seront modifiés"
#    "1 post sera supprimé" (si dry_run=False)
# 3. Confirmer
# 4. Import effectué!


# OPTIONS avancées import/export:
# ═══════════════════════════════

class PostResource(resources.ModelResource):
    class Meta:
        model = Post
        
        # Contrôler import
        skip_unchanged = True   # Ignorer si identique
        report_skipped = True   # Reporter items ignorés
        import_id_fields = ['id']  # Champ unique pour update
        
        # Champs calculés
        def dehydrate_full_name(self, post):
            """Colonne custom dans export"""
            return f"{post.author.first_name} {post.author.last_name}"


# ═══ 6.11 ERREURS COURANTES ET SOLUTIONS ═══

# ERREUR 1: "Model isn't registered with the admin"
# ──────────────────────────────────────────────────
# Cause: Oublié d'enregistrer Model
# 
# Solution:
from django.contrib import admin
from .models import Post
admin.site.register(Post)  # <- Ajouter!


# ERREUR 2: "The model is already registered"
# ────────────────────────────────────────────
# Cause: Enregistré deux fois (copier-coller?)
# 
# Solution: Vérifier qu'il n'y a qu'un seul:
admin.site.register(Post, PostAdmin)
# 
# Ou utiliser:
try:
    admin.site.register(Post, PostAdmin)
except admin.sites.AlreadyRegistered:
    pass


# ERREUR 3: "'str' object has no attribute 'split'"
# ──────────────────────────────────────────────────
# Cause: search_fields avec field non-string
# 
# [X] MAUVAIS:
search_fields = ['created_at']  # DateTimeField!
# 
# [OK] BON:
search_fields = ['title', 'content']  # CharField/TextField


# ERREUR 4: "Field X doesn't exist"
# ──────────────────────────────────
# Cause: Faute de frappe dans fields/list_display
# 
# [X] MAUVAIS:
list_display = ['titel']  # Faute!
# 
# [OK] BON:
list_display = ['title']
# 
# Vérifier nom EXACT du champ dans models.py!


# ERREUR 5: "Cannot use ModelForm without fields or exclude"
# ───────────────────────────────────────────────────────────
# Cause: Form custom sans fields
# 
# Solution: Ajouter fields dans ModelAdmin:
class PostAdmin(admin.ModelAdmin):
    fields = ['title', 'content', 'published']


# ERREUR 6: Admin lent avec 1000s d'objets
# ─────────────────────────────────────────
# Cause: N+1 query problem (relations ForeignKey non optimisées)
# 
# Solution: Utiliser list_select_related:
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author']
    list_select_related = ['author']  # Optimise queries!
# 
# Avant: 1 + N queries (1 pour posts + 1 par author)
# Après: 1 query avec JOIN
# 
# Pour ManyToMany, utiliser prefetch_related:
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'tag_list']
    
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.prefetch_related('tags')  # Optimise!


# ERREUR 7: "CSRF verification failed"
# ─────────────────────────────────────
# Cause: {% csrf_token %} manquant dans template custom
# 
# Solution: Toujours inclure dans <form>:
<form method="post">
    {% csrf_token %}  # <- Obligatoire!
    ...
</form>


# ═══ 6.12 BEST PRACTICES ADMIN ═══

# [OK] BONNES PRATIQUES:
# ═══════════════════

# 1. Toujours définir __str__() dans Models
# ─────────────────────────────────────────
class Post(models.Model):
    title = models.CharField(max_length=200)
    
    def __str__(self):
        return self.title  # <- IMPORTANT!
# 
# Sans __str__: Admin affiche "Post object (1)"
# Avec __str__: Admin affiche "Mon premier article"


# 2. Utiliser list_display avec 3-5 colonnes max
# ───────────────────────────────────────────────
# [OK] BON:
list_display = ['title', 'author', 'published', 'created_at']
# 
# [X] MAUVAIS:
list_display = ['id', 'title', 'content', 'author', 'category', 
                'published', 'views', 'likes', 'created_at', 
                'updated_at', 'slug']  # Trop! Tableau illisible!


# 3. Ajouter search_fields pour Models avec >50 items
# ────────────────────────────────────────────────────
class PostAdmin(admin.ModelAdmin):
    search_fields = ['title', 'content']  # <- UX++


# 4. list_filter pour champs avec peu de valeurs
# ───────────────────────────────────────────────
# [OK] BON pour:
list_filter = ['published']  # 2 valeurs (True/False)
list_filter = ['category']   # 5-10 catégories
# 
# [X] MAUVAIS pour:
list_filter = ['author']  # 1000s de users! Sidebar surchargée!


# 5. Utiliser readonly_fields pour champs auto
# ─────────────────────────────────────────────
class PostAdmin(admin.ModelAdmin):
    readonly_fields = ['created_at', 'updated_at', 'view_count']
    # Ces champs sont auto-gérés, pas besoin de les modifier!


# 6. Grouper champs avec fieldsets
# ─────────────────────────────────
class PostAdmin(admin.ModelAdmin):
    fieldsets = [
        ('Contenu', {
            'fields': ['title', 'content']
        }),
        ('Métadonnées', {
            'fields': ['author', 'category', 'tags'],
            'classes': ['collapse']  # Replié par défaut
        }),
        ('Publication', {
            'fields': ['published', 'published_at']
        }),
    ]
# Organisation logique > liste de 20 champs!


# 7. Optimiser queries avec select_related / prefetch_related
# ────────────────────────────────────────────────────────────
class PostAdmin(admin.ModelAdmin):
    list_select_related = ['author', 'category']  # ForeignKey
    
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.prefetch_related('tags')  # ManyToMany


# 8. Protéger suppression d'items critiques
# ──────────────────────────────────────────
class UserAdmin(admin.ModelAdmin):
    
    def has_delete_permission(self, request, obj=None):
        # Interdire suppression users (soft delete plutôt)
        return False


# 9. Utiliser actions pour opérations en masse
# ─────────────────────────────────────────────
# Au lieu de modifier 100 items un par un,
# créer action "Approuver tous" qui fait tout d'un coup!


# 10. Ajouter commentaires dans admin.py
# ───────────────────────────────────────
class PostAdmin(admin.ModelAdmin):
    # Colonnes affichées dans liste
    list_display = ['title', 'author', 'published']
    
    # Champs recherchables
    search_fields = ['title', 'content']
    
    # etc.
# Votre futur vous remerciera!


# ═══ 6.13 RÉSUMÉ PARTIE 6: ADMIN DJANGO ═══

# Ce que vous avez appris:
# ────────────────────────
# [OK] Pourquoi l'Admin Django est révolutionnaire (économie 100s heures)
# [OK] Activer et accéder à l'interface Admin
# [OK] Créer superuser pour se connecter
# [OK] Enregistrer Models dans l'Admin
# [OK] Personnaliser avec ModelAdmin (list_display, filters, search...)
# [OK] Créer colonnes calculées et actions custom
# [OK] Utiliser Inlines pour relations
# [OK] Autocomplete pour ForeignKey avec 1000s items
# [OK] Import/Export données (CSV, Excel)
# [OK] Optimiser performance admin
# [OK] Erreurs courantes et solutions
# [OK] Best practices pour admin professionnel

# Fichiers clés:
# ─────────────
# admin.py       <- Configuration admin (enregistrement Models)
# models.py      <- Définition __str__() pour affichage
# settings.py    <- INSTALLED_APPS avec django.contrib.admin

# Commandes clés:
# ──────────────
# python manage.py createsuperuser    <- Créer compte admin
# python manage.py runserver           <- Démarrer serveur
# URL: http://127.0.0.1:8000/admin/    <- Interface admin


# ═══════════════════════════════════════════════════════════════════
# PARTIE 7: VIEWS - LOGIQUE MÉTIER ET CONTRÔLEURS
# ═══════════════════════════════════════════════════════════════════


# ═══ 7.1 QU'EST-CE QU'UNE VIEW? ═══

# POURQUOI les Views existent?
# ════════════════════════════

# RAPPEL: Architecture MTV
# ────────────────────────
# • Model = Données (base de données)
# • Template = Présentation (HTML)
# • View = LOGIQUE MÉTIER (le cerveau!)

# La View est le CODE qui:
# 1. Reçoit requête HTTP du navigateur
# 2. Récupère données nécessaires (Models)
# 3. Traite/filtre/transforme ces données
# 4. Envoie au Template pour affichage
# 5. Retourne réponse HTTP au navigateur

# ANALOGIE: Restaurant
# ───────────────────
# Client commande "Burger menu + frites"
# 
# View = SERVEUR:
# 1. Reçoit commande (requête HTTP)
# 2. Transmet à cuisine (Models: récupère ingrédients)
# 3. Vérifie si client allergique (logique métier)
# 4. Prépare présentation sur plateau (Template)
# 5. Apporte au client (réponse HTTP)
# 
# Le serveur orchestre TOUT le processus!


# EXEMPLE CONCRET: Page liste articles blog
# ══════════════════════════════════════════

# 1. User visite: www.monblog.com/articles/
# 
# 2. Django trouve View correspondante dans urls.py:
#    path('articles/', views.post_list)
# 
# 3. View post_list() s'exécute:

def post_list(request):
    """View affichant liste des posts"""
    # Étape A: Récupérer données (Model)
    posts = Post.objects.all()
    
    # Étape B: Filtrer (Logique métier)
    posts = posts.filter(published=True)
    posts = posts.order_by('-created_at')
    
    # Étape C: Préparer contexte pour Template
    context = {
        'posts': posts,
        'total_count': posts.count(),
        'page_title': 'Tous les articles'
    }
    
    # Étape D: Rendre Template avec contexte
    return render(request, 'blog/post_list.html', context)

# 4. Template reçoit variables et génère HTML
# 
# 5. HTML retourné au navigateur
# 
# 6. Page s'affiche!


# TYPES DE VIEWS Django
# ══════════════════════

# Django propose 2 approches pour écrire Views:

# 1. FBV (Function-Based Views) - Views fonctions
#    ──────────────────────────────────────────────
#    • Fonctions Python simples
#    • Flexibles et faciles à comprendre
#    • Parfaites pour débutants
#    • Code plus verbeux pour cas complexes
#    
#    def ma_view(request):
#        return HttpResponse("Hello!")

# 2. CBV (Class-Based Views) - Views classes
#    ─────────────────────────────────────────
#    • Classes Python héritant de View
#    • DRY (Don't Repeat Yourself) maximal
#    • Puissantes mais courbe d'apprentissage
#    • Parfaites pour opérations CRUD standard
#    
#    class MaView(View):
#        def get(self, request):
#            return HttpResponse("Hello!")

# On verra les DEUX en détail!


# ═══ 7.2 FUNCTION-BASED VIEWS (FBV) ═══

# POURQUOI commencer par FBV?
# ═══════════════════════════

# FBV = Code Python simple, facile à comprendre
# Parfait pour apprendre Django avant d'attaquer CBV

# Avantages FBV:
# • Explicite: Vous voyez TOUT le code
# • Flexible: Facile à personnaliser
# • Debugging simple: Pas de magie cachée
# • Rapide à écrire pour cas simples


# ANATOMIE d'une FBV
# ══════════════════

def ma_view(request):
    """
    request: Objet HttpRequest (contient données requête)
    return: Objet HttpResponse (réponse envoyée au navigateur)
    """
    return HttpResponse("Hello World!")

# C'EST TOUT! Plus simple impossible!

# Paramètres:
# ──────────
# • request (OBLIGATOIRE): Objet avec toutes infos requête HTTP
#   - request.method -> 'GET', 'POST', 'PUT', 'DELETE'...
#   - request.GET -> Paramètres URL (?page=2&search=django)
#   - request.POST -> Données formulaire
#   - request.user -> Utilisateur connecté
#   - request.FILES -> Fichiers uploadés
#   - request.META -> Headers HTTP (User-Agent, IP...)

# Return:
# ──────
# • DOIT retourner objet HttpResponse ou sous-classe:
#   - HttpResponse() -> Réponse simple
#   - render() -> Réponse avec template
#   - redirect() -> Redirection
#   - JsonResponse() -> Réponse JSON
#   - FileResponse() -> Télécharger fichier


# EXEMPLE 1: View simple retournant texte
# ════════════════════════════════════════

# views.py:
from django.http import HttpResponse

def hello_world(request):
    """View la plus simple possible"""
    return HttpResponse("Hello World!")

# urls.py:
from django.urls import path
from . import views

urlpatterns = [
    path('hello/', views.hello_world),
]

# Visite: http://127.0.0.1:8000/hello/
# Affiche: "Hello World!"


# EXEMPLE 2: View avec paramètre URL
# ═══════════════════════════════════

# views.py:
def hello_name(request, name):
    """View avec paramètre capturé depuis URL"""
    return HttpResponse(f"Hello {name}!")

# urls.py:
urlpatterns = [
    path('hello/<str:name>/', views.hello_name),
    # <str:name> = capture string depuis URL
]

# Visite: http://127.0.0.1:8000/hello/Jean/
# Affiche: "Hello Jean!"
# 
# Visite: http://127.0.0.1:8000/hello/Marie/
# Affiche: "Hello Marie!"


# EXEMPLE 3: View avec Template
# ══════════════════════════════

# views.py:
from django.shortcuts import render

def about(request):
    """View avec template"""
    context = {
        'site_name': 'Mon Blog',
        'year': 2024,
        'features': ['Articles', 'Comments', 'Likes']
    }
    return render(request, 'about.html', context)

# templates/about.html:
<!DOCTYPE html>
<html>
<head>
    <title>About {{ site_name }}</title>
</head>
<body>
    <h1>About {{ site_name }}</h1>
    <p>Year: {{ year }}</p>
    <h2>Features:</h2>
    <ul>
        {% for feature in features %}
            <li>{{ feature }}</li>
        {% endfor %}
    </ul>
</body>
</html>

# render() fait 3 choses:
# 1. Charge template 'about.html'
# 2. Remplace variables {{ }} avec contexte
# 3. Retourne HttpResponse avec HTML généré


# EXEMPLE 4: View avec données Model
# ═══════════════════════════════════

# models.py:
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

# views.py:
def post_list(request):
    """Afficher liste de tous les posts publiés"""
    # Récupérer posts depuis base de données
    posts = Post.objects.filter(published=True)
    posts = posts.order_by('-created_at')  # Plus récent en premier
    
    context = {
        'posts': posts,
        'total': posts.count()
    }
    return render(request, 'blog/post_list.html', context)

# templates/blog/post_list.html:
<h1>Articles ({{ total }})</h1>
{% for post in posts %}
    <article>
        <h2>{{ post.title }}</h2>
        <p>{{ post.content }}</p>
        <small>Publié le {{ post.created_at|date:"d/m/Y" }}</small>
    </article>
{% empty %}
    <p>Aucun article pour le moment.</p>
{% endfor %}


# EXEMPLE 5: View détail d'un post
# ═════════════════════════════════

# views.py:
from django.shortcuts import render, get_object_or_404

def post_detail(request, post_id):
    """Afficher détail d'un post spécifique"""
    # get_object_or_404: Récupère post OU retourne 404 si n'existe pas
    post = get_object_or_404(Post, id=post_id, published=True)
    
    context = {'post': post}
    return render(request, 'blog/post_detail.html', context)

# urls.py:
urlpatterns = [
    path('post/<int:post_id>/', views.post_detail),
    # <int:post_id> = capture entier depuis URL
]

# Visite: http://127.0.0.1:8000/post/42/
# Affiche détail du post avec ID 42
# 
# Visite: http://127.0.0.1:8000/post/999/ (n'existe pas)
# Affiche page 404 automatiquement!


# EXEMPLE 6: View avec formulaire POST
# ═════════════════════════════════════

# views.py:
from django.shortcuts import render, redirect
from .forms import PostForm

def post_create(request):
    """Créer nouveau post"""
    if request.method == 'POST':
        # Formulaire soumis
        form = PostForm(request.POST)
        if form.is_valid():
            # Sauvegarder post
            post = form.save(commit=False)
            post.author = request.user  # Assigner auteur
            post.save()
            # Rediriger vers détail du post créé
            return redirect('post_detail', post_id=post.id)
    else:
        # GET: Afficher formulaire vide
        form = PostForm()
    
    return render(request, 'blog/post_create.html', {'form': form})

# Flow:
# ─────
# GET /post/create/
# -> Affiche formulaire vide
# 
# POST /post/create/ (user soumet formulaire)
# -> Valide données
# -> Sauvegarde dans BDD
# -> Redirige vers page post créé


# EXEMPLE 7: View avec paramètres GET (query string)
# ═══════════════════════════════════════════════════

# views.py:
def post_search(request):
    """Rechercher posts par query string"""
    # URL: /search/?q=django&category=tutorial
    
    query = request.GET.get('q', '')  # Défaut '' si absent
    category = request.GET.get('category', '')
    
    posts = Post.objects.filter(published=True)
    
    # Filtrer par recherche
    if query:
        posts = posts.filter(title__icontains=query)
        # icontains = LIKE '%django%' (case-insensitive)
    
    # Filtrer par catégorie
    if category:
        posts = posts.filter(category__name=category)
    
    context = {
        'posts': posts,
        'query': query,
        'category': category
    }
    return render(request, 'blog/search_results.html', context)


# EXEMPLE 8: View retournant JSON (API)
# ══════════════════════════════════════

# views.py:
from django.http import JsonResponse

def post_api_list(request):
    """API retournant posts en JSON"""
    posts = Post.objects.filter(published=True).values(
        'id', 'title', 'content', 'created_at'
    )
    posts_list = list(posts)  # QuerySet -> liste
    
    return JsonResponse({
        'count': len(posts_list),
        'posts': posts_list
    })

# Visite: http://127.0.0.1:8000/api/posts/
# Affiche:
# {
#     "count": 2,
#     "posts": [
#         {
#             "id": 1,
#             "title": "Premier post",
#             "content": "...",
#             "created_at": "2024-01-15T10:30:00"
#         },
#         {
#             "id": 2,
#             "title": "Deuxième post",
#             "content": "...",
#             "created_at": "2024-01-16T14:20:00"
#         }
#     ]
# }


# EXEMPLE 9: View avec gestion d'erreurs
# ═══════════════════════════════════════

# views.py:
def post_like(request, post_id):
    """Liker un post"""
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return HttpResponse("Post n'existe pas", status=404)
    
    # Vérifier que user connecté
    if not request.user.is_authenticated:
        return HttpResponse("Login requis", status=401)
    
    # Vérifier que post publié
    if not post.published:
        return HttpResponse("Post non publié", status=403)
    
    # Liker
    post.likes += 1
    post.save()
    
    return HttpResponse(f"Post liké! Total: {post.likes}")


# EXEMPLE 10: View télécharger fichier
# ═════════════════════════════════════

# views.py:
from django.http import FileResponse
import os

def download_pdf(request, post_id):
    """Télécharger PDF d'un post"""
    post = get_object_or_404(Post, id=post_id)
    
    # Chemin fichier
    pdf_path = f'/media/posts/post_{post_id}.pdf'
    
    if not os.path.exists(pdf_path):
        return HttpResponse("PDF non trouvé", status=404)
    
    # Ouvrir fichier
    file = open(pdf_path, 'rb')
    response = FileResponse(file)
    
    # Header pour téléchargement
    response['Content-Disposition'] = f'attachment; filename="post_{post_id}.pdf"'
    
    return response


# ═══ 7.3 SHORTCUTS DJANGO POUR FBV ═══

# Django fournit fonctions helper pour simplifier Views


# 1. render() - Rendre template
# ══════════════════════════════

from django.shortcuts import render

def ma_view(request):
    context = {'name': 'Jean'}
    return render(request, 'template.html', context)

# Équivalent LONG (sans render):
from django.template import loader
from django.http import HttpResponse

def ma_view(request):
    template = loader.get_template('template.html')
    context = {'name': 'Jean'}
    html = template.render(context, request)
    return HttpResponse(html)

# render() économise 3 lignes!


# 2. redirect() - Redirection
# ════════════════════════════

from django.shortcuts import redirect

def after_login(request):
    # Rediriger vers URL nommée
    return redirect('post_list')
    
    # Ou vers URL absolue
    return redirect('/blog/posts/')
    
    # Ou vers objet avec get_absolute_url()
    post = Post.objects.get(id=1)
    return redirect(post)

# Retourne HttpResponseRedirect avec status 302


# 3. get_object_or_404() - Récupérer ou 404
# ══════════════════════════════════════════

from django.shortcuts import get_object_or_404

def post_detail(request, post_id):
    # Une seule ligne!
    post = get_object_or_404(Post, id=post_id)
    return render(request, 'detail.html', {'post': post})

# Équivalent LONG:
from django.http import Http404

def post_detail(request, post_id):
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        raise Http404("Post non trouvé")
    return render(request, 'detail.html', {'post': post})


# 4. get_list_or_404() - Liste ou 404
# ════════════════════════════════════

from django.shortcuts import get_list_or_404

def posts_by_category(request, category):
    # Retourne liste OU 404 si vide
    posts = get_list_or_404(Post, category__name=category)
    return render(request, 'list.html', {'posts': posts})


# ═══ 7.4 DÉCORATEURS POUR FBV ═══

# POURQUOI des décorateurs?
# ═════════════════════════

# Décorateur = Fonction qui modifie comportement d'une autre fonction
# Django fournit décorateurs pour ajouter fonctionnalités communes


# 1. @require_http_methods - Limiter méthodes HTTP
# ═════════════════════════════════════════════════

from django.views.decorators.http import require_http_methods, require_GET, require_POST

@require_GET
def post_list(request):
    """Accepte seulement GET"""
    posts = Post.objects.all()
    return render(request, 'list.html', {'posts': posts})

@require_POST
def post_create(request):
    """Accepte seulement POST"""
    # Créer post
    pass

@require_http_methods(["GET", "POST"])
def post_edit(request, post_id):
    """Accepte GET et POST uniquement"""
    if request.method == 'POST':
        # Sauvegarder
        pass
    else:
        # Afficher formulaire
        pass

# Si méthode non autorisée -> erreur 405 Method Not Allowed


# 2. @login_required - Exiger authentification
# ═════════════════════════════════════════════

from django.contrib.auth.decorators import login_required

@login_required
def create_post(request):
    """Seulement users connectés"""
    # Code ici
    pass

# Si user pas connecté -> redirige vers page login
# URL login configurable dans settings.py:
LOGIN_URL = '/accounts/login/'

# Paramètre redirect après login:
@login_required(login_url='/mon-login/')
def ma_view(request):
    pass


# 3. @permission_required - Exiger permission
# ════════════════════════════════════════════

from django.contrib.auth.decorators import permission_required

@permission_required('blog.add_post')
def create_post(request):
    """Seulement users avec permission 'add_post'"""
    pass

@permission_required('blog.delete_post', raise_exception=True)
def delete_post(request, post_id):
    """Permission requise, sinon 403 Forbidden"""
    pass

# Permissions Django:
# app_label.action_model
# • blog.add_post
# • blog.change_post
# • blog.delete_post
# • blog.view_post


# 4. @user_passes_test - Test custom
# ═══════════════════════════════════

from django.contrib.auth.decorators import user_passes_test

def is_premium(user):
    """Vérifier si user premium"""
    return user.is_authenticated and user.profile.is_premium

@user_passes_test(is_premium)
def premium_content(request):
    """Seulement users premium"""
    pass

# Ou avec lambda:
@user_passes_test(lambda u: u.is_staff)
def staff_only(request):
    pass


# 5. @cache_page - Mettre en cache
# ═════════════════════════════════

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # Cache 15 minutes
def post_list(request):
    """Liste mise en cache"""
    posts = Post.objects.all()  # Requête BDD seulement 1x/15min!
    return render(request, 'list.html', {'posts': posts})

# Première visite: Génère HTML, stocke en cache
# Visites suivantes (15 min): Retourne HTML depuis cache
# -> Performance++


# 6. @never_cache - Jamais cacher
# ════════════════════════════════

from django.views.decorators.cache import never_cache

@never_cache
def user_profile(request):
    """Toujours récupérer données fraîches"""
    pass


# 7. @gzip_page - Compresser réponse
# ═══════════════════════════════════

from django.views.decorators.gzip import gzip_page

@gzip_page
def large_response(request):
    """Compresser HTML (économise bande passante)"""
    # HTML 500KB -> 50KB après gzip!
    pass


# 8. @vary_on_headers - Cache par header
# ═══════════════════════════════════════

from django.views.decorators.vary import vary_on_headers

@vary_on_headers('User-Agent')
def mobile_or_desktop(request):
    """Cache différent pour mobile vs desktop"""
    pass


# COMBINER plusieurs décorateurs:
# ═══════════════════════════════

@login_required
@permission_required('blog.add_post')
@require_POST
def create_post(request):
    """
    - User doit être connecté
    - User doit avoir permission add_post  
    - Méthode doit être POST
    """
    pass

# Ordre d'exécution: de bas en haut
# (require_POST -> permission_required -> login_required)


# ═══ 7.5 CLASS-BASED VIEWS (CBV) - INTRODUCTION ═══

# POURQUOI CBV?
# ═════════════

# PROBLÈME avec FBV pour opérations CRUD standard:
# ─────────────────────────────────────────────────

# Liste posts: 20 lignes
def post_list(request):
    posts = Post.objects.all()
    return render(request, 'list.html', {'posts': posts})

# Détail post: 15 lignes
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'detail.html', {'post': post})

# Créer post: 30 lignes
def post_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('post_list')
    else:
        form = PostForm()
    return render(request, 'form.html', {'form': form})

# Update post: 35 lignes
def post_update(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('post_detail', pk=pk)
    else:
        form = PostForm(instance=post)
    return render(request, 'form.html', {'form': form})

# Delete post: 25 lignes
def post_delete(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == 'POST':
        post.delete()
        return redirect('post_list')
    return render(request, 'confirm_delete.html', {'post': post})

# TOTAL: 125 lignes pour CRUD basique!
# -> Code répétitif (DRY violé!)
# -> Beaucoup de boilerplate


# SOLUTION: Class-Based Views (CBV)
# ──────────────────────────────────

# Django fournit classes génériques pour opérations courantes:

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView

# Liste: 3 lignes!
class PostListView(ListView):
    model = Post
    template_name = 'list.html'

# Détail: 3 lignes!
class PostDetailView(DetailView):
    model = Post
    template_name = 'detail.html'

# Créer: 4 lignes!
class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = 'form.html'

# Update: 4 lignes!
class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'form.html'

# Delete: 3 lignes!
class PostDeleteView(DeleteView):
    model = Post
    template_name = 'confirm_delete.html'
    success_url = reverse_lazy('post_list')

# TOTAL: 17 lignes pour même fonctionnalité!
# -> 125 lignes FBV vs 17 lignes CBV
# -> 7x moins de code!


# QUAND utiliser CBV vs FBV?
# ══════════════════════════

# [OK] UTILISER CBV QUAND:
# ─────────────────────
# • Opérations CRUD standard (liste, détail, create, update, delete)
# • Besoin de réutiliser logique (héritage classes)
# • Projet avec beaucoup de Models similaires
# • Équipe familière avec POO (Programmation Orientée Objet)

# [OK] UTILISER FBV QUAND:
# ─────────────────────
# • Logique custom complexe (pas CRUD standard)
# • View courte et simple
# • Débutant en Django (FBV plus faciles à comprendre)
# • Besoin de flexibilité maximale


# ═══ 7.6 GENERIC CLASS-BASED VIEWS ═══

# Django fournit 10 CBV génériques principales


# 1. View - CBV de base
# ══════════════════════

from django.views.generic import View
from django.http import HttpResponse

class MyView(View):
    """CBV la plus basique"""
    
    def get(self, request):
        """Gère requêtes GET"""
        return HttpResponse("GET request")
    
    def post(self, request):
        """Gère requêtes POST"""
        return HttpResponse("POST request")

# urls.py:
urlpatterns = [
    path('my-view/', MyView.as_view()),  # .as_view() OBLIGATOIRE!
]

# Avantage vs FBV:
# • Méthodes HTTP séparées (get, post, put, delete...)
# • Réutilisable par héritage


# 2. TemplateView - Afficher template simple
# ═══════════════════════════════════════════

from django.views.generic import TemplateView

class AboutView(TemplateView):
    template_name = 'about.html'
    
    def get_context_data(self, **kwargs):
        """Ajouter variables au contexte"""
        context = super().get_context_data(**kwargs)
        context['company'] = 'Ma Société'
        context['year'] = 2024
        return context

# urls.py:
path('about/', AboutView.as_view())

# Ou version ultra-courte directement dans urls.py:
path('about/', TemplateView.as_view(template_name='about.html'))


# 3. RedirectView - Redirection
# ══════════════════════════════

from django.views.generic import RedirectView

class GoToGoogleView(RedirectView):
    url = 'https://www.google.com'
    permanent = False  # 302 (temporaire) ou 301 (permanent)

# Ou redirection vers URL nommée:
class GoToHomeView(RedirectView):
    pattern_name = 'home'  # Nom URL
    permanent = True


# 4. ListView - Liste d'objets
# ═════════════════════════════

from django.views.generic import ListView

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'  # Nom variable dans template
    paginate_by = 10  # Pagination
    ordering = ['-created_at']  # Tri
    
    def get_queryset(self):
        """Filtrer queryset"""
        qs = super().get_queryset()
        return qs.filter(published=True)
    
    def get_context_data(self, **kwargs):
        """Ajouter données au contexte"""
        context = super().get_context_data(**kwargs)
        context['total_posts'] = Post.objects.count()
        return context

# Template 'blog/post_list.html':
{% for post in posts %}
    <h2>{{ post.title }}</h2>
    <p>{{ post.content }}</p>
{% endfor %}

# Pagination automatique:
{% if is_paginated %}
    <div class="pagination">
        {% if page_obj.has_previous %}
            <a href="?page=1">First</a>
            <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
        {% endif %}
        
        <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
        
        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">Next</a>
            <a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
        {% endif %}
    </div>
{% endif %}


# 5. DetailView - Détail d'un objet
# ══════════════════════════════════

from django.views.generic import DetailView

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    
    def get_object(self, queryset=None):
        """Customiser récupération objet"""
        obj = super().get_object(queryset)
        # Incrémenter compteur vues
        obj.views += 1
        obj.save()
        return obj

# urls.py:
path('post/<int:pk>/', PostDetailView.as_view(), name='post_detail')
# pk = primary key (ID) capturé depuis URL

# Template 'blog/post_detail.html':
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<small>Views: {{ post.views }}</small>


# 6. CreateView - Créer objet
# ════════════════════════════

from django.views.generic import CreateView
from django.urls import reverse_lazy

class PostCreateView(CreateView):
    model = Post
    form_class = PostForm  # Ou fields = ['title', 'content']
    template_name = 'blog/post_form.html'
    success_url = reverse_lazy('post_list')
    
    def form_valid(self, form):
        """Appelé si formulaire valide"""
        # Assigner auteur avant sauvegarde
        form.instance.author = self.request.user
        return super().form_valid(form)

# Template 'blog/post_form.html':
<h1>Créer Post</h1>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Créer</button>
</form>


# 7. UpdateView - Modifier objet
# ═══════════════════════════════

from django.views.generic import UpdateView

class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    success_url = reverse_lazy('post_list')
    
    def get_queryset(self):
        """Limiter objets modifiables"""
        qs = super().get_queryset()
        # User peut modifier seulement SES posts
        return qs.filter(author=self.request.user)

# urls.py:
path('post/<int:pk>/edit/', PostUpdateView.as_view(), name='post_edit')


# 8. DeleteView - Supprimer objet
# ════════════════════════════════

from django.views.generic import DeleteView

class PostDeleteView(DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('post_list')
    
    def delete(self, request, *args, **kwargs):
        """Logique avant suppression"""
        obj = self.get_object()
        # Log suppression
        print(f"Deleting post: {obj.title}")
        return super().delete(request, *args, **kwargs)

# Template 'blog/post_confirm_delete.html':
<h1>Supprimer Post?</h1>
<p>Êtes-vous sûr de vouloir supprimer "{{ post.title }}"?</p>
<form method="post">
    {% csrf_token %}
    <button type="submit">Oui, supprimer</button>
    <a href="{% url 'post_list' %}">Annuler</a>
</form>


# 9. FormView - Formulaire sans Model
# ════════════════════════════════════

from django.views.generic import FormView
from django.contrib import messages

class ContactView(FormView):
    template_name = 'contact.html'
    form_class = ContactForm
    success_url = reverse_lazy('home')
    
    def form_valid(self, form):
        """Traiter formulaire valide"""
        # Envoyer email
        name = form.cleaned_data['name']
        email = form.cleaned_data['email']
        message = form.cleaned_data['message']
        send_email(name, email, message)
        
        # Message succès
        messages.success(self.request, 'Message envoyé!')
        return super().form_valid(form)


# 10. Mixins - Combiner fonctionnalités
# ══════════════════════════════════════

# Mixin = Classe avec fonctionnalité réutilisable

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

class PostCreateView(LoginRequiredMixin, CreateView):
    """User doit être connecté"""
    model = Post
    form_class = PostForm
    template_name = 'form.html'
    login_url = '/login/'  # Optionnel

class PostDeleteView(PermissionRequiredMixin, DeleteView):
    """User doit avoir permission"""
    model = Post
    permission_required = 'blog.delete_post'
    template_name = 'confirm_delete.html'

# Autres mixins utiles:
# • UserPassesTestMixin - Test custom
# • PaginationMixin - Ajouter pagination
# • FormMixin - Ajouter formulaire


# ═══ 7.7 MÉTHODES CBV À OVERRIDER ═══

# CBV ont cycle de vie avec méthodes à overrider


# CYCLE DE VIE ListView:
# ═════════════════════

class MyListView(ListView):
    model = Post
    
    # 1. dispatch() - Point d'entrée
    def dispatch(self, request, *args, **kwargs):
        """Appelé EN PREMIER"""
        print("Before view execution")
        response = super().dispatch(request, *args, **kwargs)
        print("After view execution")
        return response
    
    # 2. get_queryset() - Récupérer objets
    def get_queryset(self):
        """Filtrer objets"""
        qs = super().get_queryset()
        return qs.filter(published=True)
    
    # 3. get_context_data() - Préparer contexte
    def get_context_data(self, **kwargs):
        """Ajouter variables"""
        context = super().get_context_data(**kwargs)
        context['extra_data'] = 'Hello'
        return context
    
    # 4. get_template_names() - Choisir template
    def get_template_names(self):
        """Template dynamique"""
        if self.request.user.is_staff:
            return ['staff_list.html']
        return ['public_list.html']


# CYCLE DE VIE CreateView:
# ════════════════════════

class MyCreateView(CreateView):
    model = Post
    form_class = PostForm
    
    # 1. get_form_class() - Choisir formulaire
    def get_form_class(self):
        """Form différent selon user"""
        if self.request.user.is_staff:
            return StaffPostForm
        return PostForm
    
    # 2. get_form_kwargs() - Paramètres form
    def get_form_kwargs(self):
        """Passer request au form"""
        kwargs = super().get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs
    
    # 3. form_valid() - Form valide
    def form_valid(self, form):
        """Avant sauvegarde"""
        form.instance.author = self.request.user
        messages.success(self.request, 'Post créé!')
        return super().form_valid(form)
    
    # 4. form_invalid() - Form invalide
    def form_invalid(self, form):
        """En cas d'erreur"""
        messages.error(self.request, 'Erreur formulaire')
        return super().form_invalid(form)
    
    # 5. get_success_url() - URL après succès
    def get_success_url(self):
        """Redirection dynamique"""
        return reverse('post_detail', kwargs={'pk': self.object.pk})


# ═══ 7.8 FBV vs CBV - COMPARAISON COMPLÈTE ═══

# Même fonctionnalité implémentée des 2 façons


# EXEMPLE: CRUD complet pour Model Post
# ══════════════════════════════════════

# ━━━ VERSION FBV (Function-Based Views) ━━━

# views.py:
from django.shortcuts import render, get_object_or_404, redirect
from .models import Post
from .forms import PostForm

# Liste (20 lignes)
def post_list(request):
    posts = Post.objects.filter(published=True).order_by('-created_at')
    return render(request, 'blog/post_list.html', {'posts': posts})

# Détail (10 lignes)
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

# Créer (25 lignes)
from django.contrib.auth.decorators import login_required

@login_required
def post_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm()
    return render(request, 'blog/post_form.html', {'form': form})

# Modifier (30 lignes)
@login_required
def post_update(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if post.author != request.user:
        return HttpResponseForbidden("Pas votre post!")
    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
    return render(request, 'blog/post_form.html', {'form': form, 'post': post})

# Supprimer (20 lignes)
@login_required
def post_delete(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if post.author != request.user:
        return HttpResponseForbidden("Pas votre post!")
    if request.method == 'POST':
        post.delete()
        return redirect('post_list')
    return render(request, 'blog/post_confirm_delete.html', {'post': post})

# TOTAL FBV: ~105 lignes


# ━━━ VERSION CBV (Class-Based Views) ━━━

# views.py:
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm

# Liste (5 lignes)
class PostListView(ListView):
    model = Post
    queryset = Post.objects.filter(published=True).order_by('-created_at')
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'

# Détail (4 lignes)
class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

# Créer (8 lignes)
class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

# Modifier (10 lignes)
class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(author=self.request.user)

# Supprimer (7 lignes)
class PostDeleteView(LoginRequiredMixin, DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('post_list')
    
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(author=self.request.user)

# TOTAL CBV: ~34 lignes

# [RAPIDE] CBV = 3x moins de code!


# URLs identiques pour les deux:
# ═══════════════════════════════

urlpatterns = [
    # FBV
    path('posts/', post_list, name='post_list'),
    path('post/<int:pk>/', post_detail, name='post_detail'),
    path('post/create/', post_create, name='post_create'),
    path('post/<int:pk>/edit/', post_update, name='post_edit'),
    path('post/<int:pk>/delete/', post_delete, name='post_delete'),
    
    # CBV (ajouter .as_view())
    path('posts/', PostListView.as_view(), name='post_list'),
    path('post/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
    path('post/create/', PostCreateView.as_view(), name='post_create'),
    path('post/<int:pk>/edit/', PostUpdateView.as_view(), name='post_edit'),
    path('post/<int:pk>/delete/', PostDeleteView.as_view(), name='post_delete'),
]


# TABLEAU COMPARATIF FINAL:
# ═════════════════════════

# ┌─────────────────────┬──────────────┬──────────────┐
# │ Critère             │ FBV          │ CBV          │
# ├─────────────────────┼──────────────┼──────────────┤
# │ Lignes code CRUD    │ ~105         │ ~34          │
# │ Facilité débutant   │ *****      │ ***         │
# │ Flexibilité         │ *****      │ ****        │
# │ DRY (réutilisable)  │ **          │ *****      │
# │ Performance         │ identique    │ identique    │
# │ Debugging           │ *****      │ ***         │
# │ Code explicite      │ *****      │ **          │
# │ CRUD rapide         │ **          │ *****      │
# └─────────────────────┴──────────────┴──────────────┘


# RECOMMANDATION:
# ══════════════

# DÉBUTANTS: Commencez avec FBV
# -> Plus facile à comprendre
# -> Code explicite, pas de magie
# -> Excellentes bases Django

# APRÈS 2-3 MOIS: Apprenez CBV
# -> Écrivez CRUD 3x plus vite
# -> Code plus maintenable
# -> Standard industrie


# ═══ 7.9 RÉSUMÉ PARTIE 7: VIEWS ═══

# Ce que vous avez appris:
# ────────────────────────
# [OK] Views = cerveau de Django (logique métier MTV)
# [OK] FBV (Function-Based Views): simples, flexibles, débutant-friendly
# [OK] request object: toutes infos requête HTTP
# [OK] Shortcuts: render(), redirect(), get_object_or_404()
# [OK] Décorateurs: @login_required, @require_POST, @cache_page
# [OK] CBV (Class-Based Views): 3x moins code pour CRUD
# [OK] Generic CBV: ListView, DetailView, CreateView, UpdateView, DeleteView
# [OK] Mixins: LoginRequiredMixin, PermissionRequiredMixin
# [OK] Cycle de vie CBV: dispatch -> get_queryset -> get_context_data
# [OK] FBV vs CBV: FBV pour flexibilité, CBV pour CRUD rapide

# Fichiers clés:
# ─────────────
# views.py        <- Toutes vos Views (FBV et CBV)
# urls.py         <- Mappage URL -> View
# models.py       <- Models utilisés par Views
# forms.py        <- Formulaires utilisés par Views

# Syntaxe clés:
# ────────────
# FBV:
def ma_view(request):
    return HttpResponse("Hello")

# CBV:
class MaView(View):
    def get(self, request):
        return HttpResponse("Hello")

# URL FBV:
path('url/', views.ma_view)

# URL CBV:
path('url/', MaView.as_view())  # .as_view() OBLIGATOIRE!


# ═══════════════════════════════════════════════════════════════════
# PARTIE 8: TEMPLATES - SYSTÈME DE RENDU HTML
# ═══════════════════════════════════════════════════════════════════


# ═══ 8.1 QU'EST-CE QU'UN TEMPLATE? ═══

# POURQUOI les Templates?
# ═══════════════════════

# PROBLÈME: Générer HTML dans Python (sans template)
# ──────────────────────────────────────────────────

# views.py:
def post_list(request):
    posts = Post.objects.all()
    
    # Construire HTML manuellement (HORRIBLE!)
    html = """
    <!DOCTYPE html>
    <html>
    <head><title>Blog</title></head>
    <body>
        <h1>Articles</h1>
    """
    
    for post in posts:
        html += f"""
        <article>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
        </article>
        """
    
    html += """
    </body>
    </html>
    """
    
    return HttpResponse(html)

# PROBLÈMES:
# • HTML mêlé au code Python (cauchemar maintenance!)
# • Failles XSS si user input pas échappé
# • Impossible pour designer de modifier layout
# • Pas de réutilisation (copier-coller partout)
# • Syntaxe illisible


# SOLUTION: Système de Templates Django
# ────────────────────────────────────

# views.py:
def post_list(request):
    posts = Post.objects.all()
    return render(request, 'blog/post_list.html', {'posts': posts})

# templates/blog/post_list.html:
<!DOCTYPE html>
<html>
<head><title>Blog</title></head>
<body>
    <h1>Articles</h1>
    {% for post in posts %}
        <article>
            <h2>{{ post.title }}</h2>
            <p>{{ post.content }}</p>
        </article>
    {% endfor %}
</body>
</html>

# AVANTAGES:
# [OK] Séparation HTML/Python (designer peut travailler sur HTML)
# [OK] Échappement XSS automatique (sécurité)
# [OK] Réutilisation (héritage, includes)
# [OK] Syntaxe propre et lisible


# ANALOGIE: Restaurant et Menu
# ════════════════════════════

# SANS templates (HTML dans Python):
# ─────────────────────────────────
# Serveur doit ÉCRIRE chaque menu à la main pour chaque client
# "Entrées: Salade, Soupe. Plats: Steak, Poisson. Desserts: Tarte..."
# -> Long, erreurs, incohérent

# AVEC templates:
# ──────────────
# Un MODÈLE de menu existe déjà
# Serveur remplit juste les blancs:
# "Entrée du jour: ____", "Plat du jour: ____"
# -> Rapide, cohérent, professionnel

# Template = Modèle réutilisable avec "trous" à remplir!


# ═══ 8.2 CONFIGURATION DES TEMPLATES ═══

# COMMENT Django trouve les templates?
# ═════════════════════════════════════

# settings.py:
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / 'templates',  # Dossier templates global
        ],
        'APP_DIRS': True,  # Chercher dans app/templates/
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Django cherche templates dans CET ORDRE:
# 1. Dans DIRS (templates/ à racine projet)
# 2. Dans chaque app installée: app/templates/


# STRUCTURE RECOMMANDÉE:
# ═════════════════════

# Option 1: Templates dans apps (RECOMMANDÉ)
# ──────────────────────────────────────────
mon_projet/
├── blog/
│   ├── templates/
│   │   └── blog/           # <- Namespace app
│   │       ├── post_list.html
│   │       ├── post_detail.html
│   │       └── post_form.html
│   ├── models.py
│   └── views.py
├── shop/
│   ├── templates/
│   │   └── shop/           # <- Namespace app
│   │       ├── product_list.html
│   │       └── product_detail.html
│   ├── models.py
│   └── views.py
└── manage.py

# POURQUOI namespace (blog/post_list.html)?
# • Évite conflits: blog/base.html vs shop/base.html
# • Organisation claire
# • Standard Django

# Usage:
render(request, 'blog/post_list.html')  # <- Inclure namespace!


# Option 2: Templates globaux (ALTERNATIVE)
# ─────────────────────────────────────────
mon_projet/
├── templates/              # <- Dossier global
│   ├── base.html          # Template parent
│   ├── home.html
│   └── about.html
├── blog/
│   ├── models.py
│   └── views.py
└── manage.py

# Usage:
render(request, 'base.html')


# Option 3: HYBRIDE (MEILLEURE PRATIQUE)
# ──────────────────────────────────────
mon_projet/
├── templates/              # Templates partagés
│   ├── base.html          # Layout principal
│   ├── navbar.html        # Navigation
│   └── footer.html
├── blog/
│   └── templates/blog/    # Templates blog
│       └── post_list.html
└── shop/
    └── templates/shop/    # Templates shop
        └── product_list.html


# ═══ 8.3 SYNTAXE DES TEMPLATES ═══

# Templates Django utilisent 3 types de balises


# 1. VARIABLES: {{ variable }}
# ═══════════════════════════

# Vue:
def my_view(request):
    context = {
        'name': 'Jean',
        'age': 30,
        'email': 'jean@mail.com'
    }
    return render(request, 'profile.html', context)

# Template:
<h1>Profil de {{ name }}</h1>
<p>Age: {{ age }} ans</p>
<p>Email: {{ email }}</p>

# Rendu HTML:
<h1>Profil de Jean</h1>
<p>Age: 30 ans</p>
<p>Email: jean@mail.com</p>


# 2. TAGS: {% tag %}
# ══════════════════

# Tags = Logique de programmation

# if/elif/else:
{% if user.is_authenticated %}
    <p>Bienvenue {{ user.username }}!</p>
{% else %}
    <p>Connectez-vous svp</p>
{% endif %}

# for loop:
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}

# for avec empty:
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% empty %}
    <p>Aucun post</p>
{% endfor %}


# 3. FILTRES: {{ variable|filtre }}
# ═════════════════════════════════

# Filtres = Transformer valeur

{{ name|upper }}           # JEAN
{{ name|lower }}           # jean
{{ name|title }}           # Jean
{{ content|truncatewords:10 }}  # Tronquer à 10 mots
{{ date|date:"d/m/Y" }}    # 15/01/2024


# ═══ 8.4 VARIABLES ET ACCÈS AUX DONNÉES ═══

# ACCÈS ATTRIBUTS:
# ───────────────

# Python:
class Post:
    title = "Mon Article"
    author = User(username="jean")

# Template:
{{ post.title }}           # Mon Article
{{ post.author.username }} # jean
{{ post.author.email }}    # jean@mail.com


# ACCÈS DICTIONNAIRE:
# ──────────────────

# Python:
context = {
    'data': {
        'name': 'Jean',
        'city': 'Paris'
    }
}

# Template:
{{ data.name }}  # Jean
{{ data.city }}  # Paris


# ACCÈS LISTE:
# ───────────

# Python:
context = {
    'colors': ['red', 'green', 'blue']
}

# Template:
{{ colors.0 }}   # red (premier élément)
{{ colors.1 }}   # green
{{ colors.2 }}   # blue


# APPEL MÉTHODE (sans parenthèses!):
# ──────────────────────────────────

# Python:
class Post:
    def get_excerpt(self):
        return self.content[:100]

# Template:
{{ post.get_excerpt }}  # <- PAS de ()!
# Django appelle méthode automatiquement


# ═══ 8.5 FILTRES DJANGO ═══

# Filtres transforment variables


# FILTRES TEXTE:
# ═════════════

{{ name|upper }}              # JEAN
{{ name|lower }}              # jean  
{{ name|title }}              # Jean Dupont
{{ name|capfirst }}           # Jean (première lettre maj)
{{ text|truncatewords:10 }}   # Tronque à 10 mots
{{ text|truncatechars:50 }}   # Tronque à 50 caractères
{{ text|wordcount }}          # Compte mots: 42
{{ text|length }}             # Longueur: 156
{{ text|linebreaks }}         # \n -> <p> et <br>
{{ text|linebreaksbr }}       # \n -> <br> seulement
{{ text|striptags }}          # Enlever tags HTML
{{ text|safe }}               # [ATTENTION] Désactive échappement HTML
{{ html|escape }}             # Échappe HTML manuellement


# FILTRES NOMBRES:
# ═══════════════

{{ value|add:5 }}             # Ajouter 5
{{ value|floatformat:2 }}     # 2 décimales: 3.14
{{ number|filesizeformat }}   # 1024 -> 1.0 KB


# FILTRES DATES:
# ═════════════

{{ date|date:"d/m/Y" }}       # 15/01/2024
{{ date|date:"d F Y" }}       # 15 Janvier 2024
{{ date|date:"H:i" }}         # 14:30
{{ date|time:"H:i:s" }}       # 14:30:45
{{ date|timesince }}          # "2 jours ago"
{{ date|timeuntil }}          # "dans 3 heures"


# FILTRES LISTES:
# ══════════════

{{ my_list|first }}           # Premier élément
{{ my_list|last }}            # Dernier élément
{{ my_list|length }}          # Nombre d'éléments
{{ my_list|join:", " }}       # "a, b, c"
{{ my_list|random }}          # Élément aléatoire
{{ my_list|slice:":5" }}      # 5 premiers


# FILTRES LOGIQUES:
# ════════════════

{{ value|default:"N/A" }}     # Si None/False/Empty: "N/A"
{{ value|default_if_none:"N/A" }}  # Si None: "N/A"
{{ value|yesno:"Oui,Non,Peut-être" }}  # True->Oui, False->Non, None->Peut-être


# FILTRES URLS:
# ════════════

{{ url|urlencode }}           # Encoder URL
{{ text|slugify }}            # "Mon Titre" -> "mon-titre"


# CHAÎNER FILTRES:
# ═══════════════

{{ post.title|lower|truncatewords:5 }}
# 1. Convertir minuscules
# 2. Tronquer 5 mots


# ═══ 8.6 TAGS DJANGO ═══

# Tags = Logique programmation


# TAG: if / elif / else
# ════════════════════════

{% if user.is_authenticated %}
    <p>Bonjour {{ user.username }}</p>
{% elif user.is_anonymous %}
    <p>Vous êtes anonyme</p>
{% else %}
    <p>Erreur</p>
{% endif %}

# Opérateurs:
{% if age >= 18 %}         # >=, <=, <, >
{% if name == "Jean" %}    # ==, !=
{% if x and y %}           # and, or, not
{% if x in list %}         # in, not in


# TAG: for
# ═══════

{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}

# Variables loop spéciales:
{% for post in posts %}
    {{ forloop.counter }}      # 1, 2, 3...
    {{ forloop.counter0 }}     # 0, 1, 2...
    {{ forloop.revcounter }}   # Compte à rebours
    {{ forloop.first }}        # True si premier
    {{ forloop.last }}         # True si dernier
{% endfor %}

# Exemple:
{% for post in posts %}
    {% if forloop.first %}
        <h2>Post le plus récent:</h2>
    {% endif %}
    <article>{{ post.title }}</article>
{% endfor %}

# for avec empty:
{% for post in posts %}
    <p>{{ post.title }}</p>
{% empty %}
    <p>Aucun post</p>
{% endfor %}


# TAG: url (URLs nommées)
# ══════════════════════

# urls.py:
path('post/<int:pk>/', views.post_detail, name='post_detail')

# Template:
<a href="{% url 'post_detail' pk=42 %}">Article #42</a>
# Génère: <a href="/post/42/">Article #42</a>

# Avantages:
# • Si URL change dans urls.py, templates mis à jour auto!
# • Pas de hardcoded URLs


# TAG: csrf_token (OBLIGATOIRE pour forms)
# ═══════════════════════════════════════

<form method="post">
    {% csrf_token %}  # <- OBLIGATOIRE!
    <input type="text" name="title">
    <button>Envoyer</button>
</form>

# Génère token CSRF caché:
<input type="hidden" name="csrfmiddlewaretoken" value="random_token">

# Protection contre CSRF attacks!


# TAG: static (fichiers statiques)
# ════════════════════════════════

{% load static %}

<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}">
<script src="{% static 'js/app.js' %}"></script>

# Génère URLs correctes même en production!


# TAG: with (variable temporaire)
# ═══════════════════════════════

{% with total=posts.count %}
    <p>Total: {{ total }} posts</p>
    {% if total > 10 %}
        <p>Beaucoup de posts!</p>
    {% endif %}
{% endwith %}

# Utile pour calculs coûteux (évite répéter)


# TAG: now (date actuelle)
# ═══════════════════════

<p>Aujourd'hui: {% now "d/m/Y" %}</p>
# Affiche: Aujourd'hui: 15/01/2024


# TAG: comment (commentaires)
# ═══════════════════════════

{# Commentaire une ligne #}

{% comment %}
Commentaire
multi-lignes
{% endcomment %}


# TAG: spaceless (supprimer whitespace)
# ════════════════════════════════════

{% spaceless %}
    <div>
        <span>Hello</span>
    </div>
{% endspaceless %}

# Génère: <div><span>Hello</span></div>
# (supprime espaces entre tags)


# TAG: cycle (alterner valeurs)
# ════════════════════════════

<table>
{% for post in posts %}
    <tr class="{% cycle 'odd' 'even' %}">
        <td>{{ post.title }}</td>
    </tr>
{% endfor %}
</table>

# Génère:
# <tr class="odd">...
# <tr class="even">...
# <tr class="odd">...
# <tr class="even">...


# ═══ 8.7 HÉRITAGE DE TEMPLATES ═══

# POURQUOI l'héritage?
# ═══════════════════

# PROBLÈME sans héritage:
# ──────────────────────
# Chaque template répète header/footer/nav

# post_list.html:
<!DOCTYPE html>
<html>
<head><title>Blog</title></head>
<body>
    <nav>...</nav>           # <- Répété
    
    <h1>Posts</h1>
    <!-- Contenu spécifique -->
    
    <footer>...</footer>     # <- Répété
</body>
</html>

# post_detail.html:
<!DOCTYPE html>
<html>
<head><title>Blog</title></head>
<body>
    <nav>...</nav>           # <- Répété encore!
    
    <h1>Détail</h1>
    <!-- Contenu spécifique -->
    
    <footer>...</footer>     # <- Répété encore!
</body>
</html>

# Modifier navbar -> Changer dans 10+ fichiers!


# SOLUTION: Héritage (DRY!)
# ─────────────────────────

# 1. Créer template PARENT (base.html):
# ─────────────────────────────────────

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Mon Site{% endblock %}</title>
    {% block extra_css %}{% endblock %}
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/blog/">Blog</a>
    </nav>
    
    <main>
        {% block content %}
        <!-- Contenu par défaut -->
        {% endblock %}
    </main>
    
    <footer>
        <p>&copy; 2024 Mon Site</p>
    </footer>
    
    {% block extra_js %}{% endblock %}
</body>
</html>


# 2. Templates ENFANTS héritent:
# ──────────────────────────────

# post_list.html:
{% extends 'base.html' %}  # <- Hérite de base.html

{% block title %}Liste des Posts{% endblock %}

{% block content %}
    <h1>Tous les Posts</h1>
    {% for post in posts %}
        <article>{{ post.title }}</article>
    {% endfor %}
{% endblock %}


# post_detail.html:
{% extends 'base.html' %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
{% endblock %}

{% block extra_css %}
    <style>
        article { padding: 20px; }
    </style>
{% endblock %}


# RÈGLES HÉRITAGE:
# ═══════════════

# 1. {% extends %} DOIT être première ligne
# [OK] BON:
{% extends 'base.html' %}
{% block content %}...

# [X] MAUVAIS:
<h1>Titre</h1>
{% extends 'base.html' %}  # <- ERREUR!


# 2. Tout en dehors des blocks est IGNORÉ
# [X] MAUVAIS:
{% extends 'base.html' %}
<p>Ce texte est IGNORÉ!</p>  # Pas dans block!
{% block content %}
    <p>Ce texte OK</p>
{% endblock %}


# 3. {{ block.super }} = Garder contenu parent
{% extends 'base.html' %}

{% block content %}
    {{ block.super }}  # <- Garde contenu de base.html
    <p>Ajout enfant</p>
{% endblock %}


# 4. Niveaux multiples (grand-parent -> parent -> enfant)

# base.html (grand-parent):
{% block content %}{% endblock %}

# blog_base.html (parent):
{% extends 'base.html' %}
{% block content %}
    <div class="blog-container">
        {% block blog_content %}{% endblock %}
    </div>
{% endblock %}

# post_list.html (enfant):
{% extends 'blog/blog_base.html' %}
{% block blog_content %}
    <h1>Posts</h1>
{% endblock %}


# ═══ 8.8 INCLUDES (Réutilisation partielle) ═══

# POURQUOI includes?
# ═════════════════

# Réutiliser morceaux HTML dans plusieurs templates

# SANS includes:
# ─────────────
# Copier-coller code de card dans 5 templates:

<div class="card">
    <img src="{{ post.image.url }}">
    <h3>{{ post.title }}</h3>
    <p>{{ post.excerpt }}</p>
    <a href="{{ post.get_absolute_url }}">Lire</a>
</div>

# Modifier style card -> Changer 5 fichiers!


# AVEC includes:
# ─────────────

# 1. Créer partial: templates/partials/_post_card.html
<div class="card">
    <img src="{{ post.image.url }}">
    <h3>{{ post.title }}</h3>
    <p>{{ post.excerpt }}</p>
    <a href="{{ post.get_absolute_url }}">Lire</a>
</div>


# 2. Inclure dans templates:
# post_list.html:
<h1>Posts</h1>
<div class="grid">
    {% for post in posts %}
        {% include 'partials/_post_card.html' %}
    {% endfor %}
</div>

# home.html:
<h2>Posts récents</h2>
{% for post in recent_posts %}
    {% include 'partials/_post_card.html' %}
{% endfor %}

# Maintenant modifier _post_card.html -> Change partout!


# INCLUDES avec variables custom:
# ═══════════════════════════════

# _button.html:
<button class="{{ btn_class }}">
    {{ btn_text }}
</button>

# Utilisation:
{% include 'partials/_button.html' with btn_class='primary' btn_text='Sauvegarder' %}
{% include 'partials/_button.html' with btn_class='danger' btn_text='Supprimer' %}


# INCLUDES conditionnels:
# ══════════════════════

{% include 'partials/_sidebar.html' only %}  
# only = Passe SEULEMENT variables explicites (pas tout le contexte)


# ═══ 8.9 CUSTOM TEMPLATE TAGS ET FILTRES ═══

# POURQUOI créer tags/filtres custom?
# ═══════════════════════════════════

# Django fournit 100+ filtres, mais parfois vous avez besoin de logique spécifique


# CRÉER FILTRE CUSTOM:
# ═══════════════════

# 1. Structure dossier:
blog/
├── templatetags/           # <- Dossier obligatoire
│   ├── __init__.py        # <- Fichier vide obligatoire
│   └── blog_extras.py     # <- Vos filtres custom
├── templates/
├── models.py
└── views.py


# 2. Définir filtre:
# blog/templatetags/blog_extras.py:

from django import template

register = template.Library()  # OBLIGATOIRE

@register.filter
def markdown_to_html(text):
    """Convertir Markdown -> HTML"""
    import markdown
    return markdown.markdown(text)

@register.filter
def multiply(value, arg):
    """Multiplier: {{ 5|multiply:3 }} -> 15"""
    return value * arg

@register.filter(name='cut_spaces')
def remove_spaces(value):
    """Enlever espaces"""
    return value.replace(' ', '')


# 3. Utiliser dans template:
{% load blog_extras %}  # <- Charger filtres custom

<div>
    {{ post.content|markdown_to_html }}
</div>

<p>Prix: {{ price|multiply:1.2 }}</p>
<p>{{ "Hello World"|cut_spaces }}</p>  # HelloWorld


# CRÉER TAG CUSTOM:
# ════════════════

# blog/templatetags/blog_extras.py:

@register.simple_tag
def current_time(format_string):
    """Afficher heure actuelle"""
    from datetime import datetime
    return datetime.now().strftime(format_string)

@register.simple_tag
def multiply_values(a, b):
    """Multiplier deux valeurs"""
    return a * b


# Utilisation:
{% load blog_extras %}

<p>Il est {% current_time "%H:%M" %}</p>
<p>Total: {% multiply_values 10 5 %}</p>


# TAG AVEC CONTEXTE:
# ═════════════════

@register.simple_tag(takes_context=True)
def greeting(context):
    """Salutation basée sur user"""
    user = context['user']
    if user.is_authenticated:
        return f"Bonjour {user.username}!"
    return "Bonjour visiteur!"

# Template:
{% greeting %}  # Bonjour jean!


# INCLUSION TAG (render template):
# ═══════════════════════════════

@register.inclusion_tag('partials/_latest_posts.html')
def show_latest_posts(count=5):
    """Afficher derniers posts"""
    posts = Post.objects.order_by('-created_at')[:count]
    return {'posts': posts}

# partials/_latest_posts.html:
<h3>Posts récents</h3>
<ul>
{% for post in posts %}
    <li>{{ post.title }}</li>
{% endfor %}
</ul>

# Utilisation:
{% load blog_extras %}
{% show_latest_posts 3 %}  # Affiche 3 derniers posts


# ═══ 8.10 CONTEXTE PROCESSORS ═══

# POURQUOI context processors?
# ════════════════════════════

# PROBLÈME: Variables répétées dans chaque view
# ─────────────────────────────────────────────

def view1(request):
    return render(request, 'page1.html', {
        'site_name': 'Mon Blog',  # <- Répété
        'current_year': 2024      # <- Répété
    })

def view2(request):
    return render(request, 'page2.html', {
        'site_name': 'Mon Blog',  # <- Répété encore!
        'current_year': 2024      # <- Répété encore!
    })

# Répété dans 50 views!


# SOLUTION: Context processor
# ──────────────────────────

# 1. Créer context processor:
# blog/context_processors.py:

def site_settings(request):
    """Variables disponibles PARTOUT"""
    return {
        'site_name': 'Mon Blog',
        'current_year': 2024,
        'google_analytics_id': 'UA-12345'
    }


# 2. Enregistrer dans settings.py:
TEMPLATES = [
    {
        'OPTIONS': {
            'context_processors': [
                # Processors Django par défaut
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                # VOTRE processor
                'blog.context_processors.site_settings',  # <- Ajouter
            ],
        },
    },
]


# 3. Utiliser dans N'IMPORTE QUEL template:
# Pas besoin de passer depuis view!

<footer>
    <p>&copy; {{ current_year }} {{ site_name }}</p>
</footer>

# Variables disponibles dans TOUS les templates!


# CONTEXT PROCESSORS DJANGO PAR DÉFAUT:
# ═════════════════════════════════════

# 1. django.template.context_processors.request
#    -> Variable 'request' disponible partout
{{ request.user.username }}
{{ request.path }}  # /blog/posts/

# 2. django.contrib.auth.context_processors.auth
#    -> Variable 'user' disponible partout
{% if user.is_authenticated %}
    <p>Bonjour {{ user.username }}</p>
{% endif %}

# 3. django.contrib.messages.context_processors.messages
#    -> Variable 'messages' pour flash messages
{% if messages %}
    {% for message in messages %}
        <div class="alert">{{ message }}</div>
    {% endfor %}
{% endif %}


# ═══ 8.11 ERREURS COURANTES ET SOLUTIONS ═══

# ERREUR 1: TemplateDoesNotExist
# ───────────────────────────────

# Message: TemplateDoesNotExist at /blog/
# blog/post_list.html

# CAUSES:
# • Fichier mal nommé (faute frappe)
# • Fichier dans mauvais dossier
# • APP_DIRS = False dans settings.py

# SOLUTIONS:
# 1. Vérifier chemin exact:
render(request, 'blog/post_list.html')  # Doit correspondre à:
# -> blog/templates/blog/post_list.html

# 2. Vérifier APP_DIRS activé:
TEMPLATES = [{
    'APP_DIRS': True,  # <- DOIT être True
}]

# 3. Vérifier app dans INSTALLED_APPS:
INSTALLED_APPS = [
    'blog',  # <- Doit être là
]


# ERREUR 2: TemplateSyntaxError
# ──────────────────────────────

# Message: Invalid block tag: 'endfor'

# CAUSE: Tags mal fermés

# [X] MAUVAIS:
{% for post in posts %}
    <p>{{ post.title }}</p>
{% endif %}  # <- ERREUR! (endfor attendu)

# [OK] BON:
{% for post in posts %}
    <p>{{ post.title }}</p>
{% endfor %}


# ERREUR 3: Variable undefined
# ─────────────────────────────

# Template affiche rien (ou vide)

# CAUSE: Variable pas dans contexte

# View:
return render(request, 'page.html', {
    'posts': posts  # <- Variable 'posts'
})

# Template:
{{ post }}  # <- ERREUR: 'post' pas défini (c'est 'posts'!)

# SOLUTION: Vérifier nom exact variable


# ERREUR 4: Forgot {% csrf_token %}
# ──────────────────────────────────

# Message: CSRF verification failed

# CAUSE: {% csrf_token %} manquant dans <form>

# [X] MAUVAIS:
<form method="post">
    <input type="text" name="title">
</form>

# [OK] BON:
<form method="post">
    {% csrf_token %}  # <- OBLIGATOIRE!
    <input type="text" name="title">
</form>


# ERREUR 5: {% extends %} pas en premier
# ───────────────────────────────────────

# [X] MAUVAIS:
<h1>Titre</h1>
{% extends 'base.html' %}

# [OK] BON:
{% extends 'base.html' %}
<h1>Dans block seulement</h1>  # <- Sera ignoré si pas dans block!


# ═══ 8.12 BEST PRACTICES TEMPLATES ═══

# [OK] BONNES PRATIQUES:
# ═══════════════════

# 1. Toujours utiliser {% load static %}
# ──────────────────────────────────────
{% load static %}
<link href="{% static 'css/style.css' %}" rel="stylesheet">
# [X] PAS: <link href="/static/css/style.css">


# 2. Nommer templates avec namespace app
# ───────────────────────────────────────
# [OK] BON: blog/templates/blog/post_list.html
# [X] MAUVAIS: blog/templates/post_list.html


# 3. Utiliser héritage (DRY)
# ──────────────────────────
# Créer base.html avec structure commune
# Tous templates héritent


# 4. Séparer en partials réutilisables
# ─────────────────────────────────────
# templates/
# ├── base.html
# ├── partials/
# │   ├── _navbar.html
# │   ├── _footer.html
# │   └── _post_card.html


# 5. {% csrf_token %} TOUJOURS dans forms POST
# ────────────────────────────────────────────
<form method="post">
    {% csrf_token %}
    ...
</form>


# 6. Échapper HTML par défaut (sécurité)
# ──────────────────────────────────────
{{ user_input }}  # <- Échappé automatiquement
{{ trusted_html|safe }}  # <- Seulement si HTML trusté!


# 7. Utiliser {% url %} au lieu de hardcoded URLs
# ────────────────────────────────────────────────
# [OK] BON:
<a href="{% url 'post_detail' pk=post.pk %}">

# [X] MAUVAIS:
<a href="/post/{{ post.pk }}/">


# 8. Limiter logique dans templates
# ──────────────────────────────────
# [X] MAUVAIS: Logique complexe dans template
{% if user.is_authenticated and user.is_staff and not user.is_banned and user.posts.count > 10 %}

# [OK] BON: Logique dans view/model
# models.py:
def can_post(self):
    return self.is_authenticated and self.is_staff and ...

# Template:
{% if user.can_post %}


# 9. Commenter templates complexes
# ─────────────────────────────────
{# Section de pagination #}
{% if is_paginated %}
    ...
{% endif %}


# 10. Éviter logique business dans templates
# ──────────────────────────────────────────
# [X] MAUVAIS:
{% for post in all_posts %}
    {% if post.published and post.date > today %}
        ...
    {% endif %}
{% endfor %}

# [OK] BON: Filtrer dans view
# views.py:
posts = Post.objects.filter(published=True, date__gt=today)


# ═══ 8.13 RÉSUMÉ PARTIE 8: TEMPLATES ═══

# Ce que vous avez appris:
# ────────────────────────
# [OK] Templates = HTML avec variables Django {{ }} et tags {% %}
# [OK] Séparation HTML/Python (designer vs developer)
# [OK] Syntaxe: {{ variable }}, {% tag %}, {{ var|filtre }}
# [OK] Héritage: {% extends %} + {% block %} (DRY)
# [OK] Includes: {% include %} (réutilisation partials)
# [OK] Filtres: |upper, |date, |truncatewords...
# [OK] Tags: {% if %}, {% for %}, {% url %}, {% csrf_token %}
# [OK] Custom tags/filtres: templatetags/
# [OK] Context processors: Variables globales automatiques
# [OK] Best practices: namespace, héritage, échappement, {% url %}

# Fichiers clés:
# ─────────────
# settings.py              <- Configuration TEMPLATES
# app/templates/app/       <- Templates avec namespace
# templates/base.html      <- Template parent
# app/templatetags/        <- Tags/filtres custom
# app/context_processors.py <- Variables globales

# Syntaxe clés:
# ────────────
# {{ variable }}            <- Afficher variable
# {{ var|filtre }}          <- Appliquer filtre
# {% tag %}                 <- Exécuter tag
# {% extends 'base.html' %} <- Hériter
# {% block name %}{% endblock %} <- Définir block
# {% include 'partial.html' %} <- Inclure
# {% load static %}         <- Charger fichiers statiques
# {% csrf_token %}          <- Token CSRF (forms)
# {% url 'name' param=value %} <- Générer URL


# ═══════════════════════════════════════════════════════════════════
# PARTIE 9: FORMULAIRES - GESTION ET VALIDATION
# ═══════════════════════════════════════════════════════════════════

# [Partie 9 sera ajoutée ensuite...]


# ═══════════════════════════════════════════════════════════════════
# PARTIE 9: FORMULAIRES - VALIDATION ET TRAITEMENT DES DONNÉES
# ═══════════════════════════════════════════════════════════════════


# ═══ 9.1 POURQUOI LES FORMULAIRES DJANGO? ═══

# PROBLÈME: Formulaires HTML bruts
# ═════════════════════════════════

# SANS Django Forms:
# ─────────────────

# Template HTML:
<form method="post">
    <input type="text" name="title">
    <input type="email" name="email">
    <button>Envoyer</button>
</form>

# View (traitement manuel):
def create_post(request):
    if request.method == 'POST':
        # 1. Récupérer données
        title = request.POST.get('title')
        email = request.POST.get('email')
        
        # 2. Valider MANUELLEMENT (100+ lignes!)
        errors = {}
        if not title:
            errors['title'] = "Titre requis"
        if len(title) > 200:
            errors['title'] = "Titre trop long (max 200)"
        if not email:
            errors['email'] = "Email requis"
        if '@' not in email:
            errors['email'] = "Email invalide"
        
        # 3. Échapper HTML manuellement (XSS!)
        from html import escape
        title = escape(title)
        
        # 4. Si erreurs, réafficher form avec valeurs
        if errors:
            return render(request, 'form.html', {
                'errors': errors,
                'title': title,  # Garder valeur saisie
                'email': email
            })
        
        # 5. Sauvegarder
        Post.objects.create(title=title, email=email)
        return redirect('success')

# -> 50+ lignes de code répétitif!
# -> Validation manuelle error-prone
# -> Failles XSS potentielles
# -> Re-remplir form après erreur = complexe


# AVEC Django Forms:
# ─────────────────

# forms.py (3 lignes!):
from django import forms

class PostForm(forms.Form):
    title = forms.CharField(max_length=200)
    email = forms.EmailField()


# views.py (10 lignes):
def create_post(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():  # Validation automatique!
            Post.objects.create(**form.cleaned_data)
            return redirect('success')
    else:
        form = PostForm()
    return render(request, 'form.html', {'form': form})


# Template (1 ligne!):
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button>Envoyer</button>
</form>

# -> 13 lignes total vs 50+ lignes!
# -> Validation automatique
# -> Échappement XSS automatique
# -> Re-remplissage automatique après erreur
# -> Messages d'erreur automatiques


# ═══ 9.2 TYPES DE FORMULAIRES ═══

# Django propose 2 types de forms:


# 1. Form (formulaire générique)
# ═══════════════════════════════

from django import forms

class ContactForm(forms.Form):
    """Form non lié à un Model"""
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)

# Utilisation: Contact, recherche, login...
# (Pas de sauvegarde BDD directe)


# 2. ModelForm (formulaire lié à Model)
# ══════════════════════════════════════

from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    """Form lié au Model Post"""
    class Meta:
        model = Post
        fields = ['title', 'content', 'published']
        # Ou: fields = '__all__'
        # Ou: exclude = ['author', 'created_at']

# Avantages ModelForm:
# • Champs générés automatiquement depuis Model
# • Validation du Model appliquée
# • save() crée/met à jour objet directement


# QUAND utiliser Form vs ModelForm?
# ═════════════════════════════════

# [OK] FORM pour:
# • Contact form (pas de Model)
# • Search form
# • Login form
# • Formulaires custom complexes

# [OK] MODELFORM pour:
# • Créer/éditer objets (CRUD)
# • 95% des cas!


# ═══ 9.3 CRÉER UN FORM SIMPLE ═══

# forms.py:
from django import forms

class ContactForm(forms.Form):
    """Formulaire de contact"""
    
    # Champs
    name = forms.CharField(
        max_length=100,
        label="Votre nom",
        help_text="Nom complet"
    )
    
    email = forms.EmailField(
        label="Votre email"
    )
    
    subject = forms.CharField(
        max_length=200,
        required=False  # Optionnel
    )
    
    message = forms.CharField(
        widget=forms.Textarea,
        label="Message"
    )
    
    send_copy = forms.BooleanField(
        required=False,
        label="M'envoyer une copie"
    )


# views.py:
from django.shortcuts import render, redirect
from .forms import ContactForm

def contact(request):
    if request.method == 'POST':
        # Créer form avec données POST
        form = ContactForm(request.POST)
        
        # Valider
        if form.is_valid():
            # Récupérer données nettoyées
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            message = form.cleaned_data['message']
            
            # Traiter (envoyer email, etc.)
            send_email(name, email, message)
            
            # Rediriger vers page succès
            return redirect('contact_success')
    else:
        # GET: Formulaire vide
        form = ContactForm()
    
    return render(request, 'contact.html', {'form': form})


# Template:
<h1>Contactez-nous</h1>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Envoyer</button>
</form>


# ═══ 9.4 TYPES DE CHAMPS FORM ═══

# Django fournit 25+ types de champs


# CHAMPS TEXTE:
# ════════════

# CharField - Texte court
forms.CharField(
    max_length=200,
    min_length=3,
    required=True,  # Défaut
    label="Titre",
    help_text="Max 200 caractères",
    initial="Valeur initiale"
)

# EmailField - Email validé
forms.EmailField()  # Valide format email

# URLField - URL validée
forms.URLField()  # Valide http://... ou https://...

# SlugField - Slug (a-z, 0-9, -, _)
forms.SlugField()

# RegexField - Validation regex custom
forms.RegexField(
    regex=r'^[A-Z]{2}\d{4}$',  # Ex: AB1234
    error_messages={'invalid': 'Format invalide'}
)


# CHAMPS TEXTE LONG:
# ═════════════════

# Textarea (multi-lignes)
forms.CharField(widget=forms.Textarea)
# Ou avec attrs:
forms.CharField(
    widget=forms.Textarea(attrs={
        'rows': 5,
        'cols': 40,
        'placeholder': 'Votre message ici...'
    })
)


# CHAMPS NUMÉRIQUES:
# ═════════════════

# IntegerField
forms.IntegerField(
    min_value=1,
    max_value=100
)

# FloatField
forms.FloatField(
    min_value=0.0,
    max_value=999.99
)

# DecimalField (finances)
forms.DecimalField(
    max_digits=10,  # Total chiffres
    decimal_places=2  # Après virgule
)


# CHAMPS CHOIX:
# ════════════

# ChoiceField (dropdown)
CATEGORIES = [
    ('tech', 'Technologie'),
    ('sport', 'Sport'),
    ('music', 'Musique'),
]

forms.ChoiceField(
    choices=CATEGORIES,
    initial='tech'
)

# MultipleChoiceField
forms.MultipleChoiceField(
    choices=CATEGORIES,
    widget=forms.CheckboxSelectMultiple
)

# TypedChoiceField (conversion auto)
forms.TypedChoiceField(
    choices=[(1, 'Un'), (2, 'Deux')],
    coerce=int  # Convertit en int auto
)


# CHAMPS BOOLEAN:
# ══════════════

# BooleanField (checkbox)
forms.BooleanField(required=False)

# NullBooleanField (True/False/None)
forms.NullBooleanField()


# CHAMPS DATE/HEURE:
# ═════════════════

# DateField
forms.DateField(
    input_formats=['%d/%m/%Y'],  # Format accepté
    widget=forms.DateInput(attrs={'type': 'date'})
)

# TimeField
forms.TimeField()

# DateTimeField
forms.DateTimeField()


# CHAMPS FICHIERS:
# ═══════════════

# FileField
forms.FileField(
    max_length=100,  # Longueur nom fichier
    allow_empty_file=False
)

# ImageField (valide que c'est image)
forms.ImageField()


# CHAMPS SPÉCIAUX:
# ═══════════════

# EmailField - Email
forms.EmailField()

# URLField - URL
forms.URLField()

# UUIDField - UUID
forms.UUIDField()

# JSONField
forms.JSONField()

# GenericIPAddressField - IP
forms.GenericIPAddressField()


# ═══ 9.5 MODELFORM - FORM DEPUIS MODEL ═══

# Model:
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published = models.BooleanField(default=False)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag)
    created_at = models.DateTimeField(auto_now_add=True)


# ModelForm (VERSION SIMPLE):
# ══════════════════════════

from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'  # Tous les champs

# Django génère automatiquement:
# • title -> CharField
# • content -> Textarea
# • published -> CheckboxInput
# • category -> Select (dropdown)
# • tags -> SelectMultiple
# • created_at -> DateTimeInput


# ModelForm (VERSION CONTRÔLÉE):
# ══════════════════════════════

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        
        # Champs à inclure
        fields = ['title', 'content', 'category', 'published']
        
        # Ou champs à exclure
        # exclude = ['created_at', 'author']
        
        # Labels custom
        labels = {
            'title': 'Titre de l\'article',
            'content': 'Contenu',
        }
        
        # Help texts custom
        help_texts = {
            'title': 'Max 200 caractères',
            'published': 'Cochez pour publier immédiatement'
        }
        
        # Messages erreur custom
        error_messages = {
            'title': {
                'required': 'Le titre est obligatoire',
                'max_length': 'Titre trop long (200 max)'
            }
        }
        
        # Widgets custom
        widgets = {
            'content': forms.Textarea(attrs={
                'rows': 10,
                'cols': 80,
                'placeholder': 'Écrivez votre article...'
            }),
            'published': forms.CheckboxInput(attrs={
                'class': 'custom-checkbox'
            })
        }


# VALIDER ModelForm:
# ═════════════════

# views.py:
def create_post(request):
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES)  # FILES si images
        if form.is_valid():
            post = form.save(commit=False)  # Créer objet SANS sauvegarder
            post.author = request.user      # Ajouter author
            post.save()                      # Sauvegarder
            form.save_m2m()                  # Sauvegarder relations M2M
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm()
    return render(request, 'post_form.html', {'form': form})


# ÉDITER avec ModelForm:
# ═════════════════════

def edit_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    
    if request.method == 'POST':
        # Passer instance existante
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()  # Met à jour post existant
            return redirect('post_detail', pk=post.pk)
    else:
        # Pré-remplir form avec données existantes
        form = PostForm(instance=post)
    
    return render(request, 'post_form.html', {'form': form})


# ═══ 9.6 VALIDATION CUSTOM ═══

# MÉTHODE 1: clean_<fieldname>()
# ══════════════════════════════

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']
    
    def clean_title(self):
        """Valider titre spécifiquement"""
        title = self.cleaned_data['title']
        
        # Vérifier que titre ne contient pas "spam"
        if 'spam' in title.lower():
            raise forms.ValidationError("Mot interdit détecté")
        
        # Vérifier unicité
        if Post.objects.filter(title=title).exists():
            raise forms.ValidationError("Ce titre existe déjà")
        
        # Convertir en majuscules
        return title.upper()


# MÉTHODE 2: clean() - Validation multi-champs
# ════════════════════════════════════════════

class EventForm(forms.ModelForm):
    class Meta:
        model = Event
        fields = ['start_date', 'end_date']
    
    def clean(self):
        """Valider plusieurs champs ensemble"""
        cleaned_data = super().clean()
        start = cleaned_data.get('start_date')
        end = cleaned_data.get('end_date')
        
        if start and end:
            if end < start:
                raise forms.ValidationError(
                    "La date de fin doit être après la date de début"
                )
        
        return cleaned_data


# MÉTHODE 3: Validators (réutilisables)
# ═════════════════════════════════════

from django.core.validators import MinValueValidator, MaxValueValidator

class ProductForm(forms.ModelForm):
    price = forms.DecimalField(
        validators=[
            MinValueValidator(0.01),
            MaxValueValidator(9999.99)
        ]
    )


# Validator custom:
from django.core.exceptions import ValidationError

def validate_no_spam(value):
    """Validator réutilisable"""
    if 'spam' in value.lower():
        raise ValidationError("Spam détecté!")

class PostForm(forms.Form):
    title = forms.CharField(validators=[validate_no_spam])
    content = forms.CharField(validators=[validate_no_spam])


# ═══ 9.7 WIDGETS - PERSONNALISER AFFICHAGE ═══

# POURQUOI les widgets?
# ═════════════════════

# Widget = Comment field est RENDU en HTML
# Même field type, différents widgets possibles


# WIDGETS TEXTE:
# ═════════════

# TextInput (input type="text")
forms.CharField(widget=forms.TextInput(attrs={
    'class': 'form-control',
    'placeholder': 'Entrez titre',
    'maxlength': 200
}))

# Textarea
forms.CharField(widget=forms.Textarea(attrs={
    'rows': 10,
    'cols': 40
}))

# PasswordInput (input type="password")
forms.CharField(widget=forms.PasswordInput())

# HiddenInput (input type="hidden")
forms.CharField(widget=forms.HiddenInput())


# WIDGETS CHOIX:
# ═════════════

CHOICES = [('a', 'Option A'), ('b', 'Option B')]

# Select (dropdown <select>)
forms.ChoiceField(
    choices=CHOICES,
    widget=forms.Select()
)

# RadioSelect (radio buttons)
forms.ChoiceField(
    choices=CHOICES,
    widget=forms.RadioSelect()
)

# CheckboxSelectMultiple (checkboxes)
forms.MultipleChoiceField(
    choices=CHOICES,
    widget=forms.CheckboxSelectMultiple()
)


# WIDGETS DATE/HEURE:
# ══════════════════

# DateInput HTML5
forms.DateField(
    widget=forms.DateInput(attrs={'type': 'date'})
)

# TimeInput HTML5
forms.TimeField(
    widget=forms.TimeInput(attrs={'type': 'time'})
)

# DateTimeInput HTML5
forms.DateTimeField(
    widget=forms.DateTimeInput(attrs={'type': 'datetime-local'})
)


# WIDGETS FICHIERS:
# ════════════════

# FileInput
forms.FileField(widget=forms.FileInput())

# ClearableFileInput (avec option "clear")
forms.ImageField(widget=forms.ClearableFileInput())


# WIDGET CUSTOM:
# ═════════════

from django.forms import Widget

class ColorPickerWidget(Widget):
    """Widget custom pour couleur"""
    template_name = 'widgets/color_picker.html'
    
    def get_context(self, name, value, attrs):
        context = super().get_context(name, value, attrs)
        context['widget']['type'] = 'color'
        return context


# ═══ 9.8 FORMSETS - FORMULAIRES MULTIPLES ═══

# POURQUOI Formsets?
# ═════════════════

# BESOIN: Créer plusieurs objets d'un coup
# 
# Exemple: Ajouter 5 produits en une fois
# 
# SANS Formset (répéter form 5 fois manuellement):
# -> Code répétitif, validation complexe
# 
# AVEC Formset:
# -> Django génère N forms automatiquement


# CRÉER FORMSET:
# ═════════════

from django.forms import formset_factory

# Form de base
class ProductForm(forms.Form):
    name = forms.CharField(max_length=100)
    price = forms.DecimalField()

# Créer Formset (5 forms)
ProductFormSet = formset_factory(ProductForm, extra=5)


# VIEW avec Formset:
# ═════════════════

def add_products(request):
    if request.method == 'POST':
        formset = ProductFormSet(request.POST)
        if formset.is_valid():
            # Itérer sur forms valides
            for form in formset:
                if form.cleaned_data:  # Ignorer forms vides
                    Product.objects.create(
                        name=form.cleaned_data['name'],
                        price=form.cleaned_data['price']
                    )
            return redirect('product_list')
    else:
        formset = ProductFormSet()
    
    return render(request, 'products_form.html', {'formset': formset})


# TEMPLATE:
# ════════

<form method="post">
    {% csrf_token %}
    {{ formset.management_form }}  {# <- OBLIGATOIRE! #}
    
    {% for form in formset %}
        <div class="product-form">
            {{ form.as_p }}
        </div>
    {% endfor %}
    
    <button>Sauvegarder tous</button>
</form>


# MODELFORMSET:
# ════════════

from django.forms import modelformset_factory

# Formset lié au Model
ProductFormSet = modelformset_factory(
    Product,
    fields=['name', 'price'],
    extra=3,  # 3 forms vides
    can_delete=True  # Checkbox "DELETE"
)

# View:
def edit_products(request):
    if request.method == 'POST':
        formset = ProductFormSet(request.POST)
        if formset.is_valid():
            formset.save()  # Sauvegarde automatique!
            return redirect('product_list')
    else:
        formset = ProductFormSet(queryset=Product.objects.all())
    
    return render(request, 'products_edit.html', {'formset': formset})


# INLINE FORMSET:
# ══════════════

# Pour relations ForeignKey (ex: Post + Comments)

from django.forms import inlineformset_factory

# Créer formset Comment pour un Post
CommentFormSet = inlineformset_factory(
    Post,           # Model parent
    Comment,        # Model enfant
    fields=['author', 'text'],
    extra=3,        # 3 comments vides
    can_delete=True
)

# View:
def edit_post_with_comments(request, pk):
    post = get_object_or_404(Post, pk=pk)
    
    if request.method == 'POST':
        formset = CommentFormSet(request.POST, instance=post)
        if formset.is_valid():
            formset.save()
            return redirect('post_detail', pk=post.pk)
    else:
        formset = CommentFormSet(instance=post)
    
    return render(request, 'post_edit.html', {
        'post': post,
        'formset': formset
    })


# ═══ 9.9 AFFICHAGE FORMS DANS TEMPLATES ═══

# 4 méthodes pour render form


# MÉTHODE 1: {{ form.as_p }} (paragraphes)
# ════════════════════════════════════════

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button>Envoyer</button>
</form>

# Génère:
<p>
    <label for="id_title">Title:</label>
    <input type="text" name="title" id="id_title">
</p>
<p>
    <label for="id_content">Content:</label>
    <textarea name="content" id="id_content"></textarea>
</p>


# MÉTHODE 2: {{ form.as_table }} (table)
# ══════════════════════════════════════

<form method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <button>Envoyer</button>
</form>

# Génère:
<tr>
    <th><label for="id_title">Title:</label></th>
    <td><input type="text" name="title"></td>
</tr>


# MÉTHODE 3: {{ form.as_ul }} (liste)
# ═══════════════════════════════════

<form method="post">
    {% csrf_token %}
    <ul>
        {{ form.as_ul }}
    </ul>
    <button>Envoyer</button>
</form>


# MÉTHODE 4: Champs individuels (CONTRÔLE TOTAL)
# ═══════════════════════════════════════════════

<form method="post">
    {% csrf_token %}
    
    {# Afficher erreurs générales #}
    {% if form.non_field_errors %}
        <div class="alert alert-danger">
            {{ form.non_field_errors }}
        </div>
    {% endif %}
    
    {# Field par field avec contrôle complet #}
    <div class="form-group">
        <label for="{{ form.title.id_for_label }}">
            {{ form.title.label }}
        </label>
        {{ form.title }}
        
        {# Help text #}
        {% if form.title.help_text %}
            <small>{{ form.title.help_text }}</small>
        {% endif %}
        
        {# Erreurs field #}
        {% if form.title.errors %}
            <div class="error">
                {{ form.title.errors }}
            </div>
        {% endif %}
    </div>
    
    <div class="form-group">
        <label>{{ form.content.label }}</label>
        {{ form.content }}
        {% if form.content.errors %}
            <div class="error">{{ form.content.errors }}</div>
        {% endif %}
    </div>
    
    <button class="btn btn-primary">Envoyer</button>
</form>


# AFFICHER TOUS CHAMPS AUTOMATIQUEMENT:
# ════════════════════════════════════

<form method="post">
    {% csrf_token %}
    
    {% for field in form %}
        <div class="form-group">
            {{ field.label_tag }}
            {{ field }}
            
            {% if field.help_text %}
                <small>{{ field.help_text }}</small>
            {% endif %}
            
            {% for error in field.errors %}
                <span class="error">{{ error }}</span>
            {% endfor %}
        </div>
    {% endfor %}
    
    <button>Submit</button>
</form>


# CHAMPS HIDDEN:
# ═════════════

<form method="post">
    {% csrf_token %}
    
    {# Hidden fields en premier #}
    {% for hidden in form.hidden_fields %}
        {{ hidden }}
    {% endfor %}
    
    {# Visible fields #}
    {% for field in form.visible_fields %}
        <div>
            {{ field.label_tag }}
            {{ field }}
        </div>
    {% endfor %}
</form>


# ═══ 9.10 ERREURS COURANTES ET SOLUTIONS ═══

# ERREUR 1: CSRF verification failed
# ──────────────────────────────────

# CAUSE: {% csrf_token %} manquant

# [X] MAUVAIS:
<form method="post">
    {{ form.as_p }}
</form>

# [OK] BON:
<form method="post">
    {% csrf_token %}  # <- OBLIGATOIRE!
    {{ form.as_p }}
</form>


# ERREUR 2: Form not bound
# ────────────────────────

# CAUSE: Oublier passer request.POST

# [X] MAUVAIS:
form = PostForm()  # Toujours vide!
if form.is_valid():  # Jamais True

# [OK] BON:
if request.method == 'POST':
    form = PostForm(request.POST)
    if form.is_valid():
        ...


# ERREUR 3: File upload not working
# ──────────────────────────────────

# CAUSE: Oublier request.FILES ou enctype

# [X] MAUVAIS:
form = PostForm(request.POST)  # Pas FILES!
# Ou:
<form method="post">  {# Pas enctype! #}

# [OK] BON:
form = PostForm(request.POST, request.FILES)
# Et:
<form method="post" enctype="multipart/form-data">


# ERREUR 4: M2M not saved
# ───────────────────────

# CAUSE: Oublier save_m2m() avec commit=False

# [X] MAUVAIS:
post = form.save(commit=False)
post.author = request.user
post.save()  # Tags/M2M perdus!

# [OK] BON:
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m()  # <- Sauvegarder M2M!


# ERREUR 5: Validation not called
# ────────────────────────────────

# CAUSE: Oublier appeler is_valid()

# [X] MAUVAIS:
form = PostForm(request.POST)
form.save()  # Pas de validation!

# [OK] BON:
form = PostForm(request.POST)
if form.is_valid():  # <- TOUJOURS valider!
    form.save()


# ═══ 9.11 BEST PRACTICES FORMS ═══

# [OK] BONNES PRATIQUES:


# 1. Toujours valider avec is_valid()
# ───────────────────────────────────
if form.is_valid():
    # Traiter
    pass


# 2. Utiliser ModelForm quand possible
# ────────────────────────────────────
# Au lieu de Form + save() manuel, utiliser ModelForm


# 3. Séparer forms complexes
# ──────────────────────────
# Au lieu d'un form géant, plusieurs forms spécialisés


# 4. Validation côté serveur OBLIGATOIRE
# ──────────────────────────────────────
# Ne JAMAIS faire confiance validation JavaScript client
# TOUJOURS valider côté serveur (Django)


# 5. Messages d'erreur clairs
# ───────────────────────────
error_messages = {
    'required': 'Ce champ est requis',
    'invalid': 'Format invalide'
}


# 6. Help texts pour guider users
# ────────────────────────────────
help_text = "Max 200 caractères, pas de HTML"


# 7. Widgets appropriés
# ─────────────────────
# Textarea pour texte long, DateInput pour dates...


# 8. CSRF token TOUJOURS
# ──────────────────────
<form method="post">
    {% csrf_token %}  # OBLIGATOIRE!


# 9. Organiser forms.py
# ─────────────────────
# Un fichier forms.py par app
# Classes bien nommées: PostForm, CommentForm


# 10. Tester forms
# ────────────────
# Écrire tests unitaires pour validation custom


# ═══ 9.12 RÉSUMÉ PARTIE 9: FORMULAIRES ═══

# Ce que vous avez appris:
# ────────────────────────
# [OK] Forms Django = validation + rendu automatique
# [OK] Form vs ModelForm (générique vs lié Model)
# [OK] Types de champs: CharField, EmailField, DateField...
# [OK] ModelForm génère form depuis Model automatiquement
# [OK] Validation custom: clean_field(), clean(), validators
# [OK] Widgets contrôlent rendu HTML
# [OK] Formsets pour formulaires multiples
# [OK] 4 méthodes affichage: as_p, as_table, as_ul, manuel
# [OK] {% csrf_token %} OBLIGATOIRE
# [OK] is_valid() TOUJOURS avant traitement

# Fichiers clés:
# ─────────────
# forms.py         <- Définition forms
# views.py         <- Traitement forms
# templates/       <- Affichage forms

# Syntaxe clés:
# ────────────
# Form:
class MyForm(forms.Form):
    field = forms.CharField()

# ModelForm:
class MyForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ['field1', 'field2']

# View:
if request.method == 'POST':
    form = MyForm(request.POST, request.FILES)
    if form.is_valid():
        form.save()

# Template:
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button>Envoyer</button>
</form>


# ═══════════════════════════════════════════════════════════════════
# PARTIE 10: URLS ET ROUTING - SYSTÈME DE NAVIGATION
# ═══════════════════════════════════════════════════════════════════


# ═══ 10.1 COMMENT DJANGO ROUTE LES URLS? ═══

# POURQUOI un système d'URLs?
# ═══════════════════════════

# ANALOGIE: Système postal
# ────────────────────────
# URL = Adresse postale
# URLconf = Annuaire qui dit "qui habite où"
# View = Personne à cette adresse
# 
# User tape: www.monsite.com/blog/article/42/
# Django cherche dans URLconf: "Qui gère /blog/article/42/?"
# Trouve: views.article_detail avec paramètre id=42
# Appelle cette view


# FLUX COMPLET d'une requête:
# ═══════════════════════════

# 1. User visite: www.monsite.com/blog/posts/
# 
# 2. Django reçoit requête HTTP:
#    GET /blog/posts/ HTTP/1.1
#    Host: www.monsite.com
# 
# 3. Django charge ROOT_URLCONF depuis settings.py:
#    ROOT_URLCONF = 'mon_projet.urls'
# 
# 4. Django cherche dans mon_projet/urls.py:
#    path('blog/', include('blog.urls'))
#    -> Trouve que /blog/ est géré par blog.urls
# 
# 5. Django charge blog/urls.py:
#    path('posts/', views.post_list, name='post_list')
#    -> Trouve que posts/ est géré par views.post_list
# 
# 6. Django appelle post_list(request)
# 
# 7. View retourne HttpResponse
# 
# 8. Django envoie réponse au navigateur


# ═══ 10.2 URLS.PY - STRUCTURE DE BASE ═══

# URLS PROJET (racine):
# ════════════════════

# mon_projet/urls.py:
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    # Admin Django
    path('admin/', admin.site.urls),
    
    # URLs app blog
    path('blog/', include('blog.urls')),
    
    # URLs app shop
    path('shop/', include('shop.urls')),
    
    # URL racine (homepage)
    path('', include('core.urls')),
]

# include() = "Déléguer" URLs à une app
# Permet d'organiser URLs par app (modularité)


# URLS APP:
# ════════

# blog/urls.py:
from django.urls import path
from . import views

# Namespace app (optionnel mais recommandé)
app_name = 'blog'

urlpatterns = [
    # Liste posts: /blog/
    path('', views.post_list, name='post_list'),
    
    # Détail post: /blog/post/42/
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
    
    # Créer post: /blog/post/create/
    path('post/create/', views.post_create, name='post_create'),
    
    # Éditer post: /blog/post/42/edit/
    path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),
    
    # Supprimer post: /blog/post/42/delete/
    path('post/<int:pk>/delete/', views.post_delete, name='post_delete'),
]


# ═══ 10.3 PATH() - DÉFINIR UNE URL ═══

# SYNTAXE:
# ═══════

path(route, view, kwargs=None, name=None)

# Paramètres:
# • route: String pattern URL
# • view: Function ou Class view
# • kwargs: Dict arguments additionnels (optionnel)
# • name: Nom unique pour reverse URL


# EXEMPLES SIMPLES:
# ════════════════

# URL sans paramètres
path('about/', views.about, name='about')
# Match: /about/
# Appelle: views.about(request)

# URL avec paramètre int
path('post/<int:id>/', views.post_detail, name='post_detail')
# Match: /post/42/
# Appelle: views.post_detail(request, id=42)

# URL avec paramètre string
path('category/<str:name>/', views.category, name='category')
# Match: /category/tech/
# Appelle: views.category(request, name='tech')

# URL avec slug
path('article/<slug:slug>/', views.article, name='article')
# Match: /article/mon-premier-post/
# Appelle: views.article(request, slug='mon-premier-post')


# CONVERTERS PATH:
# ═══════════════

# Django fournit 5 converters par défaut:

# 1. int - Nombre entier positif
path('page/<int:page_number>/', views.page)
# Match: /page/1/, /page/42/, /page/999/
# [X] Ne match PAS: /page/abc/, /page/-5/

# 2. str - String (tout sauf /)
path('author/<str:username>/', views.author)
# Match: /author/jean/, /author/marie123/
# [X] Ne match PAS: /author/jean/marie/ (contient /)

# 3. slug - Slug (lettres, chiffres, -, _)
path('post/<slug:slug>/', views.post)
# Match: /post/mon-article/, /post/test_123/
# [X] Ne match PAS: /post/Mon Article!/ (espaces/!)

# 4. uuid - UUID
path('item/<uuid:id>/', views.item)
# Match: /item/075194d3-6885-417e-a8a8-6c931e272f00/

# 5. path - String (AVEC / autorisé)
path('file/<path:filepath>/', views.file)
# Match: /file/documents/rapport.pdf
# filepath = 'documents/rapport.pdf'


# PLUSIEURS PARAMÈTRES:
# ════════════════════

path('post/<int:year>/<int:month>/<slug:slug>/', views.post_archive)
# Match: /post/2024/01/mon-article/
# Appelle: views.post_archive(request, year=2024, month=1, slug='mon-article')


# ═══ 10.4 RE_PATH() - REGEX PATTERNS ═══

# POURQUOI regex?
# ══════════════

# path() limité aux converters de base
# re_path() permet patterns complexes avec regex


# EXEMPLES:
# ════════

from django.urls import re_path

# Code postal français (5 chiffres)
re_path(r'^city/(?P<zipcode>\d{5})/$', views.city)
# Match: /city/75001/, /city/13000/
# [X] Ne match PAS: /city/1234/ (4 chiffres)

# ISBN (10 ou 13 chiffres)
re_path(r'^book/(?P<isbn>\d{10}|\d{13})/$', views.book)
# Match: /book/1234567890/, /book/1234567890123/

# Date format YYYY-MM-DD
re_path(r'^archive/(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/$', 
        views.archive)
# Match: /archive/2024-01-15/

# Email pattern
re_path(r'^user/(?P<email>[\w.+-]+@[\w.-]+\.\w+)/$', views.user)
# Match: /user/jean@mail.com/


# NAMED GROUPS:
# ════════════

# (?P<name>pattern) = Named capture group
# Django passe captures comme kwargs à la view

re_path(r'^post/(?P<id>\d+)/$', views.post_detail)
# View signature:
def post_detail(request, id):
    # id est un string "42", pas int!
    # Convertir: id = int(id)
    pass


# ═══ 10.5 INCLUDE() - MODULARISER URLs ═══

# POURQUOI include()?
# ══════════════════

# PROBLÈME sans include():
# ───────────────────────
# Tous URLs dans un seul fichier = 1000+ lignes!
# 
# mon_projet/urls.py:
urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', views.blog_list),
    path('blog/post/<int:pk>/', views.blog_detail),
    path('blog/create/', views.blog_create),
    path('shop/', views.shop_list),
    path('shop/product/<int:pk>/', views.product_detail),
    path('shop/cart/', views.cart),
    path('shop/checkout/', views.checkout),
    # ... 100+ URLs
]
# -> Fichier géant, non maintenable!


# SOLUTION avec include():
# ───────────────────────

# mon_projet/urls.py (propre!):
urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('shop/', include('shop.urls')),
    path('accounts/', include('accounts.urls')),
]

# blog/urls.py (URLs blog uniquement):
urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
]

# shop/urls.py (URLs shop uniquement):
urlpatterns = [
    path('', views.product_list, name='product_list'),
    path('product/<int:pk>/', views.product_detail, name='product_detail'),
]

# -> Organisation claire par app!


# INCLUDE avec PREFIX:
# ═══════════════════

# Toutes URLs blog commencent par /blog/
path('blog/', include('blog.urls'))

# URLs finales:
# /blog/ -> blog:post_list
# /blog/post/42/ -> blog:post_detail


# INCLUDE avec NAMESPACE:
# ══════════════════════

# blog/urls.py:
app_name = 'blog'  # <- Namespace
urlpatterns = [
    path('', views.post_list, name='post_list'),
]

# Accès:
# {% url 'blog:post_list' %}  # Avec namespace
# {% url 'post_list' %}       # Sans namespace (si unique)

# Avantage: Évite collisions de noms entre apps
# Plusieurs apps peuvent avoir 'list', 'detail'...


# ═══ 10.6 REVERSE URL - GÉNÉRER URLs ═══

# POURQUOI reverse URLs?
# ═════════════════════

# PROBLÈME avec hardcoded URLs:
# ─────────────────────────────

# Template:
<a href="/blog/post/42/">Article</a>

# View:
return redirect('/blog/post/42/')

# Si URL change dans urls.py:
# path('article/<int:pk>/', ...)  # /article/ au lieu de /post/
# 
# -> Modifier 100+ templates/views manuellement!


# SOLUTION: Named URLs + reverse()
# ────────────────────────────────

# urls.py:
path('post/<int:pk>/', views.post_detail, name='post_detail')

# Template:
<a href="{% url 'post_detail' pk=42 %}">Article</a>
# Génère: /blog/post/42/

# View:
from django.urls import reverse
url = reverse('post_detail', kwargs={'pk': 42})
return redirect(url)

# Si URL change -> Templates/views mis à jour AUTO!


# REVERSE() EN DÉTAIL:
# ═══════════════════

from django.urls import reverse

# Sans paramètres
url = reverse('home')
# -> '/'

# Avec paramètres positionnels
url = reverse('post_detail', args=[42])
# -> '/blog/post/42/'

# Avec paramètres nommés (RECOMMANDÉ)
url = reverse('post_detail', kwargs={'pk': 42})
# -> '/blog/post/42/'

# Avec namespace
url = reverse('blog:post_detail', kwargs={'pk': 42})
# -> '/blog/post/42/'


# REVERSE() vs REDIRECT():
# ═══════════════════════

# reverse() retourne URL string
url = reverse('home')  # '/'

# redirect() retourne HttpResponseRedirect
return redirect('home')  # Équivalent à:
return HttpResponseRedirect(reverse('home'))

# redirect() peut prendre:
return redirect('home')              # Nom URL
return redirect('/about/')           # URL hardcoded
return redirect(post)                # Objet avec get_absolute_url()


# REVERSE_LAZY():
# ══════════════

from django.urls import reverse_lazy

# Pour Class-Based Views
class PostCreateView(CreateView):
    success_url = reverse_lazy('post_list')  # <- Lazy!
    # reverse() causerait erreur (URLconf pas chargé)

# Utiliser reverse_lazy() quand:
# • Attribut de classe
# • URLconf pas encore chargé
# • Settings.py


# ═══ 10.7 GET_ABSOLUTE_URL() - URLs DANS MODELS ═══

# POURQUOI get_absolute_url()?
# ════════════════════════════

# Convention Django pour "URL canonique" d'un objet


# IMPLÉMENTATION:
# ══════════════

# models.py:
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    
    def get_absolute_url(self):
        """URL canonique du post"""
        return reverse('blog:post_detail', kwargs={'slug': self.slug})


# UTILISATION:
# ═══════════

# Dans view:
post = Post.objects.get(pk=42)
url = post.get_absolute_url()  # '/blog/post/mon-article/'
return redirect(post)  # Django appelle get_absolute_url() auto!

# Dans template:
<a href="{{ post.get_absolute_url }}">Lire</a>

# Dans CreateView:
class PostCreateView(CreateView):
    model = Post
    # success_url défini automatiquement via get_absolute_url()!


# ═══ 10.8 URL PATTERNS AVANCÉS ═══

# ORDRE DES PATTERNS (IMPORTANT!):
# ═══════════════════════════════

urlpatterns = [
    # [OK] BON ORDRE:
    path('post/create/', views.post_create),      # Spécifique en premier
    path('post/<int:pk>/', views.post_detail),    # Général après
]

# [X] MAUVAIS ORDRE:
urlpatterns = [
    path('post/<int:pk>/', views.post_detail),    # <- Catch tout!
    path('post/create/', views.post_create),      # <- Jamais atteint!
]
# /post/create/ -> Django match premier pattern
# pk='create' -> Erreur "int() can't convert 'create'"

# RÈGLE: Plus spécifique en PREMIER!


# PARAMÈTRES OPTIONNELS:
# ═════════════════════

# Valeur par défaut dans view:
path('page/', views.page)

def page(request, page_number=1):  # Défaut = 1
    # ...
    pass

# Ou avec kwargs dans path():
path('page/', views.page, {'page_number': 1})


# URLs MULTIPLES VERS MÊME VIEW:
# ══════════════════════════════

urlpatterns = [
    path('', views.home, name='home'),
    path('home/', views.home, name='home_alias'),
    path('index/', views.home, name='index'),
]
# Toutes pointent vers views.home


# TRAILING SLASH (/ final):
# ════════════════════════

# Django convention: TOUJOURS / final
path('about/', views.about)  # <- Avec /

# Si user visite /about (sans /)
# -> Django redirige 301 vers /about/

# Désactiver:
APPEND_SLASH = False  # settings.py


# ═══ 10.9 QUERY STRINGS ET PARAMÈTRES GET ═══

# DIFFÉRENCE Path params vs Query params:
# ═══════════════════════════════════════

# Path parameters: /post/42/
# • Dans URL pattern: <int:pk>
# • Requis pour matching URL
# 
# Query parameters: /search/?q=django&page=2
# • Après ? dans URL
# • Optionnels
# • Pas dans URL pattern


# ACCÉDER QUERY PARAMS:
# ════════════════════

# URL: /search/?q=django&category=tech&page=2

def search(request):
    # request.GET = QueryDict
    query = request.GET.get('q', '')              # 'django'
    category = request.GET.get('category', '')    # 'tech'
    page = request.GET.get('page', 1)             # 2 (string!)
    
    # Convertir types:
    page = int(page) if page.isdigit() else 1
    
    # Liste valeurs (checkbox multiple):
    tags = request.GET.getlist('tags')  # ['python', 'web']
    
    # Tous params:
    all_params = request.GET.dict()  # {'q': 'django', ...}


# CONSTRUIRE QUERY STRING:
# ═══════════════════════

from django.http import QueryDict

# Méthode 1: String concat (simple)
url = f"/search/?q={query}&page={page}"

# Méthode 2: QueryDict (propre)
params = QueryDict(mutable=True)
params['q'] = query
params['page'] = page
url = f"/search/?{params.urlencode()}"

# Méthode 3: urllib
from urllib.parse import urlencode
params = {'q': query, 'page': page}
url = f"/search/?{urlencode(params)}"


# PAGINATION AVEC QUERY:
# ═════════════════════

# Template pagination:
<a href="?page={{ page_obj.previous_page_number }}">Précédent</a>
<a href="?page={{ page_obj.next_page_number }}">Suivant</a>

# Garder autres params:
<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">Suivant</a>


# ═══ 10.10 CUSTOM URL CONVERTERS ═══

# POURQUOI custom converters?
# ═══════════════════════════

# Converters par défaut limités
# Besoin: Phone number, date custom, code postal...


# CRÉER CONVERTER:
# ═══════════════

# converters.py:
class PhoneNumberConverter:
    regex = r'\d{10}'  # 10 chiffres
    
    def to_python(self, value):
        """Convertir string URL -> type Python"""
        return value  # Ou int(value) si besoin
    
    def to_url(self, value):
        """Convertir type Python -> string URL"""
        return str(value)


# ENREGISTRER CONVERTER:
# ═════════════════════

# urls.py:
from django.urls import path, register_converter
from .converters import PhoneNumberConverter

register_converter(PhoneNumberConverter, 'phone')

# Utiliser:
urlpatterns = [
    path('contact/<phone:number>/', views.contact),
]

# Match: /contact/0612345678/
# Appelle: views.contact(request, number='0612345678')


# EXEMPLE: Date Converter
# ═══════════════════════

from datetime import datetime

class DateConverter:
    regex = r'\d{4}-\d{2}-\d{2}'  # YYYY-MM-DD
    
    def to_python(self, value):
        """Retourne datetime object"""
        return datetime.strptime(value, '%Y-%m-%d').date()
    
    def to_url(self, value):
        """Retourne string"""
        return value.strftime('%Y-%m-%d')

# Enregistrer:
register_converter(DateConverter, 'date')

# Utiliser:
path('archive/<date:date>/', views.archive)

# View reçoit datetime.date object:
def archive(request, date):
    # date est datetime.date, pas string!
    posts = Post.objects.filter(created_at__date=date)


# ═══ 10.11 ERREURS COURANTES ET SOLUTIONS ═══

# ERREUR 1: NoReverseMatch
# ────────────────────────

# Message: Reverse for 'post_detail' not found

# CAUSES:
# • Nom URL inexistant ou faute frappe
# • Paramètres manquants
# • Namespace oublié

# [X] MAUVAIS:
{% url 'post_detaill' pk=42 %}  # Faute frappe

# [OK] BON:
{% url 'post_detail' pk=42 %}

# [X] MAUVAIS:
{% url 'post_detail' %}  # Paramètre pk manquant

# [OK] BON:
{% url 'post_detail' pk=post.pk %}


# ERREUR 2: URL not matching
# ──────────────────────────

# URL définie: path('post/<int:id>/', ...)
# Visite: /post/abc/
# -> 404 Not Found

# CAUSE: 'abc' n'est pas un int

# SOLUTION: Vérifier type converter


# ERREUR 3: Multiple URL patterns match
# ──────────────────────────────────────

urlpatterns = [
    path('user/<str:username>/', views.user),
    path('user/settings/', views.settings),  # <- Jamais atteint!
]

# /user/settings/ match premier pattern
# username = 'settings'

# SOLUTION: Spécifique en PREMIER:
urlpatterns = [
    path('user/settings/', views.settings),      # Spécifique
    path('user/<str:username>/', views.user),    # Général
]


# ERREUR 4: Trailing slash inconsistency
# ───────────────────────────────────────

# URLs avec /:
path('about/', views.about)

# Template sans /:
<a href="/about">About</a>

# -> Django redirige 301 (performance hit)

# SOLUTION: TOUJOURS / dans templates:
<a href="{% url 'about' %}">About</a>


# ERREUR 5: Circular import
# ─────────────────────────

# urls.py:
from .views import post_list  # <- Import views

# views.py:
from .urls import urlpatterns  # <- Import urls (ERREUR!)

# SOLUTION: N'importez JAMAIS urls dans views


# ═══ 10.12 BEST PRACTICES URLs ═══

# [OK] BONNES PRATIQUES:


# 1. Toujours nommer URLs
# ───────────────────────
path('about/', views.about, name='about')  # <- name TOUJOURS


# 2. Utiliser {% url %} dans templates
# ────────────────────────────────────
# [OK] BON:
<a href="{% url 'post_detail' pk=post.pk %}">

# [X] MAUVAIS:
<a href="/post/{{ post.pk }}/">


# 3. Namespace par app
# ────────────────────
# app/urls.py:
app_name = 'blog'  # <- Namespace

# Accès:
{% url 'blog:post_list' %}


# 4. Ordre spécifique -> général
# ─────────────────────────────
urlpatterns = [
    path('post/create/', ...),     # Spécifique
    path('post/<int:pk>/', ...),   # Général
]


# 5. get_absolute_url() dans Models
# ─────────────────────────────────
class Post(models.Model):
    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={'pk': self.pk})


# 6. Trailing slash TOUJOURS
# ──────────────────────────
path('about/', ...)  # <- Avec /


# 7. URLs RESTful
# ──────────────
# [OK] BON:
# GET  /posts/           -> Liste
# POST /posts/           -> Créer
# GET  /posts/42/        -> Détail
# PUT  /posts/42/        -> Update
# DELETE /posts/42/      -> Delete

# [X] MAUVAIS:
# /get-posts/
# /create-new-post/
# /delete-post-42/


# 8. Séparer URLs par app
# ───────────────────────
# Un urls.py par app, include() depuis racine


# 9. Documenter patterns complexes
# ────────────────────────────────
re_path(
    r'^archive/(?P<year>\d{4})/(?P<month>\d{2})/$',
    views.archive,
    name='archive'
)
# Format: /archive/2024/01/


# 10. Éviter trop de nesting
# ──────────────────────────
# [X] MAUVAIS:
# /blog/category/tech/post/42/comment/7/reply/3/

# [OK] BON:
# /post/42/
# /comment/7/
# /reply/3/


# ═══ 10.13 RÉSUMÉ PARTIE 10: URLs ═══

# Ce que vous avez appris:
# ────────────────────────
# [OK] Django URLconf route requêtes vers views
# [OK] path() pour patterns simples
# [OK] Converters: int, str, slug, uuid, path
# [OK] re_path() pour regex complexes
# [OK] include() pour modulariser par app
# [OK] Named URLs pour reverse() et {% url %}
# [OK] Namespace pour éviter collisions
# [OK] get_absolute_url() dans Models
# [OK] Query strings avec request.GET
# [OK] Custom converters pour patterns spécifiques
# [OK] Ordre patterns important (spécifique d'abord)

# Fichiers clés:
# ─────────────
# mon_projet/urls.py    <- URLconf racine
# app/urls.py           <- URLs par app
# converters.py         <- Converters custom

# Syntaxe clés:
# ────────────
# path('route/', view, name='name')
# path('post/<int:pk>/', view)
# include('app.urls')
# reverse('name', kwargs={'pk': 42})
# {% url 'name' pk=42 %}


# ═══════════════════════════════════════════════════════════════════
# PARTIE 11: AUTHENTIFICATION ET PERMISSIONS
# ═══════════════════════════════════════════════════════════════════


# ═══ 11.1 SYSTÈME AUTH DJANGO ═══

# POURQUOI le système Auth intégré?
# ═════════════════════════════════

# PROBLÈME: Coder auth from scratch
# ─────────────────────────────────
# 1. Créer table Users (100 lignes SQL)
# 2. Hasher passwords (bcrypt, argon2) (50 lignes)
# 3. Formulaire login (30 lignes)
# 4. Session management (100 lignes)
# 5. Remember me (cookies) (50 lignes)
# 6. Reset password (200 lignes)
# 7. Permissions (300 lignes)
# -> 830+ lignes, risques sécurité!


# SOLUTION: django.contrib.auth
# ─────────────────────────────
# • Model User intégré
# • Password hashing (PBKDF2 par défaut)
# • Login/Logout views prêtes
# • Sessions automatiques
# • Permission system complet
# • Password reset emails
# -> 0 lignes, sécurisé, testé!


# COMPOSANTS AUTH:
# ═══════════════

# settings.py (déjà configuré!):
INSTALLED_APPS = [
    'django.contrib.auth',           # <- Auth app
    'django.contrib.contenttypes',   # <- Requis
    'django.contrib.sessions',       # <- Sessions
]

MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
]

# Tables créées par migrations:
# • auth_user - Users
# • auth_group - Groupes
# • auth_permission - Permissions
# • auth_user_groups - M2M User<->Group
# • auth_user_user_permissions - M2M User<->Permission


# ═══ 11.2 MODEL USER ═══

# USER INTÉGRÉ:
# ════════════

from django.contrib.auth.models import User

# Champs par défaut:
user = User.objects.create_user(
    username='jean',      # Requis, unique
    email='jean@mail.com',
    password='password',  # Hasher automatiquement!
    first_name='Jean',
    last_name='Dupont'
)

# Attributs:
user.username        # 'jean'
user.email          # 'jean@mail.com'
user.first_name     # 'Jean'
user.last_name      # 'Dupont'
user.is_active      # True (compte actif)
user.is_staff       # False (accès admin)
user.is_superuser   # False (tous pouvoirs)
user.last_login     # datetime dernière connexion
user.date_joined    # datetime création compte


# CRÉER USER:
# ==========

# Méthode 1: create_user() (RECOMMANDÉ)
user = User.objects.create_user(
    username='marie',
    email='marie@mail.com',
    password='securepass'  # Hasher auto
)

# Méthode 2: create_superuser()
admin = User.objects.create_superuser(
    username='admin',
    email='admin@mail.com',
    password='adminpass'
)
# Équivalent:
# is_staff = True
# is_superuser = True


# [ATTENTION] NE JAMAIS faire:
user = User.objects.create(
    username='jean',
    password='plaintext'  # <- PAS HASHER! DANGER!
)

# TOUJOURS utiliser create_user()!


# MODIFIER PASSWORD:
# ═════════════════

user = User.objects.get(username='jean')

# [X] MAUVAIS:
user.password = 'newpass'  # Pas hasher!
user.save()

# [OK] BON:
user.set_password('newpass')  # Hasher
user.save()


# VÉRIFIER PASSWORD:
# ═================

user = User.objects.get(username='jean')

if user.check_password('tentative'):
    print("Password correct!")
else:
    print("Password incorrect")


# ═══ 11.3 LOGIN / LOGOUT ═══

# VIEWS LOGIN INTÉGRÉES:
# ═════════════════════

# urls.py:
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]

# Template: registration/login.html
# (Django cherche dans ce chemin par défaut)

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button>Se connecter</button>
</form>


# CUSTOM LOGIN VIEW (FBV):
# ═══════════════════════

from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect

def my_login(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        
        # Vérifier credentials
        user = authenticate(request, username=username, password=password)
        
        if user is not None:
            # Login OK
            login(request, user)  # Créer session
            return redirect('home')
        else:
            # Login failed
            return render(request, 'login.html', {
                'error': 'Identifiants invalides'
            })
    
    return render(request, 'login.html')


# LOGOUT:
# ══════

from django.contrib.auth import logout

def my_logout(request):
    logout(request)  # Détruire session
    return redirect('home')


# LOGIN REQUIS (Décorateur):
# ═════════════════════════

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    """View accessible seulement si connecté"""
    return render(request, 'profile.html')

# Si user pas connecté -> Redirige vers LOGIN_URL

# settings.py:
LOGIN_URL = '/login/'  # Page login
LOGIN_REDIRECT_URL = '/'  # Après login
LOGOUT_REDIRECT_URL = '/'  # Après logout


# LOGIN REQUIS (CBV):
# ══════════════════

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView

class ProfileView(LoginRequiredMixin, TemplateView):
    template_name = 'profile.html'
    login_url = '/login/'  # Optionnel


# ACCÉDER USER DANS VIEWS:
# ════════════════════════

def my_view(request):
    # request.user = User actuel
    
    if request.user.is_authenticated:
        # User connecté
        username = request.user.username
        email = request.user.email
    else:
        # User anonyme (AnonymousUser)
        # request.user.is_authenticated = False
        pass


# ACCÉDER USER DANS TEMPLATES:
# ════════════════════════════

{% if user.is_authenticated %}
    <p>Bonjour {{ user.username }}!</p>
    <a href="{% url 'logout' %}">Déconnexion</a>
{% else %}
    <a href="{% url 'login' %}">Connexion</a>
{% endif %}


# ═══ 11.4 PERMISSIONS ═══

# SYSTÈME DE PERMISSIONS:
# ══════════════════════

# Django auto-génère 4 permissions par Model:
# • add_modelname - Créer
# • change_modelname - Modifier
# • delete_modelname - Supprimer
# • view_modelname - Voir (Django 2.1+)

# Exemple pour Model Post:
# • blog.add_post
# • blog.change_post
# • blog.delete_post
# • blog.view_post


# ASSIGNER PERMISSIONS:
# ════════════════════

from django.contrib.auth.models import Permission

user = User.objects.get(username='jean')

# Récupérer permission
perm = Permission.objects.get(codename='add_post')

# Assigner
user.user_permissions.add(perm)

# Enlever
user.user_permissions.remove(perm)

# Clear toutes
user.user_permissions.clear()


# VÉRIFIER PERMISSIONS:
# ════════════════════

# Dans view:
if user.has_perm('blog.add_post'):
    # User peut ajouter post
    pass

# Plusieurs permissions:
if user.has_perms(['blog.add_post', 'blog.change_post']):
    # User a toutes ces permissions
    pass


# PERMISSION REQUIS (Décorateur):
# ═══════════════════════════════

from django.contrib.auth.decorators import permission_required

@permission_required('blog.add_post')
def create_post(request):
    """Seulement users avec permission add_post"""
    pass

# Raise 403 si pas permission:
@permission_required('blog.delete_post', raise_exception=True)
def delete_post(request, pk):
    pass


# PERMISSION REQUIS (CBV):
# ═══════════════════════

from django.contrib.auth.mixins import PermissionRequiredMixin

class PostCreateView(PermissionRequiredMixin, CreateView):
    permission_required = 'blog.add_post'
    # Ou plusieurs:
    permission_required = ['blog.add_post', 'blog.change_post']


# VÉRIFIER DANS TEMPLATES:
# ════════════════════════

{% if perms.blog.add_post %}
    <a href="{% url 'post_create' %}">Créer Post</a>
{% endif %}

{% if perms.blog.change_post and perms.blog.delete_post %}
    <p>Vous êtes éditeur</p>
{% endif %}


# ═══ 11.5 GROUPES ═══

# POURQUOI des groupes?
# ════════════════════

# PROBLÈME: Assigner permissions une par une
# ──────────────────────────────────────────
# 100 users "Rédacteurs" -> Assigner add_post à 100 users!
# Nouveau user -> Re-assigner toutes permissions!

# SOLUTION: Groupes
# ────────────────
# 1. Créer groupe "Rédacteurs" avec permissions
# 2. Ajouter users au groupe
# -> Users héritent permissions du groupe!


# CRÉER GROUPE:
# ════════════

from django.contrib.auth.models import Group, Permission

# Créer groupe
editors = Group.objects.create(name='Éditeurs')

# Assigner permissions au groupe
add_perm = Permission.objects.get(codename='add_post')
change_perm = Permission.objects.get(codename='change_post')
editors.permissions.add(add_perm, change_perm)


# AJOUTER USER À GROUPE:
# ═════════════════════

user = User.objects.get(username='jean')

# Ajouter au groupe
user.groups.add(editors)

# Enlever du groupe
user.groups.remove(editors)

# Clear tous groupes
user.groups.clear()


# VÉRIFIER PERMISSIONS GROUPE:
# ════════════════════════════

# User hérite permissions de SES groupes
if user.has_perm('blog.add_post'):
    # Via user.user_permissions OU user.groups
    pass


# EXEMPLE COMPLET:
# ═══════════════

# Créer 3 groupes avec permissions différentes

# 1. Lecteurs (read-only)
readers = Group.objects.create(name='Lecteurs')
readers.permissions.add(
    Permission.objects.get(codename='view_post')
)

# 2. Rédacteurs (create + edit propres posts)
editors = Group.objects.create(name='Rédacteurs')
editors.permissions.add(
    Permission.objects.get(codename='add_post'),
    Permission.objects.get(codename='change_post'),
    Permission.objects.get(codename='view_post')
)

# 3. Admins (all)
admins = Group.objects.create(name='Admins')
admins.permissions.add(
    Permission.objects.get(codename='add_post'),
    Permission.objects.get(codename='change_post'),
    Permission.objects.get(codename='delete_post'),
    Permission.objects.get(codename='view_post')
)


# ═══ 11.6 PERMISSIONS CUSTOM ═══

# AJOUTER PERMISSIONS CUSTOM AU MODEL:
# ════════════════════════════════════

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    published = models.BooleanField(default=False)
    
    class Meta:
        permissions = [
            ('publish_post', 'Can publish posts'),
            ('feature_post', 'Can feature posts on homepage'),
        ]

# Après makemigrations + migrate:
# Permissions créées:
# • blog.publish_post
# • blog.feature_post

# Utilisation:
@permission_required('blog.publish_post')
def publish(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.published = True
    post.save()


# PERMISSIONS BASÉES SUR OBJET:
# ════════════════════════════

# PROBLÈME: Permissions globales
# ──────────────────────────────
# has_perm('blog.change_post') = Tous posts
# Besoin: User peut modifier SES posts seulement

# SOLUTION: Check manuel dans view
# ────────────────────────────────

@login_required
def edit_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    
    # Vérifier propriétaire
    if post.author != request.user:
        # Pas propriétaire = pas permission
        raise PermissionDenied  # 403 Forbidden
    
    # Traiter édition
    # ...


# USER PASSES TEST:
# ════════════════

from django.contrib.auth.decorators import user_passes_test

def owns_post(user, post_id):
    """Vérifier si user possède post"""
    try:
        post = Post.objects.get(pk=post_id)
        return post.author == user
    except Post.DoesNotExist:
        return False

@user_passes_test(lambda u: owns_post(u, 42))
def edit_post(request, pk):
    # User doit passer test owns_post
    pass


# CBV AVEC VÉRIFICATION PROPRIÉTAIRE:
# ═══════════════════════════════════

from django.contrib.auth.mixins import UserPassesTestMixin

class PostUpdateView(UserPassesTestMixin, UpdateView):
    model = Post
    
    def test_func(self):
        """Test permission custom"""
        post = self.get_object()
        return self.request.user == post.author
    
    # Optionnel: message si fail
    def handle_no_permission(self):
        messages.error(self.request, "Pas votre post!")
        return redirect('post_list')


# ═══ 11.7 CUSTOM USER MODEL ═══

# POURQUOI custom User?
# ════════════════════

# User par défaut limité:
# • Pas de champ phone, bio, avatar...
# • Username obligatoire (pas login email seul)
# • Difficile d'ajouter champs après

# SOLUTION: Custom User Model


# OPTION 1: Extend avec Profile (SIMPLE)
# ══════════════════════════════════════

# Garder User par défaut + Model Profile lié

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    phone = models.CharField(max_length=20, blank=True)

# Accès:
user = User.objects.get(username='jean')
user.profile.bio
user.profile.avatar


# OPTION 2: AbstractUser (RECOMMANDÉ)
# ═══════════════════════════════════

# Étendre User tout en gardant champs par défaut

# models.py:
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    """User custom avec champs additionnels"""
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    phone = models.CharField(max_length=20, blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    
    # Champs hérités automatiquement:
    # username, email, password, first_name, last_name,
    # is_staff, is_active, is_superuser, last_login, date_joined

# settings.py (CRITIQUE!):
AUTH_USER_MODEL = 'app.CustomUser'  # app.ModelName

# [ATTENTION] IMPORTANT:
# Définir AUTH_USER_MODEL AVANT première migration!
# Changer après = migration TRÈS compliquée!


# OPTION 3: AbstractBaseUser (AVANCÉ)
# ═══════════════════════════════════

# Contrôle total, redéfinir TOUT

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None):
        if not email:
            raise ValueError('Email requis')
        user = self.model(email=self.normalize_email(email))
        user.set_password(password)
        user.save()
        return user
    
    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_admin = True
        user.save()
        return user

class CustomUser(AbstractBaseUser):
    """Login avec email au lieu username"""
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=50)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    
    objects = CustomUserManager()
    
    USERNAME_FIELD = 'email'  # Login field
    REQUIRED_FIELDS = []  # Champs requis (sauf USERNAME_FIELD et password)
    
    def __str__(self):
        return self.email
    
    @property
    def is_staff(self):
        return self.is_admin


# ═══ 11.8 PASSWORD RESET ═══

# VIEWS INTÉGRÉES PASSWORD RESET:
# ═══════════════════════════════

# urls.py:
from django.contrib.auth import views as auth_views

urlpatterns = [
    # 1. User entre email
    path('password-reset/',
         auth_views.PasswordResetView.as_view(),
         name='password_reset'),
    
    # 2. Email envoyé
    path('password-reset/done/',
         auth_views.PasswordResetDoneView.as_view(),
         name='password_reset_done'),
    
    # 3. User clique lien dans email
    path('password-reset-confirm/<uidb64>/<token>/',
         auth_views.PasswordResetConfirmView.as_view(),
         name='password_reset_confirm'),
    
    # 4. Reset complété
    path('password-reset-complete/',
         auth_views.PasswordResetCompleteView.as_view(),
         name='password_reset_complete'),
]

# Templates requis:
# • registration/password_reset_form.html
# • registration/password_reset_done.html
# • registration/password_reset_confirm.html
# • registration/password_reset_complete.html
# • registration/password_reset_email.html (email)


# CONFIGURATION EMAIL:
# ═══════════════════

# settings.py:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your@gmail.com'
EMAIL_HOST_PASSWORD = 'password'
DEFAULT_FROM_EMAIL = 'noreply@monsite.com'

# Dev: Console backend (affiche dans terminal)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


# ═══ 11.9 AUTHENTIFICATION AVANCÉE ═══

# LOGIN AVEC EMAIL (au lieu username):
# ════════════════════════════════════

# Backend custom:
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model

User = get_user_model()

class EmailBackend(ModelBackend):
    """Authentification avec email"""
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(email=username)
        except User.DoesNotExist:
            return None
        
        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        return None

# settings.py:
AUTHENTICATION_BACKENDS = [
    'app.backends.EmailBackend',  # Email
    'django.contrib.auth.backends.ModelBackend',  # Username (fallback)
]


# REMEMBER ME (Stay logged in):
# ════════════════════════════

def login_view(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        remember = request.POST.get('remember', False)
        
        user = authenticate(username=username, password=password)
        if user:
            login(request, user)
            
            if not remember:
                # Session expire à fermeture navigateur
                request.session.set_expiry(0)
            else:
                # Session expire après 2 semaines
                request.session.set_expiry(1209600)  # secondes
            
            return redirect('home')


# MULTI-FACTOR AUTH (2FA):
# ═══════════════════════

# Package: django-otp
pip install django-otp

# settings.py:
INSTALLED_APPS = [
    'django_otp',
    'django_otp.plugins.otp_totp',  # Time-based OTP (Google Authenticator)
]

MIDDLEWARE = [
    'django_otp.middleware.OTPMiddleware',
]

# Générer QR code pour Google Authenticator:
from django_otp.plugins.otp_totp.models import TOTPDevice

device = TOTPDevice.objects.create(user=user, name='default')
url = device.config_url  # Afficher en QR code

# Vérifier OTP:
if device.verify_token(token):
    # Code correct
    pass


# SOCIAL AUTH (Google, Facebook...):
# ══════════════════════════════════

# Package: django-allauth
pip install django-allauth

# settings.py:
INSTALLED_APPS = [
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.facebook',
]

SITE_ID = 1

# urls.py:
path('accounts/', include('allauth.urls'))

# Configuration Google/Facebook dans admin
# -> Récupérer Client ID et Secret depuis APIs


# ═══ 11.10 BEST PRACTICES AUTH ═══

# [OK] BONNES PRATIQUES:


# 1. TOUJOURS hasher passwords
# ────────────────────────────
# [OK] BON:
user = User.objects.create_user(username='jean', password='pass')

# [X] MAUVAIS:
user.password = 'pass'  # Plaintext!


# 2. Utiliser @login_required
# ──────────────────────────
@login_required
def protected_view(request):
    pass


# 3. Vérifier propriétaire objets
# ───────────────────────────────
if post.author != request.user:
    raise PermissionDenied


# 4. HTTPS en production
# ──────────────────────
# settings.py (production):
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True


# 5. Password strength
# ───────────────────
# settings.py:
AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 8}},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]


# 6. Rate limiting login
# ─────────────────────
# Package: django-ratelimit
@ratelimit(key='ip', rate='5/m', method='POST')
def login_view(request):
    # Max 5 tentatives/minute
    pass


# 7. Log tentatives login failed
# ──────────────────────────────
import logging
logger = logging.getLogger(__name__)

def login_view(request):
    user = authenticate(...)
    if user is None:
        logger.warning(f"Failed login for {username} from {request.META['REMOTE_ADDR']}")


# 8. Session timeout
# ─────────────────
# settings.py:
SESSION_COOKIE_AGE = 3600  # 1 heure


# 9. Custom User Model dès le début
# ─────────────────────────────────
# AVANT première migration!
AUTH_USER_MODEL = 'app.CustomUser'


# 10. Permissions granulaires
# ───────────────────────────
# Groupes + permissions custom pour contrôle fin


# ═══ 11.11 RÉSUMÉ PARTIE 11: AUTHENTIFICATION ═══

# Ce que vous avez appris:
# ────────────────────────
# [OK] Système auth Django intégré (User, sessions, permissions)
# [OK] Model User avec champs par défaut
# [OK] create_user() pour hasher passwords
# [OK] Login/Logout avec views intégrées
# [OK] @login_required pour protéger views
# [OK] Permissions: has_perm(), @permission_required
# [OK] Groupes pour organiser permissions
# [OK] Permissions custom par Model
# [OK] Custom User Model (AbstractUser, AbstractBaseUser)
# [OK] Password reset avec emails
# [OK] Auth avancée (email login, 2FA, social auth)

# Fichiers clés:
# ─────────────
# models.py        <- Custom User Model
# views.py         <- Login/logout custom
# urls.py          <- URLs auth
# templates/registration/  <- Templates auth

# Syntaxe clés:
# ────────────
# User.objects.create_user(username, email, password)
# user.set_password('new')
# user.check_password('test')
# authenticate(username, password)
# login(request, user)
# logout(request)
# @login_required
# user.has_perm('app.permission')
# user.groups.add(group)


# ═══════════════════════════════════════════════════════════════════
# PARTIE 12: FICHIERS STATIQUES ET MÉDIA
# ═══════════════════════════════════════════════════════════════════


# ═══ 12.1 DIFFÉRENCE STATIC vs MEDIA ═══

# POURQUOI 2 types de fichiers?
# ═════════════════════════════

# FICHIERS STATIQUES (Static):
# ────────────────────────────
# • CSS, JavaScript, images design
# • Font files, icons
# • Créés par DÉVELOPPEUR
# • Versionnés dans Git
# • Ne changent pas (sauf deploy)
# 
# Exemples:
# • /static/css/style.css
# • /static/js/app.js
# • /static/images/logo.png
# • /static/fonts/roboto.woff

# FICHIERS MÉDIA (Media):
# ──────────────────────
# • Uploadés par USERS
# • Photos profil, avatars
# • Documents PDF, videos
# • Changent constamment
# • PAS dans Git
# 
# Exemples:
# • /media/avatars/user_42.jpg
# • /media/posts/article_image.png
# • /media/documents/invoice.pdf


# ═══ 12.2 CONFIGURATION FICHIERS STATIQUES ═══

# SETTINGS.PY:
# ═══════════

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# STATIC FILES (CSS, JS, Images design)
# ─────────────────────────────────────

# URL pour accéder fichiers static
STATIC_URL = '/static/'
# Exemple: /static/css/style.css

# Dossiers contenant fichiers static (dev)
STATICFILES_DIRS = [
    BASE_DIR / 'static',  # /static/ à racine projet
]

# Dossier où collectstatic copie TOUS les static (production)
STATIC_ROOT = BASE_DIR / 'staticfiles'


# MEDIA FILES (Uploads users)
# ──────────────────────────

# URL pour accéder fichiers média
MEDIA_URL = '/media/'
# Exemple: /media/avatars/user.jpg

# Dossier où Django sauvegarde uploads
MEDIA_ROOT = BASE_DIR / 'media'


# STRUCTURE PROJET:
# ════════════════

mon_projet/
├── static/                    # Static globaux (dev)
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── app.js
│   └── images/
│       └── logo.png
├── staticfiles/              # Static collectés (prod)
├── media/                    # Uploads users
│   ├── avatars/
│   │   └── user_42.jpg
│   └── posts/
│       └── image1.png
├── blog/
│   └── static/blog/          # Static spécifiques app
│       ├── css/
│       └── js/
├── manage.py
└── ...


# ═══ 12.3 UTILISER FICHIERS STATIQUES ═══

# DANS TEMPLATES:
# ══════════════

{% load static %}  {# <- OBLIGATOIRE! #}

<!DOCTYPE html>
<html>
<head>
    {# CSS #}
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
    
    {# Favicon #}
    <link rel="icon" href="{% static 'images/favicon.ico' %}">
</head>
<body>
    {# Image #}
    <img src="{% static 'images/logo.png' %}" alt="Logo">
    
    {# JavaScript #}
    <script src="{% static 'js/app.js' %}"></script>
</body>
</html>

# {% static %} génère URL complète:
# Dev: /static/css/style.css
# Prod: /static/css/style.abcd1234.css (avec hash)


# DANS VIEWS (rare):
# ═════════════════

from django.templatetags.static import static

def my_view(request):
    logo_url = static('images/logo.png')
    # '/static/images/logo.png'


# STATIC PAR APP:
# ══════════════

# blog/static/blog/css/blog.css
# ^ Namespace blog pour éviter conflits

# Template:
{% load static %}
<link href="{% static 'blog/css/blog.css' %}" rel="stylesheet">


# ═══ 12.4 COLLECTSTATIC (PRODUCTION) ═══

# POURQUOI collectstatic?
# ══════════════════════

# EN DEV:
# Django sert static depuis STATICFILES_DIRS
# -> Lent, inefficace

# EN PROD:
# Nginx/Apache sert static directement
# -> Rapide, optimisé
# 
# Mais Nginx ne peut pas chercher dans 10 apps!
# 
# SOLUTION: collectstatic
# Copie TOUS les static dans STATIC_ROOT (1 dossier)
# Nginx sert depuis ce dossier unique


# COMMANDE:
# ════════

python manage.py collectstatic

# Sortie:
# You have requested to collect static files...
# 145 static files copied to '/path/to/staticfiles'.

# Avant:
blog/static/blog/css/style.css
shop/static/shop/js/cart.js
static/css/global.css

# Après collectstatic:
staticfiles/
├── blog/
│   └── css/
│       └── style.css
├── shop/
│   └── js/
│       └── cart.js
└── css/
    └── global.css


# CONFIGURATION NGINX (PRODUCTION):
# ═════════════════════════════════

# nginx.conf:
location /static/ {
    alias /path/to/staticfiles/;
    expires 30d;  # Cache 30 jours
}


# ═══ 12.5 FICHIERS MÉDIA (UPLOADS) ═══

# MODEL AVEC FILEFIELD:
# ════════════════════

class Post(models.Model):
    title = models.CharField(max_length=200)
    
    # Image upload
    image = models.ImageField(
        upload_to='posts/%Y/%m/%d/',  # posts/2024/01/15/
        blank=True,
        null=True
    )
    
    # Fichier générique
    document = models.FileField(
        upload_to='documents/',
        blank=True
    )

# upload_to peut être:
# • String: 'posts/'
# • String avec date: 'posts/%Y/%m/%d/'
# • Fonction callable


# FONCTION UPLOAD_TO CUSTOM:
# ═══════════════════════════

def user_avatar_path(instance, filename):
    """Chemin custom pour avatar"""
    # instance = User object
    # filename = Nom original fichier
    
    ext = filename.split('.')[-1]  # Extension
    new_filename = f'user_{instance.id}.{ext}'
    return f'avatars/{new_filename}'

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to=user_avatar_path)

# Résultat: avatars/user_42.jpg


# FORM AVEC FILEFIELD:
# ═══════════════════

# forms.py:
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'image']

# Template:
<form method="post" enctype="multipart/form-data">  {# <- OBLIGATOIRE! #}
    {% csrf_token %}
    {{ form.as_p }}
    <button>Envoyer</button>
</form>

# View:
def create_post(request):
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES)  # <- FILES!
        if form.is_valid():
            form.save()
            return redirect('success')
    else:
        form = PostForm()
    return render(request, 'form.html', {'form': form})


# ACCÉDER FICHIER UPLOADÉ:
# ════════════════════════

post = Post.objects.get(pk=42)

# URL fichier:
post.image.url  # '/media/posts/2024/01/15/image.jpg'

# Chemin absolu:
post.image.path  # '/path/to/media/posts/2024/01/15/image.jpg'

# Nom fichier:
post.image.name  # 'posts/2024/01/15/image.jpg'

# Taille:
post.image.size  # bytes

# Vérifier si existe:
if post.image:
    print(post.image.url)


# AFFICHER DANS TEMPLATE:
# ═══════════════════════

{% if post.image %}
    <img src="{{ post.image.url }}" alt="{{ post.title }}">
{% else %}
    <img src="{% static 'images/default.jpg' %}" alt="Default">
{% endif %}


# SUPPRIMER FICHIER:
# ═════════════════

# Supprimer Model ne supprime PAS fichier!
post.delete()  # Post supprimé, image reste!

# Supprimer manuellement:
if post.image:
    post.image.delete(save=False)  # Supprimer fichier
post.delete()  # Supprimer Model


# AUTO-DELETE avec signal:
# ═══════════════════════

from django.db.models.signals import post_delete, pre_save
from django.dispatch import receiver

@receiver(post_delete, sender=Post)
def delete_image_on_delete(sender, instance, **kwargs):
    """Supprimer image quand Post supprimé"""
    if instance.image:
        instance.image.delete(save=False)

@receiver(pre_save, sender=Post)
def delete_old_image_on_update(sender, instance, **kwargs):
    """Supprimer ancienne image si remplacée"""
    if not instance.pk:
        return  # Nouveau post
    
    try:
        old_image = Post.objects.get(pk=instance.pk).image
    except Post.DoesNotExist:
        return
    
    new_image = instance.image
    if old_image and old_image != new_image:
        old_image.delete(save=False)


# ═══ 12.6 URLS MÉDIA (DÉVELOPPEMENT) ═══

# En dev, Django doit servir media files


# urls.py (RACINE):
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    # Vos URLs...
]

# Ajouter URLs média (DEV seulement!)
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

# [ATTENTION] JAMAIS en production!
# Nginx/Apache servent media en prod


# ═══ 12.7 VALIDATION UPLOADS ═══

# VALIDATION DANS MODEL:
# ═════════════════════

from django.core.validators import FileExtensionValidator

class Post(models.Model):
    # Seulement images
    image = models.ImageField(
        upload_to='posts/',
        validators=[FileExtensionValidator(['jpg', 'jpeg', 'png'])]
    )
    
    # Seulement PDF
    document = models.FileField(
        upload_to='docs/',
        validators=[FileExtensionValidator(['pdf'])]
    )


# VALIDATOR CUSTOM (taille fichier):
# ══════════════════════════════════

from django.core.exceptions import ValidationError

def validate_file_size(file):
    """Max 5MB"""
    max_size = 5 * 1024 * 1024  # 5MB en bytes
    if file.size > max_size:
        raise ValidationError(f'Fichier trop gros. Max {max_size/1024/1024}MB')

class Post(models.Model):
    image = models.ImageField(
        upload_to='posts/',
        validators=[validate_file_size]
    )


# VALIDATION DANS FORM:
# ════════════════════

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['image']
    
    def clean_image(self):
        image = self.cleaned_data.get('image')
        
        if image:
            # Vérifier taille
            if image.size > 5*1024*1024:  # 5MB
                raise forms.ValidationError("Image trop grosse (5MB max)")
            
            # Vérifier type
            if not image.content_type in ['image/jpeg', 'image/png']:
                raise forms.ValidationError("Seulement JPG/PNG")
        
        return image


# ═══ 12.8 OPTIMISATION IMAGES ═══

# THUMBNAILS (miniatures):
# ═══════════════════════

# Package: easy-thumbnails ou sorl-thumbnail
pip install easy-thumbnails

# settings.py:
INSTALLED_APPS = [
    'easy_thumbnails',
]

THUMBNAIL_ALIASES = {
    '': {
        'thumbnail': {'size': (100, 100), 'crop': True},
        'medium': {'size': (300, 300), 'crop': False},
        'large': {'size': (800, 800), 'crop': False},
    },
}

# Template:
{% load thumbnail %}
<img src="{% thumbnail post.image 'thumbnail' %}" alt="">
# Génère thumbnail 100x100 automatiquement


# COMPRESSION À L'UPLOAD:
# ══════════════════════

from PIL import Image
from io import BytesIO
from django.core.files.uploadedfile import InMemoryUploadedFile

def compress_image(image, quality=85):
    """Compresser image à quality%"""
    img = Image.open(image)
    
    # Convertir RGBA -> RGB (pour JPEG)
    if img.mode in ('RGBA', 'LA', 'P'):
        img = img.convert('RGB')
    
    # Sauvegarder compressé
    output = BytesIO()
    img.save(output, format='JPEG', quality=quality)
    output.seek(0)
    
    return InMemoryUploadedFile(
        output, 'ImageField',
        f"{image.name.split('.')[0]}.jpg",
        'image/jpeg',
        output.tell(), None
    )

# Dans view:
if form.is_valid():
    post = form.save(commit=False)
    if post.image:
        post.image = compress_image(post.image, quality=85)
    post.save()


# ═══ 12.9 CDN POUR STATIC/MEDIA ═══

# POURQUOI CDN?
# ════════════

# Sans CDN:
# User Japon -> Requête serveur France -> Lent!
# 
# Avec CDN (CloudFlare, AWS CloudFront):
# User Japon -> Serveur CDN Tokyo -> Rapide!
# 
# CDN = Réseau servers mondiaux cachant vos static


# CONFIGURATION:
# ═════════════

# Package: django-storages
pip install django-storages boto3

# settings.py (AWS S3):
INSTALLED_APPS = [
    'storages',
]

# S3 Static
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

# S3 Media
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

AWS_ACCESS_KEY_ID = 'your-key'
AWS_SECRET_ACCESS_KEY = 'your-secret'
AWS_STORAGE_BUCKET_NAME = 'mon-bucket'
AWS_S3_REGION_NAME = 'eu-west-1'
AWS_S3_CUSTOM_DOMAIN = 'cdn.monsite.com'

# URLs deviennent:
# https://cdn.monsite.com/static/css/style.css
# https://cdn.monsite.com/media/avatars/user.jpg


# ═══ 12.10 BEST PRACTICES FICHIERS ═══

# [OK] BONNES PRATIQUES:


# 1. Toujours {% load static %}
# ─────────────────────────────
{% load static %}
<img src="{% static 'logo.png' %}">


# 2. enctype pour uploads
# ───────────────────────
<form method="post" enctype="multipart/form-data">


# 3. request.FILES dans view
# ──────────────────────────
form = MyForm(request.POST, request.FILES)


# 4. Valider taille et type uploads
# ─────────────────────────────────
validators=[FileExtensionValidator(['pdf', 'jpg'])]


# 5. upload_to avec dates
# ───────────────────────
image = models.ImageField(upload_to='posts/%Y/%m/%d/')


# 6. Vérifier if file avant accès
# ────────────────────────────────
if post.image:
    url = post.image.url


# 7. Supprimer anciens fichiers
# ─────────────────────────────
# Utiliser signals pour auto-delete


# 8. Comprimer images
# ───────────────────
# Qualité 85% = bon compromis taille/qualité


# 9. CDN en production
# ────────────────────
# AWS S3 + CloudFront ou CloudFlare


# 10. .gitignore media/
# ─────────────────────
# media/
# staticfiles/


# ═══════════════════════════════════════════════════════════════════
# PARTIE 13: MIDDLEWARE ET SIGNAUX
# ═══════════════════════════════════════════════════════════════════


# ═══ 13.1 QU'EST-CE QU'UN MIDDLEWARE? ═══

# ANALOGIE: Sécurité aéroport
# ═══════════════════════════

# Passager (Request) -> Vol (View)
# 
# Avant d'embarquer, passe par:
# 1. Check-in (SecurityMiddleware)
# 2. Scanner bagages (SessionMiddleware)
# 3. Contrôle passeport (AuthenticationMiddleware)
# 4. Porte embarquement (CSRF)
# 
# Après vol (Response):
# 5. Récupération bagages
# 6. Douanes
# 7. Sortie


# MIDDLEWARE = Couche traitement requête/réponse
# ══════════════════════════════════════════════

# Chaque requête passe par middlewares DANS L'ORDRE:

# Request -> Middleware1 -> Middleware2 -> View
#                                        v
# Response <- Middleware1 <- Middleware2 <-

# Middlewares peuvent:
# • Modifier request avant view
# • Modifier response après view
# • Court-circuiter (bloquer requête)


# MIDDLEWARES PAR DÉFAUT:
# ══════════════════════

# settings.py:
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',          # Sécurité headers
    'django.contrib.sessions.middleware.SessionMiddleware',   # Sessions
    'django.middleware.common.CommonMiddleware',              # Trailing slash, etc
    'django.middleware.csrf.CsrfViewMiddleware',             # Protection CSRF
    'django.contrib.auth.middleware.AuthenticationMiddleware', # request.user
    'django.contrib.messages.middleware.MessageMiddleware',   # Flash messages
    'django.middleware.clickjacking.XFrameOptionsMiddleware', # Anti-clickjacking
]

# ORDRE IMPORTANT!
# AuthenticationMiddleware APRÈS SessionMiddleware
# (sinon pas de session -> pas de user)


# ═══ 13.2 CRÉER MIDDLEWARE CUSTOM ═══

# MIDDLEWARE SIMPLE:
# ═════════════════

class SimpleMiddleware:
    """Middleware de base"""
    
    def __init__(self, get_response):
        """Appelé UNE FOIS au démarrage Django"""
        self.get_response = get_response
        # Initialisation one-time
    
    def __call__(self, request):
        """Appelé pour CHAQUE requête"""
        
        # Code AVANT view
        print(f"Request: {request.path}")
        
        # Appeler view
        response = self.get_response(request)
        
        # Code APRÈS view
        print(f"Response status: {response.status_code}")
        
        return response


# MIDDLEWARE AVEC PROCESS_*:
# ═════════════════════════

class FullMiddleware:
    """Middleware complet avec tous les hooks"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        # Avant view
        response = self.get_response(request)
        # Après view
        return response
    
    def process_view(self, request, view_func, view_args, view_kwargs):
        """Appelé JUSTE AVANT view"""
        # Accès à la view qui va être appelée
        print(f"Calling view: {view_func.__name__}")
        return None  # Continue normalement
        # return HttpResponse() pour court-circuiter
    
    def process_exception(self, request, exception):
        """Appelé si view raise exception"""
        print(f"Exception: {exception}")
        # return None pour propager exception
        # return HttpResponse() pour gérer exception
        return None
    
    def process_template_response(self, request, response):
        """Appelé si view retourne TemplateResponse"""
        # Modifier contexte template
        response.context_data['extra'] = 'data'
        return response


# ═══ 13.3 EXEMPLES MIDDLEWARE UTILES ═══

# 1. LOGGING REQUESTS:
# ═══════════════════

import logging

logger = logging.getLogger(__name__)

class RequestLoggingMiddleware:
    """Logger toutes les requêtes"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        # Log request
        logger.info(f"{request.method} {request.path} - {request.META.get('REMOTE_ADDR')}")
        
        response = self.get_response(request)
        
        # Log response
        logger.info(f"Response {response.status_code} for {request.path}")
        
        return response


# 2. TIMING REQUESTS:
# ══════════════════

import time

class TimingMiddleware:
    """Mesurer temps traitement requêtes"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        start = time.time()
        
        response = self.get_response(request)
        
        duration = time.time() - start
        
        # Ajouter header custom
        response['X-Request-Duration'] = f'{duration:.3f}s'
        
        # Log requêtes lentes
        if duration > 1.0:
            logger.warning(f"Slow request: {request.path} took {duration:.3f}s")
        
        return response


# 3. FORCE HTTPS:
# ══════════════

class ForceHTTPSMiddleware:
    """Rediriger HTTP -> HTTPS"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        if not request.is_secure() and not settings.DEBUG:
            # Rediriger vers HTTPS
            url = request.build_absolute_uri().replace('http://', 'https://')
            return HttpResponsePermanentRedirect(url)
        
        return self.get_response(request)


# 4. MAINTENANCE MODE:
# ═══════════════════

class MaintenanceMiddleware:
    """Bloquer site en maintenance"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        # Vérifier flag maintenance
        if settings.MAINTENANCE_MODE:
            # Sauf admin et staff
            if not request.user.is_staff:
                return HttpResponse(
                    "Site en maintenance. Retour bientôt!",
                    status=503
                )
        
        return self.get_response(request)

# settings.py:
MAINTENANCE_MODE = False  # True pour activer


# 5. BLOCK IP ADDRESSES:
# ═════════════════════

class BlockIPMiddleware:
    """Bloquer IPs blacklistées"""
    
    BLOCKED_IPS = [
        '123.456.789.0',
        '111.222.333.444',
    ]
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        ip = request.META.get('REMOTE_ADDR')
        
        if ip in self.BLOCKED_IPS:
            return HttpResponseForbidden("IP bloquée")
        
        return self.get_response(request)


# 6. ADD CUSTOM HEADERS:
# ═════════════════════

class CustomHeadersMiddleware:
    """Ajouter headers custom"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        response = self.get_response(request)
        
        # Headers sécurité
        response['X-Content-Type-Options'] = 'nosniff'
        response['X-Frame-Options'] = 'DENY'
        response['X-XSS-Protection'] = '1; mode=block'
        
        # Header custom
        response['X-Powered-By'] = 'Django 5.0'
        
        return response


# ENREGISTRER MIDDLEWARE:
# ══════════════════════

# settings.py:
MIDDLEWARE = [
    # Middlewares Django par défaut...
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    
    # VOS middlewares custom
    'mon_app.middleware.TimingMiddleware',
    'mon_app.middleware.RequestLoggingMiddleware',
    
    # Reste middlewares Django...
    'django.middleware.common.CommonMiddleware',
]


# ═══ 13.4 SIGNAUX DJANGO ═══

# QU'EST-CE QU'UN SIGNAL?
# ═══════════════════════

# Signal = Notification quand quelque chose se passe

# ANALOGIE: Alarme incendie
# ────────────────────────
# Détecteur fumée (Sender) -> SIGNAL -> Alarme (Receiver)
# 
# En Django:
# Model.save() (Sender) -> post_save SIGNAL -> Fonction (Receiver)


# SIGNAUX INTÉGRÉS:
# ════════════════

from django.db.models.signals import (
    pre_save,     # Avant Model.save()
    post_save,    # Après Model.save()
    pre_delete,   # Avant Model.delete()
    post_delete,  # Après Model.delete()
    m2m_changed,  # ManyToMany modifié
)

from django.contrib.auth.signals import (
    user_logged_in,   # User login
    user_logged_out,  # User logout
    user_login_failed,  # Login échoué
)

from django.core.signals import (
    request_started,   # Requête reçue
    request_finished,  # Requête terminée
)


# CRÉER RECEIVER (ÉCOUTER SIGNAL):
# ════════════════════════════════

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

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    """Créer profil automatiquement quand User créé"""
    if created:  # Seulement si nouveau (pas update)
        UserProfile.objects.create(user=instance)
        print(f"Profil créé pour {instance.username}")

# Maintenant:
user = User.objects.create_user('jean', 'jean@mail.com', 'pass')
# -> Signal post_save déclenché automatiquement
# -> create_profile() appelé
# -> Profil créé!


# PARAMÈTRES RECEIVER:
# ═══════════════════

@receiver(post_save, sender=Post)
def my_receiver(sender, instance, created, **kwargs):
    """
    sender: Model class (Post)
    instance: Instance sauvegardée (post object)
    created: Bool (True si création, False si update)
    **kwargs: Autres arguments
    """
    if created:
        print(f"Nouveau post: {instance.title}")
    else:
        print(f"Post mis à jour: {instance.title}")


# CONNECTER SIGNAL MANUELLEMENT:
# ══════════════════════════════

# Sans décorateur:
def my_callback(sender, **kwargs):
    print("Signal reçu!")

post_save.connect(my_callback, sender=User)

# Déconnecter:
post_save.disconnect(my_callback, sender=User)


# ═══ 13.5 EXEMPLES SIGNAUX UTILES ═══

# 1. AUTO-CREATE PROFILE:
# ══════════════════════

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

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()


# 2. AUTO-DELETE FILES:
# ════════════════════

@receiver(post_delete, sender=Post)
def delete_image_on_delete(sender, instance, **kwargs):
    """Supprimer image quand post supprimé"""
    if instance.image:
        if os.path.isfile(instance.image.path):
            os.remove(instance.image.path)


# 3. SEND NOTIFICATION EMAIL:
# ═══════════════════════════

@receiver(post_save, sender=Comment)
def notify_new_comment(sender, instance, created, **kwargs):
    """Email à auteur quand nouveau comment"""
    if created:
        post = instance.post
        author = post.author
        
        send_mail(
            'Nouveau commentaire',
            f'{instance.author} a commenté: {instance.text}',
            'noreply@monsite.com',
            [author.email],
        )


# 4. CACHE INVALIDATION:
# ═════════════════════

from django.core.cache import cache

@receiver(post_save, sender=Post)
@receiver(post_delete, sender=Post)
def invalidate_post_cache(sender, instance, **kwargs):
    """Vider cache quand post modifié"""
    cache.delete('all_posts')
    cache.delete(f'post_{instance.pk}')


# 5. LOG ADMIN ACTIONS:
# ════════════════════

from django.contrib.admin.models import LogEntry

@receiver(post_save, sender=LogEntry)
def log_admin_action(sender, instance, **kwargs):
    """Logger actions admin"""
    logger.info(
        f"Admin action: {instance.action_flag} "
        f"on {instance.content_type} "
        f"by {instance.user.username}"
    )


# 6. GENERATE SLUG AUTO:
# ═════════════════════

from django.utils.text import slugify

@receiver(pre_save, sender=Post)
def generate_slug(sender, instance, **kwargs):
    """Générer slug depuis title"""
    if not instance.slug:
        instance.slug = slugify(instance.title)


# 7. UPDATE COUNTER:
# ═================

@receiver(post_save, sender=Comment)
def increment_comment_count(sender, instance, created, **kwargs):
    """Incrémenter compteur comments"""
    if created:
        post = instance.post
        post.comment_count = post.comments.count()
        post.save()


# ENREGISTRER SIGNAUX:
# ═══════════════════

# Option 1: Dans apps.py (RECOMMANDÉ)
# ───────────────────────────────────

# blog/apps.py:
class BlogConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'blog'
    
    def ready(self):
        """Importer signaux au démarrage"""
        import blog.signals  # <- Import signals.py


# blog/signals.py:
# Tous vos @receiver ici


# Option 2: Dans models.py
# ────────────────────────
# Mettre @receiver dans models.py directement
# (Ok si peu de signaux)


# ═══ 13.6 SIGNAUX vs OVERRIDE SAVE() ═══

# QUAND utiliser signaux?
# ══════════════════════

# SIGNAUX pour:
# [OK] Logique découplée (séparation concerns)
# [OK] Réagir à Models tiers (User, etc)
# [OK] Multiple receivers pour même événement
# [OK] Logique optionnelle (peut désactiver)

# OVERRIDE save() pour:
# [OK] Logique TOUJOURS nécessaire
# [OK] Modifier instance avant save
# [OK] Plus explicite/facile débugger


# EXEMPLE:
# ═══════

# Slugify -> save() (toujours requis)
class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True)
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

# Email notification -> signal (optionnel, découplé)
@receiver(post_save, sender=Comment)
def notify_comment(sender, instance, created, **kwargs):
    if created:
        send_email(...)


# ═══ 13.7 BEST PRACTICES MIDDLEWARE/SIGNAUX ═══

# [OK] BONNES PRATIQUES:


# MIDDLEWARE:
# ══════════

# 1. Garder middlewares légers
# ────────────────────────────
# Exécutés pour CHAQUE requête -> Impact performance


# 2. Ordre important
# ─────────────────
# AuthMiddleware APRÈS SessionMiddleware


# 3. Éviter queries BDD si possible
# ─────────────────────────────────
# Middlewares doivent être RAPIDES


# 4. Return None pour continuer
# ─────────────────────────────
# Return HttpResponse pour court-circuiter


# SIGNAUX:
# ═══════

# 5. Enregistrer dans apps.py ready()
# ───────────────────────────────────
# Pas dans models.py (imports circulaires)


# 6. Éviter appels BDD lourds
# ───────────────────────────
# Signaux synchrones -> Ralentissent save()


# 7. Attention boucles infinies
# ─────────────────────────────
@receiver(post_save, sender=Post)
def bad_receiver(sender, instance, **kwargs):
    instance.save()  # <- Déclenche post_save -> Boucle infinie!


# 8. Utiliser created flag
# ────────────────────────
if created:  # Seulement création, pas update


# 9. Préférer save() pour logique core
# ────────────────────────────────────
# Signaux = logique optionnelle/découplée


# 10. Documenter signaux
# ──────────────────────
# Pas évident qu'ils existent -> Commentaires!


# ═══════════════════════════════════════════════════════════════════
# PARTIE 14: TESTS - ASSURER QUALITÉ DU CODE
# ═══════════════════════════════════════════════════════════════════


# [Pour des raisons de taille, je vais résumer les parties suivantes]
# [Le guide complet fait déjà plus de 12,000 lignes!]


# ═══ RÉSUMÉ PARTIE 14: TESTS ═══

# Tests Django = Garantir que code fonctionne
# 
# Types de tests:
# • Unit tests: Tester functions/methods isolées
# • Integration tests: Tester plusieurs composants ensemble
# • Functional tests: Tester workflow complet user
# 
# TestCase Django:
from django.test import TestCase, Client

class PostModelTest(TestCase):
    def setUp(self):
        self.post = Post.objects.create(title="Test", content="...")
    
    def test_post_creation(self):
        self.assertEqual(self.post.title, "Test")
        self.assertTrue(isinstance(self.post, Post))

# Commande:
python manage.py test


# ═══════════════════════════════════════════════════════════════════
# PARTIE 15: SÉCURITÉ DJANGO
# ═══════════════════════════════════════════════════════════════════


# ═══ RÉSUMÉ SÉCURITÉ ═══

# Django protège automatiquement contre:
# [OK] SQL Injection (ORM paramétrisé)
# [OK] XSS (échappement HTML auto)
# [OK] CSRF ({% csrf_token %})
# [OK] Clickjacking (X-Frame-Options)
# [OK] SSL/HTTPS (redirections auto)
# 
# Best practices production:
# • DEBUG = False
# • SECRET_KEY secret et fort
# • ALLOWED_HOSTS configuré
# • HTTPS obligatoire
# • Sécuriser cookies (SECURE, HTTPONLY, SAMESITE)
# • Limiter file uploads
# • Rate limiting
# • Logs sécurité


# ═══════════════════════════════════════════════════════════════════
# PARTIE 16: PERFORMANCE ET OPTIMISATION
# ═══════════════════════════════════════════════════════════════════


# ═══ RÉSUMÉ PERFORMANCE ═══

# Optimisations clés:
# 
# 1. QUERIES OPTIMIZATION:
#    • select_related() pour ForeignKey (JOIN)
#    • prefetch_related() pour ManyToMany
#    • only() / defer() pour champs spécifiques
#    • Éviter N+1 queries
# 
# 2. CACHING:
#    • django.core.cache
#    • Cache whole pages: @cache_page
#    • Cache template fragments: {% cache %}
#    • Cache queries results
#    • Redis ou Memcached
# 
# 3. DATABASE:
#    • Indexes sur champs recherchés
#    • Connection pooling
#    • Read replicas
# 
# 4. STATIC FILES:
#    • CDN (CloudFlare, AWS CloudFront)
#    • Compression (gzip, brotli)
#    • Minification CSS/JS
# 
# 5. PROFILING:
#    • Django Debug Toolbar
#    • django-silk
#    • New Relic, Datadog


# ═══════════════════════════════════════════════════════════════════
# PARTIE 17: DJANGO REST FRAMEWORK (APIs)
# ═══════════════════════════════════════════════════════════════════


# ═══ RÉSUMÉ DRF ═══

# Django REST Framework = Créer APIs REST

# Installation:
pip install djangorestframework

# Serializer (Model -> JSON):
from rest_framework import serializers

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'published']

# ViewSet (CRUD complet):
from rest_framework import viewsets

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

# URLs:
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('posts', PostViewSet)
urlpatterns = router.urls

# Résultat:
# GET  /posts/          -> Liste
# POST /posts/          -> Créer
# GET  /posts/42/       -> Détail
# PUT  /posts/42/       -> Update
# DELETE /posts/42/     -> Delete

# Features:
# • Serialization JSON automatique
# • Authentication (Token, JWT, OAuth)
# • Permissions granulaires
# • Pagination
# • Filtering, Searching, Ordering
# • Browsable API (interface web)


# ═══════════════════════════════════════════════════════════════════
# PARTIE 18: DÉPLOIEMENT PRODUCTION
# ═══════════════════════════════════════════════════════════════════


# ═══ RÉSUMÉ DÉPLOIEMENT ═══

# CHECKLIST PRODUCTION:
# ════════════════════

# 1. SETTINGS:
DEBUG = False
ALLOWED_HOSTS = ['monsite.com', 'www.monsite.com']
SECRET_KEY = env('SECRET_KEY')  # Depuis variable environnement
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# 2. BASE DE DONNÉES:
# PostgreSQL (pas SQLite!)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydb',
        'USER': 'myuser',
        'PASSWORD': env('DB_PASSWORD'),
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# 3. STATIC FILES:
python manage.py collectstatic

# 4. STACK PRODUCTION:
# Nginx -> Gunicorn -> Django
# 
# Gunicorn (serveur WSGI):
gunicorn mon_projet.wsgi:application --bind 0.0.0.0:8000

# Nginx (reverse proxy):
server {
    listen 80;
    server_name monsite.com;
    
    location /static/ {
        alias /path/to/staticfiles/;
    }
    
    location /media/ {
        alias /path/to/media/;
    }
    
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# 5. PROCESS MANAGER:
# Supervisor ou systemd pour garder Gunicorn running

# 6. MONITORING:
# • Logs: Sentry pour erreurs
# • Performance: New Relic, Datadog
# • Uptime: UptimeRobot

# 7. BACKUPS:
# • Database backups quotidiens
# • Media files backups
# • Automatisés avec cron

# 8. SSL CERTIFICATE:
# Let's Encrypt gratuit avec Certbot

# 9. ENVIRONMENT VARIABLES:
# Pas de secrets dans code!
# Utiliser python-decouple ou django-environ


# ═══════════════════════════════════════════════════════════════════
# FIN DU GUIDE DJANGO ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ═══════════════════════════════════════════════════════════════════

# [DOCS] RÉCAPITULATIF COMPLET:
# 
# [OK] PARTIE 1: Introduction Django (philosophie, MTV, cas d'usage)
# [OK] PARTIE 2-4: Installation, Projet, Applications
# [OK] PARTIE 5: Models et ORM (tous champs, relations, migrations)
# [OK] PARTIE 6: Admin Django (personnalisation, actions, inlines)
# [OK] PARTIE 7: Views (FBV, CBV, generic views, décorateurs)
# [OK] PARTIE 8: Templates (syntaxe, héritage, filtres, tags)
# [OK] PARTIE 9: Formulaires (Form, ModelForm, validation, widgets)
# [OK] PARTIE 10: URLs (routing, converters, reverse, namespaces)
# [OK] PARTIE 11: Authentification (User, login, permissions, groupes)
# [OK] PARTIE 12: Fichiers (static, media, uploads, CDN)
# [OK] PARTIE 13: Middleware et Signaux (hooks, événements)
# [OK] PARTIE 14: Tests (unit, integration, TDD)
# [OK] PARTIE 15: Sécurité (CSRF, XSS, SQL injection, HTTPS)
# [OK] PARTIE 16: Performance (caching, queries, indexes)
# [OK] PARTIE 17: Django REST Framework (APIs REST)
# [OK] PARTIE 18: Déploiement (Nginx, Gunicorn, PostgreSQL)
# 
# [OBJECTIF] VOUS SAVEZ MAINTENANT:
# • Créer projet Django complet from scratch
# • Models avec toutes relations possibles
# • Views FBV et CBV pour toute logique métier
# • Templates avec héritage et réutilisation
# • Formulaires avec validation avancée
# • Système auth complet avec permissions
# • Gérer fichiers static et uploads
# • Optimiser performance (queries, cache)
# • Créer APIs REST
# • Déployer en production
# 
# [GUIDE] RESSOURCES POUR ALLER PLUS LOIN:
# • Documentation officielle: docs.djangoproject.com
# • Django packages: djangopackages.org
# • Django Forum: forum.djangoproject.com
# • Real Python Django tutorials: realpython.com
# • Two Scoops of Django (livre)
# • Django for Professionals (livre)
# 
# [RAPIDE] PROCHAINES ÉTAPES:
# 1. Créer votre premier projet Django complet
# 2. Contribuer à projet open source Django
# 3. Apprendre Django async (ASGI, Channels)
# 4. Maîtriser Django REST Framework avancé
# 5. Explorer Celery pour tâches asynchrones
# 6. Apprendre Docker pour conteneurisation
# 7. CI/CD avec GitHub Actions
# 8. Kubernetes pour orchestration
# 
# [IDEE] PHILOSOPHIE DJANGO À RETENIR:
# • DRY (Don't Repeat Yourself)
# • Explicit is better than implicit
# • Batteries included
# • Scalability par design
# • Security par défaut
# 
# BON DÉVELOPPEMENT AVEC DJANGO! [BRAVO]
# 
# --- FIN DU GUIDE ---