# Fichier: python_cheats/cheatsheets/django.txt
# Cheatsheet Django - Guide Complet pour Débutants


[OK] INTRODUCTION À DJANGO - QU'EST-CE QUE C'EST ?

# === Qu'est-ce qu'un framework web ? ===

# Un framework web est un ensemble d'outils qui facilite la création de sites web.
# Sans framework, il faudrait réécrire tout le code de base (connexion BDD, 
# gestion des utilisateurs, sécurité, etc.) pour chaque nouveau projet.

# Analogie: Si vous construisez une maison, un framework web est comme avoir
# des fondations, des murs et un toit déjà prêts. Vous n'avez plus qu'à
# décorer et aménager l'intérieur selon vos besoins.

# === Pourquoi Django ? ===

# Django est un framework Python pour créer des applications web professionnelles.
# Il suit la philosophie "batteries included" = tout est inclus par défaut!

# Avantages de Django:
# [OK] Ne réinventez pas la roue - Tout est déjà codé (auth, BDD, admin, etc.)
# [OK] Sécurisé par défaut - Protection contre les failles courantes
# [OK] Scalable - Utilisé par Instagram, Spotify, YouTube, Pinterest
# [OK] Rapide à développer - Créez un site complet en quelques heures
# [OK] Excellente documentation - La meilleure du monde Python
# [OK] Grande communauté - Des milliers de packages disponibles

# === Architecture MTV (Model-Template-View) ===

# Django utilise l'architecture MTV pour organiser le code:

# MODEL (M) = Base de données
#   - Définit la structure des données (ex: un article de blog a un titre, contenu, date)
#   - Django crée automatiquement les tables SQL pour vous
#   - Exemple: class Post avec title, content, created_at

# TEMPLATE (T) = Interface visuelle (HTML)
#   - Ce que l'utilisateur voit dans son navigateur
#   - Mélange HTML + variables Django
#   - Exemple: <h1>{{ post.title }}</h1>

# VIEW (V) = Logique métier (le cerveau)
#   - Reçoit requête HTTP, traite les données, retourne une page
#   - Fait le lien entre Model et Template
#   - Exemple: récupère les posts de la BDD et les envoie au template

# Flux complet:
# 1. Utilisateur demande www.monsite.com/blog/
# 2. Django trouve la VIEW correspondante
# 3. VIEW récupère données depuis MODEL (base de données)
# 4. VIEW envoie données au TEMPLATE
# 5. TEMPLATE génère HTML final
# 6. HTML est renvoyé au navigateur de l'utilisateur

# MTV vs MVC traditionnel:
# MTV (Django)          |  MVC (autres frameworks)
# Model                 |  Model
# Template              |  View
# View                  |  Controller

# === Concepts de base à comprendre ===

# PROJET = Votre site web entier
#   - Configuration globale
#   - URL principale
#   - Settings (base de données, sécurité, etc.)
#   - Exemple: "mon_site_e_commerce"

# APPLICATION = Fonctionnalité spécifique du projet
#   - Module réutilisable
#   - Un projet contient plusieurs apps
#   - Exemple: "blog", "boutique", "utilisateurs"
#   
#   Analogie: Un projet = une entreprise entière
#             Une app = un département (RH, Ventes, Marketing)

# URL = Adresse web qui pointe vers une vue
#   - www.monsite.com/blog/article-1/
#   - Django fait correspondre l'URL à une fonction Python

# MIGRATION = Changement de structure de base de données
#   - Django génère automatiquement le code SQL
#   - Permet de faire évoluer la BDD sans tout casser
#   - Comme un "git" pour votre base de données

# ORM = Object-Relational Mapping
#   - Écrire du Python au lieu de SQL
#   - Post.objects.all() au lieu de SELECT * FROM posts
#   - Plus simple et plus sécurisé

# === Prérequis ===

# Vous devez connaître:
# [OK] Python de base (variables, fonctions, classes)
# [OK] HTML de base (balises, structure)
# [OK] Notions de HTTP (GET, POST, URL)
# [X] Pas besoin de connaître SQL (Django s'en charge)
# [X] Pas besoin d'être expert Python

# Version recommandée: Django 5.1+ (LTS stable: 4.2)
# Python requis: 3.10 minimum (3.11+ recommandé)

# === Philosophie Django ===

# 1. DRY (Don't Repeat Yourself)
#    - Écrivez le code une seule fois, réutilisez-le partout
#    - Exemple: Définissez un Model, Django génère auto forms + admin

# 2. Convention over Configuration
#    - Des choix par défaut intelligents
#    - Exemple: app "blog" cherche templates dans "blog/templates/blog/"

# 3. Loose Coupling
#    - Les composants sont indépendants
#    - Exemple: Vous pouvez changer la BDD sans changer les views

# 4. Explicit is better than implicit
#    - Le code doit être clair et lisible
#    - Exemple: render(request, 'template.html', context) - on voit tout!

# === Que peut-on créer avec Django ? ===

# [OK] Blogs et sites de contenu
# [OK] Sites e-commerce
# [OK] Réseaux sociaux
# [OK] APIs REST (pour mobile, frontend JS)
# [OK] CMS (Content Management System)
# [OK] Plateformes SaaS
# [OK] Sites de streaming
# [OK] Outils internes d'entreprise

# Exemples réels utilisant Django:
# - Instagram (réseau social)
# - Spotify (streaming musique)
# - YouTube (au départ)
# - Pinterest (partage d'images)
# - The Washington Post (journal)
# - NASA (oui, l'agence spatiale!)

# === Structure d'apprentissage recommandée ===

# Pour un débutant, apprenez dans cet ordre:
# 1. Installation et premier projet (2h)
# 2. Models - Comprendre la base de données (4h)
# 3. Admin - Interface d'administration gratuite (1h)
# 4. Views et URLs - Logique et routing (3h)
# 5. Templates - Affichage HTML (3h)
# 6. Forms - Formulaires web (3h)
# 7. Users et Auth - Connexion/inscription (3h)
# 8. Static files - CSS, JS, images (2h)
# Total: ~21 heures pour être à l'aise avec Django

# === Erreurs courantes des débutants ===

# [X] Oublier d'activer l'environnement virtuel
#   Toujours faire: source .venv/bin/activate

# [X] Oublier makemigrations après changement de Model
#   Faire: python manage.py makemigrations puis migrate

# [X] Oublier {% csrf_token %} dans les formulaires POST
#   Django bloquera le formulaire par sécurité

# [X] Ne pas ajouter l'app dans INSTALLED_APPS
#   Django ne verra pas votre application!

# [X] Confondre render() et redirect()
#   render = affiche template, redirect = change d'URL

# [X] Mettre DEBUG=True en production
#   TRÈS DANGEREUX! Expose des infos sensibles

# === À quoi ressemble du code Django ? ===

# Exemple complet d'un blog simple:

# 1. Model (models.py) - Structure de données
class Post(models.Model):
    title = models.CharField(max_length=200)      # Titre (texte court)
    content = models.TextField()                   # Contenu (texte long)
    created_at = models.DateTimeField(auto_now_add=True)  # Date auto

# 2. View (views.py) - Logique
def post_list(request):
    posts = Post.objects.all()                    # Récupère tous les posts
    return render(request, 'blog/post_list.html', {'posts': posts})

# 3. URL (urls.py) - Routing
path('blog/', views.post_list, name='post_list')

# 4. Template (post_list.html) - Affichage
# <h1>Mon Blog</h1>
# {% for post in posts %}
#     <h2>{{ post.title }}</h2>
#     <p>{{ post.content }}</p>
# {% endfor %}

# C'est tout! Django crée la table SQL, gère les requêtes, affiche la page.

# === Comment ce guide est organisé ===

# Ce cheatsheet suit une progression logique:
# 1. Installation (mise en place)
# 2. Création projet/apps (structure)
# 3. Models (données)
# 4. Admin (interface gratuite)
# 5. Views (logique)
# 6. URLs (routing)
# 7. Templates (affichage)
# 8. Forms (formulaires)
# 9. Auth (utilisateurs)
# 10. Fonctionnalités avancées

# Chaque section contient:
# - Explication théorique (pourquoi?)
# - Exemples pratiques (comment?)
# - Erreurs à éviter (attention!)
# - Bonnes pratiques (recommandations)


[OK] INSTALLATION & SETUP - ÉTAPE PAR ÉTAPE


# === ÉTAPE 0: Vérifier Python (obligatoire!) ===

# Django est un framework Python, donc Python doit être installé d'abord.

# Vérifier si Python est installé
python --version                                  # Doit afficher Python 3.10+
python3 --version                                 # Sur Linux/Mac

# Si Python n'est pas installé:
# - Windows: Télécharger sur python.org
# - Mac: brew install python3
# - Linux: sudo apt install python3 python3-pip

# Pourquoi Python 3.10+ ?
# Django 5.x nécessite Python 3.10 minimum pour fonctionner.
# Recommandé: Python 3.11 ou 3.12 (plus rapide, plus récent)


# === ÉTAPE 1: Créer un environnement virtuel (CRUCIAL!) ===

# Qu'est-ce qu'un environnement virtuel ?
# C'est une copie isolée de Python pour votre projet.
# Chaque projet a son propre environnement avec ses propres packages.

# Pourquoi c'est OBLIGATOIRE ?
# [OK] Évite les conflits entre projets (projet A: Django 4.2, projet B: Django 5.1)
# [OK] Ne pollue pas votre Python système
# [OK] Facilite le déploiement (liste précise des packages)
# [X] Sans ça: cauchemar de dépendances et bugs incompréhensibles!

# Analogie: Comme avoir des bacs à sable séparés pour chaque jeu.
# Vous ne mélangez pas vos Lego avec vos Playmobil!

# Créer un dossier pour votre projet
mkdir mon_projet_django                           # Créer dossier
cd mon_projet_django                              # Entrer dedans

# Créer environnement virtuel (TOUJOURS faire ça en premier!)
python -m venv .venv                              # Crée dossier .venv
# .venv est le nom du dossier (convention standard)
# Vous pouvez l'appeler autrement: venv, env, myenv

# IMPORTANT: Le dossier .venv contient une copie de Python
# Ne JAMAIS versionner ce dossier avec git (trop gros, inutile)


# === ÉTAPE 2: Activer l'environnement virtuel ===

# TRÈS IMPORTANT: Vous devez TOUJOURS activer l'environnement virtuel
# avant de travailler sur votre projet Django!

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

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

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

# Comment savoir si c'est activé ?
# Votre terminal affiche (.venv) au début de la ligne:
# (.venv) user@ordinateur:~/mon_projet$
# ^ Ce (.venv) indique que l'environnement est actif

# Si vous oubliez d'activer:
# [X] pip install django installera Django GLOBALEMENT (mauvais!)
# [X] Vous ne pourrez pas lancer Django
# [X] Confusion totale sur quelle version est utilisée

# Pour désactiver (quand vous avez fini de travailler)
deactivate


# === ÉTAPE 3: Installer Django ===

# TOUJOURS s'assurer que l'environnement virtuel est activé AVANT!
# Vous devez voir (.venv) dans votre terminal

# Installer la dernière version de Django
pip install django

# Installer version spécifique (si besoin)
pip install django==5.1.0                         # Version exacte
pip install django==4.2.0                         # LTS (Long Term Support)

# Pourquoi différentes versions ?
# - 5.1+ : Dernières fonctionnalités, nécessite Python 3.10+
# - 4.2 LTS : Support à long terme (jusqu'en 2026), stable
# - 3.2 LTS : Vieille version, support étendu mais obsolète

# Recommandation débutant: Prenez la dernière version stable

# Vérifier que Django est bien installé
python -m django --version                        # Affiche 5.1.0 (ou votre version)
django-admin --version                            # Alternative

# Si ça ne marche pas:
# [X] Environnement virtuel pas activé -> activer avec source .venv/bin/activate
# [X] Python trop vieux -> installer Python 3.10+
# [X] Erreur d'installation -> vérifier connexion internet


# === ÉTAPE 4: Installer packages complémentaires (optionnel) ===

# Pour un projet Django complet, vous aurez besoin d'autres packages

# Pour PostgreSQL (base de données production)
pip install psycopg2-binary

# Pour MySQL
pip install mysqlclient

# Pour gérer les images (avatars, photos, etc.)
pip install pillow

# Pour les variables d'environnement (secrets, config)
pip install python-decouple

# Pour servir le site en production
pip install gunicorn

# Pour les fichiers statiques en production
pip install whitenoise

# Pour un projet complet typique:
pip install django pillow python-decouple gunicorn whitenoise

# ASTUCE: Créer un fichier requirements.txt pour sauvegarder les packages
pip freeze > requirements.txt

# Pour réinstaller tout d'un coup (sur un autre ordinateur par exemple):
pip install -r requirements.txt


# === Comprendre la structure finale ===

# Après ces étapes, votre dossier ressemble à ça:
mon_projet_django/
├── .venv/                    # Environnement virtuel (NE PAS VERSIONNER!)
│   ├── bin/                  # Exécutables Python (Scripts/ sur Windows)
│   ├── lib/                  # Packages installés (Django, etc.)
│   └── ...
└── (vide pour l'instant, projet Django sera créé après)

# Prochaine étape: créer le projet Django avec django-admin


[OK] CRÉER UN PROJET DJANGO - PREMIÈRE APPLICATION


# === ÉTAPE 1: Créer le projet Django ===

# Un PROJET = Votre site web entier
# C'est le conteneur principal qui contient toute la configuration

# Créer un nouveau projet (dans le dossier actuel)
django-admin startproject mon_site .

# ATTENTION au point "." à la fin! Il est TRÈS important:
# Sans point: django-admin startproject mon_site
#   Crée: mon_site/mon_site/settings.py (double dossier, confusant!)
# 
# Avec point: django-admin startproject mon_site .
#   Crée: mon_site/settings.py (structure propre, recommandé!)

# Pourquoi "mon_site" ?
# C'est le nom de votre projet. Choisissez un nom descriptif:
# - mon_blog
# - boutique_en_ligne
# - reseau_social
# Évitez: django, test, site (noms trop génériques)

# Structure créée automatiquement:
mon_projet_django/
├── .venv/                    # Environnement virtuel (déjà créé)
├── manage.py                 # * Script magique Django (commandes)
└── mon_site/                 # Package de configuration
    ├── __init__.py           # Indique que c'est un package Python
    ├── settings.py           # *** Configuration du projet (TRÈS IMPORTANT)
    ├── urls.py               # ** URLs principales du site
    ├── asgi.py               # Serveur asynchrone (avancé)
    └── wsgi.py               # Serveur web production (plus tard)


# === Explication des fichiers créés ===

# manage.py
# ----------
# C'est votre "couteau suisse" Django
# Permet de lancer le serveur, créer apps, faire migrations, etc.
# Vous l'utiliserez CONSTAMMENT
# NE JAMAIS MODIFIER CE FICHIER!
# 
# Commandes typiques:
# python manage.py runserver    -> Lance le site localement
# python manage.py migrate      -> Applique changements BDD
# python manage.py createsuperuser -> Crée admin

# settings.py
# -----------
# *** FICHIER LE PLUS IMPORTANT ***
# Contient TOUTE la configuration de votre site:
# - Base de données (SQLite, PostgreSQL, MySQL)
# - Applications installées
# - Sécurité (SECRET_KEY, DEBUG, ALLOWED_HOSTS)
# - Templates (où sont les fichiers HTML)
# - Static files (CSS, JS, images)
# - Internationalisation (langue, timezone)
# 
# C'est le "cerveau" de votre projet Django

# urls.py
# -------
# Définit les URLs de votre site
# Fait le lien entre URL et fonction Python
# Exemple: www.monsite.com/blog/ -> appelle la fonction blog_list()
# 
# C'est le "standard téléphonique" qui route les visiteurs

# asgi.py / wsgi.py
# -----------------
# Fichiers pour serveurs web en production
# ASGI = Asynchrone (WebSockets, temps réel)
# WSGI = Standard (sites classiques)
# Vous n'y toucherez quasiment jamais


# === ÉTAPE 2: Tester que ça marche ===

# Lancer le serveur de développement
python manage.py runserver

# Vous verrez dans le terminal:
# Starting development server at http://127.0.0.1:8000/
# Quit the server with CTRL-BREAK.

# * Ouvrir votre navigateur et aller sur: http://127.0.0.1:8000/
# ou http://localhost:8000/

# Vous devriez voir la page d'accueil Django avec une fusée! [RAPIDE]
# "The install worked successfully! Congratulations!"

# Si ça marche: BRAVO! Django est opérationnel!

# Explication de l'URL:
# - 127.0.0.1 = "localhost" = votre ordinateur
# - 8000 = port (numéro de porte sur votre ordinateur)
# - Le site n'est accessible QUE depuis votre ordinateur (pas sur Internet)

# Pour arrêter le serveur:
# Appuyez sur CTRL+C dans le terminal

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

# Rendre accessible depuis le réseau local:
python manage.py runserver 0.0.0.0:8000
# Accès depuis autre appareil: http://192.168.x.x:8000/


# === ÉTAPE 3: Créer votre première application ===

# Un PROJET contient plusieurs APPLICATIONS
# Une APPLICATION = une fonctionnalité spécifique

# Exemples d'applications:
# - blog (articles, commentaires)
# - boutique (produits, panier)
# - utilisateurs (profils, paramètres)
# - forum (discussions, messages)

# Règle d'or: UNE app = UNE fonctionnalité cohérente
# [OK] BON: app "blog" avec posts, comments, categories
# [X] MAUVAIS: app "tout" avec blog + boutique + forum (trop gros!)

# Créer une application "blog"
python manage.py startapp blog

# Structure créée:
blog/
├── migrations/              # Historique changements BDD (auto-généré)
│   └── __init__.py
├── __init__.py              # Package Python
├── admin.py                 # Configuration interface admin
├── apps.py                  # Configuration de l'app
├── models.py                # ** Vos modèles (tables BDD)
├── tests.py                 # Tests automatisés
└── views.py                 # ** Vos vues (logique)

# Vous allez principalement modifier:
# - models.py -> Définir structure de données
# - views.py -> Logique métier (fonctions)
# - admin.py -> Personnaliser l'admin Django
# - Créer urls.py -> Routes de l'application (à faire manuellement)
# - Créer templates/ -> Fichiers HTML (à faire manuellement)


# === ÉTAPE 4: Enregistrer l'application ===

# CRUCIAL! Django ne "voit" pas votre app tant qu'elle n'est pas enregistrée

# Ouvrir mon_site/settings.py
# Chercher INSTALLED_APPS (vers ligne 30)

# Avant:
INSTALLED_APPS = [
    'django.contrib.admin',           # Interface d'administration
    'django.contrib.auth',            # Authentification (users)
    'django.contrib.contenttypes',    # Types de contenu
    'django.contrib.sessions',        # Sessions utilisateur
    'django.contrib.messages',        # Messages flash
    'django.contrib.staticfiles',     # Fichiers statiques (CSS/JS)
]

# Après (ajouter votre app):
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Mes applications
    'blog',                           # Simple (OK)
    'blog.apps.BlogConfig',           # Avec config (RECOMMANDÉ)
]

# Pourquoi deux façons ?
# 'blog' -> Simple, fonctionne
# 'blog.apps.BlogConfig' -> Recommandé (plus de contrôle)

# Comment trouver BlogConfig ?
# C'est dans blog/apps.py:
# class BlogConfig(AppConfig):
#     default_auto_field = 'django.db.models.BigAutoField'
#     name = 'blog'

# ATTENTION: Si vous oubliez cette étape:
# [X] Vos models ne seront pas détectés
# [X] Migrations ne fonctionneront pas
# [X] Admin ne verra pas vos models
# -> Erreur fréquente chez les débutants!


# === ÉTAPE 5: Configuration de base (settings.py) ===

# Ouvrir mon_site/settings.py

# Langue et timezone (ligne ~106)
LANGUAGE_CODE = 'fr-fr'              # Français
TIME_ZONE = 'Europe/Paris'           # Fuseau horaire Paris
USE_I18N = True                      # Internationalisation
USE_TZ = True                        # Timezone-aware datetimes

# Pourquoi c'est important ?
# - Affichage des dates en français
# - Heures correctes pour la France
# - Formats de date européens (JJ/MM/AAAA)

# Autres langues courantes:
# LANGUAGE_CODE = 'en-us'  # Anglais américain
# LANGUAGE_CODE = 'es-es'  # Espagnol
# LANGUAGE_CODE = 'de-de'  # Allemand

# Timezones courantes:
# TIME_ZONE = 'Europe/Paris'     # Paris, France
# TIME_ZONE = 'Europe/London'    # Londres
# TIME_ZONE = 'America/New_York' # New York
# TIME_ZONE = 'UTC'              # Temps universel

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


# === Structure complète après ces étapes ===

mon_projet_django/
├── .venv/                    # Environnement virtuel
├── manage.py                 # Script Django
├── mon_site/                 # Configuration projet
│   ├── __init__.py
│   ├── settings.py           # * Configuration
│   ├── urls.py               # URLs principales
│   ├── asgi.py
│   └── wsgi.py
└── blog/                     # Votre première app
    ├── migrations/
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py             # * À modifier
    ├── tests.py
    └── views.py              # * À modifier

# Prochaine étape: Créer des models (tables de base de données)


[OK] APPLICATIONS DJANGO


# Django organise le code en "applications"
# Un projet = plusieurs applications
# Application = module réutilisable avec fonctionnalité spécifique

# === Créer une application ===

# Depuis la racine du projet
python manage.py startapp blog
python manage.py startapp accounts
python manage.py startapp shop

# Structure d'une app:
blog/
├── __init__.py
├── admin.py                    # Configuration admin
├── apps.py                     # Configuration app
├── migrations/                 # Migrations base de données
│   └── __init__.py
├── models.py                   # Modèles (base de données)
├── tests.py                    # Tests
├── views.py                    # Vues (logique)
└── urls.py                     # URLs (à créer manuellement)

# === Enregistrer l'application ===

# Dans myproject/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Vos applications
    'blog',                      # Simple
    'blog.apps.BlogConfig',     # Avec config (recommandé)
    'accounts',
    'shop',
]

# === Structure projet complet ===

myproject/
├── manage.py
├── myproject/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── blog/
│   ├── migrations/
│   ├── templates/
│   │   └── blog/
│   ├── static/
│   │   └── blog/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── views.py
│   ├── urls.py
│   ├── forms.py               # Formulaires (à créer)
│   └── tests.py
├── accounts/
├── templates/                  # Templates globaux
├── static/                     # Static files globaux
├── media/                      # Fichiers uploadés
├── .env                        # Variables d'environnement
├── .gitignore
├── requirements.txt
└── README.md


[OK] SERVEUR DE DÉVELOPPEMENT


# Lancer serveur de développement
python manage.py runserver

# Par défaut: http://127.0.0.1:8000/

# Spécifier port
python manage.py runserver 8080
python manage.py runserver 0.0.0.0:8000        # Accessible depuis réseau

# Avec rechargement automatique (défaut)
python manage.py runserver

# Sans rechargement automatique
python manage.py runserver --noreload

# Avec settings différent
python manage.py runserver --settings=myproject.settings_dev

# IMPORTANT: NE JAMAIS utiliser runserver en production!
# C'est un serveur de développement seulement


[OK] CONFIGURATION (SETTINGS.PY)


# === Configuration de base ===

# myproject/settings.py

# Secret key (CHANGER EN PRODUCTION!)
SECRET_KEY = 'django-insecure-your-secret-key'

# Debug mode (False en production!)
DEBUG = True

# Hosts autorisés
ALLOWED_HOSTS = []                              # Dev
ALLOWED_HOSTS = ['localhost', '127.0.0.1']      # Dev
ALLOWED_HOSTS = ['monsite.com', 'www.monsite.com']  # Production
ALLOWED_HOSTS = ['*']                           # Accepte tout (DEV SEULEMENT!)

# Applications installées
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Vos apps
    'blog',
    'accounts',
]

# Middleware (ordre important!)
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# Configuration URLs racine
ROOT_URLCONF = 'myproject.urls'

# Configuration templates
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],       # Templates globaux
        'APP_DIRS': True,                        # Cherche dans apps
        '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',
            ],
        },
    },
]

# Application WSGI
WSGI_APPLICATION = 'myproject.wsgi.application'

# === Configuration base de données ===

# SQLite (par défaut, dev)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# PostgreSQL (recommandé production)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydatabase',
        'USER': 'mydatabaseuser',
        'PASSWORD': 'mypassword',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# MySQL
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydatabase',
        'USER': 'mydatabaseuser',
        'PASSWORD': 'mypassword',
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4',
        },
    }
}

# Plusieurs bases de données
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'main_db',
    },
    'users_db': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'users_db',
    },
}

# === Internationalisation ===

LANGUAGE_CODE = 'fr-fr'                # Langue
TIME_ZONE = 'Europe/Paris'             # Fuseau horaire
USE_I18N = True                        # Internationalisation
USE_TZ = True                          # Timezone aware

# Langues disponibles
LANGUAGES = [
    ('fr', 'Français'),
    ('en', 'English'),
]

# === Fichiers statiques (CSS, JS, images) ===

STATIC_URL = '/static/'                # URL pour accéder aux statiques
STATIC_ROOT = BASE_DIR / 'staticfiles' # Dossier collecté (production)
STATICFILES_DIRS = [                   # Dossiers statiques additionnels
    BASE_DIR / 'static',
]

# === Fichiers média (uploads utilisateurs) ===

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# === Sécurité ===

# En production
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# === Email ===

# Console (dev)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# SMTP (production)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-password'
DEFAULT_FROM_EMAIL = 'noreply@monsite.com'

# === Variables d'environnement ===

# Utiliser python-decouple
from decouple import config

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

# Fichier .env (ne PAS versionner!)
SECRET_KEY=django-insecure-your-secret-key
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=postgresql://user:password@localhost/dbname


[OK] MODELS (MODÈLES) - COMPRENDRE LA BASE DE DONNÉES


# === Qu'est-ce qu'un Model ? ===

# Un MODEL = Une table dans votre base de données
# C'est la structure de vos données

# ANALOGIE: Un fichier Excel
# - Model = Le fichier Excel entier (ex: "Clients")
# - Chaque colonne = Un champ du model (ex: "nom", "email", "age")
# - Chaque ligne = Une instance du model (ex: "Jean Dupont", "jean@mail.com", 35)

# Exemple concret:
# Model "Post" (article de blog)
#   Colonnes: titre, contenu, date_creation, auteur
#   Ligne 1: "Django pour débutants", "Voici comment...", 2024-01-15, Jean
#   Ligne 2: "Python avancé", "Les décorateurs sont...", 2024-01-20, Marie

# Pourquoi utiliser des Models ?
# [OK] Django génère automatiquement les tables SQL (pas besoin d'écrire SQL!)
# [OK] Code Python au lieu de SQL (plus lisible, plus sécurisé)
# [OK] Changements faciles (modifier le model, Django met à jour la BDD)
# [OK] Validation automatique (email valide, nombre positif, etc.)


# === ÉTAPE 1: Créer votre premier Model ===

# Ouvrir blog/models.py

# Fichier par défaut (vide):
from django.db import models

# Create your models here.

# Créons un Model "Post" pour des articles de blog:

from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    """
    Model représentant un article de blog.
    Chaque Post a un titre, du contenu, une date, et un auteur.
    """
    
    # Champ titre (texte court, max 200 caractères)
    title = models.CharField(max_length=200)
    
    # Champ contenu (texte long, illimité)
    content = models.TextField()
    
    # Date de création (auto-remplie à la création)
    created_at = models.DateTimeField(auto_now_add=True)
    
    # Date de modification (auto-mise à jour à chaque save)
    updated_at = models.DateTimeField(auto_now=True)
    
    # Publié ou brouillon ?
    published = models.BooleanField(default=False)
    
    # Auteur (lien vers User Django)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    
    # Méthode spéciale: comment afficher un Post
    def __str__(self):
        return self.title
    
    # Métadonnées du model
    class Meta:
        ordering = ['-created_at']  # Tri par date (plus récent en premier)
        verbose_name = 'Article'
        verbose_name_plural = 'Articles'

# EXPLICATION LIGNE PAR LIGNE:

# class Post(models.Model):
#   - "class" = on crée une classe Python
#   - "Post" = nom du model (TOUJOURS singulier, CamelCase)
#   - "(models.Model)" = hérite de Model Django (obligatoire!)

# title = models.CharField(max_length=200)
#   - "title" = nom de la colonne en BDD
#   - "CharField" = champ texte court
#   - "max_length=200" = maximum 200 caractères (obligatoire pour CharField)
#   -> SQL: VARCHAR(200)

# content = models.TextField()
#   - "TextField" = texte long (articles, descriptions)
#   -> SQL: TEXT (illimité)

# created_at = models.DateTimeField(auto_now_add=True)
#   - "DateTimeField" = date + heure
#   - "auto_now_add=True" = remplit automatiquement à la création (UNE SEULE FOIS)
#   -> Exemple: 2024-01-15 14:30:25

# updated_at = models.DateTimeField(auto_now=True)
#   - "auto_now=True" = met à jour automatiquement à CHAQUE save()
#   -> Utile pour "Dernière modification"

# published = models.BooleanField(default=False)
#   - "BooleanField" = True ou False (oui/non)
#   - "default=False" = par défaut = False (brouillon)
#   -> SQL: BOOLEAN

# author = models.ForeignKey(User, on_delete=models.CASCADE)
#   - "ForeignKey" = relation vers autre table (lien)
#   - "User" = table des utilisateurs Django
#   - "on_delete=models.CASCADE" = si user supprimé, supprimer ses posts
#   -> SQL: FOREIGN KEY

# def __str__(self):
#   - Méthode spéciale Python qui définit comment afficher l'objet
#   - Sans ça: Post object (1) (pas lisible)
#   - Avec ça: "Mon super article" (lisible!)
#   -> Utilisé partout dans Django (admin, shell, templates)

# class Meta:
#   - Métadonnées du model (options)
#   - "ordering" = ordre par défaut (-created_at = plus récent d'abord)
#   - "verbose_name" = nom singulier affiché dans l'admin
#   - "verbose_name_plural" = nom pluriel affiché dans l'admin


# === Types de champs (Fields) les plus courants ===

# TEXTE
# ------
title = models.CharField(max_length=100)          # Texte court (nom, titre)
description = models.TextField()                  # Texte long (article, bio)
email = models.EmailField()                       # Email (validation auto)
url = models.URLField()                          # URL (validation auto)
slug = models.SlugField(unique=True)             # URL-friendly (mon-article)

# NOMBRES
# -------
age = models.IntegerField()                      # Nombre entier (-∞ à +∞)
price = models.DecimalField(max_digits=10, decimal_places=2)  # Prix: 99.99
rating = models.FloatField()                     # Nombre décimal
quantity = models.PositiveIntegerField()         # Entier positif (0 à +∞)

# Pourquoi DecimalField pour les prix ?
# FloatField a des erreurs d'arrondi: 0.1 + 0.2 = 0.30000000000000004
# DecimalField est EXACT: parfait pour l'argent!

# DATES & HEURES
# --------------
birth_date = models.DateField()                  # Date seulement (2024-01-15)
created_at = models.DateTimeField(auto_now_add=True)  # Date + heure
meeting_time = models.TimeField()                # Heure seulement (14:30:00)

# BOOLÉENS (OUI/NON)
# ------------------
is_active = models.BooleanField(default=True)    # True ou False
published = models.BooleanField(default=False)

# FICHIERS
# --------
image = models.ImageField(upload_to='images/')   # Image (nécessite Pillow)
document = models.FileField(upload_to='docs/')   # Fichier quelconque
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)

# upload_to = sous-dossier dans MEDIA_ROOT
# blank=True = optionnel dans formulaires
# null=True = peut être NULL en BDD

# CHOIX (LISTE DÉROULANTE)
# ------------------------
class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Brouillon'),        # ('valeur_bdd', 'Label affiché')
        ('published', 'Publié'),
        ('archived', 'Archivé'),
    ]
    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default='draft'
    )

# Utilisation:
# post.status = 'published'
# post.get_status_display()  -> "Publié"


# === Options des champs (très importantes!) ===

field = models.CharField(
    max_length=100,                    # Longueur max (obligatoire pour CharField)
    
    # Validation
    null=True,                         # Autoriser NULL en BDD ? (par défaut: False)
    blank=True,                        # Autoriser vide dans formulaires ? (par défaut: False)
    unique=True,                       # Valeur unique dans toute la table ?
    default='valeur',                  # Valeur par défaut
    
    # Affichage
    verbose_name='Nom du champ',       # Label affiché (admin, forms)
    help_text='Texte d\'aide',         # Aide sous le champ
    
    # Base de données
    db_index=True,                     # Créer index (recherche plus rapide)
    editable=False,                    # Non modifiable dans forms/admin
    primary_key=True,                  # Clé primaire (rare, Django en crée une auto)
)

# IMPORTANT: Différence entre null et blank
# null=True -> Concerne la BASE DE DONNÉES (peut être NULL)
# blank=True -> Concerne les FORMULAIRES (peut être vide)

# Exemples:
# Champ obligatoire partout
field = models.CharField(max_length=100)  # null=False, blank=False (défaut)

# Optionnel dans formulaires, mais doit avoir valeur en BDD
field = models.CharField(max_length=100, blank=True, default='')

# Optionnel partout (pour nombres, dates, fichiers, relations)
age = models.IntegerField(null=True, blank=True)
image = models.ImageField(upload_to='images/', null=True, blank=True)


# === ÉTAPE 2: Créer les migrations ===

# Les MIGRATIONS = Instructions pour modifier la base de données
# Django les génère automatiquement à partir de vos models!

# Détecter les changements dans vos models
python manage.py makemigrations

# Sortie dans le terminal:
# Migrations for 'blog':
#   blog/migrations/0001_initial.py
#     - Create model Post

# Que s'est-il passé ?
# Django a créé un fichier blog/migrations/0001_initial.py
# Ce fichier contient les instructions pour créer la table "Post" en SQL

# IMPORTANT: Vous devez faire makemigrations à CHAQUE modification de model!
# - Ajout d'un champ ? -> makemigrations
# - Suppression d'un champ ? -> makemigrations  
# - Modification d'options ? -> makemigrations

# Voir le SQL qui sera exécuté (optionnel, pour comprendre)
python manage.py sqlmigrate blog 0001

# Sortie: Le vrai SQL généré
# CREATE TABLE "blog_post" (
#     "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
#     "title" varchar(200) NOT NULL,
#     "content" text NOT NULL,
#     ...
# );

# Vous ne devez JAMAIS écrire ce SQL! Django le fait pour vous!


# === ÉTAPE 3: Appliquer les migrations ===

# Maintenant, il faut vraiment créer la table dans la base de données

python manage.py migrate

# Sortie:
# Operations to perform:
#   Apply all migrations: admin, auth, contenttypes, sessions, blog
# Running migrations:
#   Applying blog.0001_initial... OK

# Que s'est-il passé ?
# Django a exécuté le SQL et créé la table "blog_post" dans la BDD!

# Par défaut, Django utilise SQLite (fichier db.sqlite3)
# C'est parfait pour développement, simple, pas de config

# Où est la base de données ?
# Fichier db.sqlite3 à la racine du projet
# Vous pouvez l'ouvrir avec DB Browser for SQLite pour voir les tables

# WORKFLOW COMPLET:
# 1. Modifier models.py
# 2. python manage.py makemigrations  (génère fichier migration)
# 3. python manage.py migrate         (applique à la BDD)


# === Relations entre Models ===

# Les relations permettent de lier des tables entre elles

# 1. ONE-TO-MANY (Un-à-Plusieurs) - ForeignKey
# ----------------------------------------------
# Exemple: Un auteur peut avoir plusieurs articles
#          Un article a UN SEUL auteur

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    # Un User peut avoir plein de Posts
    # Un Post a UN SEUL User

# on_delete options (QUE FAIRE si l'objet lié est supprimé ?):
# - CASCADE: Supprimer aussi cet objet (si user supprimé -> supprimer ses posts)
# - PROTECT: EMPÊCHER suppression (erreur si user a des posts)
# - SET_NULL: Mettre NULL (author=None, nécessite null=True)
# - SET_DEFAULT: Mettre valeur par défaut
# - DO_NOTHING: Ne rien faire (DANGEREUX, éviter!)

# Utilisation:
user = User.objects.get(username='jean')
post = Post.objects.create(title='Mon article', author=user)
print(post.author.username)  # 'jean'

# Relation inverse (depuis User vers Posts):
user.post_set.all()  # Tous les posts de cet user

# Avec related_name (plus lisible):
class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')

user.posts.all()  # Plus clair que post_set!


# 2. MANY-TO-MANY (Plusieurs-à-Plusieurs) - ManyToManyField
# ----------------------------------------------------------
# Exemple: Un article peut avoir plusieurs tags
#          Un tag peut être sur plusieurs articles

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

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag)
    # Un Post a plusieurs Tags
    # Un Tag est sur plusieurs Posts

# Utilisation:
post = Post.objects.create(title='Django tutorial')
tag1 = Tag.objects.create(name='Python')
tag2 = Tag.objects.create(name='Web')

post.tags.add(tag1, tag2)        # Ajouter tags
post.tags.all()                  # Voir tous les tags du post
post.tags.remove(tag1)           # Retirer un tag

tag1.post_set.all()              # Tous les posts avec ce tag


# 3. ONE-TO-ONE (Un-à-Un) - OneToOneField
# ----------------------------------------
# Exemple: Un user a UN SEUL profil
#          Un profil appartient à UN SEUL user

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

# Utilisation:
user = User.objects.get(username='jean')
profile = UserProfile.objects.create(
    user=user,
    bio='Développeur Django passionné'
)

# Accès direct:
user.userprofile.bio  # 'Développeur Django passionné'
profile.user.username  # 'jean'


# === Méthodes personnalisées sur les Models ===

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    views = models.PositiveIntegerField(default=0)
    
    def __str__(self):
        """Représentation string du model"""
        return self.title
    
    def get_short_content(self):
        """Retourne les 100 premiers caractères du contenu"""
        if len(self.content) > 100:
            return self.content[:100] + '...'
        return self.content
    
    def increment_views(self):
        """Incrémenter le nombre de vues"""
        self.views += 1
        self.save()
    
    def is_recent(self):
        """Vérifie si l'article a moins de 7 jours"""
        from datetime import timedelta
        from django.utils import timezone
        return self.created_at >= timezone.now() - timedelta(days=7)
    
    def get_absolute_url(self):
        """URL de l'article (convention Django)"""
        from django.urls import reverse
        return reverse('post_detail', kwargs={'pk': self.pk})

# Utilisation:
post = Post.objects.get(id=1)
print(post.get_short_content())  # "Django est un framework..."
post.increment_views()           # views = 1
if post.is_recent():
    print("Article récent!")


# === Validation personnalisée ===

from django.core.exceptions import ValidationError

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    
    def clean(self):
        """Validation personnalisée (appelée avant save)"""
        # Titre trop court ?
        if len(self.title) < 5:
            raise ValidationError('Le titre doit faire au moins 5 caractères')
        
        # Contenu avec mots interdits ?
        forbidden_words = ['spam', 'viagra', 'casino']
        for word in forbidden_words:
            if word in self.content.lower():
                raise ValidationError(f'Le mot "{word}" est interdit')
    
    def save(self, *args, **kwargs):
        """Override save pour valider avant sauvegarde"""
        self.full_clean()  # Appelle clean()
        super().save(*args, **kwargs)

# Utilisation:
try:
    post = Post(title='ab', content='test')  # Titre trop court
    post.save()  # Lève ValidationError
except ValidationError as e:
    print(e)


# === Propriétés calculées ===

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    
    @property
    def word_count(self):
        """Nombre de mots (propriété calculée, pas en BDD)"""
        return len(self.content.split())
    
    @property
    def reading_time(self):
        """Temps de lecture estimé en minutes"""
        words = self.word_count
        return round(words / 200)  # 200 mots/min en moyenne

# Utilisation (comme un attribut):
post = Post.objects.get(id=1)
print(f"Mots: {post.word_count}")           # Mots: 543
print(f"Lecture: {post.reading_time} min")  # Lecture: 3 min

# ATTENTION: @property n'est PAS sauvegardé en BDD!
# C'est calculé à chaque accès


# === Résumé pour débutants ===

# 1. Un Model = Une table dans la BDD
# 2. Chaque attribut = Une colonne
# 3. CharField, TextField, IntegerField, etc. = Types de colonnes
# 4. ForeignKey = Relation (lien vers autre table)
# 5. Après modification: makemigrations puis migrate
# 6. __str__() = Comment afficher l'objet
# 7. class Meta = Options du model (tri, noms, etc.)

# Prochaine étape: Utiliser vos models dans l'admin Django!

# === Types de champs ===

# Texte
title = models.CharField(max_length=200)            # Texte court
slug = models.SlugField(max_length=200, unique=True)
content = models.TextField()                         # Texte long
email = models.EmailField()
url = models.URLField()

# Nombres
age = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
quantity = models.PositiveIntegerField()
small_number = models.SmallIntegerField()
big_number = models.BigIntegerField()

# Booléens
published = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)

# Dates et heures
created_at = models.DateTimeField(auto_now_add=True)  # À la création
updated_at = models.DateTimeField(auto_now=True)       # À chaque save
birth_date = models.DateField()
appointment_time = models.TimeField()
duration = models.DurationField()

# Fichiers
image = models.ImageField(upload_to='images/')         # Nécessite Pillow
document = models.FileField(upload_to='documents/')
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)

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

# JSON (PostgreSQL)
data = models.JSONField(default=dict, blank=True)

# UUID
import uuid
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

# === Options de champs ===

field = models.CharField(
    max_length=100,
    null=True,                  # NULL autorisé en BDD
    blank=True,                 # Vide autorisé dans formulaires
    default='valeur',           # Valeur par défaut
    unique=True,                # Valeur unique
    choices=CHOICES,            # Liste de choix
    help_text='Aide',           # Texte d'aide
    verbose_name='Nom',         # Label
    db_index=True,              # Créer index
    editable=False,             # Non éditable dans admin
    primary_key=True,           # Clé primaire (rare, auto créée)
)

# === Relations entre models ===

# One-to-Many (ForeignKey)
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    text = models.TextField()

# on_delete options:
# CASCADE - Supprime les objets liés
# PROTECT - Empêche suppression si objets liés existent
# SET_NULL - Met NULL (nécessite null=True)
# SET_DEFAULT - Met valeur par défaut
# SET(...) - Met valeur spécifique
# DO_NOTHING - Ne fait rien (attention!)

# Many-to-Many
class Post(models.Model):
    tags = models.ManyToManyField('Tag')

class Tag(models.Model):
    name = models.CharField(max_length=50)

# Many-to-Many avec table intermédiaire
class Post(models.Model):
    tags = models.ManyToManyField('Tag', through='PostTag')

class Tag(models.Model):
    name = models.CharField(max_length=50)

class PostTag(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_date = models.DateTimeField(auto_now_add=True)

# One-to-One
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField()
    avatar = models.ImageField(upload_to='avatars/')

# Self-referencing (arbre, hiérarchie)
class Category(models.Model):
    name = models.CharField(max_length=100)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)

# === Héritage de models ===

# Abstract base class (pas de table créée)
class BaseModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        abstract = True

class Post(BaseModel):
    title = models.CharField(max_length=200)
    # Hérite de created_at et updated_at

# Multi-table inheritance (table pour chaque model)
class Place(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=200)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)

# Proxy models (même table, comportement différent)
class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

class MyPerson(Person):
    class Meta:
        proxy = True
        ordering = ['last_name']

# === Méthodes personnalisées ===

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    def get_short_content(self):
        """Retourne les 100 premiers caractères"""
        return self.content[:100] + '...'
    
    def days_since_creation(self):
        """Nombre de jours depuis la création"""
        from django.utils import timezone
        delta = timezone.now() - self.created_at
        return delta.days
    
    def get_absolute_url(self):
        """URL de l'objet"""
        from django.urls import reverse
        return reverse('post_detail', kwargs={'pk': self.pk})
    
    def save(self, *args, **kwargs):
        """Override save pour logique personnalisée"""
        # Faire quelque chose avant save
        self.title = self.title.upper()
        super().save(*args, **kwargs)
        # Faire quelque chose après save

# === Managers personnalisés ===

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(published=True)

class Post(models.Model):
    title = models.CharField(max_length=200)
    published = models.BooleanField(default=False)
    
    objects = models.Manager()           # Manager par défaut
    published_objects = PublishedManager()  # Manager personnalisé

# Utilisation:
Post.objects.all()                    # Tous les posts
Post.published_objects.all()          # Seulement publiés

# === Validation personnalisée ===

from django.core.exceptions import ValidationError

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    
    def clean(self):
        """Validation personnalisée"""
        if len(self.title) < 5:
            raise ValidationError('Le titre doit faire au moins 5 caractères')
        if 'spam' in self.content.lower():
            raise ValidationError('Le contenu contient des mots interdits')
    
    def save(self, *args, **kwargs):
        self.full_clean()  # Appelle clean()
        super().save(*args, **kwargs)


[OK] MIGRATIONS


# Les migrations gèrent l'évolution du schéma de base de données
# Elles sont automatiquement générées à partir des models

# === Créer migrations ===

# Détecter changements dans models
python manage.py makemigrations

# Pour une app spécifique
python manage.py makemigrations blog

# Avec nom personnalisé
python manage.py makemigrations --name add_slug_to_post

# Voir le SQL qui sera exécuté
python manage.py sqlmigrate blog 0001

# Vérifier problèmes sans créer migration
python manage.py makemigrations --check
python manage.py makemigrations --dry-run

# === Appliquer migrations ===

# Appliquer toutes les migrations
python manage.py migrate

# Appliquer migration spécifique
python manage.py migrate blog
python manage.py migrate blog 0003

# Revenir en arrière
python manage.py migrate blog 0002         # Revient à la migration 0002
python manage.py migrate blog zero         # Annule toutes les migrations

# Avec fake (marque comme appliquée sans exécuter)
python manage.py migrate --fake
python manage.py migrate blog 0003 --fake

# === Lister migrations ===

# Voir toutes les migrations
python manage.py showmigrations

# Pour une app
python manage.py showmigrations blog

# Format plan
python manage.py showmigrations --plan

# === Structure d'une migration ===

# blog/migrations/0001_initial.py
from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True
    
    dependencies = []
    
    operations = [
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.BigAutoField(primary_key=True)),
                ('title', models.CharField(max_length=200)),
                ('content', models.TextField()),
                ('created_at', models.DateTimeField(auto_now_add=True)),
            ],
        ),
    ]

# === Migration vide (pour code custom) ===

python manage.py makemigrations --empty blog --name populate_slugs

# Éditer la migration créée:
from django.db import migrations

def populate_slugs(apps, schema_editor):
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.slug = post.title.lower().replace(' ', '-')
        post.save()

class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0002_post_slug'),
    ]
    
    operations = [
        migrations.RunPython(populate_slugs),
    ]

# === Squash migrations (combiner) ===

# Combiner migrations 0001 à 0004
python manage.py squashmigrations blog 0001 0004

# === Problèmes courants ===

# Conflit de migrations
python manage.py makemigrations --merge

# Réinitialiser migrations d'une app
# 1. Supprimer les fichiers de migration (sauf __init__.py)
# 2. Supprimer les tables de la BDD
python manage.py migrate blog zero --fake
# 3. Supprimer fichiers migrations
rm blog/migrations/0*.py
# 4. Recréer migrations
python manage.py makemigrations blog
python manage.py migrate blog --fake-initial

# === Bonnes pratiques ===

# [OK] Toujours créer migrations après changement de models
# [OK] Versionner les fichiers de migration
# [OK] Tester migrations en dev avant production
# [OK] Faire backup BDD avant migrations en production
# [OK] Ne jamais modifier migrations déjà appliquées
# [OK] Utiliser migrations pour données (RunPython) si nécessaire


[OK] QUERYSETS (REQUÊTES ORM)


# Django ORM permet d'interagir avec la BDD sans SQL
# QuerySet = collection d'objets de la BDD

# === Récupérer objets ===

# Tous les objets
Post.objects.all()

# Filtrer
Post.objects.filter(published=True)
Post.objects.filter(title='Mon titre')
Post.objects.filter(title__icontains='django')    # Contient (insensible casse)

# Exclure
Post.objects.exclude(published=False)

# Get (une seule instance, lève exception si 0 ou >1 résultat)
post = Post.objects.get(id=1)
post = Post.objects.get(slug='mon-article')

# Get avec gestion exception
from django.shortcuts import get_object_or_404
post = get_object_or_404(Post, id=1)              # 404 si pas trouvé

# First / Last
first_post = Post.objects.first()
last_post = Post.objects.last()

# Existe?
exists = Post.objects.filter(title='Test').exists()

# Compter
count = Post.objects.count()
count = Post.objects.filter(published=True).count()

# === Lookups (filtres avancés) ===

# Exact (défaut)
Post.objects.filter(title__exact='Mon titre')
Post.objects.filter(title='Mon titre')            # Équivalent

# Insensible à la casse
Post.objects.filter(title__iexact='mon titre')

# Contains
Post.objects.filter(title__contains='Django')
Post.objects.filter(title__icontains='django')    # Insensible casse

# Starts with / Ends with
Post.objects.filter(title__startswith='Django')
Post.objects.filter(title__istartswith='django')
Post.objects.filter(title__endswith='Python')
Post.objects.filter(title__iendswith='python')

# In
Post.objects.filter(id__in=[1, 2, 3])
Post.objects.filter(status__in=['published', 'draft'])

# Greater than / Less than
Post.objects.filter(id__gt=5)                     # >
Post.objects.filter(id__gte=5)                    # >=
Post.objects.filter(id__lt=10)                    # <
Post.objects.filter(id__lte=10)                   # <=

# Range
Post.objects.filter(id__range=(1, 10))            # BETWEEN

# Date
from datetime import date
Post.objects.filter(created_at__date=date(2024, 1, 1))
Post.objects.filter(created_at__year=2024)
Post.objects.filter(created_at__month=12)
Post.objects.filter(created_at__day=25)
Post.objects.filter(created_at__week_day=2)       # 1=dimanche, 2=lundi

# NULL
Post.objects.filter(author__isnull=True)
Post.objects.filter(author__isnull=False)

# === Opérateurs logiques ===

from django.db.models import Q

# AND (par défaut)
Post.objects.filter(published=True, author__username='john')
Post.objects.filter(published=True).filter(author__username='john')  # Équivalent

# OR
Post.objects.filter(Q(published=True) | Q(author__username='john'))

# AND + OR
Post.objects.filter(
    Q(published=True) & (Q(title__contains='Django') | Q(title__contains='Python'))
)

# NOT
Post.objects.filter(~Q(published=True))

# === Relations (Foreign Key) ===

# Accès direct
comment = Comment.objects.get(id=1)
post = comment.post                               # Accède au post
author = post.author                              # Accède à l'auteur

# Filtrer par relation
Comment.objects.filter(post__title='Mon article')
Comment.objects.filter(post__author__username='john')

# Reverse relation (depuis post vers comments)
post = Post.objects.get(id=1)
comments = post.comment_set.all()                 # Tous les comments du post

# Avec related_name
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')

post.comments.all()                               # Plus lisible!

# Filtrer sur reverse relation
Post.objects.filter(comments__text__contains='super')

# === Select related (optimisation) ===

# Sans select_related (N+1 queries)
comments = Comment.objects.all()
for comment in comments:
    print(comment.post.title)                     # Query pour chaque post!

# Avec select_related (1 query avec JOIN)
comments = Comment.objects.select_related('post')
for comment in comments:
    print(comment.post.title)                     # Pas de query!

# Plusieurs relations
comments = Comment.objects.select_related('post', 'post__author')

# === Prefetch related (Many-to-Many) ===

# Sans prefetch_related (N+1 queries)
posts = Post.objects.all()
for post in posts:
    print(post.tags.all())                        # Query pour chaque post!

# Avec prefetch_related (2 queries)
posts = Post.objects.prefetch_related('tags')
for post in posts:
    print(post.tags.all())                        # Pas de query!

# Avec filtrage
from django.db.models import Prefetch
posts = Post.objects.prefetch_related(
    Prefetch('comments', queryset=Comment.objects.filter(approved=True))
)

# === Ordering (tri) ===

# Ascendant
Post.objects.order_by('created_at')
Post.objects.order_by('title')

# Descendant
Post.objects.order_by('-created_at')
Post.objects.order_by('-title')

# Plusieurs champs
Post.objects.order_by('-created_at', 'title')

# Aléatoire
Post.objects.order_by('?')

# Reverse
Post.objects.order_by('-created_at').reverse()

# === Agrégation ===

from django.db.models import Count, Sum, Avg, Max, Min

# Count
Post.objects.count()
Post.objects.filter(published=True).count()

# Aggregate (retourne dict)
from django.db.models import Avg
result = Post.objects.aggregate(Avg('rating'))
# {'rating__avg': 4.5}

result = Post.objects.aggregate(
    avg_rating=Avg('rating'),
    max_rating=Max('rating'),
    min_rating=Min('rating'),
    total_views=Sum('views')
)

# Annotate (ajoute champ calculé à chaque objet)
posts = Post.objects.annotate(num_comments=Count('comments'))
for post in posts:
    print(f"{post.title}: {post.num_comments} commentaires")

# Filtrer sur annotate
popular_posts = Post.objects.annotate(
    num_comments=Count('comments')
).filter(num_comments__gt=10)

# === Distinct ===

# Valeurs uniques
Post.objects.values('author').distinct()

# Avec PostgreSQL (distinct sur champs spécifiques)
Post.objects.distinct('author')

# === Values & Values list ===

# Values (retourne dict)
Post.objects.values('id', 'title')
# [{'id': 1, 'title': 'Post 1'}, {'id': 2, 'title': 'Post 2'}]

# Values list (retourne tuples)
Post.objects.values_list('id', 'title')
# [(1, 'Post 1'), (2, 'Post 2')]

# Flat (une seule valeur)
Post.objects.values_list('title', flat=True)
# ['Post 1', 'Post 2', 'Post 3']

# === Slicing (pagination) ===

# Premiers 5
Post.objects.all()[:5]

# Du 6ème au 10ème
Post.objects.all()[5:10]

# Tous sauf les 5 premiers
Post.objects.all()[5:]

# ATTENTION: pas de slicing négatif!
# Post.objects.all()[-5:]  # ERROR!

# === Update ===

# Update un objet
post = Post.objects.get(id=1)
post.title = 'Nouveau titre'
post.save()

# Update plusieurs objets (plus efficace)
Post.objects.filter(published=False).update(published=True)
Post.objects.filter(author__username='john').update(status='archived')

# Update avec F() pour calculs
from django.db.models import F
Post.objects.filter(published=True).update(views=F('views') + 1)

# === Delete ===

# Delete un objet
post = Post.objects.get(id=1)
post.delete()

# Delete plusieurs objets
Post.objects.filter(published=False).delete()
Post.objects.filter(created_at__year=2020).delete()

# Delete tous (ATTENTION!)
Post.objects.all().delete()

# === Raw SQL (si vraiment nécessaire) ===

# Raw queries
posts = Post.objects.raw('SELECT * FROM blog_post WHERE published = %s', [True])

# Execute SQL directement
from django.db import connection
with connection.cursor() as cursor:
    cursor.execute("SELECT * FROM blog_post WHERE id = %s", [1])
    row = cursor.fetchone()

# === QuerySet chaining ===

# Les querysets sont lazy (exécutés seulement quand nécessaire)
posts = Post.objects.all()                        # Pas de query
posts = posts.filter(published=True)              # Pas de query
posts = posts.order_by('-created_at')             # Pas de query
list(posts)                                       # Query exécutée!

# Chaînage
posts = (Post.objects
         .filter(published=True)
         .select_related('author')
         .prefetch_related('tags')
         .order_by('-created_at')[:10])

# === Bulk operations (performance) ===

# Bulk create
posts = [
    Post(title='Post 1', content='Content 1'),
    Post(title='Post 2', content='Content 2'),
    Post(title='Post 3', content='Content 3'),
]
Post.objects.bulk_create(posts)

# Bulk update
posts = Post.objects.filter(published=False)
for post in posts:
    post.published = True
Post.objects.bulk_update(posts, ['published'])


[OK] VIEWS (VUES) - LE CERVEAU DE VOTRE APPLICATION


# === Qu'est-ce qu'une View ? ===

# Une VIEW = Une fonction Python qui traite une requête et retourne une réponse
# C'est le "cerveau" qui fait le lien entre vos données (Models) et l'affichage (Templates)

# ANALOGIE: Un serveur dans un restaurant
# 1. Client demande un plat (requête HTTP)
# 2. Serveur (View) va en cuisine chercher les ingrédients (Models/BDD)
# 3. Serveur prépare le plat sur une assiette (Template)
# 4. Serveur apporte le plat au client (réponse HTTP)

# Flux complet d'une requête Django:
# 1. Utilisateur tape: www.monsite.com/blog/
# 2. Django trouve la View correspondante (via urls.py)
# 3. View récupère les données depuis la BDD (Models)
# 4. View envoie les données au Template HTML
# 5. Template génère le HTML final
# 6. View retourne le HTML au navigateur
# 7. Utilisateur voit la page!

# Il existe 2 types de Views:
# - Function-Based Views (FBV) = Fonctions Python simples (pour débutants)
# - Class-Based Views (CBV) = Classes Python (plus avancé, moins de code)


# === ÉTAPE 1: Créer votre première View (FBV) ===

# Ouvrir blog/views.py

# Fichier par défaut (vide):
from django.shortcuts import render

# Create your views here.

# View la plus simple possible:
from django.http import HttpResponse

def hello(request):
    """View basique qui retourne du texte brut"""
    return HttpResponse("Bonjour! Bienvenue sur mon blog Django!")

# Explication ligne par ligne:
# - def hello(request): = fonction Python normale
# - request = objet contenant toutes les infos de la requête HTTP
#   (méthode GET/POST, paramètres URL, user connecté, cookies, etc.)
# - HttpResponse() = réponse HTTP basique (texte brut)
# - return = renvoie la réponse au navigateur

# IMPORTANT: TOUTES les views doivent:
# 1. Accepter "request" comme premier paramètre
# 2. Retourner un objet HttpResponse (ou sous-classe)

# Cette view affiche juste du texte, pas de HTML!


# === View avec Template (affichage HTML) ===

# Dans la vraie vie, vous voulez afficher du HTML, pas du texte brut!

def blog_home(request):
    """View qui affiche la page d'accueil du blog"""
    return render(request, 'blog/home.html')

# Explication:
# - render() = fonction Django qui:
#   1. Charge le template HTML
#   2. Le transforme en HTML final
#   3. Le retourne comme HttpResponse
# - 'blog/home.html' = chemin vers le template
#   Django cherche dans: blog/templates/blog/home.html

# Pourquoi blog/templates/blog/home.html (double "blog") ?
# C'est le NAMESPACING pour éviter les conflits
# Si vous avez app1/home.html et app2/home.html, Django les différencie!


# === View avec données (Context) ===

# Pour passer des données au template, utilisez le "context"

from django.shortcuts import render
from .models import Post

def post_list(request):
    """Affiche la liste de tous les articles"""
    # 1. Récupérer les données depuis la BDD
    posts = Post.objects.all()  # Tous les posts
    
    # 2. Préparer le contexte (dictionnaire Python)
    context = {
        'posts': posts,                # Liste de posts
        'title': 'Mon Blog',          # Titre de la page
        'total': posts.count()        # Nombre total
    }
    
    # 3. Envoyer au template
    return render(request, 'blog/post_list.html', context)

# Dans le template (post_list.html), vous pouvez utiliser:
# {{ title }}  -> "Mon Blog"
# {{ total }}  -> 42
# {% for post in posts %}...{% endfor %}  -> Boucle sur les posts

# RAPPEL: Le context est un dictionnaire Python
# Clé = nom de la variable dans le template
# Valeur = données à afficher


# === View avec paramètres d'URL ===

# Souvent, vous voulez afficher UN objet spécifique (ex: article #5)

def post_detail(request, post_id):
    """Affiche le détail d'un article spécifique"""
    # Récupérer le post avec cet ID
    post = Post.objects.get(id=post_id)
    
    return render(request, 'blog/post_detail.html', {'post': post})

# post_id vient de l'URL: /blog/5/ -> post_id=5
# On verra comment configurer ça dans la section URLs

# PROBLÈME: Si le post n'existe pas, ça crash! (DoesNotExist error)

# SOLUTION: Utiliser get_object_or_404() (recommandé!)
from django.shortcuts import get_object_or_404

def post_detail(request, post_id):
    """Affiche le détail d'un article (avec gestion 404)"""
    post = get_object_or_404(Post, id=post_id)
    # Si post existe -> retourne le post
    # Si post n'existe pas -> affiche page 404 automatiquement!
    
    return render(request, 'blog/post_detail.html', {'post': post})

# Autre exemple avec slug au lieu d'ID:
def post_by_slug(request, slug):
    """Affiche un article par son slug"""
    post = get_object_or_404(Post, slug=slug)
    return render(request, 'blog/post_detail.html', {'post': post})

# URL: /blog/mon-premier-article/ -> slug="mon-premier-article"


# === View avec méthodes GET et POST (formulaires) ===

# GET = Afficher une page (lecture)
# POST = Soumettre des données (écriture, formulaire)

def contact(request):
    """Page de contact avec formulaire"""
    if request.method == 'POST':
        # Formulaire soumis (bouton cliqué)
        name = request.POST.get('name')      # Récupère champ "name"
        email = request.POST.get('email')    # Récupère champ "email"
        message = request.POST.get('message')
        
        # Traiter les données (envoyer email, sauver en BDD, etc.)
        # ...
        
        # Rediriger vers page de succès
        return redirect('success')
    else:
        # Méthode GET = afficher le formulaire vide
        return render(request, 'blog/contact.html')

# Explication du flux:
# 1. Utilisateur arrive sur /contact/ (GET)
#    -> Affiche formulaire vide
# 2. Utilisateur remplit et clique "Envoyer" (POST)
#    -> Traite les données
#    -> Redirige vers page de succès

# request.POST = dictionnaire avec les données du formulaire
# request.POST.get('name') = valeur du champ avec name="name"
# request.POST['name'] = pareil mais crash si absent (éviter)

# IMPORTANT: Toujours vérifier request.method!
# Sans ça, le code POST s'exécute même en GET (bugs bizarres)


# === Redirection ===

from django.shortcuts import redirect

def old_page(request):
    """Redirige vers la nouvelle page"""
    return redirect('new_page')  # Nom de l'URL
    # return redirect('/nouvelle-page/')  # URL directe
    # return redirect('post_detail', post_id=5)  # Avec paramètres

# redirect() retourne une réponse HTTP 302 (redirection)
# Le navigateur charge automatiquement la nouvelle page

# Différence redirect() vs render():
# - render() = affiche un template (URL ne change pas)
# - redirect() = change l'URL et charge une autre page


# === Réponses JSON (pour APIs) ===

from django.http import JsonResponse

def api_posts(request):
    """Retourne les posts en format JSON"""
    posts = Post.objects.all().values('id', 'title', 'published')
    # values() retourne des dictionnaires au lieu d'objets
    
    return JsonResponse(list(posts), safe=False)

# Résultat dans le navigateur:
# [
#   {"id": 1, "title": "Mon article", "published": true},
#   {"id": 2, "title": "Autre article", "published": false}
# ]

# safe=False nécessaire pour listes (par défaut: seulement dicts)

# JSON simple:
def api_status(request):
    return JsonResponse({
        'status': 'ok',
        'message': 'API fonctionne!',
        'count': Post.objects.count()
    })


# === Gestion d'erreurs ===

from django.http import Http404

def post_detail(request, post_id):
    """Affiche un post ou 404"""
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        raise Http404("Article introuvable")
    
    return render(request, 'blog/post_detail.html', {'post': post})

# Ou plus simple avec get_object_or_404():
post = get_object_or_404(Post, id=post_id)

# Autres erreurs HTTP:
from django.http import HttpResponseForbidden, HttpResponseBadRequest

def admin_only(request):
    if not request.user.is_staff:
        return HttpResponseForbidden("Accès réservé aux admins")
    return render(request, 'admin_page.html')


# === L'objet request (très important!) ===

def my_view(request):
    """Exemple montrant toutes les infos disponibles dans request"""
    
    # Méthode HTTP
    method = request.method  # 'GET', 'POST', 'PUT', 'DELETE'...
    
    # Paramètres GET (?page=2&search=django)
    page = request.GET.get('page', 1)        # 2 (ou 1 par défaut)
    search = request.GET.get('search', '')   # 'django'
    
    # Données POST (formulaires)
    username = request.POST.get('username')
    
    # Fichiers uploadés
    uploaded_file = request.FILES.get('document')
    
    # Utilisateur connecté
    user = request.user
    if user.is_authenticated:
        print(f"Utilisateur: {user.username}")
    else:
        print("Utilisateur anonyme")
    
    # Chemin de l'URL
    path = request.path              # '/blog/posts/'
    full_path = request.get_full_path()  # '/blog/posts/?page=2'
    
    # Headers HTTP
    user_agent = request.headers.get('User-Agent')
    
    # Cookies
    session_id = request.COOKIES.get('sessionid')
    
    # Session (données temporaires)
    cart = request.session.get('cart', [])
    request.session['cart'] = [1, 2, 3]  # Sauvegarder
    
    # Vérifier si AJAX
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
    
    # Adresse IP
    ip = request.META.get('REMOTE_ADDR')
    
    return render(request, 'template.html')


# === Décorateurs pour Views (sécurité, contrôle) ===

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 non connecté -> redirige vers /login/

@login_required(login_url='/connexion/')
def profile(request):
    """Avec URL de login personnalisée"""
    return render(request, 'profile.html')


# Vérifier permissions
from django.contrib.auth.decorators import permission_required

@permission_required('blog.add_post')
def create_post(request):
    """Seulement si user a permission 'add_post'"""
    return render(request, 'create_post.html')


# Limiter méthodes HTTP
from django.views.decorators.http import require_http_methods, require_POST

@require_http_methods(["GET", "POST"])
def contact(request):
    """Accepte seulement GET et POST"""
    pass

@require_POST
def delete_post(request, post_id):
    """Accepte seulement POST (pas GET)"""
    post = get_object_or_404(Post, id=post_id)
    post.delete()
    return redirect('post_list')


# === Class-Based Views (CBV) - Aperçu ===

# Les CBV sont une alternative aux FBV (plus avancé)
# Django fournit des classes pré-faites pour les cas courants

from django.views.generic import ListView, DetailView

class PostListView(ListView):
    """Liste de posts (equivalent à post_list en FBV)"""
    model = Post                          # Model à lister
    template_name = 'blog/post_list.html' # Template
    context_object_name = 'posts'         # Nom dans le template
    paginate_by = 10                      # Pagination automatique

# C'est tout! Django génère automatiquement la view!
# Équivalent à ~15 lignes de FBV

class PostDetailView(DetailView):
    """Détail d'un post (equivalent à post_detail en FBV)"""
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

# Avantages CBV:
# [OK] Moins de code
# [OK] Réutilisable
# [OK] Pagination, filtres automatiques

# Inconvénients CBV:
# [X] Plus complexe à comprendre (débutants)
# [X] Moins flexible pour logique custom

# Recommandation: Commencez avec FBV, passez aux CBV quand à l'aise


# === Exemples complets de Views courantes ===

# 1. Liste avec filtrage
def post_list(request):
    """Liste de posts avec filtre catégorie"""
    category = request.GET.get('category')  # ?category=django
    
    posts = Post.objects.filter(published=True)
    
    if category:
        posts = posts.filter(category=category)
    
    return render(request, 'blog/post_list.html', {
        'posts': posts,
        'current_category': category
    })

# 2. Recherche
def search(request):
    """Recherche dans les posts"""
    query = request.GET.get('q', '')  # ?q=django
    
    if query:
        posts = Post.objects.filter(
            title__icontains=query
        ) | Post.objects.filter(
            content__icontains=query
        )
    else:
        posts = Post.objects.none()  # Queryset vide
    
    return render(request, 'blog/search.html', {
        'posts': posts,
        'query': query
    })

# 3. Création (avec formulaire Django - section suivante)
from .forms import PostForm

def post_create(request):
    """Créer un nouveau post"""
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user  # Ajouter auteur
            post.save()
            return redirect('post_detail', post_id=post.id)
    else:
        form = PostForm()
    
    return render(request, 'blog/post_form.html', {'form': form})

# 4. Modification
def post_update(request, post_id):
    """Modifier un post existant"""
    post = get_object_or_404(Post, id=post_id)
    
    # Vérifier que c'est l'auteur
    if post.author != request.user:
        return HttpResponseForbidden("Vous n'êtes pas l'auteur")
    
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES, instance=post)
        if form.is_valid():
            form.save()
            return redirect('post_detail', post_id=post.id)
    else:
        form = PostForm(instance=post)
    
    return render(request, 'blog/post_form.html', {
        'form': form,
        'post': post
    })

# 5. Suppression
@require_POST
def post_delete(request, post_id):
    """Supprimer un post"""
    post = get_object_or_404(Post, id=post_id)
    
    # Vérifier que c'est l'auteur
    if post.author != request.user:
        return HttpResponseForbidden("Vous n'êtes pas l'auteur")
    
    post.delete()
    return redirect('post_list')


# === Résumé Views ===

# 1. View = Fonction Python qui traite requête et retourne réponse
# 2. Toujours accepter "request" comme paramètre
# 3. render() pour afficher template HTML
# 4. redirect() pour changer de page
# 5. get_object_or_404() pour récupérer objet ou 404
# 6. request.method pour différencier GET/POST
# 7. request.GET pour paramètres URL (?page=2)
# 8. request.POST pour données formulaire
# 9. request.user pour utilisateur connecté
# 10. Context = dictionnaire de données pour le template

# Prochaine étape: Connecter vos Views aux URLs!

# === Class-Based Views (CBV) ===

from django.views import View
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView

# Vue générique simple
class HomeView(View):
    def get(self, request):
        return HttpResponse("Bienvenue!")
    
    def post(self, request):
        # Traiter POST
        return HttpResponse("POST reçu")

# ListView (liste d'objets)
class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'        # Défaut: blog/post_list.html
    context_object_name = 'posts'                 # Défaut: post_list
    paginate_by = 10                              # Pagination
    
    def get_queryset(self):
        # Personnaliser queryset
        return Post.objects.filter(published=True).order_by('-created_at')
    
    def get_context_data(self, **kwargs):
        # Ajouter données au contexte
        context = super().get_context_data(**kwargs)
        context['title'] = 'Liste des articles'
        return context

# DetailView (détail d'un objet)
class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    
    # Utilise pk ou slug automatiquement depuis URL

# CreateView (créer objet)
from django.urls import reverse_lazy

class PostCreateView(CreateView):
    model = Post
    template_name = 'blog/post_form.html'
    fields = ['title', 'content', 'published']    # Champs du formulaire
    success_url = reverse_lazy('post_list')       # Redirection après succès
    
    def form_valid(self, form):
        # Avant save
        form.instance.author = self.request.user
        return super().form_valid(form)

# UpdateView (modifier objet)
class PostUpdateView(UpdateView):
    model = Post
    template_name = 'blog/post_form.html'
    fields = ['title', 'content', 'published']
    
    def get_success_url(self):
        return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})

# DeleteView (supprimer objet)
class PostDeleteView(DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('post_list')

# Mixins (fonctionnalités réutilisables)
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin

class PostCreateView(LoginRequiredMixin, CreateView):
    login_url = '/login/'                         # URL de connexion
    model = Post
    fields = ['title', 'content']

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = ['title', 'content']
    
    def test_func(self):
        # Seul l'auteur peut modifier
        post = self.get_object()
        return self.request.user == post.author

# === Decorators pour FBV ===

from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.http import require_http_methods, require_GET, require_POST
from django.views.decorators.cache import cache_page

# Login requis
@login_required
def my_view(request):
    return render(request, 'template.html')

@login_required(login_url='/login/')
def my_view(request):
    return render(request, 'template.html')

# Permission requise
@permission_required('blog.add_post')
def create_post(request):
    # ...
    pass

# Méthode HTTP spécifique
@require_http_methods(["GET", "POST"])
def my_view(request):
    pass

@require_GET
def my_view(request):
    pass

@require_POST
def my_view(request):
    pass

# Cache
@cache_page(60 * 15)  # Cache 15 minutes
def my_view(request):
    pass

# === Request object ===

def my_view(request):
    # Méthode HTTP
    request.method                                # 'GET', 'POST', etc.
    
    # GET parameters
    search = request.GET.get('search', '')        # ?search=django
    page = request.GET.get('page', 1)
    
    # POST data
    username = request.POST.get('username')
    password = request.POST['password']           # Lève KeyError si absent
    
    # Fichiers uploadés
    file = request.FILES.get('document')
    
    # User
    user = request.user                           # User ou AnonymousUser
    if request.user.is_authenticated:
        # User connecté
        pass
    
    # Path
    request.path                                  # '/blog/posts/'
    request.get_full_path()                       # '/blog/posts/?page=2'
    
    # Headers
    request.headers.get('User-Agent')
    request.META['HTTP_USER_AGENT']               # Ancienne méthode
    
    # Cookies
    request.COOKIES.get('session_id')
    
    # Session
    request.session['cart'] = [1, 2, 3]
    cart = request.session.get('cart', [])
    
    # AJAX
    request.headers.get('X-Requested-With') == 'XMLHttpRequest'

# === Response types ===

from django.http import HttpResponse, JsonResponse, FileResponse, StreamingHttpResponse

# HTML
return HttpResponse('<h1>Hello</h1>')
return HttpResponse('<h1>Hello</h1>', content_type='text/html')

# Template
return render(request, 'template.html', {'key': 'value'})

# JSON
return JsonResponse({'status': 'ok', 'data': [1, 2, 3]})

# Redirect
return redirect('post_list')
return redirect('/posts/')
return redirect('post_detail', pk=1)

# 404
return HttpResponseNotFound('<h1>Page non trouvée</h1>')
raise Http404("Objet introuvable")

# Autres codes HTTP
return HttpResponse('Créé', status=201)
return HttpResponseBadRequest('Requête invalide')
return HttpResponseForbidden('Accès interdit')
return HttpResponseServerError('Erreur serveur')

# File download
response = FileResponse(open('file.pdf', 'rb'))
response['Content-Disposition'] = 'attachment; filename="file.pdf"'
return response

# Streaming
def streaming_view(request):
    def generate():
        for i in range(1000):
            yield f"Line {i}\n"
    return StreamingHttpResponse(generate())


[OK] URLS (ROUTAGE)


# Les URLs mappent les chemins vers les vues

# === Configuration principale ===

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),          # Include URLs d'une app
    path('accounts/', include('django.contrib.auth.urls')),
]

# Servir fichiers média en dev
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# === URLs d'application ===

# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'                                 # Namespace (optionnel)

urlpatterns = [
    # Path simple
    path('', views.post_list, name='post_list'),
    
    # Avec paramètre
    path('post/<int:post_id>/', views.post_detail, name='post_detail'),
    path('post/<slug:slug>/', views.post_by_slug, name='post_by_slug'),
    
    # Plusieurs paramètres
    path('category/<slug:category>/post/<int:pk>/', views.post_detail),
    
    # CBV
    path('posts/', views.PostListView.as_view(), name='post_list'),
    path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
    path('post/new/', views.PostCreateView.as_view(), name='post_create'),
    path('post/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_update'),
    path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]

# === Types de path converters ===

# str - Tout sauf '/' (défaut)
path('articles/<str:section>/', views.section)

# int - Entier positif
path('post/<int:pk>/', views.post_detail)

# slug - Lettres, nombres, tirets, underscores
path('post/<slug:slug>/', views.post_by_slug)

# uuid - UUID
path('object/<uuid:uuid>/', views.object_detail)

# path - Tout y compris '/'
path('file/<path:filepath>/', views.file_detail)

# === Regex patterns (avancé) ===

from django.urls import re_path

urlpatterns = [
    re_path(r'^articles/(?P<year>[0-9]{4})/, views.year_archive),
    re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/, views.month_archive),
]

# === Reverse URLs (générer URL depuis name) ===

from django.urls import reverse

# Dans views
url = reverse('post_list')                        # '/blog/'
url = reverse('post_detail', args=[1])            # '/blog/post/1/'
url = reverse('post_detail', kwargs={'pk': 1})    # '/blog/post/1/'

# Avec namespace
url = reverse('blog:post_detail', kwargs={'pk': 1})

# Dans templates
{% url 'post_list' %}
{% url 'post_detail' post.pk %}
{% url 'blog:post_detail' pk=post.pk %}

# === Include avec prefix ===

# myproject/urls.py
urlpatterns = [
    path('api/v1/', include('api.urls')),
    path('api/v2/', include('api.urls_v2')),
]


[OK] TEMPLATES - AFFICHAGE HTML DYNAMIQUE


# === Qu'est-ce qu'un Template ? ===

# Un TEMPLATE = Fichier HTML avec variables et logique Django
# C'est ce que l'utilisateur voit dans son navigateur!

# ANALOGIE: Un formulaire à remplir
# - HTML = structure du formulaire (titres, cases, mise en page)
# - Variables Django = cases vides à remplir ({{ nom }}, {{ age }})
# - View = remplit le formulaire avec les vraies données

# HTML statique (sans Django):
# <h1>Bienvenue Jean</h1>
# Problème: Toujours "Jean", même si autre utilisateur connecté!

# Template Django (dynamique):
# <h1>Bienvenue {{ user.username }}</h1>
# Résultat: "Bienvenue Jean", "Bienvenue Marie", etc. (dynamique!)

# Django Template Language (DTL) = Syntaxe spéciale dans HTML:
# {{ variable }}  -> Affiche une variable
# {% tag %}       -> Logique (if, for, etc.)
# {# commentaire #} -> Commentaire (invisible dans HTML final)


# === ÉTAPE 1: Créer la structure des templates ===

# Par convention Django, les templates vont dans:
# app/templates/app/nom_template.html

# Pourquoi double "app" (blog/templates/blog/) ?
# NAMESPACING! Si 2 apps ont "home.html", Django les différencie:
# - blog/templates/blog/home.html -> 'blog/home.html'
# - shop/templates/shop/home.html -> 'shop/home.html'

# Créer la structure manuellement:
mon_projet_django/
├── blog/
│   ├── templates/          # Créer ce dossier
│   │   └── blog/          # Créer ce sous-dossier (namespace)
│   │       ├── base.html  # Template de base (squelette)
│   │       ├── post_list.html
│   │       ├── post_detail.html
│   │       └── post_form.html
│   ├── models.py
│   ├── views.py
│   └── urls.py

# Ou templates globaux (pour tout le projet):
mon_projet_django/
├── templates/              # Créer à la racine du projet
│   ├── base.html
│   └── includes/
│       ├── header.html
│       └── footer.html

# Pour utiliser templates globaux, configurer settings.py:
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # Ajouter ce chemin!
        'APP_DIRS': True,  # Cherche aussi dans app/templates/
        'OPTIONS': {...},
    },
]


# === ÉTAPE 2: Créer un template de base (base.html) ===

# Le template de base = Squelette réutilisé par tous les autres templates
# Contient: header, footer, navigation (parties communes)

# blog/templates/blog/base.html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
    {# Titre de la page (changeable dans chaque template) #}
    <title>{% block title %}Mon Blog Django{% endblock %}</title>
    
    {# CSS global #}
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
    
    {# CSS supplémentaire (optionnel pour chaque page) #}
    {% block extra_css %}{% endblock %}
</head>
<body>
    {# Header (navigation) #}
    <header>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <div class="container">
                <a class="navbar-brand" href="{% url 'blog:post_list' %}">Mon Blog</a>
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'blog:post_list' %}">Accueil</a>
                    </li>
                    {% if user.is_authenticated %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'blog:post_create' %}">Nouvel Article</a>
                        </li>
                        <li class="nav-item">
                            <span class="nav-link">Bonjour {{ user.username }}</span>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'login' %}">Connexion</a>
                        </li>
                    {% endif %}
                </ul>
            </div>
        </nav>
    </header>
    
    {# Contenu principal (différent pour chaque page) #}
    <main class="container my-4">
        {% block content %}
        {# Ce bloc sera remplacé par le contenu spécifique de chaque page #}
        {% endblock %}
    </main>
    
    {# Footer #}
    <footer class="bg-light py-3 mt-5">
        <div class="container text-center">
            <p>&copy; 2024 Mon Blog Django. Tous droits réservés.</p>
        </div>
    </footer>
    
    {# JavaScript #}
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

# Explication des concepts:

# {% block title %}...{% endblock %}
#   - Définit un BLOC remplaçable
#   - Chaque page peut mettre son propre titre
#   - Si non remplacé: garde le contenu par défaut

# {% url 'blog:post_list' %}
#   - Génère l'URL depuis le name (plus de hardcode!)
#   - Django remplace par /blog/ automatiquement

# {% if user.is_authenticated %}
#   - Condition: si utilisateur connecté
#   - Affiche menu différent selon connexion

# {{ user.username }}
#   - Variable: affiche le nom d'utilisateur
#   - Django remplace par "Jean", "Marie", etc.

# {# Commentaire #}
#   - Commentaire Django (invisible dans HTML final)
#   - Différent de <!-- commentaire HTML --> (visible dans source)


# === ÉTAPE 3: Créer un template enfant (avec héritage) ===

# blog/templates/blog/post_list.html
{% extends 'blog/base.html' %}

{% block title %}Liste des articles - Mon Blog{% endblock %}

{% block content %}
    <h1>Liste des articles</h1>
    
    {% if posts %}
        {# Il y a des posts: afficher la liste #}
        <div class="row">
            {% for post in posts %}
                <div class="col-md-6 mb-4">
                    <div class="card">
                        <div class="card-body">
                            <h2 class="card-title">{{ post.title }}</h2>
                            <p class="text-muted">
                                Par {{ post.author.username }} 
                                le {{ post.created_at|date:"d/m/Y" }}
                            </p>
                            <p class="card-text">
                                {{ post.content|truncatewords:50 }}
                            </p>
                            <a href="{% url 'blog:post_detail' pk=post.pk %}" class="btn btn-primary">
                                Lire la suite
                            </a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>
    {% else %}
        {# Pas de posts: afficher message #}
        <div class="alert alert-info">
            <p>Aucun article pour le moment.</p>
        </div>
    {% endif %}
{% endblock %}

# Explication ligne par ligne:

# {% extends 'blog/base.html' %}
#   - DOIT être la PREMIÈRE ligne (obligatoire!)
#   - Hérite de base.html (récupère header, footer, etc.)
#   - Ce template ne définit que les blocs à changer

# {% block title %}...{% endblock %}
#   - Remplace le titre de base.html
#   - "Liste des articles - Mon Blog" au lieu de "Mon Blog Django"

# {% block content %}...{% endblock %}
#   - Remplace le contenu principal de base.html
#   - Tout le code HTML ici

# {% if posts %}
#   - Condition: si la variable "posts" existe et n'est pas vide
#   - posts vient du context de la view!

# {% for post in posts %}
#   - Boucle sur chaque post de la liste
#   - post = variable temporaire (comme en Python)

# {{ post.title }}
#   - Affiche l'attribut title du post
#   - Django accède automatiquement aux attributs du model

# {{ post.author.username }}
#   - Suit les relations: post -> author (User) -> username
#   - Django fait automatiquement le JOIN SQL!

# {{ post.created_at|date:"d/m/Y" }}
#   - | = pipe, applique un FILTRE
#   - date:"d/m/Y" = formate la date (15/01/2024)

# {{ post.content|truncatewords:50 }}
#   - truncatewords:50 = garde seulement 50 premiers mots
#   - Ajoute "..." à la fin

# {% url 'blog:post_detail' pk=post.pk %}
#   - Génère URL avec paramètre
#   - Django remplace par /blog/post/5/

# {% else %}
#   - Sinon (si posts est vide)
#   - Affiche message "Aucun article"

# {% endif %}
#   - Ferme le {% if %} (obligatoire!)


# === ÉTAPE 4: Template de détail ===

# blog/templates/blog/post_detail.html
{% extends 'blog/base.html' %}

{% block title %}{{ post.title }} - Mon Blog{% endblock %}

{% block content %}
    <article>
        <h1>{{ post.title }}</h1>
        
        <div class="text-muted mb-3">
            <small>
                Par <strong>{{ post.author.username }}</strong>
                le {{ post.created_at|date:"d F Y à H:i" }}
                {% if post.updated_at != post.created_at %}
                    (modifié le {{ post.updated_at|date:"d/m/Y" }})
                {% endif %}
            </small>
        </div>
        
        {% if post.image %}
            <img src="{{ post.image.url }}" alt="{{ post.title }}" class="img-fluid mb-3">
        {% endif %}
        
        <div class="content">
            {# linebreaks = convertit sauts de ligne en <br> #}
            {{ post.content|linebreaks }}
        </div>
        
        {# Afficher tags s'il y en a #}
        {% if post.tags.all %}
            <div class="mt-3">
                <strong>Tags:</strong>
                {% for tag in post.tags.all %}
                    <span class="badge bg-secondary">{{ tag.name }}</span>
                {% endfor %}
            </div>
        {% endif %}
        
        {# Boutons d'action (seulement pour l'auteur) #}
        {% if user == post.author %}
            <div class="mt-4">
                <a href="{% url 'blog:post_update' pk=post.pk %}" class="btn btn-warning">
                    Modifier
                </a>
                <a href="{% url 'blog:post_delete' pk=post.pk %}" class="btn btn-danger">
                    Supprimer
                </a>
            </div>
        {% endif %}
    </article>
    
    {# Section commentaires #}
    <hr class="my-5">
    <h3>Commentaires ({{ post.comments.count }})</h3>
    
    {% for comment in post.comments.all %}
        <div class="card mb-2">
            <div class="card-body">
                <strong>{{ comment.author.username }}</strong>
                <small class="text-muted">le {{ comment.created_at|date:"d/m/Y" }}</small>
                <p>{{ comment.text }}</p>
            </div>
        </div>
    {% empty %}
        <p class="text-muted">Aucun commentaire pour le moment.</p>
    {% endfor %}
{% endblock %}

# Nouveaux concepts:

# {% if post.updated_at != post.created_at %}
#   - Comparaison: si date modif ≠ date création
#   - Affiche "modifié le..." seulement si changé

# {% if post.image %}
#   - Vérifie si l'image existe
#   - Évite erreur si champ vide

# {{ post.image.url }}
#   - URL du fichier uploadé
#   - Ex: /media/images/photo.jpg

# {{ post.content|linebreaks }}
#   - Convertit \n en <br> et \n\n en <p>
#   - Préserve la mise en forme

# {% for tag in post.tags.all %}
#   - Boucle sur relation Many-to-Many
#   - Django fait le JOIN automatiquement!

# {% if user == post.author %}
#   - Compare objets: user connecté == auteur du post?
#   - Affiche boutons seulement à l'auteur

# {% empty %}
#   - Alternative à {% else %} dans les boucles
#   - Exécuté si la liste est VIDE
#   - Plus lisible que if/else


# === Variables dans les templates ===

# Afficher variables
{{ variable }}                    # Affiche la variable
{{ post.title }}                  # Attribut d'objet
{{ post.author.username }}        # Relation (ForeignKey)
{{ user.first_name }}             # User Django
{{ post.tags.all }}               # QuerySet
{{ my_list.0 }}                   # Premier élément liste (index 0)
{{ my_dict.key }}                 # Valeur d'un dict

# Méthodes (SANS parenthèses!)
{{ post.get_absolute_url }}       # Appelle la méthode
{{ post.comments.count }}         # Compte les comments
{{ text.upper }}                  # Majuscules

# IMPORTANT: Pas de parenthèses dans les templates!
# Python: post.get_url()
# Template: {{ post.get_url }}  (Django ajoute () automatiquement)


# === Filtres (transformations) ===

# Les filtres transforment les variables avec | (pipe)

# TEXTE
{{ text|lower }}                  # minuscules
{{ text|upper }}                  # MAJUSCULES
{{ text|title }}                  # Première Lettre Majuscule
{{ text|capfirst }}               # Première lettre de phrase

{{ text|truncatewords:50 }}       # Garde 50 premiers mots + "..."
{{ text|truncatechars:100 }}      # Garde 100 premiers caractères
{{ text|slice:":50" }}            # 50 premiers caractères (slice Python)

{{ text|length }}                 # Longueur (nombre de caractères)
{{ text|wordcount }}              # Nombre de mots

{{ text|linebreaks }}             # \n -> <br>, \n\n -> <p>
{{ text|linebreaksbr }}           # \n -> <br> seulement
{{ text|striptags }}              # Retire balises HTML

{{ url|urlencode }}               # Encode URL (espace -> %20)
{{ text|slugify }}                # "Mon Article" -> "mon-article"

# NOMBRES
{{ number|add:5 }}                # Addition (+5)
{{ price|floatformat:2 }}         # 2 décimales (99.99)
{{ size|filesizeformat }}         # "1.5 MB", "3.2 GB"

# 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" }}             # Heure seulement
{{ date|timesince }}              # "il y a 2 heures"
{{ date|timeuntil }}              # "dans 3 jours"

# Formats de date courants:
# d = jour (01-31)
# m = mois (01-12)
# Y = année (2024)
# F = mois en lettres (janvier)
# H = heure 24h (00-23)
# i = minutes (00-59)

# LISTES
{{ list|first }}                  # Premier élément
{{ list|last }}                   # Dernier élément
{{ list|join:", " }}              # "a, b, c"
{{ list|length }}                 # Nombre d'éléments

# CONDITIONS
{{ value|default:"Aucune valeur" }}  # Si vide: "Aucune valeur"
{{ value|default_if_none:"N/A" }}    # Si None: "N/A"
{{ bool|yesno:"Oui,Non,Peut-être" }} # True->Oui, False->Non, None->Peut-être

# SÉCURITÉ
{{ html|safe }}                   # Ne PAS échapper HTML (ATTENTION!)
{{ html|escape }}                 # Échapper HTML (par défaut)
{{ html|escapejs }}               # Échapper pour JavaScript

# Chaîner filtres
{{ post.title|lower|truncatewords:10 }}
# 1. Minuscules
# 2. Garde 10 premiers mots


# === Tags (logique) ===

# IF / ELIF / ELSE
{% if age >= 18 %}
    <p>Majeur</p>
{% elif age >= 16 %}
    <p>Mineur émancipable</p>
{% else %}
    <p>Mineur</p>
{% endif %}

# Opérateurs de comparaison:
# == != < > <= >= in not in

{% if user.is_authenticated %}
    <p>Connecté</p>
{% endif %}

{% if posts %}
    <p>Il y a {{ posts|length }} articles</p>
{% endif %}

{% if 'admin' in user.username %}
    <p>Administrateur détecté</p>
{% endif %}

# AND / OR / NOT
{% if user.is_authenticated and user.is_staff %}
    <p>Admin connecté</p>
{% endif %}

{% if x > 5 or y < 10 %}
    <p>Condition vraie</p>
{% endif %}

{% if not user.is_active %}
    <p>Compte désactivé</p>
{% endif %}


# FOR (boucles)
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}

# Variables de boucle (forloop)
{% for post in posts %}
    <p>
        Article {{ forloop.counter }}     {# 1, 2, 3... #}
        ({{ forloop.counter0 }})          {# 0, 1, 2... #}
        
        {% if forloop.first %}
            <span class="badge">Nouveau</span>
        {% endif %}
        
        {% if forloop.last %}
            <span>Dernier article</span>
        {% endif %}
    </p>
{% endfor %}

# forloop.counter = numéro (commence à 1)
# forloop.counter0 = numéro (commence à 0)
# forloop.revcounter = compte à l'envers
# forloop.first = True si premier élément
# forloop.last = True si dernier élément
# forloop.parentloop = boucle parente (boucles imbriquées)

# FOR...EMPTY (si liste vide)
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% empty %}
    <p>Aucun article.</p>
{% endfor %}


# WITH (variables temporaires)
{% with total=post.comments.count %}
    <p>{{ total }} commentaire{{ total|pluralize }}</p>
{% endwith %}

# Utile pour éviter requêtes multiples:
# [X] MAUVAIS (3 requêtes):
# {{ post.comments.count }} commentaires
# {{ post.comments.count }} réponses
# {{ post.comments.count }} avis

# [OK] BON (1 requête):
# {% with count=post.comments.count %}
#     {{ count }} commentaires
#     {{ count }} réponses  
# {% endwith %}


# === URLs dans templates ===

# Générer URL depuis name
{% url 'post_list' %}
# Résultat: /blog/

{% url 'post_detail' pk=5 %}
# Résultat: /blog/post/5/

{% url 'blog:post_detail' pk=post.pk %}
# Avec namespace

# Dans href
<a href="{% url 'post_detail' pk=post.pk %}">Lire</a>

# Stocker dans variable
{% url 'post_detail' pk=post.pk as post_url %}
<a href="{{ post_url }}">Lien</a>


# === Include (réutiliser templates) ===

# Créer templates partiels réutilisables

# templates/includes/post_card.html
<div class="card">
    <div class="card-body">
        <h3>{{ post.title }}</h3>
        <p>{{ post.content|truncatewords:30 }}</p>
    </div>
</div>

# Utiliser dans autre template:
{% for post in posts %}
    {% include 'includes/post_card.html' %}
{% endfor %}

# Avec variables custom:
{% include 'includes/post_card.html' with post=featured_post %}

# Include conditionnel:
{% include 'includes/sidebar.html' only %}  # Seulement variables explicites


# === Load (charger fonctionnalités) ===

# Charger tags personnalisés
{% load blog_tags %}
{{ total_posts }}  # Tag personnalisé

# Charger static files
{% load static %}
<img src="{% static 'images/logo.png' %}" alt="Logo">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<script src="{% static 'js/main.js' %}"></script>

# Charger plusieurs
{% load static blog_tags %}


# === Static files (CSS, JS, images) ===

# TOUJOURS utiliser {% static %}, jamais hardcoder!

# [X] MAUVAIS:
<img src="/static/images/logo.png">

# [OK] BON:
{% load static %}
<img src="{% static 'images/logo.png' %}">

# Pourquoi?
# En production, STATIC_URL peut changer:
# Dev: /static/
# Prod: https://cdn.monsite.com/static/
# {% static %} s'adapte automatiquement!


# === CSRF (sécurité formulaires) ===

# OBLIGATOIRE dans TOUS les formulaires POST!
<form method="post">
    {% csrf_token %}
    <input type="text" name="title">
    <button type="submit">Envoyer</button>
</form>

# Sans {% csrf_token %}: Erreur 403 Forbidden!
# C'est une protection contre attaques CSRF


# === Commentaires ===

{# Ceci est un commentaire Django #}
{# Invisible dans HTML final #}
{# Peut être sur plusieurs
   lignes #}

{% comment %}
Commentaire multiligne
Plus lisible
{% endcomment %}

<!-- Commentaire HTML standard -->
<!-- Visible dans le code source de la page! -->


# === Résumé Templates ===

# 1. Template = HTML + variables Django ({{  }}) + logique ({% %})
# 2. Structure: app/templates/app/template.html (namespacing)
# 3. {% extends %} = hériter d'un template parent
# 4. {% block %} = zone remplaçable
# 5. {{ variable }} = afficher variable
# 6. {{ var|filtre }} = transformer variable
# 7. {% if %} / {% for %} = logique
# 8. {% url 'name' %} = générer URL
# 9. {% load static %} / {% static %} = fichiers CSS/JS/images
# 10. {% csrf_token %} = sécurité formulaires (OBLIGATOIRE!)
# 11. {% include %} = réutiliser template partiel
# 12. {# commentaire #} = commentaire Django

# Prochaine étape: Créer des formulaires pour interagir avec l'utilisateur!

# === Variables ===

{{ variable }}                                    # Afficher variable
{{ post.title }}                                  # Attribut
{{ post.get_absolute_url }}                       # Méthode
{{ post.author.username }}                        # Relation
{{ my_list.0 }}                                   # Index liste
{{ my_dict.key }}                                 # Clé dict

# Avec défaut si vide
{{ variable|default:"Aucune valeur" }}

# === Filters (filtres) ===

{{ text|lower }}                                  # Minuscules
{{ text|upper }}                                  # Majuscules
{{ text|title }}                                  # Première lettre maj
{{ text|capfirst }}                               # Première lettre phrase

{{ text|truncatewords:50 }}                       # Tronquer (mots)
{{ text|truncatechars:100 }}                      # Tronquer (caractères)
{{ text|slice:":50" }}                            # Slice

{{ value|length }}                                # Longueur
{{ value|first }}                                 # Premier élément
{{ value|last }}                                  # Dernier élément
{{ value|join:", " }}                             # Join liste

{{ value|add:5 }}                                 # Addition
{{ value|divisibleby:3 }}                         # Divisible par

{{ date|date:"d/m/Y" }}                           # Format date
{{ date|time:"H:i" }}                             # Format time
{{ date|timesince }}                              # "il y a 2 jours"
{{ date|timeuntil }}                              # "dans 3 heures"

{{ url|urlencode }}                               # URL encode
{{ html|safe }}                                   # Ne pas échapper HTML
{{ html|escape }}                                 # Échapper HTML
{{ html|striptags }}                              # Retirer tags HTML

{{ value|filesizeformat }}                        # "1.5 MB"
{{ value|pluralize }}                             # Pluriel ("s", "x", "ies")
{{ number|floatformat:2 }}                        # 2 décimales

{{ value|default_if_none:"N/A" }}                 # Défaut si None
{{ value|yesno:"Oui,Non,Peut-être" }}             # Boolean en texte

# Chaîner filtres
{{ text|lower|truncatewords:20 }}
{{ date|date:"d/m/Y"|default:"Date inconnue" }}

# === Tags ===

# If
{% if user.is_authenticated %}
    <p>Bonjour {{ user.username }}</p>
{% else %}
    <p>Veuillez vous connecter</p>
{% endif %}

{% if posts %}
    {% for post in posts %}
        ...
    {% endfor %}
{% else %}
    <p>Aucun article</p>
{% endif %}

# Opérateurs
{% if age >= 18 %}
{% if name == "John" %}
{% if value != 0 %}
{% if user.is_authenticated and user.is_staff %}
{% if x > 5 or y < 10 %}
{% if not user.is_active %}

# For
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}

# Variables de boucle
{% for post in posts %}
    {{ forloop.counter }}                         # 1, 2, 3...
    {{ forloop.counter0 }}                        # 0, 1, 2...
    {{ forloop.revcounter }}                      # N, N-1, N-2...
    {{ forloop.first }}                           # True si premier
    {{ forloop.last }}                            # True si dernier
    {{ forloop.parentloop }}                      # Boucle parente
{% endfor %}

# Empty
{% for post in posts %}
    <p>{{ post.title }}</p>
{% empty %}
    <p>Aucun article</p>
{% endfor %}

# With (variables temporaires)
{% with total=post.comments.count %}
    <p>{{ total }} commentaire{{ total|pluralize }}</p>
{% endwith %}

# URL
<a href="{% url 'post_list' %}">Articles</a>
<a href="{% url 'post_detail' post.pk %}">Détail</a>
<a href="{% url 'post_detail' pk=post.pk %}">Détail</a>

# Static files
{% load static %}
<img src="{% static 'images/logo.png' %}" alt="Logo">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<script src="{% static 'js/main.js' %}"></script>

# Include
{% include 'includes/header.html' %}
{% include 'includes/sidebar.html' with title="Catégories" %}

# CSRF (obligatoire dans formulaires POST!)
<form method="post">
    {% csrf_token %}
    ...
</form>

# Comment
{# Ceci est un commentaire #}
{% comment %}
    Commentaire
    multi-lignes
{% endcomment %}

# === Template tags personnalisés ===

# blog/templatetags/blog_tags.py
from django import template

register = template.Library()

@register.simple_tag
def total_posts():
    from blog.models import Post
    return Post.objects.count()

@register.filter
def repeat(value, times):
    return value * int(times)

# Dans template
{% load blog_tags %}
{{ total_posts }}
{{ "Hello"|repeat:3 }}

# === Context processors ===

# Ajouter variables disponibles dans tous les templates

# myproject/context_processors.py
def site_info(request):
    return {
        'site_name': 'Mon Site',
        'current_year': 2024,
    }

# settings.py
TEMPLATES = [
    {
        'OPTIONS': {
            'context_processors': [
                ...
                'myproject.context_processors.site_info',
            ],
        },
    },
]

# Dans tous les templates
{{ site_name }}
{{ current_year }}


[OK] FORMULAIRES (FORMS) - INTERAGIR AVEC L'UTILISATEUR


# === Qu'est-ce qu'un formulaire Django ? ===

# Un FORMULAIRE = Moyen de collecter et valider des données utilisateur
# Django fournit un système de formulaires puissant et sécurisé

# ANALOGIE: Formulaire papier administratif
# - Champs à remplir (nom, email, message)
# - Validation (email valide? nombre positif?)
# - Traitement (enregistrer en BDD, envoyer email)

# Sans Django Forms (HTML pur):
# [X] Écrire HTML manuellement pour chaque champ
# [X] Valider les données à la main (regex, vérifications)
# [X] Gérer les erreurs soi-même
# [X] Protection CSRF manuelle
# [X] Beaucoup de code répétitif!

# Avec Django Forms:
# [OK] Génération automatique du HTML
# [OK] Validation automatique
# [OK] Gestion des erreurs intégrée
# [OK] Protection CSRF automatique
# [OK] Nettoyage des données
# [OK] Moins de code!

# Il existe 2 types de formulaires:
# 1. Form = Formulaire générique (pour tout)
# 2. ModelForm = Formulaire basé sur un Model (plus courant)


# === ÉTAPE 1: Créer un ModelForm (le plus courant) ===

# ModelForm = Formulaire généré automatiquement depuis un Model
# Parfait pour créer/modifier des objets en BDD

# Créer le fichier blog/forms.py (n'existe pas par défaut!)

# blog/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    """Formulaire pour créer/modifier un Post"""
    
    class Meta:
        model = Post                    # Model à utiliser
        fields = ['title', 'content', 'published']  # Champs du formulaire
        
        # Ou tous les champs:
        # fields = '__all__'
        
        # Ou exclure certains:
        # exclude = ['author', 'created_at']
        
        # Labels personnalisés (optionnel)
        labels = {
            'title': 'Titre de l\'article',
            'content': 'Contenu',
            'published': 'Publier immédiatement',
        }
        
        # Textes d'aide (optionnel)
        help_texts = {
            'title': 'Minimum 10 caractères recommandés',
            'content': 'Utilisez Markdown si vous le souhaitez',
        }
        
        # Widgets (personnaliser les champs HTML)
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Entrez le titre...'
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10,
                'placeholder': 'Écrivez votre article...'
            }),
            'published': forms.CheckboxInput(attrs={
                'class': 'form-check-input'
            }),
        }

# Explication:

# class PostForm(forms.ModelForm):
#   Hérite de ModelForm (pas Model!)
#   Génère automatiquement formulaire depuis le Model Post

# class Meta:
#   Configuration du formulaire

# model = Post
#   Quel Model utiliser comme base

# fields = ['title', 'content', 'published']
#   Quels champs du Model inclure
#   Django génère automatiquement les bons types de champs!
#   CharField -> <input type="text">
#   TextField -> <textarea>
#   BooleanField -> <input type="checkbox">
#   etc.

# labels = {...}
#   Texte affiché au-dessus du champ
#   Par défaut: nom du champ ("Title", "Content")

# widgets = {...}
#   Personnaliser le HTML généré
#   Ajouter classes CSS, placeholders, attributs HTML


# === ÉTAPE 2: Utiliser le formulaire dans une View ===

# Créer un nouvel article (CREATE)

# blog/views.py
from django.shortcuts import render, redirect
from .forms import PostForm

def post_create(request):
    """View pour créer un nouveau post"""
    
    if request.method == 'POST':
        # Formulaire soumis (bouton "Envoyer" cliqué)
        form = PostForm(request.POST, request.FILES)  # FILES pour images
        
        if form.is_valid():
            # Données valides!
            # form.cleaned_data = données nettoyées et validées
            
            # Méthode 1: Sauvegarder directement
            # post = form.save()
            
            # Méthode 2: Modifier avant sauvegarde (recommandé)
            post = form.save(commit=False)  # Ne sauvegarde PAS encore
            post.author = request.user      # Ajouter l'auteur
            post.save()                     # Maintenant sauvegarder
            
            # Rediriger vers le post créé
            return redirect('blog:post_detail', pk=post.pk)
        
        # Si pas valide: form contient les erreurs
        # Le template réaffichera le form avec les erreurs
    
    else:
        # Méthode GET: afficher formulaire vide
        form = PostForm()
    
    return render(request, 'blog/post_form.html', {'form': form})

# Explication du flux:

# 1. Utilisateur arrive sur /post/new/ (GET)
#    -> request.method = 'GET'
#    -> form = PostForm() (vide)
#    -> Affiche template avec formulaire vide

# 2. Utilisateur remplit et clique "Envoyer" (POST)
#    -> request.method = 'POST'
#    -> form = PostForm(request.POST) (avec données)
#    -> if form.is_valid(): vérifie tout
#    -> Si OK: sauvegarde et redirige
#    -> Si erreur: réaffiche form avec messages d'erreur

# form.is_valid()
#   Vérifie TOUTES les validations:
#   - Champs obligatoires remplis?
#   - Email valide?
#   - Nombre dans la bonne plage?
#   - Longueur max respectée?
#   - Validations personnalisées (clean methods)

# form.save(commit=False)
#   Crée l'objet SANS le sauvegarder en BDD
#   Utile pour ajouter des données avant save
#   Exemple: ajouter author, modifier un champ, etc.

# request.FILES
#   IMPORTANT pour les fichiers uploadés (images, documents)
#   Sans ça: les fichiers ne sont pas traités!


# Modifier un article (UPDATE)

def post_update(request, pk):
    """View pour modifier un post existant"""
    
    # Récupérer le post
    post = get_object_or_404(Post, pk=pk)
    
    # Vérifier que c'est l'auteur
    if post.author != request.user:
        return HttpResponseForbidden("Vous n'êtes pas l'auteur")
    
    if request.method == 'POST':
        # Formulaire soumis avec les modifications
        form = PostForm(request.POST, request.FILES, instance=post)
        #                                              ^ IMPORTANT!
        # instance=post indique qu'on MODIFIE cet objet
        
        if form.is_valid():
            form.save()  # Met à jour le post existant
            return redirect('blog:post_detail', pk=post.pk)
    else:
        # GET: afficher formulaire pré-rempli avec données actuelles
        form = PostForm(instance=post)
    
    context = {
        'form': form,
        'post': post,  # Pour afficher "Modifier: Titre du post"
    }
    return render(request, 'blog/post_form.html', context)

# Différence CREATE vs UPDATE:
# CREATE: PostForm() -> formulaire vide
# UPDATE: PostForm(instance=post) -> formulaire pré-rempli


# === ÉTAPE 3: Afficher le formulaire dans un Template ===

# blog/templates/blog/post_form.html
{% extends 'blog/base.html' %}

{% block title %}
    {% if post %}Modifier{% else %}Créer{% endif %} un article
{% endblock %}

{% block content %}
    <h1>
        {% if post %}
            Modifier: {{ post.title }}
        {% else %}
            Créer un nouvel article
        {% endif %}
    </h1>
    
    {# Afficher les erreurs globales (non liées à un champ) #}
    {% if form.non_field_errors %}
        <div class="alert alert-danger">
            {{ form.non_field_errors }}
        </div>
    {% endif %}
    
    {# Le formulaire #}
    <form method="post" enctype="multipart/form-data">
        {% csrf_token %}
        
        {# Méthode 1: Affichage automatique complet #}
        {{ form.as_p }}
        
        <button type="submit" class="btn btn-primary">
            {% if post %}Mettre à jour{% else %}Créer{% endif %}
        </button>
        <a href="{% url 'blog:post_list' %}" class="btn btn-secondary">
            Annuler
        </a>
    </form>
{% endblock %}

# Explication:

# <form method="post" enctype="multipart/form-data">
#   method="post" = envoyer données en POST (obligatoire!)
#   enctype="multipart/form-data" = OBLIGATOIRE si upload de fichiers
#   Sans ça: les images/fichiers ne marchent pas!

# {% csrf_token %}
#   Protection contre attaques CSRF (obligatoire!)
#   Django génère un token secret unique
#   Sans ça: erreur 403 Forbidden

# {{ form.as_p }}
#   Génère automatiquement TOUT le HTML du formulaire
#   Chaque champ dans un <p>
#   Inclut labels, inputs, messages d'erreur
#   C'est MAGIQUE!

# Autres méthodes d'affichage:
# {{ form.as_table }} = dans un <table>
# {{ form.as_ul }} = dans une <ul>


# Méthode 2: Affichage manuel (contrôle total)

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    {# Boucle sur tous les champs #}
    {% for field in form %}
        <div class="mb-3">
            {# Label #}
            {{ field.label_tag }}
            
            {# Input #}
            {{ field }}
            
            {# Texte d'aide #}
            {% if field.help_text %}
                <small class="text-muted">{{ field.help_text }}</small>
            {% endif %}
            
            {# Erreurs du champ #}
            {% if field.errors %}
                <div class="alert alert-danger">
                    {{ field.errors }}
                </div>
            {% endif %}
        </div>
    {% endfor %}
    
    <button type="submit" class="btn btn-primary">Envoyer</button>
</form>


# Méthode 3: Affichage champ par champ (ultra contrôlé)

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    <div class="mb-3">
        <label for="{{ form.title.id_for_label }}">Titre:</label>
        {{ form.title }}
        {% if form.title.errors %}
            <div class="alert alert-danger">{{ form.title.errors }}</div>
        {% endif %}
    </div>
    
    <div class="mb-3">
        <label for="{{ form.content.id_for_label }}">Contenu:</label>
        {{ form.content }}
        {% if form.content.errors %}
            <div class="alert alert-danger">{{ form.content.errors }}</div>
        {% endif %}
    </div>
    
    <div class="form-check">
        {{ form.published }}
        <label class="form-check-label" for="{{ form.published.id_for_label }}">
            Publier
        </label>
    </div>
    
    <button type="submit" class="btn btn-primary">Envoyer</button>
</form>


# === Validation personnalisée ===

# Valider UN champ spécifique

# blog/forms.py
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'published']
    
    def clean_title(self):
        """Validation personnalisée du titre"""
        title = self.cleaned_data.get('title')
        
        # Titre trop court?
        if len(title) < 5:
            raise forms.ValidationError(
                'Le titre doit contenir au moins 5 caractères.'
            )
        
        # Mot interdit?
        if 'spam' in title.lower():
            raise forms.ValidationError(
                'Le mot "spam" est interdit dans le titre.'
            )
        
        # Titre déjà existant?
        if Post.objects.filter(title=title).exists():
            raise forms.ValidationError(
                'Un article avec ce titre existe déjà.'
            )
        
        return title  # Retourner la valeur nettoyée

# Règle: Méthode clean_<nom_du_champ>()
# clean_title() pour le champ "title"
# clean_email() pour le champ "email"
# etc.


# Valider PLUSIEURS champs ensemble

def clean(self):
    """Validation globale (plusieurs champs)"""
    cleaned_data = super().clean()
    title = cleaned_data.get('title')
    content = cleaned_data.get('content')
    
    # Le titre ne doit pas être dans le contenu
    if title and content:
        if title.lower() in content.lower():
            raise forms.ValidationError(
                'Le titre ne doit pas apparaître dans le contenu.'
            )
    
    # Si publié, le contenu doit faire au moins 100 mots
    if cleaned_data.get('published'):
        if len(content.split()) < 100:
            raise forms.ValidationError(
                'Un article publié doit contenir au moins 100 mots.'
            )
    
    return cleaned_data


# === Form classique (pas basé sur Model) ===

# Pour formulaires qui ne correspondent pas à un Model
# Exemple: formulaire de contact, recherche, etc.

# blog/forms.py
from django import forms

class ContactForm(forms.Form):
    """Formulaire de contact (pas de Model)"""
    
    name = forms.CharField(
        max_length=100,
        label='Votre nom',
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Jean Dupont'
        })
    )
    
    email = forms.EmailField(
        label='Votre email',
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': 'jean@example.com'
        })
    )
    
    subject = forms.CharField(
        max_length=200,
        label='Sujet',
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    
    message = forms.CharField(
        widget=forms.Textarea(attrs={
            'class': 'form-control',
            'rows': 5
        }),
        label='Message'
    )
    
    # Case à cocher obligatoire
    agree = forms.BooleanField(
        required=True,
        label='J\'accepte les conditions d\'utilisation'
    )
    
    def clean_email(self):
        """Validation email"""
        email = self.cleaned_data.get('email')
        
        # Bloquer certains domaines
        blocked_domains = ['tempmail.com', 'throwaway.email']
        domain = email.split('@')[1]
        
        if domain in blocked_domains:
            raise forms.ValidationError(
                'Les emails jetables ne sont pas acceptés.'
            )
        
        return email

# View pour Form classique

def contact(request):
    """View formulaire de contact"""
    
    if request.method == 'POST':
        form = ContactForm(request.POST)
        
        if form.is_valid():
            # Récupérer les données nettoyées
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            subject = form.cleaned_data['subject']
            message = form.cleaned_data['message']
            
            # Envoyer l'email
            from django.core.mail import send_mail
            send_mail(
                subject=f'Contact: {subject}',
                message=f'De: {name} <{email}>\n\n{message}',
                from_email='noreply@monsite.com',
                recipient_list=['contact@monsite.com'],
            )
            
            # Afficher message de succès
            from django.contrib import messages
            messages.success(request, 'Votre message a été envoyé!')
            
            return redirect('blog:post_list')
    else:
        form = ContactForm()
    
    return render(request, 'blog/contact.html', {'form': form})


# === Types de champs de formulaire ===

from django import forms

# TEXTE
name = forms.CharField(max_length=100)
email = forms.EmailField()
url = forms.URLField()
slug = forms.SlugField()
text = forms.CharField(widget=forms.Textarea)

# NOMBRES
age = forms.IntegerField(min_value=0, max_value=120)
price = forms.DecimalField(max_digits=10, decimal_places=2)
rating = forms.FloatField(min_value=0, max_value=5)

# BOOLÉENS
agree = forms.BooleanField()
subscribe = forms.BooleanField(required=False)  # Optionnel

# CHOIX
GENRE_CHOICES = [
    ('M', 'Homme'),
    ('F', 'Femme'),
    ('O', 'Autre'),
]
genre = forms.ChoiceField(choices=GENRE_CHOICES)
genre_radio = forms.ChoiceField(
    choices=GENRE_CHOICES,
    widget=forms.RadioSelect  # Boutons radio au lieu de liste
)

# Choix multiples
INTERESTS = [
    ('tech', 'Technologie'),
    ('sport', 'Sport'),
    ('art', 'Art'),
]
interests = forms.MultipleChoiceField(choices=INTERESTS)
interests_checkbox = forms.MultipleChoiceField(
    choices=INTERESTS,
    widget=forms.CheckboxSelectMultiple  # Cases à cocher
)

# DATES
birth_date = forms.DateField(
    widget=forms.DateInput(attrs={'type': 'date'})  # HTML5 date picker
)
appointment = forms.DateTimeField()
start_time = forms.TimeField()

# FICHIERS
photo = forms.FileField()
photo = forms.ImageField()  # Vérifie que c'est une image
documents = forms.FileField(
    widget=forms.ClearableFileInput(attrs={'multiple': True})  # Multiple
)

# RELATIONS (pour ModelForm)
author = forms.ModelChoiceField(queryset=User.objects.all())
tags = forms.ModelMultipleChoiceField(queryset=Tag.objects.all())

# Champ caché (valeur pré-remplie, invisible)
user_id = forms.CharField(widget=forms.HiddenInput())


# === Widgets (personnaliser rendu HTML) ===

# Widget = Comment le champ est rendu en HTML

from django import forms

# TextInput (défaut pour CharField)
forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Texte'})

# Textarea
forms.Textarea(attrs={'rows': 10, 'cols': 50})

# PasswordInput (masque le texte)
forms.PasswordInput(attrs={'class': 'form-control'})

# EmailInput (validation HTML5)
forms.EmailInput(attrs={'class': 'form-control'})

# NumberInput
forms.NumberInput(attrs={'min': 0, 'max': 100})

# DateInput (avec date picker HTML5)
forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})

# CheckboxInput
forms.CheckboxInput(attrs={'class': 'form-check-input'})

# Select (liste déroulante)
forms.Select(attrs={'class': 'form-select'})

# RadioSelect (boutons radio)
forms.RadioSelect(attrs={'class': 'form-check-input'})

# CheckboxSelectMultiple (cases à cocher)
forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'})

# FileInput
forms.FileInput(attrs={'class': 'form-control'})

# ClearableFileInput (avec bouton "Effacer")
forms.ClearableFileInput(attrs={'class': 'form-control'})

# HiddenInput (champ caché)
forms.HiddenInput()


# === Formsets (formulaires multiples) ===

# Formset = Plusieurs formulaires identiques sur une page
# Exemple: Ajouter plusieurs produits d'un coup

from django.forms import formset_factory

# Créer formset depuis Form
ContactFormSet = formset_factory(ContactForm, extra=3)  # 3 formulaires

# View
def multiple_contacts(request):
    if request.method == 'POST':
        formset = ContactFormSet(request.POST)
        if formset.is_valid():
            for form in formset:
                if form.cleaned_data:  # Si formulaire rempli
                    # Traiter chaque formulaire
                    name = form.cleaned_data['name']
                    # ...
            return redirect('success')
    else:
        formset = ContactFormSet()
    
    return render(request, 'contacts.html', {'formset': formset})

# Template
<form method="post">
    {% csrf_token %}
    {{ formset.management_form }}  {# Obligatoire! #}
    {% for form in formset %}
        {{ form.as_p }}
        <hr>
    {% endfor %}
    <button type="submit">Envoyer tout</button>
</form>


# === Résumé Formulaires ===

# 1. Form = formulaire générique
# 2. ModelForm = formulaire depuis Model (plus courant)
# 3. Créer forms.py dans l'app
# 4. View: if POST -> valider et sauver, else -> afficher vide
# 5. form.is_valid() = valider toutes les données
# 6. form.save() = sauvegarder en BDD (ModelForm)
# 7. form.cleaned_data = données nettoyées et validées
# 8. Template: {% csrf_token %} obligatoire!
# 9. {{ form.as_p }} = affichage automatique
# 10. clean_<field>() = validation personnalisée
# 11. enctype="multipart/form-data" si fichiers uploadés
# 12. Widget = personnaliser rendu HTML

# Prochaine étape: Gérer les utilisateurs et l'authentification!

# === Types de champs ===

# Texte
name = forms.CharField(max_length=100)
email = forms.EmailField()
url = forms.URLField()
slug = forms.SlugField()
text = forms.CharField(widget=forms.Textarea)

# Nombres
age = forms.IntegerField(min_value=0, max_value=120)
price = forms.DecimalField(max_digits=10, decimal_places=2)
rating = forms.FloatField()

# Choix
CHOICES = [
    ('draft', 'Brouillon'),
    ('published', 'Publié'),
]
status = forms.ChoiceField(choices=CHOICES)
status = forms.ChoiceField(choices=CHOICES, widget=forms.RadioSelect)

# Multiple choice
categories = forms.MultipleChoiceField(choices=CHOICES)
categories = forms.MultipleChoiceField(choices=CHOICES, widget=forms.CheckboxSelectMultiple)

# Booléen
agree = forms.BooleanField()
agree = forms.BooleanField(required=False)

# Date et temps
birth_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
appointment = forms.DateTimeField()
start_time = forms.TimeField()

# Fichiers
photo = forms.FileField()
photo = forms.ImageField()
documents = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))

# Relations (ModelForm)
author = forms.ModelChoiceField(queryset=User.objects.all())
tags = forms.ModelMultipleChoiceField(queryset=Tag.objects.all())

# Champ caché
hidden_field = forms.CharField(widget=forms.HiddenInput())

# === Widgets personnalisés ===

from django import forms

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Titre...',
                'required': True,
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10,
                'cols': 50,
            }),
            'published_date': forms.DateInput(attrs={
                'type': 'date',
                'class': 'form-control',
            }),
            'status': forms.Select(attrs={
                'class': 'form-select',
            }),
        }

# === Formsets (formulaires multiples) ===

from django.forms import formset_factory, modelformset_factory, inlineformset_factory

# Formset basique
ContactFormSet = formset_factory(ContactForm, extra=3)  # 3 formulaires vides

def multiple_contacts(request):
    if request.method == 'POST':
        formset = ContactFormSet(request.POST)
        if formset.is_valid():
            for form in formset:
                # Traiter chaque formulaire
                if form.cleaned_data:
                    name = form.cleaned_data['name']
                    # ...
            return redirect('success')
    else:
        formset = ContactFormSet()
    
    return render(request, 'contacts.html', {'formset': formset})

# Template formset
<form method="post">
    {% csrf_token %}
    {{ formset.management_form }}
    {% for form in formset %}
        {{ form.as_p }}
        <hr>
    {% endfor %}
    <button type="submit">Envoyer</button>
</form>

# ModelFormSet
PostFormSet = modelformset_factory(
    Post,
    fields=['title', 'content'],
    extra=2,                                      # Formulaires vides
    can_delete=True                               # Checkbox de suppression
)

def manage_posts(request):
    if request.method == 'POST':
        formset = PostFormSet(request.POST)
        if formset.is_valid():
            formset.save()
            return redirect('post_list')
    else:
        formset = PostFormSet(queryset=Post.objects.filter(published=False))
    
    return render(request, 'manage_posts.html', {'formset': formset})

# InlineFormSet (relations)
CommentFormSet = inlineformset_factory(
    Post,                                         # Parent model
    Comment,                                      # Child model
    fields=['text', 'author'],
    extra=1,
    can_delete=True
)

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, 'edit_post.html', {'post': post, 'formset': formset})

# === Validation personnalisée ===

# Validation au niveau du champ
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'
    
    def clean_title(self):
        title = self.cleaned_data.get('title')
        if 'spam' in title.lower():
            raise forms.ValidationError('Titre invalide')
        return title
    
    def clean_content(self):
        content = self.cleaned_data.get('content')
        if len(content) < 100:
            raise forms.ValidationError('Contenu trop court (minimum 100 caractères)')
        return content

# Validation globale
def clean(self):
    cleaned_data = super().clean()
    title = cleaned_data.get('title')
    content = cleaned_data.get('content')
    
    if title and content:
        if title.lower() in content.lower():
            raise forms.ValidationError('Le titre ne doit pas apparaître dans le contenu')
    
    return cleaned_data

# === Erreurs de formulaire ===

# Dans template
{% if form.errors %}
    <div class="alert alert-danger">
        <ul>
            {% for field, errors in form.errors.items %}
                {% for error in errors %}
                    <li><strong>{{ field }}:</strong> {{ error }}</li>
                {% endfor %}
            {% endfor %}
        </ul>
    </div>
{% endif %}

# Erreurs par champ
{% for field in form %}
    {{ field.label_tag }}
    {{ field }}
    {% if field.errors %}
        <div class="error">
            {{ field.errors }}
        </div>
    {% endif %}
{% endfor %}

# Dans la vue (ajouter erreur manuellement)
form.add_error('title', 'Ce titre existe déjà')
form.add_error(None, 'Erreur globale du formulaire')


[OK] ADMIN DJANGO - INTERFACE D'ADMINISTRATION GRATUITE


# === Qu'est-ce que l'Admin Django ? ===

# L'ADMIN = Interface d'administration WEB complète et GRATUITE!
# Django génère automatiquement une interface pour gérer vos données

# C'est comme avoir un panneau d'administration phpMyAdmin, mais:
# [OK] Automatique (basé sur vos models)
# [OK] Personnalisable (filtres, recherche, actions)
# [OK] Sécurisé (authentification requise)
# [OK] Responsive (fonctionne sur mobile)
# [OK] Prêt à l'emploi (zéro ligne de code!)

# ANALOGIE: Vous créez une table Excel, et Django vous donne automatiquement
# une interface web pour ajouter, modifier, supprimer, filtrer, chercher...

# Pourquoi c'est GÉNIAL ?
# Sans l'admin: Il faudrait coder toutes les pages CRUD manuellement
# (Create, Read, Update, Delete = Créer, Lire, Modifier, Supprimer)
# Avec l'admin: Tout est automatique en 2 lignes de code!


# === ÉTAPE 1: Créer un superutilisateur ===

# L'admin nécessite un compte administrateur pour y accéder

python manage.py createsuperuser

# Django vous demande:
# Username (leave blank to use 'votre_nom_ordinateur'): admin
# Email address: admin@example.com
# Password: ********  (min 8 caractères, pas trop simple)
# Password (again): ********

# Conseils pour le password:
# [X] admin123 (trop faible)
# [X] password (trop faible)  
# [OK] Django2024!Secure (bon)

# IMPORTANT: Notez bien ces identifiants!
# Vous les utiliserez pour vous connecter à l'admin

# Si vous oubliez le password:
python manage.py changepassword admin


# === ÉTAPE 2: Accéder à l'admin ===

# Lancer le serveur
python manage.py runserver

# Ouvrir le navigateur sur: http://127.0.0.1:8000/admin/
# Ou: http://localhost:8000/admin/

# Vous verrez la page de connexion Django administration
# Entrez votre username et password

# Une fois connecté, vous voyez:
# - AUTHENTICATION AND AUTHORIZATION
#   - Groups
#   - Users
# 
# Pour l'instant, vous pouvez gérer les utilisateurs, mais pas vos models!


# === ÉTAPE 3: Enregistrer vos models dans l'admin ===

# Par défaut, vos models ne sont PAS visibles dans l'admin
# Il faut les enregistrer explicitement

# Ouvrir blog/admin.py

# Fichier par défaut (vide):
from django.contrib import admin

# Register your models here.

# Méthode 1: Enregistrement simple (2 lignes)
from django.contrib import admin
from .models import Post

admin.site.register(Post)

# C'est tout! Rechargez l'admin, vous voyez maintenant:
# - BLOG
#   - Posts

# Cliquez sur "Posts" -> Interface complète pour gérer vos articles!
# Vous pouvez: Ajouter, Modifier, Supprimer, Filtrer, Chercher


# === ÉTAPE 4: Personnaliser l'affichage (IMPORTANT) ===

# Par défaut, l'admin affiche "Post object (1)", "Post object (2)"...
# Pas très lisible!

# C'est là que __str__() dans le model est important:

# Dans blog/models.py
class Post(models.Model):
    title = models.CharField(max_length=200)
    
    def __str__(self):
        return self.title  # Maintenant affiche le titre!

# Résultat: "Mon premier article", "Django tutorial"... (lisible!)


# === Personnalisation avancée de l'admin ===

# Méthode 2: Avec classe d'administration (recommandé)

# blog/admin.py
from django.contrib import admin
from .models import Post

@admin.register(Post)  # Décorateur (alternative à admin.site.register)
class PostAdmin(admin.ModelAdmin):
    # Colonnes affichées dans la liste
    list_display = ['title', 'author', 'published', 'created_at']
    
    # Filtres latéraux
    list_filter = ['published', 'created_at', 'author']
    
    # Barre de recherche
    search_fields = ['title', 'content']
    
    # Champs modifiables directement dans la liste
    list_editable = ['published']
    
    # Pagination (nombre d'objets par page)
    list_per_page = 25
    
    # Tri par défaut
    ordering = ['-created_at']
    
    # Hiérarchie par date (navigation par mois/année)
    date_hierarchy = 'created_at'
    
    # Auto-remplir slug depuis title
    prepopulated_fields = {'slug': ('title',)}  # Si vous avez un champ slug

# Explication des options:

# list_display = ['title', 'author', 'published', 'created_at']
#   Colonnes affichées dans le tableau
#   Par défaut: seulement __str__()
#   Avec ça: tableau complet avec plusieurs colonnes!

# list_filter = ['published', 'created_at']
#   Filtres dans la barre latérale droite
#   Cliquez sur "Publié: Oui" pour voir seulement les publiés
#   Cliquez sur "Aujourd'hui" pour voir ceux d'aujourd'hui

# search_fields = ['title', 'content']
#   Barre de recherche en haut
#   Cherche dans title ET content
#   Supporte: cherche partielle, insensible à la casse

# list_editable = ['published']
#   Modifiable DIRECTEMENT dans la liste (sans ouvrir l'objet)
#   Cochez/décochez "published" et cliquez "Save"
#   Gain de temps énorme!

# list_per_page = 25
#   Nombre d'objets par page
#   Par défaut: 100 (trop si grosse BDD)

# ordering = ['-created_at']
#   Tri par défaut (- = descendant, plus récent d'abord)

# date_hierarchy = 'created_at'
#   Navigation par date en haut: 2024 > Janvier > 15
#   Super pratique pour articles de blog!

# prepopulated_fields = {'slug': ('title',)}
#   Remplit automatiquement le slug depuis le titre
#   "Mon Article" -> "mon-article"


# === Méthodes personnalisées dans list_display ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'get_word_count', 'created_at']
    
    def get_word_count(self, obj):
        """Affiche le nombre de mots (colonne calculée)"""
        return len(obj.content.split())
    
    # Personnaliser le header de la colonne
    get_word_count.short_description = 'Mots'
    
    # Permettre le tri sur cette colonne (si propriété du model)
    # get_word_count.admin_order_field = 'content'

# Résultat: Colonne "Mots" avec 543, 1234, etc.


# === Organiser le formulaire d'édition ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # Organiser en sections
    fieldsets = (
        ('Contenu principal', {
            'fields': ('title', 'slug', 'content')
        }),
        ('Métadonnées', {
            'fields': ('author', 'published', 'tags'),
            'classes': ('collapse',)  # Section repliable
        }),
        ('Dates', {
            'fields': ('created_at', 'updated_at'),
            'classes': ('collapse',)
        }),
    )
    
    # Ou liste simple (sans sections)
    # fields = ['title', 'content', 'author', 'published']
    
    # Champs en lecture seule (non modifiables)
    readonly_fields = ['created_at', 'updated_at']

# Explication:
# fieldsets = sections du formulaire
# 'fields': champs dans cette section
# 'classes': ('collapse',) = section repliée par défaut


# === Actions personnalisées ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'published']
    actions = ['make_published', 'make_draft']
    
    @admin.action(description='Publier les articles sélectionnés')
    def make_published(self, request, queryset):
        """Action: Publier plusieurs articles d'un coup"""
        updated = queryset.update(published=True)
        self.message_user(request, f'{updated} article(s) publié(s).')
    
    @admin.action(description='Mettre en brouillon')
    def make_draft(self, request, queryset):
        """Action: Mettre en brouillon"""
        updated = queryset.update(published=False)
        self.message_user(request, f'{updated} article(s) en brouillon.')

# Utilisation:
# 1. Cochez plusieurs articles
# 2. Sélectionnez l'action dans le menu déroulant
# 3. Cliquez "Go"
# -> Tous les articles sélectionnés sont publiés!


# === Models liés (Inline) ===

# Afficher et modifier des objets liés sur la même page

# Exemple: Éditer les commentaires d'un post directement sur la page du post

from .models import Post, Comment

class CommentInline(admin.TabularInline):  # Ou StackedInline
    model = Comment
    extra = 1  # Nombre de formulaires vides
    fields = ['author', 'text', 'approved']

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    inlines = [CommentInline]

# Résultat: Quand vous éditez un Post, vous voyez ses Comments dessous!
# Vous pouvez ajouter/modifier/supprimer des comments sans quitter la page

# TabularInline = Format tableau (compact)
# StackedInline = Format vertical (un par un)


# === Personnaliser l'apparence de l'admin ===

# Dans admin.py ou urls.py (racine du projet)

from django.contrib import admin

# Changer le titre et header
admin.site.site_header = 'Mon Blog - Administration'
admin.site.site_title = 'Admin Mon Blog'
admin.site.index_title = 'Tableau de bord'

# Résultat:
# Header: "Mon Blog - Administration" (au lieu de "Django administration")
# Onglet navigateur: "Admin Mon Blog"
# Page d'accueil: "Tableau de bord"


# === Permissions dans l'admin ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    def has_add_permission(self, request):
        """Qui peut AJOUTER des posts ?"""
        return request.user.is_superuser  # Seulement superusers
    
    def has_change_permission(self, request, obj=None):
        """Qui peut MODIFIER des posts ?"""
        if obj and obj.author != request.user:
            return False  # Seulement l'auteur peut modifier son post
        return True
    
    def has_delete_permission(self, request, obj=None):
        """Qui peut SUPPRIMER des posts ?"""
        return request.user.is_superuser  # Seulement superusers
    
    def get_queryset(self, request):
        """Filtrer les posts visibles"""
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs  # Superuser voit tout
        return qs.filter(author=request.user)  # User voit seulement ses posts


# === Filtres personnalisés ===

from django.contrib import admin

class PublishedFilter(admin.SimpleListFilter):
    title = 'statut de publication'
    parameter_name = 'published'
    
    def lookups(self, request, model_admin):
        """Options du filtre"""
        return (
            ('yes', 'Publié'),
            ('no', 'Brouillon'),
            ('recent', 'Publié récemment'),
        )
    
    def queryset(self, request, queryset):
        """Filtrer le queryset"""
        if self.value() == 'yes':
            return queryset.filter(published=True)
        if self.value() == 'no':
            return queryset.filter(published=False)
        if self.value() == 'recent':
            from datetime import timedelta
            from django.utils import timezone
            date = timezone.now() - timedelta(days=7)
            return queryset.filter(published=True, created_at__gte=date)

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_filter = [PublishedFilter, 'created_at']


# === Résumé Admin Django ===

# 1. Créer superuser: python manage.py createsuperuser
# 2. Accès: http://localhost:8000/admin/
# 3. Enregistrer model: admin.site.register(Post) dans admin.py
# 4. Personnaliser: classe PostAdmin avec list_display, list_filter, etc.
# 5. __str__() dans model pour affichage lisible
# 6. Actions pour opérations en masse
# 7. Inline pour models liés
# 8. Permissions pour contrôler l'accès

# L'admin est parfait pour:
# [OK] Gestion de contenu (CMS)
# [OK] Outils internes d'entreprise
# [OK] Backoffice e-commerce
# [OK] Prototype rapide

# L'admin n'est PAS pour:
# [X] Interface utilisateur finale (trop "admin")
# [X] Design personnalisé (limité)
# -> Pour ça, créez vos propres views/templates!

# Prochaine étape: Créer des views pour afficher les données côté utilisateur

# === Options de configuration ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # Colonnes affichées dans la liste
    list_display = ['title', 'author', 'status', 'created_at', 'get_comments_count']
    
    # Liens cliquables (par défaut: premier champ)
    list_display_links = ['title']
    
    # Champs éditables directement dans la liste
    list_editable = ['status', 'published']
    
    # Filtres latéraux
    list_filter = ['status', 'created_at', 'author', 'tags']
    
    # Barre de recherche
    search_fields = ['title', 'content', 'author__username']
    
    # Hiérarchie par date
    date_hierarchy = 'created_at'
    
    # Tri par défaut
    ordering = ['-created_at', 'title']
    
    # Pagination
    list_per_page = 50
    
    # Actions de masse
    actions = ['make_published', 'make_draft']
    
    # Auto-remplissage slug depuis title
    prepopulated_fields = {'slug': ('title',)}
    
    # Champs read-only
    readonly_fields = ['created_at', 'updated_at', 'get_word_count']
    
    # Organisation du formulaire
    fieldsets = (
        ('Contenu', {
            'fields': ('title', 'slug', 'content')
        }),
        ('Métadonnées', {
            'fields': ('author', 'status', 'published', 'tags'),
            'classes': ('collapse',)                  # Collapsable
        }),
        ('Dates', {
            'fields': ('created_at', 'updated_at'),
            'classes': ('collapse',)
        }),
    )
    
    # Ou plus simple (sans fieldsets)
    fields = ['title', 'slug', 'content', 'author', 'status']
    
    # Exclure champs
    exclude = ['created_at', 'updated_at']
    
    # Filtres horizontaux (Many-to-Many)
    filter_horizontal = ['tags']
    # filter_vertical = ['tags']                    # Vertical
    
    # Autocomplete pour ForeignKey
    autocomplete_fields = ['author', 'category']
    
    # Raw ID fields (pour grandes tables)
    raw_id_fields = ['author']
    
    # Champs avec calendrier
    # (automatique pour DateField/DateTimeField)
    
    # Méthodes personnalisées pour list_display
    def get_comments_count(self, obj):
        return obj.comments.count()
    get_comments_count.short_description = 'Commentaires'
    
    def get_word_count(self, obj):
        return len(obj.content.split())
    get_word_count.short_description = 'Mots'

# === Actions personnalisées ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'status']
    actions = ['make_published', 'make_draft', 'delete_selected']
    
    @admin.action(description='Publier les articles sélectionnés')
    def make_published(self, request, queryset):
        updated = queryset.update(status='published')
        self.message_user(request, f'{updated} article(s) publié(s).')
    
    @admin.action(description='Mettre en brouillon')
    def make_draft(self, request, queryset):
        updated = queryset.update(status='draft')
        self.message_user(request, f'{updated} article(s) en brouillon.')

# === Inline models (relations) ===

from django.contrib import admin

class CommentInline(admin.TabularInline):      # Ou StackedInline
    model = Comment
    extra = 1                                   # Formulaires vides
    fields = ['author', 'text', 'approved']
    readonly_fields = ['created_at']

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'created_at']
    inlines = [CommentInline]

# StackedInline (vertical)
class CommentInline(admin.StackedInline):
    model = Comment
    extra = 1
    
# TabularInline (table, plus compact)
class CommentInline(admin.TabularInline):
    model = Comment
    extra = 0
    can_delete = True
    show_change_link = True                     # Lien vers objet

# === Personnaliser l'admin ===

# Changer titre et header
admin.site.site_header = 'Mon Administration'
admin.site.site_title = 'Admin Mon Site'
admin.site.index_title = 'Tableau de bord'

# Dans admin.py ou urls.py
from django.contrib import admin
admin.site.site_header = 'Blog Administration'
admin.site.site_title = 'Blog Admin'
admin.site.index_title = 'Bienvenue sur l\'admin du blog'

# === Permissions ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author']
    
    def has_add_permission(self, request):
        # Qui peut ajouter
        return request.user.is_superuser
    
    def has_change_permission(self, request, obj=None):
        # Qui peut modifier
        if obj and request.user != obj.author:
            return False
        return True
    
    def has_delete_permission(self, request, obj=None):
        # Qui peut supprimer
        return request.user.is_superuser
    
    def get_queryset(self, request):
        # Filtrer objets visibles
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(author=request.user)

# === Formulaires personnalisés ===

from django import forms

class PostAdminForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'content': forms.Textarea(attrs={'rows': 20}),
        }
    
    def clean_title(self):
        title = self.cleaned_data.get('title')
        if len(title) < 10:
            raise forms.ValidationError('Titre trop court')
        return title

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    form = PostAdminForm

# === Media (CSS/JS personnalisés) ===

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    class Media:
        css = {
            'all': ('css/admin-custom.css',)
        }
        js = ('js/admin-custom.js',)


[OK] AUTHENTIFICATION & USERS - GÉRER LES UTILISATEURS


# === Qu'est-ce que l'authentification ? ===

# AUTHENTIFICATION = Système de connexion/inscription/gestion des users
# Django fournit un système d'authentification complet et sécurisé!

# ANALOGIE: Carte d'identité et badge d'accès
# - Inscription = Créer sa carte d'identité
# - Connexion = Montrer sa carte pour entrer
# - Permissions = Certains badges donnent accès à certaines zones
# - Déconnexion = Rendre son badge en sortant

# Django inclut GRATUITEMENT:
# [OK] Model User (table utilisateurs)
# [OK] Système de connexion/déconnexion
# [OK] Hashage sécurisé des passwords
# [OK] Permissions et groupes
# [OK] Formulaires d'auth
# [OK] Vues prêtes à l'emploi
# [OK] Décorateurs @login_required

# Vous n'avez PRESQUE RIEN à coder!


# === ÉTAPE 1: Comprendre le Model User ===

# Django fournit un Model User par défaut
# Table: auth_user (créée automatiquement avec migrations)

from django.contrib.auth.models import User

# Attributs du User Django:
# - username (obligatoire, unique)
# - email (optionnel)
# - first_name (prénom)
# - last_name (nom)
# - password (hashé automatiquement, JAMAIS en clair!)
# - is_active (compte actif?)
# - is_staff (accès admin?)
# - is_superuser (tous les droits?)
# - last_login (dernière connexion)
# - date_joined (date d'inscription)

# Créer un utilisateur (dans shell ou views):
user = User.objects.create_user(
    username='jean',
    email='jean@example.com',
    password='motdepasse123',  # Django hash automatiquement!
    first_name='Jean',
    last_name='Dupont'
)

# IMPORTANT: TOUJOURS utiliser create_user(), JAMAIS create()!
# create_user() hash le password
# create() met le password en clair (DANGER!)

# Créer superuser (admin)
user = User.objects.create_superuser(
    username='admin',
    email='admin@example.com',
    password='admin123'
)

# Ou en ligne de commande:
python manage.py createsuperuser


# Modifier un user
user = User.objects.get(username='jean')
user.first_name = 'Jean-Pierre'
user.email = 'jeanpierre@example.com'
user.save()

# Changer password
user.set_password('nouveau_password')  # Hash automatiquement!
user.save()

# JAMAIS faire:
# user.password = 'nouveau'  # [X] En clair, pas hashé!

# Vérifier password
if user.check_password('motdepasse123'):
    print("Password correct!")


# === ÉTAPE 2: URLs d'authentification Django ===

# Django fournit des vues d'auth prêtes à l'emploi!

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('accounts/', include('django.contrib.auth.urls')),  # <- Ajouter ça!
]

# Cette ligne ajoute automatiquement ces URLs:
# /accounts/login/ -> Page de connexion
# /accounts/logout/ -> Déconnexion
# /accounts/password_change/ -> Changer son password
# /accounts/password_change/done/ -> Confirmation changement
# /accounts/password_reset/ -> Réinitialiser password (email)
# /accounts/password_reset/done/ -> Email envoyé
# /accounts/reset/<uidb64>/<token>/ -> Formulaire nouveau password
# /accounts/reset/done/ -> Password réinitialisé

# C'est MAGIQUE! Tout est déjà codé!


# === ÉTAPE 3: Créer les templates d'authentification ===

# Django fournit les vues, MAIS vous devez créer les templates

# Créer la structure:
templates/
└── registration/  # Nom imposé par Django!
    ├── login.html
    ├── logged_out.html
    └── password_change_form.html

# templates/registration/login.html
{% extends 'base.html' %}

{% block title %}Connexion{% endblock %}

{% block content %}
    <div class="container" style="max-width: 400px;">
        <h2>Connexion</h2>
        
        {% if form.errors %}
            <div class="alert alert-danger">
                Nom d'utilisateur ou mot de passe incorrect.
            </div>
        {% endif %}
        
        {% if next %}
            {% if user.is_authenticated %}
                <div class="alert alert-warning">
                    Vous n'avez pas accès à cette page.
                </div>
            {% endif %}
        {% endif %}
        
        <form method="post" action="{% url 'login' %}">
            {% csrf_token %}
            
            <div class="mb-3">
                <label for="{{ form.username.id_for_label }}">
                    Nom d'utilisateur:
                </label>
                {{ form.username }}
            </div>
            
            <div class="mb-3">
                <label for="{{ form.password.id_for_label }}">
                    Mot de passe:
                </label>
                {{ form.password }}
            </div>
            
            <button type="submit" class="btn btn-primary">
                Se connecter
            </button>
            
            {# URL vers laquelle rediriger après connexion #}
            <input type="hidden" name="next" value="{{ next }}">
        </form>
        
        <p class="mt-3">
            <a href="{% url 'password_reset' %}">Mot de passe oublié ?</a>
        </p>
    </div>
{% endblock %}

# Explication:

# {% if form.errors %}
#   Si username/password incorrect, affiche message d'erreur

# {{ next }}
#   URL vers laquelle rediriger après connexion
#   Exemple: user veut accéder /blog/post/new/ (protégé)
#   Django redirige vers /login/?next=/blog/post/new/
#   Après connexion: redirige vers /blog/post/new/

# action="{% url 'login' %}"
#   Soumet vers /accounts/login/ (view Django)


# templates/registration/logged_out.html
{% extends 'base.html' %}

{% block title %}Déconnecté{% endblock %}

{% block content %}
    <div class="container text-center">
        <h2>Vous êtes déconnecté</h2>
        <p>Vous avez été déconnecté avec succès.</p>
        <a href="{% url 'login' %}" class="btn btn-primary">
            Se reconnecter
        </a>
        <a href="{% url 'blog:post_list' %}" class="btn btn-secondary">
            Retour à l'accueil
        </a>
    </div>
{% endblock %}


# === ÉTAPE 4: Configurer les redirections (settings.py) ===

# myproject/settings.py

# URL de la page de connexion
LOGIN_URL = '/accounts/login/'  # Défaut, pas besoin de changer

# Où rediriger après connexion réussie
LOGIN_REDIRECT_URL = '/'  # Par défaut: /accounts/profile/
# Ou:
LOGIN_REDIRECT_URL = 'blog:post_list'  # Nom d'URL

# Où rediriger après déconnexion
LOGOUT_REDIRECT_URL = '/'  # Par défaut: /accounts/logged_out/
# Ou:
LOGOUT_REDIRECT_URL = 'blog:post_list'


# === ÉTAPE 5: Protéger des views (login requis) ===

# Décorateur @login_required = Forcer connexion

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    """View accessible seulement si connecté"""
    return render(request, 'accounts/profile.html')

# Si user PAS connecté:
# -> Redirige vers /accounts/login/?next=/profile/
# -> Après connexion: retour sur /profile/

@login_required(login_url='/connexion/')
def profile(request):
    """Avec URL de login personnalisée"""
    return render(request, 'accounts/profile.html')


# Pour Class-Based Views:
from django.contrib.auth.mixins import LoginRequiredMixin

class ProfileView(LoginRequiredMixin, DetailView):
    """CBV avec login requis"""
    login_url = '/accounts/login/'  # Optionnel
    model = UserProfile
    template_name = 'profile.html'

# IMPORTANT: LoginRequiredMixin AVANT DetailView!


# === ÉTAPE 6: Utiliser user dans views ===

def my_view(request):
    """Accéder à l'utilisateur connecté"""
    
    # Récupérer user
    user = request.user
    
    # Vérifier si connecté
    if user.is_authenticated:
        print(f"User connecté: {user.username}")
        print(f"Email: {user.email}")
        print(f"Nom complet: {user.get_full_name()}")
    else:
        print("User anonyme (pas connecté)")
    
    # Vérifier rôles
    if user.is_staff:
        print("User est staff (accès admin)")
    
    if user.is_superuser:
        print("User est superuser (tous les droits)")
    
    # Récupérer objets de l'user
    my_posts = Post.objects.filter(author=user)
    
    return render(request, 'template.html')


# Exemple: Créer post avec author automatique

@login_required
def post_create(request):
    """Créer post (author = user connecté)"""
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user  # <- Author automatique!
            post.save()
            return redirect('blog:post_detail', pk=post.pk)
    else:
        form = PostForm()
    
    return render(request, 'blog/post_form.html', {'form': form})


# === ÉTAPE 7: Utiliser user dans templates ===

# {{ user }} est TOUJOURS disponible dans les templates!

{% if user.is_authenticated %}
    <p>Bonjour {{ user.username }} !</p>
    <p>Email: {{ user.email }}</p>
    
    <a href="{% url 'logout' %}">Déconnexion</a>
    
    {% if user.is_staff %}
        <a href="{% url 'admin:index' %}">Administration</a>
    {% endif %}
{% else %}
    <p>Vous n'êtes pas connecté.</p>
    <a href="{% url 'login' %}">Connexion</a>
    <a href="{% url 'signup' %}">Inscription</a>
{% endif %}

# Afficher nom complet
<p>{{ user.get_full_name }}</p>  # "Jean Dupont"
<p>{{ user.get_short_name }}</p>  # "Jean"


# === ÉTAPE 8: Créer page d'inscription ===

# Django NE fournit PAS de vue d'inscription par défaut
# Il faut la créer nous-mêmes (simple!)

# accounts/forms.py (créer ce fichier)
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class SignUpForm(UserCreationForm):
    """Formulaire d'inscription personnalisé"""
    email = forms.EmailField(
        required=True,
        widget=forms.EmailInput(attrs={'class': 'form-control'})
    )
    first_name = forms.CharField(
        max_length=50,
        required=True,
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    last_name = forms.CharField(
        max_length=50,
        required=True,
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    
    class Meta:
        model = User
        fields = [
            'username',
            'email',
            'first_name',
            'last_name',
            'password1',  # Password
            'password2',  # Confirmation password
        ]
    
    def save(self, commit=True):
        """Override save pour ajouter email"""
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        if commit:
            user.save()
        return user


# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from .forms import SignUpForm

def signup(request):
    """View d'inscription"""
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            # Connecter automatiquement après inscription
            login(request, user)
            return redirect('blog:post_list')
    else:
        form = SignUpForm()
    
    return render(request, 'registration/signup.html', {'form': form})


# accounts/urls.py (créer ce fichier)
from django.urls import path
from . import views

urlpatterns = [
    path('signup/', views.signup, name='signup'),
]


# myproject/urls.py (ajouter)
urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('accounts/', include('accounts.urls')),  # <- Ajouter
    path('blog/', include('blog.urls')),
]


# templates/registration/signup.html
{% extends 'base.html' %}

{% block title %}Inscription{% endblock %}

{% block content %}
    <div class="container" style="max-width: 500px;">
        <h2>Inscription</h2>
        
        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            
            <button type="submit" class="btn btn-primary">
                S'inscrire
            </button>
        </form>
        
        <p class="mt-3">
            Déjà inscrit ? <a href="{% url 'login' %}">Se connecter</a>
        </p>
    </div>
{% endblock %}


# === Permissions avancées ===

# Vérifier permission spécifique

from django.contrib.auth.decorators import permission_required

@permission_required('blog.add_post')
def create_post(request):
    """Seulement si user a permission 'add_post'"""
    return render(request, 'create_post.html')

@permission_required('blog.change_post', raise_exception=True)
def edit_post(request, pk):
    """Avec raise_exception=True: erreur 403 au lieu de redirect"""
    post = get_object_or_404(Post, pk=pk)
    # ...
    return render(request, 'edit_post.html')


# Dans views (vérification manuelle)
if request.user.has_perm('blog.add_post'):
    # User peut ajouter posts
    pass

if request.user.has_perms(['blog.add_post', 'blog.delete_post']):
    # User a les 2 permissions
    pass


# Pour CBV:
from django.contrib.auth.mixins import PermissionRequiredMixin

class PostCreateView(PermissionRequiredMixin, CreateView):
    permission_required = 'blog.add_post'
    model = Post
    fields = ['title', 'content']


# Test personnalisé (user passes test)
from django.contrib.auth.decorators import user_passes_test

def is_author(user):
    """Vérifie si user est dans groupe Authors"""
    return user.groups.filter(name='Authors').exists()

@user_passes_test(is_author)
def author_dashboard(request):
    """Dashboard réservé aux auteurs"""
    return render(request, 'dashboard.html')


# Pour CBV:
from django.contrib.auth.mixins import UserPassesTestMixin

class PostUpdateView(UserPassesTestMixin, UpdateView):
    model = Post
    fields = ['title', 'content']
    
    def test_func(self):
        """Vérifier que c'est l'auteur du post"""
        post = self.get_object()
        return self.request.user == post.author


# === Groupes d'utilisateurs ===

from django.contrib.auth.models import Group

# Créer groupe
authors = Group.objects.create(name='Authors')
editors = Group.objects.create(name='Editors')

# Ajouter user à groupe
user.groups.add(authors)
user.groups.add(authors, editors)  # Plusieurs groupes

# Retirer user de groupe
user.groups.remove(authors)

# Vérifier appartenance
if user.groups.filter(name='Authors').exists():
    print("User est auteur")


# === Profil utilisateur (étendre User) ===

# User Django est limité (username, email, password)
# Pour ajouter des champs: créer Model UserProfile

# accounts/models.py
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class UserProfile(models.Model):
    """Profil utilisateur étendu"""
    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}"

# Créer profile automatiquement à la création user
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    """Signal: créer profile quand user créé"""
    if created:
        UserProfile.objects.create(user=instance)

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

# Utilisation:
user = User.objects.get(username='jean')
print(user.profile.bio)  # Accès au profil
print(user.profile.avatar.url)  # URL de l'avatar


# === Résumé Authentification ===

# 1. Django fournit système d'auth complet (User, login, logout)
# 2. Model User: username, email, password (hashé!), is_staff, etc.
# 3. create_user() pour créer users (hash password auto)
# 4. include('django.contrib.auth.urls') -> URLs d'auth gratuites
# 5. Créer templates dans registration/
# 6. @login_required = protéger une view
# 7. request.user = user connecté (toujours disponible)
# 8. {{ user }} dans templates
# 9. Créer SignUpForm pour inscription
# 10. UserProfile (OneToOne) pour étendre User
# 11. Permissions: has_perm(), @permission_required
# 12. Groupes: organiser users par rôle

# Django gère la sécurité:
# [OK] Passwords hashés (jamais en clair!)
# [OK] Protection CSRF automatique
# [OK] Session sécurisée
# [OK] Validation des formulaires
# -> Vous n'avez presque rien à faire!

# Prochaine étape: Déployer votre site Django en production!

# === Login / Logout ===

# views.py
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required

def login_view(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            # Rediriger vers page demandée ou home
            next_url = request.GET.get('next', 'home')
            return redirect(next_url)
        else:
            # Mauvais credentials
            messages.error(request, 'Username ou password incorrect')
    
    return render(request, 'accounts/login.html')

def logout_view(request):
    logout(request)
    return redirect('home')

# Template login
<form method="post">
    {% csrf_token %}
    <input type="text" name="username" placeholder="Username">
    <input type="password" name="password" placeholder="Password">
    <button type="submit">Se connecter</button>
</form>

# === Vues protégées ===

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    return render(request, 'accounts/profile.html')

@login_required(login_url='/login/')
def profile(request):
    return render(request, 'accounts/profile.html')

# CBV
from django.contrib.auth.mixins import LoginRequiredMixin

class ProfileView(LoginRequiredMixin, DetailView):
    login_url = '/login/'
    model = User
    template_name = 'profile.html'

# 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 %}

# === Permissions ===

from django.contrib.auth.decorators import permission_required

@permission_required('blog.add_post')
def create_post(request):
    # ...
    pass

@permission_required('blog.change_post', raise_exception=True)
def edit_post(request, pk):
    # ...
    pass

# Vérifier permission
if user.has_perm('blog.add_post'):
    # ...
    pass

if user.has_perms(['blog.add_post', 'blog.change_post']):
    # ...
    pass

# CBV
from django.contrib.auth.mixins import PermissionRequiredMixin

class PostCreateView(PermissionRequiredMixin, CreateView):
    permission_required = 'blog.add_post'
    model = Post
    fields = ['title', 'content']

# Test utilisateur personnalisé
from django.contrib.auth.decorators import user_passes_test

def is_author(user):
    return user.groups.filter(name='Authors').exists()

@user_passes_test(is_author)
def author_dashboard(request):
    # ...
    pass

# CBV
from django.contrib.auth.mixins import UserPassesTestMixin

class PostUpdateView(UserPassesTestMixin, UpdateView):
    model = Post
    fields = ['title', 'content']
    
    def test_func(self):
        post = self.get_object()
        return self.request.user == post.author

# === Groupes ===

from django.contrib.auth.models import Group

# Créer groupe
authors_group = Group.objects.create(name='Authors')

# Ajouter user à groupe
user.groups.add(authors_group)
user.groups.add(authors_group, editors_group)

# Retirer user de groupe
user.groups.remove(authors_group)

# Vérifier appartenance
if user.groups.filter(name='Authors').exists():
    # ...
    pass

# Permissions pour groupe
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get_for_model(Post)
permission = Permission.objects.get(
    codename='add_post',
    content_type=content_type
)
authors_group.permissions.add(permission)

# === User personnalisé ===

# accounts/models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    
    def __str__(self):
        return self.username

# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'

# IMPORTANT: Faire ça AVANT première migration!

# Utiliser dans models
from django.conf import settings

class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

# Ou
from django.contrib.auth import get_user_model

User = get_user_model()

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)

# === Formulaires d'authentification ===

from django.contrib.auth.forms import UserCreationForm, UserChangeForm

# Inscription
from django.contrib.auth.forms import UserCreationForm

def signup_view(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('home')
    else:
        form = UserCreationForm()
    
    return render(request, 'accounts/signup.html', {'form': form})

# Formulaire personnalisé
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm

class SignUpForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=50, required=True)
    last_name = forms.CharField(max_length=50, required=True)
    
    class Meta:
        model = User
        fields = ['username', 'email', 'first_name', 'last_name', 'password1', 'password2']
    
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        if commit:
            user.save()
        return user

# === URLs auth Django ===

# myproject/urls.py
from django.contrib.auth import views as auth_views

urlpatterns = [
    # Login
    path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
    
    # Logout
    path('logout/', auth_views.LogoutView.as_view(next_page='home'), name='logout'),
    
    # Change password
    path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
    
    # Reset password
    path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

# Ou inclure toutes les URLs auth
urlpatterns = [
    path('accounts/', include('django.contrib.auth.urls')),
]

# URLs incluses:
# accounts/login/
# accounts/logout/
# accounts/password_change/
# accounts/password_change/done/
# accounts/password_reset/
# accounts/password_reset/done/
# accounts/reset/<uidb64>/<token>/
# accounts/reset/done/

# Settings pour auth
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

# === Signaux utilisateur ===

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_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()


[OK] FICHIERS STATIQUES & MÉDIA


# === Fichiers statiques (CSS, JS, images du site) ===

# settings.py
STATIC_URL = '/static/'                           # URL d'accès
STATIC_ROOT = BASE_DIR / 'staticfiles'            # Collecte (production)
STATICFILES_DIRS = [                              # Dossiers additionnels
    BASE_DIR / 'static',
]

# Structure
myproject/
├── static/                                       # Statiques globaux
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       └── logo.png
├── blog/
│   └── static/
│       └── blog/                                 # Namespace
│           ├── css/
│           └── js/
└── staticfiles/                                  # Collectés (production)

# Dans templates
{% load static %}

<link rel="stylesheet" href="{% static 'css/style.css' %}">
<script src="{% static 'js/main.js' %}"></script>
<img src="{% static 'images/logo.png' %}" alt="Logo">

# Statiques d'une app
<link rel="stylesheet" href="{% static 'blog/css/blog.css' %}">

# Collecter fichiers statiques (production)
python manage.py collectstatic

# Avec confirmation
python manage.py collectstatic --noinput

# Clear avant de collecter
python manage.py collectstatic --clear

# === Fichiers média (uploads utilisateurs) ===

# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Structure
myproject/
└── media/
    ├── avatars/
    ├── documents/
    └── images/

# URLs (dev seulement!)
# myproject/urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ...
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# Model avec upload
class Post(models.Model):
    title = models.CharField(max_length=200)
    image = models.ImageField(upload_to='posts/')  # media/posts/
    document = models.FileField(upload_to='documents/%Y/%m/%d/')  # Organisé par date

# Upload avec fonction
def user_avatar_path(instance, filename):
    # Fichier uploadé à MEDIA_ROOT/avatars/user_<id>/<filename>
    return f'avatars/user_{instance.id}/{filename}'

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to=user_avatar_path)

# Dans template
{% if post.image %}
    <img src="{{ post.image.url }}" alt="{{ post.title }}">
{% endif %}

<a href="{{ post.document.url }}">Télécharger</a>

# Propriétés fichier
{{ post.image.url }}                              # URL
{{ post.image.path }}                             # Chemin système
{{ post.image.size }}                             # Taille (bytes)
{{ post.image.name }}                             # Nom fichier
{{ post.image.width }}                            # Largeur (image)
{{ post.image.height }}                           # Hauteur (image)

# === Upload dans formulaire ===

# Form
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'image']

# View
def create_post(request):
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES)  # IMPORTANT: request.FILES
        if form.is_valid():
            form.save()
            return redirect('success')
    else:
        form = PostForm()
    return render(request, 'form.html', {'form': form})

# Template
<form method="post" enctype="multipart/form-data">  {# IMPORTANT: enctype #}
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Envoyer</button>
</form>

# === Validation fichiers ===

from django.core.validators import FileExtensionValidator

class Post(models.Model):
    document = models.FileField(
        upload_to='documents/',
        validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx'])]
    )

# Validation personnalisée
from django.core.exceptions import ValidationError

def validate_file_size(value):
    filesize = value.size
    if filesize > 5242880:  # 5MB
        raise ValidationError("Fichier trop volumineux (max 5MB)")

class Post(models.Model):
    image = models.ImageField(
        upload_to='images/',
        validators=[validate_file_size]
    )

# === Supprimer anciens fichiers ===

from django.db.models.signals import pre_save, post_delete
from django.dispatch import receiver

@receiver(pre_save, sender=Post)
def delete_old_file(sender, instance, **kwargs):
    if instance.pk:
        try:
            old_file = Post.objects.get(pk=instance.pk).image
        except Post.DoesNotExist:
            return
        
        new_file = instance.image
        if old_file and old_file != new_file:
            if os.path.isfile(old_file.path):
                os.remove(old_file.path)

@receiver(post_delete, sender=Post)
def delete_file_on_delete(sender, instance, **kwargs):
    if instance.image:
        if os.path.isfile(instance.image.path):
            os.remove(instance.image.path)


[OK] MESSAGES


# Système de messages flash

from django.contrib import messages

def my_view(request):
    # Ajouter message
    messages.success(request, 'Opération réussie!')
    messages.info(request, 'Information importante')
    messages.warning(request, 'Attention!')
    messages.error(request, 'Erreur!')
    messages.debug(request, 'Message de debug')
    
    return redirect('home')

# Dans template (base.html)
{% if messages %}
    <div class="messages">
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }}">
                {{ message }}
            </div>
        {% endfor %}
    </div>
{% endif %}

# Avec Bootstrap
{% if messages %}
    {% for message in messages %}
        <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
            {{ message }}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    {% endfor %}
{% endif %}

# Tags personnalisés
# settings.py
from django.contrib.messages import constants as messages

MESSAGE_TAGS = {
    messages.DEBUG: 'secondary',
    messages.INFO: 'info',
    messages.SUCCESS: 'success',
    messages.WARNING: 'warning',
    messages.ERROR: 'danger',
}

# Message avec extra_tags
messages.success(request, 'Profil mis à jour', extra_tags='profile')


[OK] SESSIONS


# Django gère les sessions automatiquement

def my_view(request):
    # Lire session
    cart = request.session.get('cart', [])
    
    # Écrire session
    request.session['cart'] = [1, 2, 3]
    request.session['user_preferences'] = {
        'theme': 'dark',
        'language': 'fr'
    }
    
    # Supprimer clé
    if 'cart' in request.session:
        del request.session['cart']
    
    # Vider session
    request.session.flush()
    
    # Set expiration
    request.session.set_expiry(300)               # 5 minutes
    request.session.set_expiry(0)                 # Expire à fermeture navigateur
    
    # Test existence
    if 'cart' in request.session:
        # ...
        pass

# Settings
# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db'  # Base de données (défaut)
# SESSION_ENGINE = 'django.contrib.sessions.backends.cache'  # Cache
# SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'  # Cache + BDD
# SESSION_ENGINE = 'django.contrib.sessions.backends.file'  # Fichiers
# SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'  # Cookies

SESSION_COOKIE_AGE = 1209600                      # 2 semaines (secondes)
SESSION_COOKIE_NAME = 'sessionid'
SESSION_SAVE_EVERY_REQUEST = False                # Sauver à chaque requête
SESSION_EXPIRE_AT_BROWSER_CLOSE = False


[OK] EMAILS


# === Configuration email ===

# settings.py

# Console (développement)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# SMTP (production)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
DEFAULT_FROM_EMAIL = 'noreply@example.com'
SERVER_EMAIL = 'server@example.com'

# === Envoyer email ===

from django.core.mail import send_mail

# Email simple
send_mail(
    subject='Sujet du mail',
    message='Contenu du message',
    from_email='from@example.com',
    recipient_list=['to@example.com'],
    fail_silently=False,
)

# Email avec HTML
from django.core.mail import EmailMultiAlternatives

subject = 'Bienvenue'
text_content = 'Bienvenue sur notre site!'
html_content = '<h1>Bienvenue sur notre site!</h1>'
from_email = 'from@example.com'
to = ['to@example.com']

msg = EmailMultiAlternatives(subject, text_content, from_email, to)
msg.attach_alternative(html_content, "text/html")
msg.send()

# Email avec pièce jointe
from django.core.mail import EmailMessage

email = EmailMessage(
    subject='Document',
    body='Veuillez trouver le document en pièce jointe',
    from_email='from@example.com',
    to=['to@example.com'],
)
email.attach_file('/path/to/file.pdf')
email.send()

# Email avec template
from django.core.mail import send_mail
from django.template.loader import render_to_string

html_message = render_to_string('emails/welcome.html', {
    'user': user,
    'activation_link': activation_link,
})

send_mail(
    subject='Bienvenue',
    message='',  # Texte brut (optionnel)
    from_email='from@example.com',
    recipient_list=[user.email],
    html_message=html_message,
)

# Envoyer à plusieurs destinataires
from django.core.mail import send_mass_mail

message1 = ('Sujet 1', 'Message 1', 'from@example.com', ['to1@example.com'])
message2 = ('Sujet 2', 'Message 2', 'from@example.com', ['to2@example.com'])

send_mass_mail((message1, message2), fail_silently=False)

# === Template email ===

<!-- templates/emails/welcome.html -->
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; }
        .header { background-color: #4CAF50; color: white; padding: 20px; }
        .content { padding: 20px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Bienvenue {{ user.username }}!</h1>
    </div>
    <div class="content">
        <p>Merci de vous être inscrit.</p>
        <p>
            <a href="{{ activation_link }}">Activer votre compte</a>
        </p>
    </div>
</body>
</html>


[OK] PAGINATION


# === Pagination manuelle ===

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def post_list(request):
    post_list = Post.objects.all().order_by('-created_at')
    
    paginator = Paginator(post_list, 10)          # 10 posts par page
    page_number = request.GET.get('page')
    
    try:
        posts = paginator.page(page_number)
    except PageNotAnInteger:
        # Si page n'est pas un entier, afficher première page
        posts = paginator.page(1)
    except EmptyPage:
        # Si page hors limites, afficher dernière page
        posts = paginator.page(paginator.num_pages)
    
    return render(request, 'blog/post_list.html', {'posts': posts})

# Template
{% for post in posts %}
    <h2>{{ post.title }}</h2>
{% endfor %}

<!-- Pagination -->
<div class="pagination">
    {% if posts.has_previous %}
        <a href="?page=1">« première</a>
        <a href="?page={{ posts.previous_page_number }}">précédente</a>
    {% endif %}
    
    <span>
        Page {{ posts.number }} sur {{ posts.paginator.num_pages }}
    </span>
    
    {% if posts.has_next %}
        <a href="?page={{ posts.next_page_number }}">suivante</a>
        <a href="?page={{ posts.paginator.num_pages }}">dernière »</a>
    {% endif %}
</div>

# Pagination avec numéros de pages
<div class="pagination">
    {% for num in posts.paginator.page_range %}
        {% if posts.number == num %}
            <span class="current">{{ num }}</span>
        {% else %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
</div>

# === Pagination avec ListView ===

from django.views.generic import ListView

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10
    ordering = ['-created_at']

# Template (même chose)

# === Propriétés pagination ===

posts.number                                      # Numéro page actuelle
posts.paginator.num_pages                         # Nombre total pages
posts.paginator.count                             # Nombre total objets
posts.has_previous()                              # Booléen
posts.has_next()                                  # Booléen
posts.previous_page_number()                      # Numéro page précédente
posts.next_page_number()                          # Numéro page suivante
posts.start_index()                               # Index premier objet
posts.end_index()                                 # Index dernier objet


[OK] SIGNALS (SIGNAUX)


# Les signals permettent de déclencher des actions automatiquement

from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from django.dispatch import receiver
from django.contrib.auth.models import User

# === Types de signals ===

# Models:
# pre_init, post_init
# pre_save, post_save
# pre_delete, post_delete
# m2m_changed

# Requests:
# request_started, request_finished

# Database:
# connection_created

# === Utilisation basique ===

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

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()

# Enregistrer signal manuellement
from django.db.models.signals import post_save

def create_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

post_save.connect(create_profile, sender=User)

# === Pre_save vs Post_save ===

@receiver(pre_save, sender=Post)
def pre_save_handler(sender, instance, **kwargs):
    # Avant sauvegarde en BDD
    # Modifier instance ici
    instance.slug = instance.title.lower().replace(' ', '-')

@receiver(post_save, sender=Post)
def post_save_handler(sender, instance, created, **kwargs):
    # Après sauvegarde en BDD
    # created = True si nouvel objet
    if created:
        # Nouvel objet créé
        send_notification(instance)
    else:
        # Objet existant modifié
        update_cache(instance)

# === Delete signals ===

@receiver(pre_delete, sender=Post)
def pre_delete_handler(sender, instance, **kwargs):
    # Avant suppression
    # Supprimer fichiers associés
    if instance.image:
        instance.image.delete()

@receiver(post_delete, sender=Post)
def post_delete_handler(sender, instance, **kwargs):
    # Après suppression
    log_deletion(instance)

# === M2M changed ===

from django.db.models.signals import m2m_changed

@receiver(m2m_changed, sender=Post.tags.through)
def tags_changed(sender, instance, action, **kwargs):
    if action == "post_add":
        # Tags ajoutés
        print(f"Tags ajoutés à {instance}")
    elif action == "post_remove":
        # Tags retirés
        print(f"Tags retirés de {instance}")
    elif action == "post_clear":
        # Tous les tags retirés
        print(f"Tous les tags retirés de {instance}")

# === Désactiver signals temporairement ===

from django.db.models.signals import post_save

# Déconnecter
post_save.disconnect(create_profile, sender=User)

# ... faire des opérations ...

# Reconnecter
post_save.connect(create_profile, sender=User)

# Avec context manager
from contextlib import contextmanager

@contextmanager
def disable_signals():
    post_save.disconnect(create_profile, sender=User)
    yield
    post_save.connect(create_profile, sender=User)

# Utilisation
with disable_signals():
    user = User.objects.create(username='test')

# === Enregistrer dans apps.py ===

# blog/apps.py
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'blog'
    
    def ready(self):
        import blog.signals  # Importer signals


[OK] MIDDLEWARE


# Middleware = composant qui traite requêtes/réponses

# === Créer middleware ===

# myproject/middleware.py
class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        # Code exécuté AVANT la vue
        print(f"Requête: {request.path}")
        
        response = self.get_response(request)
        
        # Code exécuté APRÈS la vue
        print(f"Réponse: {response.status_code}")
        
        return response
    
    def process_exception(self, request, exception):
        # Traiter les exceptions
        print(f"Exception: {exception}")
        return None

# Middleware fonctionnel (simple)
def simple_middleware(get_response):
    def middleware(request):
        # Avant vue
        print(f"Path: {request.path}")
        
        response = get_response(request)
        
        # Après vue
        return response
    
    return middleware

# === Enregistrer middleware ===

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    # Votre middleware
    'myproject.middleware.SimpleMiddleware',
]

# === Exemples middleware utiles ===

# Mesurer temps de réponse
import time

class ResponseTimeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        start_time = time.time()
        response = self.get_response(request)
        duration = time.time() - start_time
        
        response['X-Response-Time'] = f"{duration:.3f}s"
        return response

# Forcer HTTPS
class ForceHTTPSMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        if not request.is_secure() and not request.META.get('HTTP_X_FORWARDED_PROTO') == 'https':
            url = request.build_absolute_uri(request.get_full_path())
            secure_url = url.replace('http://', 'https://')
            return HttpResponsePermanentRedirect(secure_url)
        
        return self.get_response(request)

# Bloquer IP
class BlockIPMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.blocked_ips = ['192.168.1.100']
    
    def __call__(self, request):
        ip = request.META.get('REMOTE_ADDR')
        if ip in self.blocked_ips:
            return HttpResponseForbidden('Accès refusé')
        
        return self.get_response(request)


[OK] CACHE


# === Configuration cache ===

# settings.py

# Memcached
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

# Redis
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
    }
}

# Database
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
        'LOCATION': 'my_cache_table',
    }
}
# Créer table: python manage.py createcachetable

# Filesystem
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/tmp/django_cache',
    }
}

# Local memory (dev)
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

# Dummy (désactivé)
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    }
}

# === Utiliser cache ===

from django.core.cache import cache

# Set
cache.set('my_key', 'my_value', timeout=300)      # 5 minutes
cache.set('user_data', {'name': 'John'}, timeout=60 * 60)  # 1 heure

# Get
value = cache.get('my_key')                       # None si absent
value = cache.get('my_key', 'default_value')      # Avec défaut

# Add (seulement si n'existe pas)
cache.add('my_key', 'value')

# Delete
cache.delete('my_key')

# Get or set
value = cache.get_or_set('my_key', lambda: expensive_computation(), timeout=300)

# Get many
data = cache.get_many(['key1', 'key2', 'key3'])
# {'key1': 'value1', 'key2': 'value2'}

# Set many
cache.set_many({'key1': 'value1', 'key2': 'value2'}, timeout=300)

# Delete many
cache.delete_many(['key1', 'key2'])

# Clear all
cache.clear()

# Increment / Decrement
cache.set('counter', 0)
cache.incr('counter')                             # +1
cache.incr('counter', 5)                          # +5
cache.decr('counter')                             # -1

# === Cache per-view ===

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # Cache 15 minutes
def my_view(request):
    # ...
    return render(request, 'template.html')

# Avec paramètre
@cache_page(60 * 15, key_prefix="site1")
def my_view(request):
    # ...
    pass

# CBV
from django.utils.decorators import method_decorator

@method_decorator(cache_page(60 * 15), name='dispatch')
class MyView(View):
    def get(self, request):
        # ...
        pass

# === Template fragment caching ===

{% load cache %}

{% cache 500 sidebar %}
    <!-- Contenu coûteux à générer -->
    {% for item in items %}
        <p>{{ item }}</p>
    {% endfor %}
{% endcache %}

# Avec variables
{% cache 500 sidebar request.user.username %}
    <!-- Cache différent par user -->
{% endcache %}

# === Low-level cache API ===

from django.core.cache import cache

def get_posts():
    posts = cache.get('all_posts')
    if posts is None:
        posts = list(Post.objects.all())
        cache.set('all_posts', posts, 60 * 15)
    return posts

# Invalider cache
def create_post(request):
    # ... créer post ...
    cache.delete('all_posts')                     # Invalider cache
    return redirect('post_list')


[OK] TESTS


# === Types de tests ===

# Tests unitaires - TestCase
# Tests d'intégration - TransactionTestCase
# Tests API - APITestCase (DRF)
# Tests fonctionnels - LiveServerTestCase

# === Structure tests ===

# blog/tests.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Post

class PostModelTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Données pour tous les tests de la classe
        cls.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        cls.post = Post.objects.create(
            title='Test Post',
            content='Test content',
            author=cls.user
        )
    
    def setUp(self):
        # Exécuté avant CHAQUE test
        pass
    
    def test_post_creation(self):
        """Test création post"""
        self.assertEqual(self.post.title, 'Test Post')
        self.assertEqual(self.post.author, self.user)
        self.assertIsNotNone(self.post.created_at)
    
    def test_post_str(self):
        """Test __str__"""
        self.assertEqual(str(self.post), 'Test Post')
    
    def test_post_absolute_url(self):
        """Test get_absolute_url"""
        url = self.post.get_absolute_url()
        self.assertEqual(url, f'/post/{self.post.pk}/')
    
    def tearDown(self):
        # Exécuté après CHAQUE test
        pass

# === Test des vues ===

class PostViewsTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.post = Post.objects.create(
            title='Test Post',
            content='Content',
            author=self.user
        )
    
    def test_post_list_view(self):
        """Test vue liste"""
        response = self.client.get(reverse('post_list'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'blog/post_list.html')
        self.assertContains(response, 'Test Post')
        self.assertIn('posts', response.context)
    
    def test_post_detail_view(self):
        """Test vue détail"""
        response = self.client.get(
            reverse('post_detail', kwargs={'pk': self.post.pk})
        )
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'blog/post_detail.html')
        self.assertEqual(response.context['post'], self.post)
    
    def test_post_detail_not_found(self):
        """Test 404"""
        response = self.client.get(
            reverse('post_detail', kwargs={'pk': 9999})
        )
        self.assertEqual(response.status_code, 404)
    
    def test_post_create_view_login_required(self):
        """Test login requis"""
        response = self.client.get(reverse('post_create'))
        self.assertEqual(response.status_code, 302)  # Redirect
        self.assertRedirects(response, '/login/?next=/post/create/')
    
    def test_post_create_view_authenticated(self):
        """Test création avec auth"""
        self.client.login(username='testuser', password='testpass123')
        response = self.client.post(reverse('post_create'), {
            'title': 'New Post',
            'content': 'New content',
        })
        self.assertEqual(response.status_code, 302)
        self.assertTrue(Post.objects.filter(title='New Post').exists())

# === Test des formulaires ===

from .forms import PostForm

class PostFormTest(TestCase):
    def test_post_form_valid(self):
        """Test formulaire valide"""
        form = PostForm(data={
            'title': 'Test Title',
            'content': 'Test content'
        })
        self.assertTrue(form.is_valid())
    
    def test_post_form_invalid(self):
        """Test formulaire invalide"""
        form = PostForm(data={
            'title': '',  # Vide
            'content': 'Test content'
        })
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)
    
    def test_post_form_validation(self):
        """Test validation personnalisée"""
        form = PostForm(data={
            'title': 'ab',  # Trop court
            'content': 'Test content'
        })
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)

# === Assertions disponibles ===

self.assertEqual(a, b)
self.assertNotEqual(a, b)
self.assertTrue(x)
self.assertFalse(x)
self.assertIs(a, b)
self.assertIsNot(a, b)
self.assertIsNone(x)
self.assertIsNotNone(x)
self.assertIn(a, b)
self.assertNotIn(a, b)
self.assertIsInstance(a, b)
self.assertNotIsInstance(a, b)

# Nombres
self.assertGreater(a, b)
self.assertGreaterEqual(a, b)
self.assertLess(a, b)
self.assertLessEqual(a, b)
self.assertAlmostEqual(a, b, places=7)

# Collections
self.assertListEqual(a, b)
self.assertTupleEqual(a, b)
self.assertSetEqual(a, b)
self.assertDictEqual(a, b)

# Strings
self.assertRegex(text, regex)
self.assertNotRegex(text, regex)

# Django specifics
self.assertTemplateUsed(response, 'template.html')
self.assertTemplateNotUsed(response, 'template.html')
self.assertRedirects(response, '/url/')
self.assertContains(response, 'text')
self.assertNotContains(response, 'text')
self.assertFormError(response, 'form', 'field', 'error')
self.assertQuerysetEqual(qs1, qs2)

# Exceptions
with self.assertRaises(ValueError):
    # code qui lève ValueError
    pass

# === Exécuter tests ===

# Tous les tests
python manage.py test

# App spécifique
python manage.py test blog

# Classe spécifique
python manage.py test blog.tests.PostModelTest

# Méthode spécifique
python manage.py test blog.tests.PostModelTest.test_post_creation

# Avec verbosité
python manage.py test --verbosity=2

# Garder base de données
python manage.py test --keepdb

# Parallèle
python manage.py test --parallel

# === Coverage ===

# Installer coverage
pip install coverage

# Exécuter avec coverage
coverage run --source='.' manage.py test

# Rapport
coverage report

# Rapport HTML
coverage html
# Ouvrir htmlcov/index.html

# === Mocking ===

from unittest.mock import patch, Mock

class EmailTest(TestCase):
    @patch('django.core.mail.send_mail')
    def test_send_email(self, mock_send_mail):
        # Appeler fonction qui envoie email
        send_welcome_email('test@example.com')
        
        # Vérifier appel
        mock_send_mail.assert_called_once()
        args, kwargs = mock_send_mail.call_args
        self.assertEqual(kwargs['recipient_list'], ['test@example.com'])

# === Fixtures ===

# blog/fixtures/test_data.json
[
  {
    "model": "blog.post",
    "pk": 1,
    "fields": {
      "title": "Test Post",
      "content": "Content",
      "author": 1
    }
  }
]

# Utiliser fixture
class PostTest(TestCase):
    fixtures = ['test_data.json']
    
    def test_something(self):
        post = Post.objects.get(pk=1)
        # ...

# Créer fixture depuis BDD
python manage.py dumpdata blog.Post --indent=2 > blog/fixtures/posts.json


[OK] COMMANDES MANAGEMENT


# === Commandes intégrées ===

# Créer projet/app
django-admin startproject myproject
python manage.py startapp myapp

# Base de données
python manage.py makemigrations
python manage.py migrate
python manage.py sqlmigrate blog 0001
python manage.py showmigrations
python manage.py dbshell

# Users
python manage.py createsuperuser
python manage.py changepassword username

# Serveur
python manage.py runserver
python manage.py runserver 8080
python manage.py runserver 0.0.0.0:8000

# Static files
python manage.py collectstatic
python manage.py findstatic css/style.css

# Shell
python manage.py shell
python manage.py shell_plus                       # Nécessite django-extensions

# Tests
python manage.py test
python manage.py test blog

# Autres
python manage.py check                            # Vérifier projet
python manage.py check --deploy                   # Vérifier prod
python manage.py flush                            # Vider BDD
python manage.py dumpdata                         # Exporter données
python manage.py loaddata fixture.json            # Importer données
python manage.py inspectdb                        # Générer models depuis BDD
python manage.py clearsessions                    # Nettoyer sessions
python manage.py createcachetable                 # Créer table cache

# === Créer commande personnalisée ===

# blog/management/commands/populate_posts.py
from django.core.management.base import BaseCommand
from blog.models import Post
from django.contrib.auth.models import User

class Command(BaseCommand):
    help = 'Populate database with sample posts'
    
    def add_arguments(self, parser):
        parser.add_argument(
            'count',
            type=int,
            help='Number of posts to create'
        )
        parser.add_argument(
            '--author',
            type=str,
            default='admin',
            help='Username of author'
        )
    
    def handle(self, *args, **options):
        count = options['count']
        author_username = options['author']
        
        try:
            author = User.objects.get(username=author_username)
        except User.DoesNotExist:
            self.stdout.write(
                self.style.ERROR(f'User {author_username} does not exist')
            )
            return
        
        created_count = 0
        for i in range(count):
            post = Post.objects.create(
                title=f'Post {i+1}',
                content=f'Content for post {i+1}',
                author=author
            )
            created_count += 1
            
            # Progress
            if (i + 1) % 10 == 0:
                self.stdout.write(f'Created {i+1} posts...')
        
        self.stdout.write(
            self.style.SUCCESS(f'Successfully created {created_count} posts')
        )

# Exécuter commande
python manage.py populate_posts 100
python manage.py populate_posts 50 --author=john

# === Styles de sortie ===

self.stdout.write(self.style.SUCCESS('Success message'))
self.stdout.write(self.style.ERROR('Error message'))
self.stdout.write(self.style.WARNING('Warning message'))
self.stdout.write(self.style.NOTICE('Notice message'))
self.stdout.write(self.style.HTTP_INFO('Info message'))


[OK] DJANGO REST FRAMEWORK (API)


# Django REST Framework pour créer APIs REST

# === Installation ===

pip install djangorestframework

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

# === Serializers ===

# blog/serializers.py
from rest_framework import serializers
from .models import Post, Comment

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'post', 'author', 'text', 'created_at']
        read_only_fields = ['created_at']

class PostSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.username', read_only=True)
    comments = CommentSerializer(many=True, read_only=True)
    comments_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'slug', 'content', 'author', 'author_name', 
                  'published', 'created_at', 'updated_at', 'comments', 'comments_count']
        read_only_fields = ['created_at', 'updated_at', 'author']
    
    def get_comments_count(self, obj):
        return obj.comments.count()
    
    def validate_title(self, value):
        """Validation personnalisée"""
        if len(value) < 10:
            raise serializers.ValidationError('Title too short')
        return value
    
    def create(self, validated_data):
        """Override create"""
        validated_data['author'] = self.context['request'].user
        return super().create(validated_data)

# Serializer simple (sans model)
class ContactSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=100)
    email = serializers.EmailField()
    message = serializers.CharField()

# === Views API ===

from rest_framework import viewsets, generics, status
from rest_framework.decorators import api_view, action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny

# APIView (bas niveau)
from rest_framework.views import APIView

class PostListView(APIView):
    def get(self, request):
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# Generic Views
class PostListCreateView(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]

class PostDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

# ViewSet (le plus puissant)
class PostViewSet(viewsets.ModelViewSet):
    """
    ViewSet qui fournit automatiquement:
    - list (GET /posts/)
    - create (POST /posts/)
    - retrieve (GET /posts/{id}/)
    - update (PUT /posts/{id}/)
    - partial_update (PATCH /posts/{id}/)
    - destroy (DELETE /posts/{id}/)
    """
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]
    filterset_fields = ['published', 'author']
    search_fields = ['title', 'content']
    ordering_fields = ['created_at', 'title']
    
    def get_queryset(self):
        """Filtrer queryset"""
        queryset = Post.objects.all()
        published = self.request.query_params.get('published')
        if published is not None:
            queryset = queryset.filter(published=published)
        return queryset
    
    def perform_create(self, serializer):
        """Override create pour ajouter author"""
        serializer.save(author=self.request.user)
    
    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        """Action personnalisée: POST /posts/{id}/publish/"""
        post = self.get_object()
        post.published = True
        post.save()
        serializer = self.get_serializer(post)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def recent(self, request):
        """Action personnalisée: GET /posts/recent/"""
        recent_posts = Post.objects.order_by('-created_at')[:10]
        serializer = self.get_serializer(recent_posts, many=True)
        return Response(serializer.data)

# ReadOnly ViewSet
class PostReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Post.objects.filter(published=True)
    serializer_class = PostSerializer

# === URLs API ===

# blog/urls.py
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
from django.urls import path, include
from . import views

# Avec Router (ViewSets)
router = DefaultRouter()
router.register(r'posts', views.PostViewSet)
router.register(r'comments', views.CommentViewSet)

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

# URLs générées automatiquement:
# GET    /posts/                  -> list
# POST   /posts/                  -> create
# GET    /posts/{id}/             -> retrieve
# PUT    /posts/{id}/             -> update
# PATCH  /posts/{id}/             -> partial_update
# DELETE /posts/{id}/             -> destroy
# POST   /posts/{id}/publish/     -> action personnalisée
# GET    /posts/recent/           -> action personnalisée

# Sans Router (Generic Views)
urlpatterns = [
    path('posts/', views.PostListCreateView.as_view(), name='post-list'),
    path('posts/<int:pk>/', views.PostDetailView.as_view(), name='post-detail'),
]

# Function-based views
@api_view(['GET', 'POST'])
def post_list(request):
    if request.method == 'GET':
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)
    
    elif request.method == 'POST':
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(['GET', 'PUT', 'DELETE'])
def post_detail(request, pk):
    try:
        post = Post.objects.get(pk=pk)
    except Post.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)
    
    if request.method == 'GET':
        serializer = PostSerializer(post)
        return Response(serializer.data)
    
    elif request.method == 'PUT':
        serializer = PostSerializer(post, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    elif request.method == 'DELETE':
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

# === Authentication ===

# Token Authentication
# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework.authtoken',
]

# Créer tables
python manage.py migrate

# Obtenir token
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)
print(token.key)

# Ou automatiquement à la création user
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

# Endpoint pour obtenir token
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    path('api-token-auth/', obtain_auth_token),
]

# Utiliser token
# Header: Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

# === Permissions ===

from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # Read permissions (GET, HEAD, OPTIONS)
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # Write permissions (POST, PUT, DELETE)
        return obj.author == request.user

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthorOrReadOnly]

# Permissions disponibles:
# AllowAny - Tout le monde
# IsAuthenticated - Utilisateurs authentifiés
# IsAdminUser - Staff seulement
# IsAuthenticatedOrReadOnly - Lecture publique, écriture auth

# === Filtering & Search ===

pip install django-filter

# settings.py
INSTALLED_APPS = [
    ...
    'django_filters',
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
}

# ViewSet
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    filterset_fields = ['published', 'author']     # ?published=true
    search_fields = ['title', 'content']           # ?search=django
    ordering_fields = ['created_at', 'title']      # ?ordering=-created_at

# Filterset personnalisé
import django_filters

class PostFilter(django_filters.FilterSet):
    title = django_filters.CharFilter(lookup_expr='icontains')
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    
    class Meta:
        model = Post
        fields = ['published', 'author']

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    filterset_class = PostFilter

# === Pagination ===

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

# Pagination personnalisée
from rest_framework.pagination import PageNumberPagination

class CustomPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    pagination_class = CustomPagination

# Response paginée:
# {
#   "count": 100,
#   "next": "http://api.example.com/posts/?page=2",
#   "previous": null,
#   "results": [...]
# }

# === Throttling (rate limiting) ===

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}


[OK] CELERY (TÂCHES ASYNCHRONES)


# Celery pour tâches en arrière-plan

# === Installation ===

pip install celery redis

# === Configuration ===

# myproject/celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

# myproject/__init__.py
from .celery import app as celery_app

__all__ = ('celery_app',)

# settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Paris'

# === Créer tâche ===

# blog/tasks.py
from celery import shared_task
from django.core.mail import send_mail
from .models import Post

@shared_task
def send_email_notification(post_id):
    """Envoyer notification email"""
    post = Post.objects.get(id=post_id)
    send_mail(
        subject=f'New post: {post.title}',
        message=post.content,
        from_email='noreply@example.com',
        recipient_list=['admin@example.com'],
    )
    return f'Email sent for post {post_id}'

@shared_task
def process_image(image_path):
    """Traiter image"""
    # Traitement long...
    import time
    time.sleep(10)
    return f'Image {image_path} processed'

@shared_task
def cleanup_old_posts():
    """Nettoyer vieux posts"""
    from datetime import datetime, timedelta
    threshold = datetime.now() - timedelta(days=365)
    deleted = Post.objects.filter(created_at__lt=threshold).delete()
    return f'Deleted {deleted[0]} old posts'

# === Exécuter tâche ===

from blog.tasks import send_email_notification, process_image

# Exécuter immédiatement (asynchrone)
send_email_notification.delay(post_id=1)

# Avec paramètres
process_image.delay('/path/to/image.jpg')

# Exécuter avec délai
send_email_notification.apply_async(args=[1], countdown=60)  # 60 secondes

# Exécuter à une date précise
from datetime import datetime, timedelta
eta = datetime.now() + timedelta(hours=1)
send_email_notification.apply_async(args=[1], eta=eta)

# === Tâches périodiques ===

pip install django-celery-beat

# settings.py
INSTALLED_APPS = [
    ...
    'django_celery_beat',
]

python manage.py migrate django_celery_beat

# myproject/celery.py
from celery.schedules import crontab

app.conf.beat_schedule = {
    'cleanup-every-night': {
        'task': 'blog.tasks.cleanup_old_posts',
        'schedule': crontab(hour=2, minute=0),    # 2h du matin
    },
    'send-report-weekly': {
        'task': 'blog.tasks.send_weekly_report',
        'schedule': crontab(day_of_week=1, hour=9, minute=0),  # Lundi 9h
    },
    'update-cache-every-5-minutes': {
        'task': 'blog.tasks.update_cache',
        'schedule': 300.0,                        # 300 secondes
    },
}

# === Lancer Celery ===

# Worker
celery -A myproject worker -l info

# Beat (scheduler pour tâches périodiques)
celery -A myproject beat -l info

# Les deux en même temps (dev seulement)
celery -A myproject worker --beat -l info

# Avec concurrence
celery -A myproject worker -l info --concurrency=4

# === Monitoring ===

# Flower (web UI)
pip install flower
celery -A myproject flower

# Accès: http://localhost:5555


[OK] WEBSOCKETS (CHANNELS)


# Django Channels pour WebSockets et async

# === Installation ===

pip install channels channels-redis

# settings.py
INSTALLED_APPS = [
    'channels',
    ...
]

ASGI_APPLICATION = 'myproject.asgi.application'

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

# === Configuration ASGI ===

# myproject/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

django_asgi_app = get_asgi_application()

from chat.routing import websocket_urlpatterns

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            URLRouter(websocket_urlpatterns)
        )
    ),
})

# === Consumer ===

# chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = f'chat_{self.room_name}'
        
        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        
        await self.accept()
    
    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )
    
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        
        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )
    
    async def chat_message(self, event):
        message = event['message']
        
        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message
        }))

# === Routing ===

# chat/routing.py
from django.urls import path
from . import consumers

websocket_urlpatterns = [
    path('ws/chat/<str:room_name>/', consumers.ChatConsumer.as_asgi()),
]

# === Template WebSocket ===

<!-- chat/room.html -->
<script>
    const roomName = "{{ room_name }}";
    const chatSocket = new WebSocket(
        'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
    );
    
    chatSocket.onmessage = function(e) {
        const data = JSON.parse(e.data);
        document.querySelector('#chat-log').value += (data.message + '\n');
    };
    
    chatSocket.onclose = function(e) {
        console.error('Chat socket closed unexpectedly');
    };
    
    document.querySelector('#chat-message-submit').onclick = function(e) {
        const messageInputDom = document.querySelector('#chat-message-input');
        const message = messageInputDom.value;
        chatSocket.send(JSON.stringify({
            'message': message
        }));
        messageInputDom.value = '';
    };
</script>


[OK] SÉCURITÉ


# === Settings de sécurité (production) ===

# settings.py

# Secret key (JAMAIS commiter!)
SECRET_KEY = os.environ.get('SECRET_KEY')

# Debug (False en production!)
DEBUG = False

# Hosts autorisés
ALLOWED_HOSTS = ['monsite.com', 'www.monsite.com']

# HTTPS
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# HSTS
SECURE_HSTS_SECONDS = 31536000                    # 1 an
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# Content Security
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'

# CSRF
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'

# Session
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_AGE = 3600                         # 1 heure

# === Protection CSRF ===

# Dans formulaires (automatique avec Django forms)
<form method="post">
    {% csrf_token %}
    ...
</form>

# AJAX avec CSRF
// Obtenir token
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

const csrftoken = getCookie('csrftoken');

// Fetch avec CSRF
fetch(url, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': csrftoken
    },
    body: JSON.stringify(data)
});

# Exempter vue de CSRF (ATTENTION!)
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_api_view(request):
    # ...
    pass

# === SQL Injection (protection automatique) ===

# [OK] BON (utilise ORM - protégé)
Post.objects.filter(title=user_input)

# [X] MAUVAIS (vulnérable)
Post.objects.raw(f"SELECT * FROM post WHERE title = '{user_input}'")

# [OK] BON (avec paramètres)
Post.objects.raw("SELECT * FROM post WHERE title = %s", [user_input])

# === XSS (protection automatique) ===

# Django échappe HTML automatiquement dans templates
{{ user_input }}  # Échappé automatiquement

# Désactiver (ATTENTION!)
{{ user_input|safe }}                             # Marque comme sûr
{% autoescape off %}{{ user_input }}{% endautoescape %}

# === Validation input ===

# Toujours valider avec forms
class ContactForm(forms.Form):
    email = forms.EmailField()                    # Valide email
    age = forms.IntegerField(min_value=0, max_value=120)

# Validation personnalisée
def clean_email(self):
    email = self.cleaned_data['email']
    if User.objects.filter(email=email).exists():
        raise forms.ValidationError('Email already exists')
    return email

# === Rate limiting ===

pip install django-ratelimit

from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='5/m')                  # 5 requêtes par minute
def my_view(request):
    # ...
    pass

@ratelimit(key='user', rate='100/h')              # 100 requêtes par heure
def api_view(request):
    # ...
    pass

# === Checker sécurité ===

python manage.py check --deploy

# Liste des problèmes de sécurité potentiels


[OK] DÉPLOIEMENT


# === Checklist pré-déploiement ===

# 1. Settings production
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com']
SECRET_KEY = os.environ.get('SECRET_KEY')

# 2. Base de données production
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST'),
        'PORT': '5432',
    }
}

# 3. Static files
python manage.py collectstatic

# 4. Migrations
python manage.py migrate

# 5. Créer superuser
python manage.py createsuperuser

# === Gunicorn (serveur WSGI) ===

pip install gunicorn

# Lancer
gunicorn myproject.wsgi:application

# Avec options
gunicorn myproject.wsgi:application \
    --bind 0.0.0.0:8000 \
    --workers 3 \
    --timeout 60 \
    --access-logfile - \
    --error-logfile -

# === Nginx (reverse proxy) ===

# /etc/nginx/sites-available/myproject
server {
    listen 80;
    server_name monsite.com www.monsite.com;
    
    location = /favicon.ico { access_log off; log_not_found off; }
    
    location /static/ {
        alias /home/user/myproject/staticfiles/;
    }
    
    location /media/ {
        alias /home/user/myproject/media/;
    }
    
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Activer site
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

# === Systemd service ===

# /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=user
Group=www-data
WorkingDirectory=/home/user/myproject
ExecStart=/home/user/myproject/.venv/bin/gunicorn \
          --workers 3 \
          --bind unix:/home/user/myproject/myproject.sock \
          myproject.wsgi:application

[Install]
WantedBy=multi-user.target

# Commandes
sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn
sudo systemctl restart gunicorn

# === Whitenoise (static files) ===

pip install whitenoise

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

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# === Variables d'environnement ===

pip install python-decouple

# .env (ne PAS versionner!)
SECRET_KEY=your-secret-key
DEBUG=False
DATABASE_URL=postgresql://user:password@localhost/dbname
ALLOWED_HOSTS=monsite.com,www.monsite.com

# settings.py
from decouple import config, Csv

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

# === Docker ===

# Dockerfile
FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

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

COPY . /app/

RUN python manage.py collectstatic --noinput

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

# docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
      web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgresql://myuser:mypassword@db:5432/mydb
  
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

# Commandes Docker
docker-compose up -d
docker-compose down
docker-compose logs -f web
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser

# === Heroku ===

# Installer Heroku CLI
# https://devcenter.heroku.com/articles/heroku-cli

# Login
heroku login

# Créer app
heroku create myapp-name

# Ajouter PostgreSQL
heroku addons:create heroku-postgresql:hobby-dev

# Procfile
web: gunicorn myproject.wsgi

# runtime.txt
python-3.11.0

# requirements.txt
django==5.1.0
gunicorn==21.2.0
psycopg2-binary==2.9.9
whitenoise==6.6.0
python-decouple==3.8

# settings.py pour Heroku
import dj_database_url

# Database
DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL')
    )
}

# Whitenoise
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')

# Deploy
git push heroku main

# Migrations
heroku run python manage.py migrate

# Créer superuser
heroku run python manage.py createsuperuser

# Logs
heroku logs --tail

# Variables d'environnement
heroku config:set SECRET_KEY=your-secret-key
heroku config:set DEBUG=False

# === DigitalOcean App Platform ===

# .do/app.yaml
name: myproject
region: fra
services:
  - name: web
    github:
      repo: your-username/your-repo
      branch: main
      deploy_on_push: true
    build_command: |
      python manage.py collectstatic --noinput
    run_command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8080
    environment_slug: python
    instance_count: 1
    instance_size_slug: basic-xxs
    http_port: 8080
    envs:
      - key: SECRET_KEY
        scope: RUN_TIME
        type: SECRET
      - key: DEBUG
        scope: RUN_TIME
        value: "False"
databases:
  - name: db
    engine: PG
    production: false

# === AWS Elastic Beanstalk ===

# Installer EB CLI
pip install awsebcli

# Initialiser
eb init

# Créer environnement
eb create myapp-env

# .ebextensions/django.config
option_settings:
  aws:elasticbeanstalk:container:python:
    WSGIPath: myproject.wsgi:application
  aws:elasticbeanstalk:application:environment:
    DJANGO_SETTINGS_MODULE: myproject.settings
    SECRET_KEY: your-secret-key

# Deploy
eb deploy

# Logs
eb logs

# === Railway ===

# railway.json
{
  "build": {
    "builder": "NIXPACKS"
  },
  "deploy": {
    "startCommand": "python manage.py migrate && gunicorn myproject.wsgi",
    "restartPolicyType": "ON_FAILURE"
  }
}

# Procfile
web: python manage.py migrate && gunicorn myproject.wsgi

# Deploy: connecter repo GitHub sur Railway dashboard

# === Monitoring & Logging ===

# Sentry (erreurs)
pip install sentry-sdk

# settings.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn="your-sentry-dsn",
    integrations=[DjangoIntegration()],
    traces_sample_rate=1.0,
    send_default_pii=True
)

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/error.log',
            'formatter': 'verbose',
        },
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'verbose',
        },
    },
    'root': {
        'handlers': ['console', 'file'],
        'level': 'INFO',
    },
    'loggers': {
        'django': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': False,
        },
    },
}


[OK] PERFORMANCE & OPTIMISATION


# === Database queries ===

# Problème N+1
# [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!

# Many-to-Many
# [X] MAUVAIS
posts = Post.objects.all()
for post in posts:
    print(post.tags.all())  # Query pour chaque post!

# [OK] BON (avec prefetch_related)
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
    print(post.tags.all())  # Pas de query!

# === Index database ===

class Post(models.Model):
    title = models.CharField(max_length=200, db_index=True)  # Index simple
    slug = models.SlugField(unique=True)  # Index unique automatique
    
    class Meta:
        indexes = [
            models.Index(fields=['created_at']),  # Index simple
            models.Index(fields=['author', '-created_at']),  # Index composite
            models.Index(fields=['title'], name='title_idx'),  # Avec nom
        ]

# === Query optimization ===

# Only (charger seulement certains champs)
posts = Post.objects.only('id', 'title')

# Defer (exclure certains champs)
posts = Post.objects.defer('content')  # Ne charge pas content

# Values / Values_list (dictionnaires au lieu d'objets)
posts = Post.objects.values('id', 'title')  # Plus léger
titles = Post.objects.values_list('title', flat=True)

# Count efficace
# [X] MAUVAIS
count = len(Post.objects.all())  # Charge tous les objets!

# [OK] BON
count = Post.objects.count()  # Query COUNT() en SQL

# Exists
# [X] MAUVAIS
if Post.objects.filter(published=True):  # Charge objets

# [OK] BON
if Post.objects.filter(published=True).exists():  # Query EXISTS()

# Bulk operations
# [X] MAUVAIS (N queries)
for i in range(1000):
    Post.objects.create(title=f'Post {i}')

# [OK] BON (1 query)
posts = [Post(title=f'Post {i}') for i in range(1000)]
Post.objects.bulk_create(posts, batch_size=100)

# Update
# [X] MAUVAIS (N queries)
for post in Post.objects.filter(published=False):
    post.published = True
    post.save()

# [OK] BON (1 query)
Post.objects.filter(published=False).update(published=True)

# === Cache strategique ===

from django.core.cache import cache
from django.views.decorators.cache import cache_page

# Cache vue entière
@cache_page(60 * 15)  # 15 minutes
def post_list(request):
    posts = Post.objects.all()
    return render(request, 'post_list.html', {'posts': posts})

# Cache requête coûteuse
def get_statistics():
    stats = cache.get('site_stats')
    if stats is None:
        stats = {
            'total_posts': Post.objects.count(),
            'total_users': User.objects.count(),
            'total_comments': Comment.objects.count(),
        }
        cache.set('site_stats', stats, 60 * 60)  # 1 heure
    return stats

# Invalider cache
def create_post(request):
    # ... créer post ...
    cache.delete('site_stats')
    cache.delete_pattern('post_list_*')

# Cache template fragment
{% load cache %}
{% cache 500 sidebar request.user.id %}
    <!-- Contenu coûteux -->
{% endcache %}

# === Database connection pooling ===

# Avec django-db-pool
pip install django-db-pool

DATABASES = {
    'default': {
        'ENGINE': 'django_db_pool.backends.postgresql',
        'POOL_OPTIONS': {
            'POOL_SIZE': 10,
            'MAX_OVERFLOW': 10,
        },
        # ... autres options
    }
}

# === Pagination efficace ===

# Toujours paginer grandes listes
from django.core.paginator import Paginator

posts = Post.objects.all().order_by('-created_at')
paginator = Paginator(posts, 25)  # 25 par page

# CBV avec pagination
class PostListView(ListView):
    model = Post
    paginate_by = 25

# === Static files optimization ===

# Compression et minification
pip install django-compressor

INSTALLED_APPS += ['compressor']

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'compressor.finders.CompressorFinder',
]

# Template
{% load compress %}
{% compress css %}
    <link rel="stylesheet" href="{% static 'css/style1.css' %}">
    <link rel="stylesheet" href="{% static 'css/style2.css' %}">
{% endcompress %}

# CDN pour static files
# settings.py
STATIC_URL = 'https://cdn.example.com/static/'

# === Debug Toolbar ===

pip install django-debug-toolbar

# settings.py (DEV seulement!)
if DEBUG:
    INSTALLED_APPS += ['debug_toolbar']
    MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
    INTERNAL_IPS = ['127.0.0.1']

# urls.py
if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [
        path('__debug__/', include(debug_toolbar.urls)),
    ] + urlpatterns


[OK] BONNES PRATIQUES


# === Structure projet recommandée ===

myproject/
├── .env                          # Variables d'environnement (ne PAS versionner)
├── .gitignore
├── README.md
├── requirements/
│   ├── base.txt                  # Dépendances communes
│   ├── dev.txt                   # Développement
│   ├── prod.txt                  # Production
│   └── test.txt                  # Tests
├── manage.py
├── myproject/
│   ├── __init__.py
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py               # Settings communs
│   │   ├── dev.py                # Développement
│   │   ├── prod.py               # Production
│   │   └── test.py               # Tests
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── apps/                         # Applications du projet
│   ├── blog/
│   ├── accounts/
│   └── api/
├── templates/                    # Templates globaux
│   ├── base.html
│   └── includes/
├── static/                       # Static globaux
│   ├── css/
│   ├── js/
│   └── images/
├── media/                        # Uploads (ne PAS versionner)
├── staticfiles/                  # Collectés (ne PAS versionner)
├── tests/                        # Tests
└── docs/                         # Documentation

# === Settings modulaires ===

# myproject/settings/base.py
from pathlib import Path
from decouple import config

BASE_DIR = Path(__file__).resolve().parent.parent.parent

SECRET_KEY = config('SECRET_KEY')

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Apps tierces
    'rest_framework',
    'django_filters',
    # Apps locales
    'apps.blog',
    'apps.accounts',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# myproject/settings/dev.py
from .base import *

DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# myproject/settings/prod.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())

DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL')
    )
}

# Security
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# Lancer avec settings spécifique
python manage.py runserver --settings=myproject.settings.dev
python manage.py migrate --settings=myproject.settings.prod

# Ou variable d'environnement
export DJANGO_SETTINGS_MODULE=myproject.settings.dev

# === Requirements modulaires ===

# requirements/base.txt
django==5.1.0
psycopg2-binary==2.9.9
pillow==10.1.0
python-decouple==3.8

# requirements/dev.txt
-r base.txt
django-debug-toolbar==4.2.0
ipython==8.18.0
black==23.11.0
flake8==6.1.0

# requirements/prod.txt
-r base.txt
gunicorn==21.2.0
whitenoise==6.6.0
sentry-sdk==1.38.0

# requirements/test.txt
-r base.txt
pytest==7.4.3
pytest-django==4.7.0
pytest-cov==4.1.0
factory-boy==3.3.0

# Installer
pip install -r requirements/dev.txt

# === Conventions de nommage ===

# Models - Singular, CamelCase
class Post(models.Model):
    pass

class UserProfile(models.Model):
    pass

# Variables - snake_case
user_profile = UserProfile.objects.get(id=1)
post_list = Post.objects.all()

# Fonctions/méthodes - snake_case, verbes
def get_published_posts():
    pass

def send_email_notification():
    pass

# Classes - CamelCase
class PostListView(ListView):
    pass

class EmailService:
    pass

# Constantes - UPPER_CASE
MAX_UPLOAD_SIZE = 5242880
DEFAULT_PAGINATION = 25

# URLs - kebab-case
path('user-profile/', views.profile)
path('blog-posts/', views.post_list)

# === Type hints (Python 3.5+) ===

from django.http import HttpRequest, HttpResponse
from typing import List, Optional
from .models import Post

def get_post(post_id: int) -> Optional[Post]:
    try:
        return Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return None

def post_list(request: HttpRequest) -> HttpResponse:
    posts: List[Post] = Post.objects.all()
    return render(request, 'post_list.html', {'posts': posts})

# === Docstrings ===

def send_email_notification(user_id: int, subject: str) -> bool:
    """
    Envoie une notification email à un utilisateur.
    
    Args:
        user_id: ID de l'utilisateur
        subject: Sujet de l'email
    
    Returns:
        True si envoyé avec succès, False sinon
    
    Raises:
        User.DoesNotExist: Si l'utilisateur n'existe pas
    """
    user = User.objects.get(id=user_id)
    send_mail(subject, 'Message', 'from@example.com', [user.email])
    return True

# === Settings sensibles ===

# [X] JAMAIS dans le code
SECRET_KEY = 'django-insecure-hardcoded-key'  # MAUVAIS!
DATABASE_PASSWORD = 'mypassword'              # MAUVAIS!

# [OK] TOUJOURS dans variables d'environnement
SECRET_KEY = os.environ.get('SECRET_KEY')
DATABASE_PASSWORD = config('DB_PASSWORD')

# .env
SECRET_KEY=your-secret-key-here
DB_PASSWORD=your-db-password

# .gitignore
.env
db.sqlite3
*.pyc
__pycache__/
media/
staticfiles/
.venv/

# === Tests systématiques ===

# Couverture minimale
# - Models: création, __str__, méthodes
# - Views: status codes, templates, contexte
# - Forms: validation, clean methods
# - URLs: routing correct

# Exécuter avant chaque commit
python manage.py test
python manage.py check --deploy
black .
flake8 .

# === Logging approprié ===

import logging

logger = logging.getLogger(__name__)

def process_payment(amount: float):
    logger.info(f'Processing payment: ${amount}')
    try:
        # Traitement...
        logger.info('Payment successful')
    except Exception as e:
        logger.error(f'Payment failed: {e}', exc_info=True)
        raise

# === Documentation ===

# README.md minimum
"""
# Mon Projet Django

## Installation

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements/dev.txt
cp .env.example .env  # Configurer variables
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
```

## Tests
```bash
python manage.py test
```

## Déploiement
Voir docs/deployment.md
"""


[OK] PACKAGES UTILES


# === Authentification & Sécurité ===

# django-allauth - Auth complet (social, email, etc.)
pip install django-allauth

# django-guardian - Permissions par objet
pip install django-guardian

# django-axes - Protection brute force
pip install django-axes

# django-otp - Two-factor authentication
pip install django-otp

# === API ===

# djangorestframework - API REST
pip install djangorestframework

# django-cors-headers - CORS pour API
pip install django-cors-headers

# drf-spectacular - Documentation OpenAPI/Swagger
pip install drf-spectacular

# django-filter - Filtres avancés
pip install django-filter

# === Base de données ===

# psycopg2-binary - PostgreSQL
pip install psycopg2-binary

# mysqlclient - MySQL
pip install mysqlclient

# django-extensions - Commandes utiles
pip install django-extensions

# === Formulaires & Admin ===

# django-crispy-forms - Formulaires Bootstrap
pip install django-crispy-forms crispy-bootstrap5

# django-import-export - Import/Export admin
pip install django-import-export

# django-admin-sortable2 - Tri drag&drop
pip install django-admin-sortable2

# === Images & Fichiers ===

# pillow - Traitement images
pip install pillow

# django-imagekit - Thumbnails automatiques
pip install django-imagekit

# django-storages - Storage S3/Cloud
pip install django-storages boto3

# === Tâches asynchrones ===

# celery - Tâches async
pip install celery redis

# django-celery-beat - Tâches périodiques
pip install django-celery-beat

# === Cache & Performance ===

# django-redis - Cache Redis
pip install django-redis

# django-cachalot - Cache ORM
pip install django-cachalot

# django-compressor - Compression static
pip install django-compressor

# === Développement ===

# django-debug-toolbar - Debug
pip install django-debug-toolbar

# ipython - Shell amélioré
pip install ipython

# black - Formatage code
pip install black

# flake8 - Linting
pip install flake8

# pytest-django - Tests avec pytest
pip install pytest-django

# factory-boy - Fixtures pour tests
pip install factory-boy

# === Monitoring ===

# sentry-sdk - Error tracking
pip install sentry-sdk

# django-silk - Profiling
pip install django-silk

# === Contenu ===

# django-ckeditor - Éditeur WYSIWYG
pip install django-ckeditor

# django-taggit - Tags
pip install django-taggit

# django-mptt - Arbres (catégories)
pip install django-mptt

# === Internationalisation ===

# django-modeltranslation - Traduction models
pip install django-modeltranslation

# django-rosetta - Interface traduction
pip install django-rosetta

# === Autres ===

# python-decouple - Variables d'environnement
pip install python-decouple

# django-environ - Config environnement
pip install django-environ

# whitenoise - Static files
pip install whitenoise

# gunicorn - Serveur WSGI
pip install gunicorn

# dj-database-url - Parse DATABASE_URL
pip install dj-database-url


[OK] RESSOURCES & DOCUMENTATION


# === Documentation officielle ===

# Django
https://docs.djangoproject.com/

# Django REST Framework
https://www.django-rest-framework.org/

# Celery
https://docs.celeryq.dev/

# === Tutoriels ===

# Django Girls Tutorial (débutants)
https://tutorial.djangogirls.org/

# Django for Beginners (livre)
https://djangoforbeginners.com/

# Real Python - Django Tutorials
https://realpython.com/tutorials/django/

# === Packages ===

# Django Packages (répertoire)
https://djangopackages.org/

# Awesome Django (liste curatée)
https://github.com/wsvincent/awesome-django

# === Communauté ===

# Django Forum
https://forum.djangoproject.com/

# Django Discord
https://discord.gg/xcRH6mN4fa

# Reddit r/django
https://reddit.com/r/django

# Stack Overflow
https://stackoverflow.com/questions/tagged/django

# === Podcasts ===

# Django Chat
https://djangochat.com/

# Talk Python (épisodes Django)
https://talkpython.fm/

# === Newsletters ===

# Django News
https://django-news.com/

# PyCoders Weekly
https://pycoders.com/

# === Conférences ===

# DjangoCon US
https://djangocon.us/

# DjangoCon Europe
https://djangocon.eu/

# === Blogs ===

# Django project blog
https://www.djangoproject.com/weblog/

# Simple is Better Than Complex
https://simpleisbetterthancomplex.com/

# William Vincent
https://wsvincent.com/

# === Outils ===

# Django Cheat Sheet
https://www.webforefront.com/django/

# Classy Class-Based Views
https://ccbv.co.uk/

# Classy Django REST Framework
https://www.cdrf.co/

# === GitHub ===

# Django source code
https://github.com/django/django

# Django REST Framework
https://github.com/encode/django-rest-framework


[OK] AIDE-MÉMOIRE RAPIDE


# === Commandes essentielles ===

# Projet
django-admin startproject myproject
python manage.py startapp myapp

# Serveur
python manage.py runserver

# Base de données
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser

# Shell
python manage.py shell

# Static files
python manage.py collectstatic

# Tests
python manage.py test

# === Workflow développement ===

# 1. Créer environnement virtuel
python -m venv .venv
source .venv/bin/activate  # Linux/Mac
.venv\Scripts\activate     # Windows

# 2. Installer Django
pip install django

# 3. Créer projet
django-admin startproject myproject .

# 4. Créer app
python manage.py startapp blog

# 5. Ajouter app dans INSTALLED_APPS (settings.py)

# 6. Créer models (blog/models.py)

# 7. Créer migrations
python manage.py makemigrations

# 8. Appliquer migrations
python manage.py migrate

# 9. Créer superuser
python manage.py createsuperuser

# 10. Enregistrer models dans admin (blog/admin.py)

# 11. Créer views (blog/views.py)

# 12. Créer URLs (blog/urls.py)

# 13. Inclure URLs dans projet (myproject/urls.py)

# 14. Créer templates (blog/templates/blog/)

# 15. Lancer serveur
python manage.py runserver

# === Snippets courants ===

# Model
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.title

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

# URL
path('posts/', views.post_list, name='post_list'),

# Template
{% for post in posts %}
    <h2>{{ post.title }}</h2>
    <p>{{ post.content }}</p>
{% endfor %}

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

# Admin
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'created_at']


[OK] RÉSUMÉ COMPLET - WORKFLOW DJANGO DE A à Z


# === RÉCAPITULATIF: Créer un blog Django complet ===

# Ce résumé montre TOUTES les étapes pour créer un blog fonctionnel
# en partant de zéro. Suivez dans l'ordre!


# ═══════════════════════════════════════════════════════════════
# PHASE 1: INSTALLATION & SETUP (15 minutes)
# ═══════════════════════════════════════════════════════════════

# 1. Vérifier Python installé
python --version  # Doit être 3.10+

# 2. Créer dossier projet
mkdir mon_blog
cd mon_blog

# 3. Créer environnement virtuel
python -m venv .venv

# 4. Activer environnement virtuel
source .venv/bin/activate  # Linux/Mac
.venv\Scripts\activate     # Windows
# Vous devez voir (.venv) dans le terminal!

# 5. Installer Django
pip install django pillow  # pillow pour les images

# 6. Créer projet Django
django-admin startproject config .
# "config" = nom du projet (settings, urls)
# "." = crée dans dossier actuel (propre!)

# 7. Tester que ça marche
python manage.py runserver
# Ouvrir http://localhost:8000/ -> Page avec fusée [RAPIDE]


# ═══════════════════════════════════════════════════════════════
# PHASE 2: CRÉER L'APPLICATION BLOG (10 minutes)
# ═══════════════════════════════════════════════════════════════

# 8. Créer app blog
python manage.py startapp blog

# 9. Enregistrer l'app dans settings.py
# config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Mes apps
    'blog.apps.BlogConfig',  # <- Ajouter cette ligne
]

# 10. Configurer langue et timezone
# config/settings.py (ligne ~106)
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'


# ═══════════════════════════════════════════════════════════════
# PHASE 3: CRÉER LES MODELS (15 minutes)
# ═══════════════════════════════════════════════════════════════

# 11. Définir le model Post
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse

class Post(models.Model):
    """Article de blog"""
    title = models.CharField(max_length=200, verbose_name='Titre')
    slug = models.SlugField(max_length=200, unique=True)
    content = models.TextField(verbose_name='Contenu')
    image = models.ImageField(upload_to='posts/', blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published = models.BooleanField(default=False)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    
    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Article'
        verbose_name_plural = 'Articles'
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={'pk': self.pk})

# 12. Créer les migrations
python manage.py makemigrations

# 13. Appliquer les migrations
python manage.py migrate

# 14. Créer superuser
python manage.py createsuperuser
# Username: admin
# Email: admin@example.com
# Password: admin123 (pour dev, choisissez mieux en prod!)


# ═══════════════════════════════════════════════════════════════
# PHASE 4: CONFIGURER L'ADMIN (5 minutes)
# ═══════════════════════════════════════════════════════════════

# 15. Enregistrer Post dans l'admin
# blog/admin.py
from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'published', 'created_at']
    list_filter = ['published', 'created_at']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ('title',)}
    date_hierarchy = 'created_at'
    list_editable = ['published']

# 16. Accéder à l'admin
python manage.py runserver
# Aller sur http://localhost:8000/admin/
# Se connecter avec admin/admin123
# Créer quelques articles de test!


# ═══════════════════════════════════════════════════════════════
# PHASE 5: CRÉER LES VIEWS (20 minutes)
# ═══════════════════════════════════════════════════════════════

# 17. Créer les views
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from .models import Post
from .forms import PostForm

def post_list(request):
    """Liste des articles publiés"""
    posts = Post.objects.filter(published=True)
    return render(request, 'blog/post_list.html', {'posts': posts})

def post_detail(request, pk):
    """Détail d'un article"""
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

@login_required
def post_create(request):
    """Créer un article"""
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect('blog:post_detail', pk=post.pk)
    else:
        form = PostForm()
    return render(request, 'blog/post_form.html', {'form': form})

@login_required
def post_update(request, pk):
    """Modifier un article"""
    post = get_object_or_404(Post, pk=pk)
    if post.author != request.user:
        return redirect('blog:post_list')
    
    if request.method == 'POST':
        form = PostForm(request.POST, request.FILES, instance=post)
        if form.is_valid():
            form.save()
            return redirect('blog:post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
    
    return render(request, 'blog/post_form.html', {'form': form, 'post': post})

@login_required
def post_delete(request, pk):
    """Supprimer un article"""
    post = get_object_or_404(Post, pk=pk)
    if post.author == request.user:
        post.delete()
    return redirect('blog:post_list')


# ═══════════════════════════════════════════════════════════════
# PHASE 6: CRÉER LE FORMULAIRE (5 minutes)
# ═══════════════════════════════════════════════════════════════

# 18. Créer le formulaire
# blog/forms.py (créer ce fichier)
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'slug', 'content', 'image', 'published']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'slug': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
            'image': forms.FileInput(attrs={'class': 'form-control'}),
            'published': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
        }


# ═══════════════════════════════════════════════════════════════
# PHASE 7: CONFIGURER LES URLS (10 minutes)
# ═══════════════════════════════════════════════════════════════

# 19. Créer URLs de l'app blog
# blog/urls.py (créer ce fichier)
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
    path('post/new/', views.post_create, name='post_create'),
    path('post/<int:pk>/edit/', views.post_update, name='post_update'),
    path('post/<int:pk>/delete/', views.post_delete, name='post_delete'),
]

# 20. Inclure URLs blog dans projet
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),  # Blog à la racine
    path('accounts/', include('django.contrib.auth.urls')),  # Auth Django
]

# Servir fichiers média en développement
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


# ═══════════════════════════════════════════════════════════════
# PHASE 8: CRÉER LES TEMPLATES (30 minutes)
# ═══════════════════════════════════════════════════════════════

# 21. Créer structure templates
mkdir -p blog/templates/blog
mkdir -p templates/registration

# 22. Template de base
# blog/templates/blog/base.html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Mon Blog{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{% url 'blog:post_list' %}">Mon Blog</a>
            <ul class="navbar-nav ms-auto">
                {% if user.is_authenticated %}
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'blog:post_create' %}">Nouvel Article</a>
                    </li>
                    <li class="nav-item">
                        <span class="nav-link">{{ user.username }}</span>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'logout' %}">Déconnexion</a>
                    </li>
                {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'login' %}">Connexion</a>
                    </li>
                {% endif %}
            </ul>
        </div>
    </nav>
    
    <main class="container my-4">
        {% block content %}{% endblock %}
    </main>
    
    <footer class="bg-light py-3 mt-5">
        <div class="container text-center">
            <p>&copy; 2024 Mon Blog Django</p>
        </div>
    </footer>
</body>
</html>

# 23. Template liste
# blog/templates/blog/post_list.html
{% extends 'blog/base.html' %}

{% block title %}Accueil - Mon Blog{% endblock %}

{% block content %}
    <h1>Articles</h1>
    
    {% if posts %}
        <div class="row">
            {% for post in posts %}
                <div class="col-md-6 mb-4">
                    <div class="card">
                        {% if post.image %}
                            <img src="{{ post.image.url }}" class="card-img-top" alt="{{ post.title }}">
                        {% endif %}
                        <div class="card-body">
                            <h2 class="card-title">{{ post.title }}</h2>
                            <p class="text-muted">
                                Par {{ post.author.username }} le {{ post.created_at|date:"d/m/Y" }}
                            </p>
                            <p class="card-text">{{ post.content|truncatewords:50 }}</p>
                            <a href="{% url 'blog:post_detail' pk=post.pk %}" class="btn btn-primary">
                                Lire la suite
                            </a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>
    {% else %}
        <p class="alert alert-info">Aucun article pour le moment.</p>
    {% endif %}
{% endblock %}

# 24. Template détail
# blog/templates/blog/post_detail.html
{% extends 'blog/base.html' %}

{% block title %}{{ post.title }} - Mon Blog{% endblock %}

{% block content %}
    <article>
        <h1>{{ post.title }}</h1>
        
        <p class="text-muted">
            Par {{ post.author.username }} le {{ post.created_at|date:"d F Y" }}
        </p>
        
        {% if post.image %}
            <img src="{{ post.image.url }}" class="img-fluid mb-3" alt="{{ post.title }}">
        {% endif %}
        
        <div class="content">
            {{ post.content|linebreaks }}
        </div>
        
        {% if user == post.author %}
            <div class="mt-4">
                <a href="{% url 'blog:post_update' pk=post.pk %}" class="btn btn-warning">
                    Modifier
                </a>
                <form method="post" action="{% url 'blog:post_delete' pk=post.pk %}" class="d-inline">
                    {% csrf_token %}
                    <button type="submit" class="btn btn-danger" onclick="return confirm('Supprimer cet article ?')">
                        Supprimer
                    </button>
                </form>
            </div>
        {% endif %}
    </article>
    
    <div class="mt-4">
        <a href="{% url 'blog:post_list' %}" class="btn btn-secondary"><- Retour</a>
    </div>
{% endblock %}

# 25. Template formulaire
# blog/templates/blog/post_form.html
{% extends 'blog/base.html' %}

{% block title %}
    {% if post %}Modifier{% else %}Créer{% endif %} un article
{% endblock %}

{% block content %}
    <h1>{% if post %}Modifier{% else %}Créer{% endif %} un article</h1>
    
    <form method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="btn btn-primary">
            {% if post %}Mettre à jour{% else %}Créer{% endif %}
        </button>
        <a href="{% url 'blog:post_list' %}" class="btn btn-secondary">Annuler</a>
    </form>
{% endblock %}

# 26. Template login
# templates/registration/login.html
{% extends 'blog/base.html' %}

{% block title %}Connexion{% endblock %}

{% block content %}
    <div style="max-width: 400px; margin: 0 auto;">
        <h2>Connexion</h2>
        
        {% if form.errors %}
            <div class="alert alert-danger">Identifiants incorrects.</div>
        {% endif %}
        
        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-primary">Se connecter</button>
        </form>
    </div>
{% endblock %}


# ═══════════════════════════════════════════════════════════════
# PHASE 9: CONFIGURER FICHIERS MÉDIA (5 minutes)
# ═══════════════════════════════════════════════════════════════

# 27. Configurer média dans settings.py
# config/settings.py (à la fin)
import os

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')


# ═══════════════════════════════════════════════════════════════
# PHASE 10: TESTER L'APPLICATION (10 minutes)
# ═══════════════════════════════════════════════════════════════

# 28. Lancer le serveur
python manage.py runserver

# 29. Tester toutes les fonctionnalités:
# [OK] http://localhost:8000/ -> Liste des articles
# [OK] http://localhost:8000/admin/ -> Admin (ajouter articles)
# [OK] http://localhost:8000/accounts/login/ -> Connexion
# [OK] Cliquer "Nouvel Article" -> Créer article
# [OK] Cliquer sur un article -> Voir détail
# [OK] Cliquer "Modifier" -> Modifier article
# [OK] Cliquer "Supprimer" -> Supprimer article

# 30. Créer quelques articles de test via l'admin
# Ajouter des images
# Tester published=True/False


# ═══════════════════════════════════════════════════════════════
# STRUCTURE FINALE DU PROJET
# ═══════════════════════════════════════════════════════════════

mon_blog/
├── .venv/                    # Environnement virtuel
├── blog/                     # Application blog
│   ├── migrations/
│   ├── templates/
│   │   └── blog/
│   │       ├── base.html
│   │       ├── post_list.html
│   │       ├── post_detail.html
│   │       └── post_form.html
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── models.py
│   ├── urls.py
│   ├── views.py
│   └── tests.py
├── config/                   # Configuration projet
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── media/                    # Fichiers uploadés
│   └── posts/
├── templates/                # Templates globaux
│   └── registration/
│       └── login.html
├── db.sqlite3                # Base de données
├── manage.py
└── requirements.txt


# ═══════════════════════════════════════════════════════════════
# COMMANDES DJANGO LES PLUS UTILISÉES
# ═══════════════════════════════════════════════════════════════

# Serveur
python manage.py runserver

# Migrations
python manage.py makemigrations    # Détecter changements models
python manage.py migrate           # Appliquer migrations

# Users
python manage.py createsuperuser   # Créer admin

# Shell
python manage.py shell             # Console Python interactive

# Projet/Apps
django-admin startproject nom .
python manage.py startapp nom

# Static files
python manage.py collectstatic     # Collecter (production)

# Tests
python manage.py test              # Lancer tests


# ═══════════════════════════════════════════════════════════════
# CHECKLIST DÉBUTANT
# ═══════════════════════════════════════════════════════════════

# Avant de coder:
# [OK] Environnement virtuel activé? (voir (.venv) dans terminal)
# [OK] Django installé? (pip list | grep Django)

# Après changement de model:
# [OK] python manage.py makemigrations
# [OK] python manage.py migrate

# Après création d'app:
# [OK] Ajouter dans INSTALLED_APPS (settings.py)

# Avant de créer view:
# [OK] Model existe?
# [OK] Form existe (si nécessaire)?

# Après création de view:
# [OK] URL créée dans urls.py?
# [OK] Template existe?

# Dans les formulaires:
# [OK] {% csrf_token %} présent?
# [OK] method="post"?
# [OK] enctype="multipart/form-data" si fichiers?

# Problème fréquent:
# [X] Page 404 -> Vérifier urls.py
# [X] Template not found -> Vérifier chemin template
# [X] Module not found -> App pas dans INSTALLED_APPS
# [X] CSRF error -> Oublié {% csrf_token %}
# [X] Fichier pas uploadé -> Oublié request.FILES ou enctype


# ═══════════════════════════════════════════════════════════════
# FÉLICITATIONS! [BRAVO]
# ═══════════════════════════════════════════════════════════════

# Vous avez créé votre premier blog Django complet avec:
# [OK] Liste des articles
# [OK] Détail d'un article
# [OK] Création d'article (avec auth)
# [OK] Modification d'article (seulement auteur)
# [OK] Suppression d'article (seulement auteur)
# [OK] Upload d'images
# [OK] Admin Django
# [OK] Authentification

# Prochaines étapes (pour aller plus loin):
# -> Ajouter commentaires
# -> Ajouter catégories/tags
# -> Ajouter recherche
# -> Ajouter pagination
# -> Améliorer le design
# -> Déployer en production (Heroku, DigitalOcean, etc.)


# ═══════════════════════════════════════════════════════════════
# RESSOURCES POUR CONTINUER
# ═══════════════════════════════════════════════════════════════

# Documentation officielle Django:
# https://docs.djangoproject.com/

# Tutorial Django Girls (en français):
# https://tutorial.djangogirls.org/fr/

# Django for Beginners (livre):
# https://djangoforbeginners.com/

# Real Python - Django Tutorials:
# https://realpython.com/tutorials/django/

# Django Forum (poser questions):
# https://forum.djangoproject.com/

# Stack Overflow (tag django):
# https://stackoverflow.com/questions/tagged/django


# ═══════════════════════════════════════════════════════════════
# FIN DU CHEATSHEET DJANGO POUR DÉBUTANTS
# ═══════════════════════════════════════════════════════════════

# Ce guide vous a montré:
# [OK] Qu'est-ce que Django et pourquoi l'utiliser
# [OK] Installation complète étape par étape
# [OK] Structure d'un projet Django
# [OK] Models (base de données)
# [OK] Admin Django (interface gratuite)
# [OK] Views (logique métier)
# [OK] URLs (routage)
# [OK] Templates (affichage HTML)
# [OK] Formulaires (interaction utilisateur)
# [OK] Authentification (users, login, permissions)
# [OK] Workflow complet A->Z

# Vous avez maintenant les bases solides pour:
# -> Créer vos propres projets Django
# -> Comprendre le code Django existant
# -> Continuer votre apprentissage
# -> Construire des applications web professionnelles

# Bon courage et amusez-vous bien avec Django! [RAPIDE]