# ============================================================================
# [LIVRE] FLASK - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER FLASK DE ZÉRO À EXPERT
#
# Ce guide est organisé en 4 parties progressives :
#
# PARTIE 1 : FONDAMENTAUX (flask_partie1.txt)
# - Chapitre 0 : Introduction à Flask
# - Chapitre 1 : Première Application
# - Chapitre 2 : Routes et URLs Dynamiques
# - Chapitre 3 : Templates Jinja2
# - Chapitre 4 : Fichiers Statiques
#
# PARTIE 2 : FORMULAIRES ET DONNÉES (flask_partie2.txt)
# - Chapitre 5 : Forms et Validation (Flask-WTF)
# - Chapitre 6 : Base de Données (SQLAlchemy)
# - Chapitre 7 : Migrations de Base de Données
# - Chapitre 8 : Relations entre Tables
#
# PARTIE 3 : FONCTIONNALITÉS AVANCÉES (flask_partie3.txt)
# - Chapitre 9 : Authentification (Flask-Login)
# - Chapitre 10 : Blueprints et Organisation
# - Chapitre 11 : Sessions et Cookies
# - Chapitre 12 : API REST
# - Chapitre 13 : Upload de Fichiers
# - Chapitre 14 : Email et Tâches Asynchrones
#
# PARTIE 4 : PRODUCTION ET OPTIMISATION (flask_partie4.txt)
# - Chapitre 15 : Testing
# - Chapitre 16 : Sécurité
# - Chapitre 17 : Performance et Caching
# - Chapitre 18 : Deployment
# - Chapitre 19 : Logging et Monitoring
# - Chapitre 20 : Best Practices
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~20-25 heures
# [DOCS] PRÉREQUIS : Python de base (variables, fonctions, classes)
#
# [IDEE] COMMENT UTILISER CE GUIDE :
# 1. Lisez les parties dans l'ordre
# 2. Testez TOUS les exemples
# 3. Faites les exercices pratiques
# 4. Créez vos propres projets
#
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ? -> Explications pas à pas
POURQUOI ? -> Raisons et contexte
QUAND ? -> Cas d'usage concrets
PRATIQUE -> Exemples réels et exercices

Ce guide vise à être VOTRE SEULE RÉFÉRENCE Flask !
"""

# ============================================================================
# [NOTE] CONVENTIONS UTILISÉES DANS CE GUIDE
# ============================================================================

"""
[IDEE] Information importante
[REFLEXION] Question / Réflexion
[OK] Bonne pratique
[X] Mauvaise pratique
[ATTENTION] Attention / Avertissement
[CLE] Point clé à retenir
[COURS] Exercice pratique
[DOCS] Résumé
[OBJECTIF] Objectif
[TEMPS] Temps estimé
[RAPIDE] Prêt pour la suite
"""

# ============================================================================
# [OUTILS] CONFIGURATION DE L'ENVIRONNEMENT
# ============================================================================

"""
AVANT DE COMMENCER

1. INSTALLER PYTHON
   - Version recommandée : Python 3.8+
   - Vérifier : python --version

2. CRÉER UN ENVIRONNEMENT VIRTUEL
   C'est OBLIGATOIRE pour chaque projet !
"""

# Créer l'environnement virtuel
python -m venv venv

# Activer l'environnement
# Windows (PowerShell)
venv\Scripts\Activate.ps1
# Windows (CMD)
venv\Scripts\activate.bat
# Mac/Linux
source venv/bin/activate

# Installer Flask
pip install flask

# Installer extensions courantes (optionnel pour démarrer)
pip install flask-sqlalchemy flask-wtf flask-login email-validator

# Créer requirements.txt (pour partager dépendances)
pip freeze > requirements.txt

"""
[IDEE] POURQUOI UN ENVIRONNEMENT VIRTUEL ?

Sans venv :
[X] Packages globaux (conflits entre projets)
[X] Versions incompatibles
[X] Difficile à partager
[X] Problèmes lors du déploiement

Avec venv :
[OK] Isolation complète par projet
[OK] Gestion propre des dépendances
[OK] requirements.txt facile à créer
[OK] Déploiement simplifié
"""

# ============================================================================
# [DOSSIER] STRUCTURE DE PROJET RECOMMANDÉE
# ============================================================================

"""
PETIT PROJET (jusqu'à 1000 lignes)
----------------------------------
"""
mon_projet/
├── venv/                  # Environnement virtuel (ne pas commiter!)
├── app.py                 # Application principale
├── templates/             # Templates HTML
│   ├── base.html
│   ├── index.html
│   └── about.html
├── static/                # Fichiers statiques
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       └── logo.png
├── requirements.txt       # Dépendances
└── README.md             # Documentation

"""
PROJET MOYEN (1000-10000 lignes)
--------------------------------
"""
mon_projet/
├── venv/
├── app/
│   ├── __init__.py       # Créer l'application
│   ├── models.py         # Modèles de base de données
│   ├── forms.py          # Formulaires
│   ├── routes.py         # Routes
│   ├── templates/
│   └── static/
├── config.py             # Configuration
├── requirements.txt
└── run.py               # Point d'entrée

"""
GRAND PROJET (10000+ lignes)
----------------------------
"""
mon_projet/
├── venv/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── main/            # Blueprint principal
│   │   ├── __init__.py
│   │   └── routes.py
│   ├── auth/            # Blueprint authentification
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── routes.py
│   ├── api/             # Blueprint API
│   │   ├── __init__.py
│   │   └── routes.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── main/
│   │   ├── auth/
│   │   └── errors/
│   └── static/
├── tests/               # Tests unitaires
│   ├── conftest.py
│   ├── test_auth.py
│   └── test_models.py
├── migrations/          # Migrations DB
├── config.py
├── requirements.txt
└── run.py

"""
[IDEE] ÉVOLUTION NATURELLE

Démarrez simple, évoluez progressivement :
1. Prototype -> Structure simple (app.py)
2. Croissance -> Structure moyenne (app/)
3. Production -> Structure avec blueprints
"""

# ============================================================================
# [COURS] EXERCICE PRÉPARATOIRE
# ============================================================================

"""
AVANT DE COMMENCER LES CHAPITRES

1. [OK] Vérifier que Python est installé
2. [OK] Créer un environnement virtuel
3. [OK] Installer Flask
4. [OK] Créer la structure de base :
   - Dossier projet
   - app.py vide
   - Dossiers templates/ et static/

5. [OK] Tester l'installation :
"""

# Créer app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def test():
    return "Flask fonctionne ! [BRAVO]"

if __name__ == '__main__':
    app.run(debug=True)

# Lancer : python app.py
# Ouvrir : http://127.0.0.1:5000
# Vous devriez voir "Flask fonctionne ! [BRAVO]"

"""
Si ça marche, vous êtes prêt ! [RAPIDE]
Commencez par flask_partie1.txt
"""

# ============================================================================
# [DOCS] RESSOURCES COMPLÉMENTAIRES
# ============================================================================

"""
DOCUMENTATION OFFICIELLE
- Flask : https://flask.palletsprojects.com/
- Jinja2 : https://jinja.palletsprojects.com/
- SQLAlchemy : https://www.sqlalchemy.org/
- WTForms : https://wtforms.readthedocs.io/

TUTORIELS RECOMMANDÉS
- Flask Mega-Tutorial : https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
- Real Python Flask : https://realpython.com/tutorials/flask/

COMMUNAUTÉ
- GitHub : https://github.com/pallets/flask
- Discord : Flask Community Server
- Reddit : r/flask

EXTENSIONS POPULAIRES
- Flask-SQLAlchemy : ORM
- Flask-WTF : Formulaires
- Flask-Login : Authentification
- Flask-Migrate : Migrations DB
- Flask-Mail : Emails
- Flask-CORS : CORS pour APIs
- Flask-JWT-Extended : JWT pour APIs
- Flask-Admin : Interface admin
- Flask-Caching : Cache
"""

# ============================================================================
# [OBJECTIF] PLAN D'APPRENTISSAGE RECOMMANDÉ
# ============================================================================

"""
SEMAINE 1 : FONDAMENTAUX (Partie 1)
- Jour 1-2 : Chapitres 0-1 (Introduction, Première app)
- Jour 3-4 : Chapitre 2 (Routes)
- Jour 5-7 : Chapitre 3-4 (Templates, Static)
[COURS] Projet : Site portfolio simple

SEMAINE 2 : DONNÉES (Partie 2)
- Jour 1-2 : Chapitre 5 (Forms)
- Jour 3-5 : Chapitres 6-7 (Database, Migrations)
- Jour 6-7 : Chapitre 8 (Relations)
[COURS] Projet : Blog avec commentaires

SEMAINE 3 : AVANCÉ (Partie 3)
- Jour 1-2 : Chapitre 9 (Authentification)
- Jour 3-4 : Chapitres 10-11 (Blueprints, Sessions)
- Jour 5-7 : Chapitres 12-14 (API, Upload, Email)
[COURS] Projet : Réseau social simple

SEMAINE 4 : PRODUCTION (Partie 4)
- Jour 1-2 : Chapitres 15-16 (Tests, Sécurité)
- Jour 3-4 : Chapitre 17 (Performance)
- Jour 5-7 : Chapitres 18-20 (Deployment, Best Practices)
[COURS] Projet : Déployer une app complète

APRÈS 4 SEMAINES :
[OK] Vous maîtrisez Flask !
[OK] Vous pouvez créer des applications professionnelles
[OK] Vous êtes prêt pour des projets réels
"""

# ============================================================================
# [IDEE] CONSEILS POUR RÉUSSIR
# ============================================================================

"""
1. PRATIQUEZ QUOTIDIENNEMENT
   - 1-2 heures par jour > 10 heures le weekend
   - Tapez TOUS les exemples (ne copiez-collez pas !)
   - Expérimentez, cassez, réparez

2. COMPRENEZ AVANT DE MÉMORISER
   - Posez-vous "Pourquoi ?" à chaque ligne
   - Utilisez un débogueur (VS Code, PyCharm)
   - Lisez les messages d'erreur attentivement

3. CONSTRUISEZ DES PROJETS
   - Commencez simple, complexifiez progressivement
   - Terminez vos projets (même imparfaits !)
   - Partagez sur GitHub

4. REJOIGNEZ LA COMMUNAUTÉ
   - Posez des questions (Stack Overflow, Reddit)
   - Aidez les débutants (meilleure façon d'apprendre !)
   - Contribuez à des projets open-source

5. GARDEZ CE GUIDE
   - Revenez régulièrement aux bases
   - Utilisez-le comme référence
   - Annotez, ajoutez vos notes
"""

# ============================================================================
# [RAPIDE] C'EST PARTI !
# ============================================================================

"""
Vous êtes maintenant prêt à commencer votre voyage Flask ! [BRAVO]

-> PROCHAINE ÉTAPE : Ouvrez flask_partie1.txt

Bonne chance et amusez-vous bien ! [FORCE]

N'oubliez pas : Tout développeur expert était un jour débutant.
La seule différence ? Ils ont persisté et pratiqué.

Vous pouvez le faire ! *
"""
# ============================================================================
# [LIVRE] FLASK - PARTIE 1 : FONDAMENTAUX
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 0 : Introduction complète à Flask
# - Chapitre 1 : Première Application détaillée
# - Chapitre 2 : Routes et URLs Dynamiques
# - Chapitre 3 : Templates Jinja2 en profondeur
# - Chapitre 4 : Fichiers Statiques et Assets
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Python de base
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION COMPLÈTE À FLASK
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est Flask et comment il fonctionne
[OK] Pourquoi choisir Flask plutôt qu'un autre framework
[OK] Quand utiliser Flask
[OK] Comment installer et configurer Flask
[OK] Les concepts fondamentaux de Flask
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE FLASK ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

Flask est un micro-framework web pour Python qui vous permet de créer
des applications web rapidement et simplement.

[IDEE] MICRO-FRAMEWORK signifie :
- Core minimal (petit et léger)
- Extensible avec des plugins
- Vous contrôlez tout
- Pas de décisions imposées


ANALOGIE SIMPLE [CONSTRUCTION]

Imaginez construire une maison :

DJANGO (Framework Full-Stack) :
[ENTREPRISE] Kit de maison préfabriquée
- Tout est inclus (murs, toit, plomberie, électricité)
- Structure imposée
- Démarrage rapide mais moins flexible

FLASK (Micro-Framework) :
[BRICK] Matériaux de base
- Vous avez les fondations
- Vous choisissez le reste
- Plus de travail initial
- Total contrôle et flexibilité

FASTAPI (Framework API) :
[USINE] Kit industriel moderne
- Optimisé pour les APIs
- Très performant
- Moderne (async natif)


COMPOSANTS DE BASE DE FLASK

Flask vous donne :
[OK] Un serveur web de développement (Werkzeug)
[OK] Un routeur d'URLs (associe URLs -> fonctions Python)
[OK] Un moteur de templates (Jinja2 pour HTML dynamique)
[OK] Des outils de requête/réponse HTTP
[OK] Un système de debug pratique

Flask ne vous donne PAS (mais vous pouvez ajouter) :
[X] ORM de base de données -> Ajoutez SQLAlchemy
[X] Système d'authentification -> Ajoutez Flask-Login
[X] Gestion de formulaires -> Ajoutez Flask-WTF
[X] Interface d'administration -> Ajoutez Flask-Admin
[X] Migration de base de données -> Ajoutez Flask-Migrate


[IDEE] PHILOSOPHIE "BATTERIES NOT INCLUDED"

Flask = Core minimal + Extensions au besoin
- Vous assemblez votre propre stack
- Vous n'emportez que ce dont vous avez besoin
- Liberté totale mais plus de responsabilités
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] FLASK VS AUTRES FRAMEWORKS
# ----------------------------------------------------------------------------

"""
COMPARAISON DÉTAILLÉE


┌─────────────┬────────────┬────────────┬───────────────┐
│             │   FLASK    │   DJANGO   │    FASTAPI    │
├─────────────┼────────────┼────────────┼───────────────┤
│ Taille      │   Léger    │    Lourd   │     Léger     │
│ Courbe      │   Facile   │   Moyenne  │    Facile     │
│ Flexibilité │   Haute    │   Moyenne  │     Haute     │
│ Batteries   │   Non      │    Oui     │     Non       │
│ Async       │   Optionnel│   Depuis   │     Natif     │
│             │            │   3.0      │               │
│ ORM         │   Choix    │   Django   │     Choix     │
│             │            │   ORM      │               │
│ Admin       │   Optionnel│   Inclus   │     Non       │
│ Forms       │   Optionnel│   Inclus   │     Non       │
│ Type Hints  │   Optionnel│   Optionnel│     Requis    │
│ API         │   Possible │   DRF      │    Optimisé   │
│ Templates   │   Jinja2   │   Django   │     Non       │
│             │            │   Templates│    (optionnel)│
└─────────────┴────────────┴────────────┴───────────────┘


FLASK - AVANTAGES [OK]

1. SIMPLICITÉ
   - Démarrage en 5 lignes de code
   - Concepts faciles à comprendre
   - Idéal pour apprendre le web

2. FLEXIBILITÉ
   - Vous choisissez vos outils
   - Pas de structure imposée
   - Facile à adapter à vos besoins

3. LÉGÈRETÉ
   - Petit footprint mémoire
   - Démarrage rapide
   - Parfait pour microservices

4. DOCUMENTATION
   - Très bien documenté
   - Grande communauté
   - Nombreux tutoriels

5. ÉVOLUTIVITÉ
   - Commencez simple
   - Ajoutez des features progressivement
   - Scalable avec la bonne architecture


FLASK - INCONVÉNIENTS [X]

1. PLUS DE DÉCISIONS
   - Choix du ORM
   - Choix de l'auth
   - Structure du projet

2. CONFIGURATION INITIALE
   - Plus de setup que Django
   - Assemblage des extensions

3. CONVENTIONS
   - Moins de "best practices" imposées
   - Risque de mauvaise architecture

4. COMPLEXITÉ CROISSANTE
   - Grands projets deviennent complexes
   - Nécessite discipline et organisation


DJANGO - QUAND LE CHOISIR ?

[OK] Grande application avec admin
[OK] Besoin de tout rapidement
[OK] Équipe habituée à Django
[OK] Application "standard" (blog, e-commerce)
[OK] Pas besoin de flexibilité extrême

[X] API pure (FastAPI mieux)
[X] Microservice léger (Flask mieux)
[X] Architecture non-standard


FASTAPI - QUAND LE CHOISIR ?

[OK] API REST/GraphQL pure
[OK] Besoin de haute performance
[OK] Documentation auto (Swagger/OpenAPI)
[OK] Type hints Python
[OK] Async/await natif

[X] Application avec templates HTML
[X] Besoin d'admin intégré
[X] Équipe pas familière avec async
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] QUAND UTILISER FLASK ?
# ----------------------------------------------------------------------------

"""
CAS D'USAGE IDÉAUX POUR FLASK


1. PROTOTYPES ET MVPs [RAPIDE]
   POURQUOI : Rapidité de développement
   COMMENT : Structure simple, itération rapide
   EXEMPLE : Tester une idée de startup
   
   Code :
   """
from flask import Flask
app = Flask(__name__)

@app.route('/')
def mvp():
    return "MVP landing page"

if __name__ == '__main__':
    app.run()
# -> MVP en 6 lignes !

"""
2. PETITES À MOYENNES APPLICATIONS WEB [MOBILE]
   POURQUOI : Juste ce qu'il faut de features
   QUAND : < 10,000 lignes de code
   EXEMPLES :
   - Blog personnel
   - Portfolio
   - Site de restaurant
   - Outil interne d'entreprise
   
   
3. APIS REST [PLUGIN]
   POURQUOI : Léger et flexible
   QUAND : API simple à moyenne complexité
   EXEMPLES :
   - API mobile backend
   - API pour SPA (React, Vue)
   - Webhooks
   - Intégrations tierces
   
   Code :
   """
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    # Logique de récupération
    return {'id': user_id, 'name': 'Alice'}

"""
4. MICROSERVICES [OBJECTIF]
   POURQUOI : Footprint minimal
   QUAND : Architecture microservices
   EXEMPLES :
   - Service d'authentification
   - Service de paiement
   - Service de notification
   - Service de recherche


5. APPRENTISSAGE [DOCS]
   POURQUOI : Concepts clairs et progressifs
   QUAND : Apprendre le dev web
   AVANTAGES :
   - Comprendre HTTP
   - Apprendre routing, templates, DB
   - Base pour autres frameworks


6. OUTILS INTERNES D'ENTREPRISE [ENTREPRISE]
   POURQUOI : Rapidité, personnalisation
   EXEMPLES :
   - Dashboard de monitoring
   - Outil de reporting
   - Interface de gestion
   - Automation web


QUAND FLASK N'EST PAS IDÉAL [X]

1. TRÈS GRANDE APPLICATION MONOLITHIQUE
   [X] > 50,000 lignes
   [X] Nombreuses apps interconnectées
   [X] Équipe très grande
   -> Préférez Django

2. APPLICATION TEMPS RÉEL
   [X] Chat en temps réel
   [X] Jeu multijoueur
   [X] Streaming de données
   -> Préférez Node.js (Socket.io) ou FastAPI (WebSockets)

3. API HAUTE PERFORMANCE CRITIQUE
   [X] Millions de requêtes/sec
   [X] Latence < 10ms
   [X] Async intensif
   -> Préférez FastAPI ou Go

4. CMS STANDARD
   [X] WordPress-like
   [X] Admin riche requis
   [X] Plugins écosystème
   -> Préférez Django ou Wagtail
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION ÉTAPE PAR ÉTAPE


ÉTAPE 1 : VÉRIFIER PYTHON
------------------------
"""

# Terminal/PowerShell
python --version
# Devrait afficher : Python 3.8.x ou supérieur

# Si pas installé :
# - Windows : python.org
# - Mac : brew install python3
# - Linux : apt install python3 / yum install python3

"""
[IDEE] VERSIONS PYTHON

Minimale : Python 3.7
Recommandée : Python 3.10+
Dernière : Python 3.12 (décembre 2023)

Flask supporte toutes les versions actives de Python.


ÉTAPE 2 : CRÉER UN PROJET
-------------------------
"""

# Créer le dossier projet
mkdir mon_app_flask
cd mon_app_flask

"""
ÉTAPE 3 : ENVIRONNEMENT VIRTUEL
-------------------------------

[IDEE] POURQUOI UN VENV ?

1. ISOLATION [VERROUILLE]
   - Chaque projet a ses propres packages
   - Pas de conflits entre projets
   - Versions indépendantes

2. REPRODUCTIBILITÉ [SYNC]
   - requirements.txt partage dépendances
   - Toute l'équipe a le même environnement
   - Déploiement fiable

3. PROPRETÉ [NETTOYAGE]
   - Python global reste propre
   - Facile de supprimer un projet
   - Test de nouvelles versions safe


SANS VENV ([X] Problèmes)
"""

pip install flask==2.0.0  # Version pour projet A
# 6 mois plus tard...
pip install flask==3.0.0  # Version pour projet B
# -> Projet A cassé ! [!]

"""
AVEC VENV ([OK] Pas de problème)
"""

# Projet A
cd projet_a
python -m venv venv
source venv/bin/activate
pip install flask==2.0.0  # Installé dans venv de projet A

# Projet B
cd ../projet_b
python -m venv venv
source venv/bin/activate
pip install flask==3.0.0  # Installé dans venv de projet B

# Les deux coexistent sans problème ! [OK]

"""
CRÉER LE VENV
"""

# Créer (une seule fois par projet)
python -m venv venv

"""
[IDEE] EXPLICATION : python -m venv venv
                    │    │  │    │
                    │    │  │    └─ Nom du dossier (conventionnellement "venv")
                    │    │  └────── Module venv (virtual environment)
                    │    └───────── Option -m (exécuter un module)
                    └────────────── Python


ACTIVER LE VENV
"""

# Windows PowerShell
venv\Scripts\Activate.ps1

# Windows CMD
venv\Scripts\activate.bat

# Mac/Linux/Git Bash
source venv/bin/activate

# Vous verrez (venv) devant votre prompt :
# (venv) C:\projets\mon_app>

"""
[IDEE] DÉSACTIVER LE VENV
"""
deactivate
# Le (venv) disparaît

"""
[ATTENTION] TOUJOURS ACTIVER AVANT DE TRAVAILLER !

Commande typique :
"""
cd mon_projet
source venv/bin/activate  # ou .ps1 sur Windows
python app.py


"""
ÉTAPE 4 : INSTALLER FLASK
-------------------------
"""

# Venv activé !
pip install flask

"""
[IDEE] QUE SE PASSE-T-IL ?

pip install flask installe :
- Flask lui-même
- Werkzeug (serveur WSGI)
- Jinja2 (moteur de templates)
- Click (CLI)
- ItsDangerous (signatures sécurisées)
- MarkupSafe (échappement HTML)


VÉRIFIER L'INSTALLATION
"""

python -c "import flask; print(flask.__version__)"
# Affiche : 3.0.0 (ou version installée)

# Ou dans Python :
python
>>> import flask
>>> flask.__version__
'3.0.0'
>>> exit()

"""
INSTALLER DES EXTENSIONS (OPTIONNEL POUR DÉBUTER)
"""

pip install flask-sqlalchemy  # ORM base de données
pip install flask-wtf          # Formulaires
pip install flask-login        # Authentification
pip install email-validator    # Validation emails

"""
ÉTAPE 5 : CRÉER requirements.txt
--------------------------------

[IDEE] POURQUOI requirements.txt ?

- Partage les dépendances avec l'équipe
- Reproduit l'environnement ailleurs
- Nécessaire pour déploiement
"""

# Générer requirements.txt
pip freeze > requirements.txt

# Contenu de requirements.txt :
"""
Flask==3.0.0
Werkzeug==3.0.0
Jinja2==3.1.2
...
"""

# Installer depuis requirements.txt (nouveau setup)
pip install -r requirements.txt


"""
ÉTAPE 6 : STRUCTURE DE BASE
---------------------------
"""

# Créer les dossiers
mkdir templates
mkdir static

# Arborescence :
"""
mon_app_flask/
├── venv/               # <- Environnement virtuel (ne pas commiter !)
├── templates/          # <- Templates HTML
├── static/             # <- CSS, JS, images
├── app.py             # <- Application principale
└── requirements.txt   # <- Dépendances
"""


"""
ÉTAPE 7 : .gitignore (SI VOUS UTILISEZ GIT)
-------------------------------------------
"""

# Créer .gitignore
"""
# Python
__pycache__/
*.py[cod]
*$py.class
*.so

# Environnement virtuel
venv/
env/
ENV/

# Flask
instance/
.webassets-cache

# IDE
.vscode/
.idea/
*.swp
*.swo

# Système
.DS_Store
Thumbs.db

# Environnement
.env
.env.local

# Base de données
*.db
*.sqlite
"""

"""
[IDEE] POURQUOI .gitignore ?

NE PAS COMMITER :
[X] venv/ -> Trop gros, recréable avec requirements.txt
[X] __pycache__/ -> Fichiers compilés temporaires
[X] .env -> Secrets et mots de passe
[X] *.db -> Base de données (contient données sensibles)
"""


"""
[DOCS] RÉCAPITULATIF INSTALLATION

[OK] Python installé et vérifié
[OK] Projet créé avec venv
[OK] Flask installé
[OK] Structure de base créée
[OK] requirements.txt généré
[OK] .gitignore configuré (si Git)

[OBJECTIF] VOUS ÊTES PRÊT POUR LE CHAPITRE 1 ! [RAPIDE]
"""


# ============================================================================
# [GUIDE] CHAPITRE 1 : PREMIÈRE APPLICATION FLASK
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer une application Flask minimale
[OK] Comprendre chaque ligne de code
[OK] Lancer le serveur de développement
[OK] Gérer plusieurs routes
[OK] Retourner différents types de contenu
[OK] Utiliser le mode debug
[OK] Comprendre le cycle requête-réponse
"""


# ----------------------------------------------------------------------------
# [DEMARRAGE] HELLO WORLD : DÉCRYPTAGE LIGNE PAR LIGNE
# ----------------------------------------------------------------------------

"""
CRÉER app.py
"""

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, World!"

if __name__ == '__main__':
    app.run(debug=True)

"""
[IDEE] DÉCRYPTAGE COMPLET


LIGNE 1 : from flask import Flask
---------------------------------

[REFLEXION] QUE FAIT-ELLE ?
Importe la classe Flask depuis le module flask

[REFLEXION] POURQUOI Flask avec majuscule ?
Convention Python : les classes commencent par une majuscule

ANALOGIE :
from flask import Flask
       │           │
       │           └─ La classe (le moule)
       └──────────── Le package (la boîte)

C'est comme :
from voiture import Moteur
       │              │
     Package        Classe


LIGNE 2 : app = Flask(__name__)
-------------------------------

[REFLEXION] QUE FAIT-ELLE ?
Crée une INSTANCE de l'application Flask

[REFLEXION] QU'EST-CE QU'UNE INSTANCE ?
Instance = Objet créé à partir d'une classe
Classe = Moule, Instance = Objet fait avec le moule

Flask = Moule
app = Notre application spécifique

[REFLEXION] POURQUOI __name__ ?
__name__ est une variable magique Python :

- Si fichier exécuté directement : __name__ = '__main__'
- Si fichier importé : __name__ = nom du module

Exemple :
"""

# app.py
print(__name__)

# Exécution directe : python app.py
# Affiche : __main__

# Dans autre fichier :
# from app import something
# Affiche : app

"""
[REFLEXION] POURQUOI FLASK UTILISE __name__ ?

Flask utilise __name__ pour :

1. TROUVER LES RESSOURCES
   Flask cherche templates/ et static/ relativement au module
   
2. LOGGING ET DEBUG
   Pour savoir quel module génère les logs


LIGNE 3 : @app.route('/')
-------------------------

[REFLEXION] QUE FAIT-ELLE ?
C'est un DÉCORATEUR qui associe une URL à une fonction

[REFLEXION] QU'EST-CE QU'UN DÉCORATEUR ?
Un décorateur est une fonction qui modifie une autre fonction

Syntaxe :
@decorateur
def fonction():
    pass

Est équivalent à :
def fonction():
    pass
fonction = decorateur(fonction)


DÉCORATEUR ROUTE EN DÉTAIL :
"""

@app.route('/')
def hello():
    return "Hello"

# Est équivalent à :
def hello():
    return "Hello"
hello = app.route('/')(hello)

"""
Le décorateur @app.route('/') fait plusieurs choses :

1. ENREGISTRE LA ROUTE
   Flask ajoute '/' dans sa table de routage
   
2. ASSOCIE LA FONCTION
   Quand quelqu'un visite '/', Flask appelle hello()
   
3. RETOURNE UNE FONCTION MODIFIÉE
   La fonction est "wrappée" pour gérer la requête HTTP


VISUALISATION DU ROUTAGE :

Table de routage Flask :
┌─────────┬──────────────┬─────────┐
│  URL    │  Fonction    │ Méthodes│
├─────────┼──────────────┼─────────┤
│  /      │  hello()     │  GET    │
└─────────┴──────────────┴─────────┘

Quand requête arrive :
GET / -> Flask cherche dans la table -> Trouve hello() -> Appelle hello()


LIGNE 4 : def hello():
----------------------

[REFLEXION] QUE FAIT-ELLE ?
Définit une FONCTION VIEW (vue)

[REFLEXION] QU'EST-CE QU'UNE VIEW ?
View = Fonction qui gère une URL
Input : Requête HTTP
Output : Réponse HTTP


LIGNE 5 : return "Hello, World!"
--------------------------------

[REFLEXION] QUE FAIT-ELLE ?
Retourne le contenu à envoyer au navigateur

[REFLEXION] QUE PEUT-ON RETURN ?

1. STRING simple (comme ici)
   return "Hello"
   -> Content-Type: text/html
   -> Status Code: 200

2. TUPLE (contenu, status_code)
   return "Not Found", 404
   
3. TUPLE (contenu, status_code, headers)
   return "OK", 200, {'X-Custom': 'value'}
   
4. DICT (converti en JSON automatiquement)
   return {'message': 'Hello'}
   -> Content-Type: application/json
   
5. RESPONSE OBJECT (plus de contrôle)
   from flask import make_response
   resp = make_response("Hello")
   resp.headers['X-Custom'] = 'value'
   return resp


LIGNES 7-8 : if __name__ == '__main__':
---------------------------------------

[REFLEXION] QUE FONT-ELLES ?
Lancent le serveur seulement si fichier exécuté directement

[REFLEXION] POURQUOI ?

Scénario 1 : Exécution directe
python app.py
-> __name__ == '__main__' est True
-> app.run() s'exécute
-> Serveur démarre [OK]

Scénario 2 : Import dans autre fichier
# autre_fichier.py
from app import app
-> __name__ == 'app' (pas '__main__')
-> app.run() ne s'exécute pas
-> Serveur ne démarre pas [OK]

AVANTAGE :
- Évite de lancer le serveur accidentellement lors d'imports
- Permet d'importer app pour les tests
- Pattern standard Python


LIGNE 8 : app.run(debug=True)
-----------------------------

[REFLEXION] QUE FAIT-ELLE ?
Lance le serveur de développement Flask

[REFLEXION] QUE FAIT debug=True ?

1. AUTO-RELOAD [SYNC]
   Détecte changements de code
   -> Redémarre automatiquement le serveur
   
   Modifiez app.py -> Sauvegardez -> Serveur redémarre !

2. DEBUGGER INTERACTIF [BUG]
   En cas d'erreur :
   -> Affiche traceback détaillé
   -> Console Python interactive dans le navigateur
   -> Inspect variables à chaque étape

3. MESSAGES DÉTAILLÉS [NOTE]
   Logs verbeux pour comprendre ce qui se passe

[ATTENTION] JAMAIS debug=True EN PRODUCTION !
Raisons de sécurité :
- Expose le code source
- Console Python = shell distant
- Informations sensibles dans les tracebacks


OPTIONS DE app.run() :
"""

app.run(
    host='0.0.0.0',      # Écoute sur toutes les interfaces (accessible réseau)
    port=5000,           # Port (défaut: 5000)
    debug=True,          # Mode debug
    use_reloader=True,   # Auto-reload (défaut: True si debug=True)
    use_debugger=True,   # Debugger interactif (défaut: True si debug=True)
    threaded=True,       # Multi-threading (défaut: True)
)


# ----------------------------------------------------------------------------
# [RAPIDE] LANCER L'APPLICATION
# ----------------------------------------------------------------------------

"""
MÉTHODE 1 : EXÉCUTION DIRECTE (Recommandée pour débuter)
"""

python app.py

# Output :
"""
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 123-456-789
"""

"""
[IDEE] COMPRENDRE L'OUTPUT

"Serving Flask app 'app'"
-> Nom du module Flask

"Debug mode: on"
-> Mode debug activé

"Running on http://127.0.0.1:5000"
-> URL pour accéder à l'app
  127.0.0.1 = localhost = votre machine
  :5000 = port 5000

"Debugger PIN: 123-456-789"
-> Code pour déverrouiller le debugger dans le navigateur
  (en cas d'erreur)


MÉTHODE 2 : FLASK CLI (Méthode pro)
"""

# 1. Définir l'app
export FLASK_APP=app.py       # Mac/Linux
set FLASK_APP=app.py          # Windows CMD
$env:FLASK_APP="app.py"       # Windows PowerShell

# 2. Lancer
flask run

# Avec debug
flask run --debug

# Sur port spécifique
flask run --port 8000

# Accessible depuis le réseau
flask run --host=0.0.0.0

"""
[IDEE] AVANTAGES FLASK CLI

[OK] Plus professionnel
[OK] Plus d'options
[OK] Standard Flask
[OK] Fonctionne avec factory pattern


MÉTHODE 3 : AVEC CONFIGURATION
"""

# config.py
class Config:
    DEBUG = True
    SECRET_KEY = 'dev-secret-key'

# app.py
app.config.from_object('config.Config')
app.run()


"""
OUVRIR DANS LE NAVIGATEUR

1. Ouvrez votre navigateur
2. Allez à : http://127.0.0.1:5000
   ou : http://localhost:5000
3. Vous devriez voir : "Hello, World!"

[OK] Félicitations ! Votre première app Flask fonctionne ! [BRAVO]


ARRÊTER LE SERVEUR

Terminal : CTRL + C
Le serveur s'arrête proprement
"""


# ----------------------------------------------------------------------------
# [WEB] ROUTES MULTIPLES
# ----------------------------------------------------------------------------

"""
AJOUTER PLUSIEURS PAGES
"""

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    """
    Page d'accueil
    URL : http://localhost:5000/
    """
    return "Bienvenue sur la page d'accueil !"

@app.route('/about')
def about():
    """
    Page à propos
    URL : http://localhost:5000/about
    """
    return "Ceci est la page À Propos"

@app.route('/contact')
def contact():
    """
    Page contact
    URL : http://localhost:5000/contact
    """
    return """
    <h1>Contactez-nous</h1>
    <p>Email : contact@example.com</p>
    <p>Tél : 01 23 45 67 89</p>
    """

@app.route('/services')
def services():
    """
    Page services
    URL : http://localhost:5000/services
    """
    return """
    <h1>Nos Services</h1>
    <ul>
        <li>Service 1</li>
        <li>Service 2</li>
        <li>Service 3</li>
    </ul>
    """

if __name__ == '__main__':
    app.run(debug=True)

"""
[IDEE] TESTER LES ROUTES

Lancez le serveur, puis visitez :
- http://localhost:5000/         -> Page d'accueil
- http://localhost:5000/about    -> À propos
- http://localhost:5000/contact  -> Contact
- http://localhost:5000/services -> Services
- http://localhost:5000/xyz      -> 404 Not Found


[REFLEXION] QUE SE PASSE-T-IL POUR /xyz ?

1. Requête arrive : GET /xyz
2. Flask cherche dans sa table de routage
3. Aucune route ne correspond
4. Flask retourne automatiquement une page 404

Table de routage :
┌───────────┬──────────────┐
│   URL     │   Fonction   │
├───────────┼──────────────┤
│   /       │   home()     │
│   /about  │   about()    │
│   /contact│   contact()  │
│   /services│  services() │
└───────────┴──────────────┘
      v
  /xyz ? -> Non trouvé -> 404
"""


# ----------------------------------------------------------------------------
# [NOTE] RETOURNER DIFFÉRENTS CONTENUS
# ----------------------------------------------------------------------------

"""
1. HTML SIMPLE
"""

@app.route('/html')
def html_page():
    return """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Ma Page</title>
        <style>
            body { font-family: Arial; }
            h1 { color: blue; }
        </style>
    </head>
    <body>
        <h1>Titre</h1>
        <p>Contenu de la page</p>
    </body>
    </html>
    """
# -> Content-Type: text/html

"""
2. JSON (pour APIs)
"""

@app.route('/api/data')
def api_data():
    return {
        'status': 'success',
        'data': {
            'users': 150,
            'posts': 1250
        }
    }
# -> Content-Type: application/json
# Flask convertit automatiquement dict en JSON !

"""
3. TEXTE BRUT
"""

@app.route('/text')
def text_page():
    from flask import make_response
    resp = make_response("Ceci est du texte brut")
    resp.headers['Content-Type'] = 'text/plain'
    return resp

"""
4. STATUT HTTP PERSONNALISÉ
"""

@app.route('/not-found')
def not_found():
    return "Cette ressource n'existe pas", 404

@app.route('/forbidden')
def forbidden():
    return "Accès interdit", 403

@app.route('/created')
def created():
    return "Ressource créée", 201

"""
5. HEADERS PERSONNALISÉS
"""

@app.route('/custom-headers')
def custom_headers():
    return "OK", 200, {
        'X-Custom-Header': 'MyValue',
        'X-Powered-By': 'Flask'
    }

"""
6. REDIRECTION
"""

from flask import redirect, url_for

@app.route('/old-page')
def old_page():
    # Rediriger vers nouvelle page
    return redirect(url_for('new_page'))

@app.route('/new-page')
def new_page():
    return "Nouvelle page !"

# Ou redirection externe :
@app.route('/google')
def to_google():
    return redirect('https://www.google.com')


# ----------------------------------------------------------------------------
# [SYNC] COMPRENDRE LE CYCLE REQUÊTE-RÉPONSE
# ----------------------------------------------------------------------------

"""
LE CYCLE COMPLET


1. CLIENT (Navigateur)
   │
   │ Envoie requête HTTP
   │ GET http://localhost:5000/about
   v

2. SERVEUR FLASK
   │
   ├─ Reçoit la requête
   │  - Méthode : GET
   │  - URL : /about
   │  - Headers : User-Agent, Accept, etc.
   │
   ├─ Routing
   │  - Cherche /about dans table de routage
   │  - Trouve about()
   │
   ├─ Appelle la fonction view
   │  about() s'exécute
   │
   ├─ Génère la réponse
   │  - Contenu : "Ceci est la page À Propos"
   │  - Status : 200 OK
   │  - Headers : Content-Type: text/html
   │
   v

3. RÉPONSE HTTP
   │
   │ Envoyée au client
   │
   v

4. CLIENT (Navigateur)
   │
   ├─ Reçoit la réponse
   ├─ Parse le HTML
   └─ Affiche la page


VISUALISATION DÉTAILLÉE :


REQUÊTE HTTP (de navigateur vers Flask)
----------------------------------------
GET /about HTTP/1.1
Host: localhost:5000
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml
Accept-Language: fr-FR,fr;q=0.9
Connection: keep-alive


RÉPONSE HTTP (de Flask vers navigateur)
----------------------------------------
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 32
Server: Werkzeug/3.0.0 Python/3.11.0
Date: Thu, 18 Dec 2025 10:30:00 GMT

Ceci est la page À Propos


[IDEE] COMPOSANTS D'UNE REQUÊTE HTTP

1. MÉTHODE (GET, POST, PUT, DELETE, etc.)
2. URL (/about)
3. VERSION HTTP (HTTP/1.1)
4. HEADERS (métadonnées)
5. BODY (optionnel, pour POST/PUT)


[IDEE] COMPOSANTS D'UNE RÉPONSE HTTP

1. VERSION HTTP (HTTP/1.1)
2. STATUS CODE (200, 404, 500, etc.)
3. STATUS TEXT (OK, Not Found, etc.)
4. HEADERS (métadonnées)
5. BODY (contenu)


STATUS CODES COURANTS

2xx - Succès
200 OK : Requête réussie
201 Created : Ressource créée
204 No Content : Succès sans contenu

3xx - Redirection
301 Moved Permanently : Redirection permanente
302 Found : Redirection temporaire
304 Not Modified : Ressource non modifiée

4xx - Erreur client
400 Bad Request : Requête invalide
401 Unauthorized : Non authentifié
403 Forbidden : Non autorisé
404 Not Found : Ressource introuvable
422 Unprocessable Entity : Données invalides

5xx - Erreur serveur
500 Internal Server Error : Erreur serveur
502 Bad Gateway : Problème de proxy
503 Service Unavailable : Service indisponible
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 1 : SITE MULTI-PAGES
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer un site avec navigation


CAHIER DES CHARGES :

1. Page d'accueil (/)
   - Titre "Bienvenue"
   - Description
   - Liens vers autres pages

2. Page À propos (/about)
   - Informations sur vous
   - Lien retour accueil

3. Page Projets (/projects)
   - Liste de 3 projets
   - Lien retour accueil

4. Page Contact (/contact)
   - Email
   - Téléphone
   - Lien retour accueil


SOLUTION :
"""

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return """
    <!DOCTYPE html>
    <html lang="fr">
    <head>
        <meta charset="UTF-8">
        <title>Mon Site</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                max-width: 800px;
                margin: 50px auto;
                padding: 20px;
            }
            nav {
                background: #333;
                padding: 15px;
                margin-bottom: 30px;
            }
            nav a {
                color: white;
                text-decoration: none;
                margin-right: 20px;
            }
            nav a:hover {
                text-decoration: underline;
            }
        </style>
    </head>
    <body>
        <nav>
            <a href="/">Accueil</a>
            <a href="/about">À propos</a>
            <a href="/projects">Projets</a>
            <a href="/contact">Contact</a>
        </nav>
        
        <h1>Bienvenue sur mon site !</h1>
        <p>Je suis un développeur passionné par le web.</p>
        <p>Découvrez mes projets et n'hésitez pas à me contacter !</p>
    </body>
    </html>
    """

@app.route('/about')
def about():
    return """
    <!DOCTYPE html>
    <html lang="fr">
    <head>
        <meta charset="UTF-8">
        <title>À propos</title>
    </head>
    <body>
        <h1>À propos de moi</h1>
        <p>Je suis développeur web spécialisé en Python et Flask.</p>
        <p>J'aime créer des applications web élégantes et performantes.</p>
        <a href="/"><- Retour à l'accueil</a>
    </body>
    </html>
    """

@app.route('/projects')
def projects():
    return """
    <!DOCTYPE html>
    <html lang="fr">
    <head>
        <meta charset="UTF-8">
        <title>Mes Projets</title>
    </head>
    <body>
        <h1>Mes Projets</h1>
        <ul>
            <li><strong>Blog Personnel</strong> - Application Flask avec SQLAlchemy</li>
            <li><strong>API REST</strong> - API pour application mobile</li>
            <li><strong>Dashboard</strong> - Interface de monitoring</li>
        </ul>
        <a href="/"><- Retour à l'accueil</a>
    </body>
    </html>
    """

@app.route('/contact')
def contact():
    return """
    <!DOCTYPE html>
    <html lang="fr">
    <head>
        <meta charset="UTF-8">
        <title>Contact</title>
    </head>
    <body>
        <h1>Contactez-moi</h1>
        <p><strong>Email :</strong> contact@example.com</p>
        <p><strong>Téléphone :</strong> +33 1 23 45 67 89</p>
        <p><strong>LinkedIn :</strong> linkedin.com/in/monprofil</p>
        <a href="/"><- Retour à l'accueil</a>
    </body>
    </html>
    """

if __name__ == '__main__':
    app.run(debug=True)

"""
[OBJECTIF] TESTEZ VOTRE SOLUTION

1. Lancez : python app.py
2. Visitez http://localhost:5000
3. Naviguez entre les pages
4. Vérifiez que tous les liens fonctionnent


[IDEE] LIMITATIONS DE CETTE APPROCHE

[X] HTML répété (nav, head, etc.)
[X] Difficile à maintenir
[X] Pas de styles partagés
[X] Code HTML dans Python (illisible)

-> Solution au Chapitre 3 : Templates Jinja2 !
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 1
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

[OK] Anatomie d'une application Flask
[OK] Signification de chaque ligne
[OK] Rôle de __name__ et app.run()
[OK] Décorateurs et routing
[OK] Différentes méthodes de lancement
[OK] Routes multiples
[OK] Retourner divers contenus
[OK] Cycle requête-réponse HTTP
[OK] Premier exercice pratique


[CLE] POINTS CLÉS

1. Flask(__name__) crée l'application
2. @app.route() associe URL -> fonction
3. Fonction view retourne contenu pour navigateur
4. debug=True pour développement
5. if __name__ == '__main__': pour éviter double exécution


[OBJECTIF] AVANT DE CONTINUER

Assurez-vous de pouvoir :
[OK] Créer une app Flask de zéro
[OK] Ajouter plusieurs routes
[OK] Lancer le serveur
[OK] Comprendre le flux d'une requête
[OK] Retourner HTML simple


-> PROCHAINE ÉTAPE : Chapitre 2 - Routes Dynamiques !

Vous allez apprendre :
- Routes avec paramètres (/user/<username>)
- Types de paramètres (int, float, path, uuid)
- Méthodes HTTP (GET, POST, PUT, DELETE)
- url_for() pour générer des URLs
- Et bien plus !

Prêt ? C'est parti ! [RAPIDE]
"""


# ============================================================================
# CE FICHIER CONTINUERA AVEC LES CHAPITRES 2, 3, ET 4...
# POUR NE PAS DÉPASSER LA LIMITE, JE VAIS CRÉER DES FICHIERS SÉPARÉS
# ============================================================================
# ============================================================================
# [LIVRE] FLASK - PARTIE 1 (SUITE) : ROUTES, TEMPLATES ET FICHIERS STATIQUES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 2 : Routes et URLs Dynamiques
# - Chapitre 3 : Templates Jinja2
# - Chapitre 4 : Fichiers Statiques
#
# [TEMPS] TEMPS : ~4-6 heures
# [DOCS] PRÉREQUIS : Chapitre 0 et 1 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 2 : ROUTES ET URLs DYNAMIQUES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des routes dynamiques avec paramètres
[OK] Utiliser différents types de paramètres (int, float, path, uuid)
[OK] Gérer plusieurs paramètres dans une URL
[OK] Créer des routes optionnelles
[OK] Gérer différentes méthodes HTTP (GET, POST, PUT, DELETE)
[OK] Générer des URLs avec url_for()
[OK] Créer des routes avec expressions régulières
"""


# ----------------------------------------------------------------------------
# [MELANGE] ROUTES DYNAMIQUES : POURQUOI ET COMMENT
# ----------------------------------------------------------------------------

"""
[REFLEXION] PROBLÈME : URLS STATIQUES

Imaginez un site avec des profils utilisateurs...
"""

# [X] APPROCHE NAÏVE (Ne faites JAMAIS ça !)
@app.route('/user/alice')
def user_alice():
    return "Profil d'Alice"

@app.route('/user/bob')
def user_bob():
    return "Profil de Bob"

@app.route('/user/charlie')
def user_charlie():
    return "Profil de Charlie"

# Avec 10,000 utilisateurs ? [!]
# -> 10,000 routes !
# -> Code impossible à maintenir

"""
[OK] SOLUTION : ROUTES DYNAMIQUES
"""

@app.route('/user/<username>')
def user_profile(username):
    """
    [IDEE] ROUTE DYNAMIQUE
    
    <username> = PARAMÈTRE VARIABLE
    - Capture n'importe quelle valeur dans l'URL
    - La passe automatiquement à la fonction
    
    Une seule route pour TOUS les utilisateurs !
    """
    return f"Profil de {username}"

"""
[REFLEXION] COMMENT ÇA MARCHE ?

URL demandée : /user/alice
             │      │
             │      └─ Valeur capturée : 'alice'
             └──────── Route matchée : /user/<username>

Flask :
1. Voit que /user/alice matche /user/<username>
2. Extrait 'alice' comme valeur de username
3. Appelle user_profile(username='alice')
4. Fonction reçoit username = 'alice'


EXEMPLES D'URLS MATCHÉES :

/user/alice     -> username = 'alice'
/user/bob       -> username = 'bob'
/user/john123   -> username = 'john123'
/user/marie_2024 -> username = 'marie_2024'


[IDEE] RÈGLES IMPORTANTES

1. Nom du paramètre URL doit correspondre au nom de l'argument fonction
   @app.route('/user/<username>')
   def profile(username):  # [OK] Nom identique
   
   @app.route('/user/<username>')
   def profile(name):  # [X] Erreur !

2. Un paramètre = une seule "partie" d'URL (entre /)
   /user/<username>      [OK] OK
   /user/<first>/<last>  [OK] OK (deux paramètres)
   /<username>/profile   [OK] OK
   /user<username>       [X] username doit être seul entre /
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] TYPES DE PARAMÈTRES
# ----------------------------------------------------------------------------

"""
Par défaut, paramètres = STRING
Mais on peut spécifier le type !


SYNTAXE : <type:nom_variable>


1. STRING (par défaut)
---------------------
"""

@app.route('/user/<username>')
def user_profile(username):
    """
    username est automatiquement une string
    
    Accepte : lettres, chiffres, - et _
    N'accepte PAS : /
    """
    return f"<h1>Profil de {username}</h1>"

# Exemples d'URLs :
# /user/alice       [OK] username = 'alice'
# /user/bob123      [OK] username = 'bob123'
# /user/marie-durand [OK] username = 'marie-durand'
# /user/john_doe    [OK] username = 'john_doe'
# /user/alice/posts [X] 404 (/ non autorisé dans string)

"""
2. INT (entier positif)
----------------------
"""

@app.route('/post/<int:post_id>')
def show_post(post_id):
    """
    [IDEE] <int:post_id>
    
    - Accepte SEULEMENT des nombres entiers positifs
    - Convertit automatiquement en int Python
    - Si pas un nombre -> 404
    
    POURQUOI C'EST UTILE ?
    - Validation automatique (pas besoin de vérifier)
    - Type-safe (post_id est toujours un int)
    - URLs plus claires
    """
    # post_id est un int, pas une string !
    # Pas besoin de int(post_id)
    
    return f"<h1>Post #{post_id}</h1>"

# Exemples d'URLs :
# /post/1       [OK] post_id = 1 (int)
# /post/123     [OK] post_id = 123 (int)
# /post/999999  [OK] post_id = 999999 (int)
# /post/0       [OK] post_id = 0 (int)
# /post/abc     [X] 404 (pas un nombre)
# /post/-5      [X] 404 (négatif non autorisé)
# /post/12.5    [X] 404 (décimal non autorisé)

"""
[IDEE] UTILISATION TYPIQUE : IDs de base de données
"""

@app.route('/post/<int:post_id>')
def show_post(post_id):
    # Chercher en base de données
    # post = Post.query.get(post_id)  # SQLAlchemy
    
    # Simulation
    posts = {
        1: {'title': 'Premier post', 'content': 'Contenu...'},
        2: {'title': 'Deuxième post', 'content': 'Autre contenu...'}
    }
    
    post = posts.get(post_id)
    
    if post:
        return f"""
        <h1>{post['title']}</h1>
        <p>{post['content']}</p>
        """
    else:
        return "Post non trouvé", 404

"""
3. FLOAT (nombre décimal)
------------------------
"""

@app.route('/product/<int:product_id>/price/<float:price>')
def product_price(product_id, price):
    """
    [IDEE] <float:price>
    
    - Accepte nombres décimaux (avec .)
    - Convertit automatiquement en float Python
    """
    return f"Produit #{product_id} coûte {price:.2f}€"

# Exemples d'URLs :
# /product/1/price/19.99    [OK] product_id=1, price=19.99
# /product/5/price/100      [OK] product_id=5, price=100.0 (converti en float)
# /product/2/price/9.5      [OK] product_id=2, price=9.5
# /product/1/price/abc      [X] 404 (pas un nombre)

"""
4. PATH (accepte les slashes /)
-------------------------------
"""

@app.route('/files/<path:filepath>')
def show_file(filepath):
    """
    [IDEE] <path:filepath>
    
    DIFFÉRENCE avec string :
    - string : N'accepte PAS /
    - path : Accepte /
    
    UTILISATION : Chemins de fichiers
    """
    return f"Fichier demandé : {filepath}"

# Exemples d'URLs :
# /files/document.pdf              [OK] filepath = 'document.pdf'
# /files/folder/file.txt           [OK] filepath = 'folder/file.txt'
# /files/a/b/c/d/document.pdf      [OK] filepath = 'a/b/c/d/document.pdf'
# /files/images/2024/photo.jpg     [OK] filepath = 'images/2024/photo.jpg'

"""
[IDEE] CAS D'USAGE : Servir des fichiers
"""

@app.route('/files/<path:filepath>')
def serve_file(filepath):
    from flask import send_from_directory
    import os
    
    # Dossier racine des fichiers
    files_dir = '/path/to/files'
    
    # Vérifications de sécurité
    full_path = os.path.join(files_dir, filepath)
    
    if os.path.exists(full_path) and os.path.isfile(full_path):
        return send_from_directory(files_dir, filepath)
    else:
        return "Fichier non trouvé", 404

"""
5. UUID (Universally Unique Identifier)
---------------------------------------
"""

@app.route('/object/<uuid:object_id>')
def show_object(object_id):
    """
    [IDEE] <uuid:object_id>
    
    - Accepte SEULEMENT des UUIDs valides
    - Format : 8-4-4-4-12 caractères hexadécimaux
    - Exemple : 550e8400-e29b-41d4-a716-446655440000
    
    UTILISATION :
    - IDs distribués
    - Clés primaires non-séquentielles
    - Sécurité (non devinable)
    """
    return f"Object ID: {object_id}"

# Exemples d'URLs :
# /object/550e8400-e29b-41d4-a716-446655440000  [OK] UUID valide
# /object/123e4567-e89b-12d3-a456-426614174000  [OK] UUID valide
# /object/not-a-uuid                            [X] 404
# /object/123                                   [X] 404

"""
[IDEE] GÉNÉRATION D'UUID EN PYTHON
"""

import uuid

# Générer un UUID
new_id = uuid.uuid4()
# UUID('550e8400-e29b-41d4-a716-446655440000')

# Utiliser dans URL
url = f"/object/{new_id}"
# /object/550e8400-e29b-41d4-a716-446655440000


# ----------------------------------------------------------------------------
# [NOMBRE] PARAMÈTRES MULTIPLES
# ----------------------------------------------------------------------------

"""
COMBINER PLUSIEURS PARAMÈTRES
"""

@app.route('/blog/<int:year>/<int:month>/<int:day>')
def blog_archive(year, month, day):
    """
    Archive de blog par date
    
    URL : /blog/2024/12/18
    """
    return f"""
    <h1>Archive du {day:02d}/{month:02d}/{year}</h1>
    <p>Articles publiés ce jour-là...</p>
    """

# /blog/2024/12/18    -> year=2024, month=12, day=18
# /blog/2024/1/5      -> year=2024, month=1, day=5
# /blog/abc/12/18     -> 404 (year n'est pas int)

"""
[IDEE] VALIDATION SUPPLÉMENTAIRE
"""

@app.route('/blog/<int:year>/<int:month>/<int:day>')
def blog_archive(year, month, day):
    # Flask valide déjà que ce sont des int
    # Mais on peut ajouter validation métier
    
    if not (1 <= month <= 12):
        return "Mois invalide", 400
    
    if not (1 <= day <= 31):
        return "Jour invalide", 400
    
    if year < 2000 or year > 2030:
        return "Année invalide", 400
    
    return f"Archive du {day:02d}/{month:02d}/{year}"

"""
EXEMPLE : E-COMMERCE
"""

@app.route('/shop/<category>/<int:product_id>')
def product(category, product_id):
    """
    Produit dans une catégorie
    
    URL : /shop/electronics/123
    """
    return f"""
    <h1>Produit #{product_id}</h1>
    <p>Catégorie : {category}</p>
    """

# /shop/electronics/123    -> category='electronics', product_id=123
# /shop/books/456          -> category='books', product_id=456
# /shop/clothing/789       -> category='clothing', product_id=789

"""
EXEMPLE : RÉSEAU SOCIAL
"""

@app.route('/user/<username>/posts/<int:post_id>')
def user_post(username, post_id):
    """
    Post spécifique d'un utilisateur
    
    URL : /user/alice/posts/42
    """
    return f"""
    <h1>Post #{post_id} de {username}</h1>
    """

# /user/alice/posts/42     -> username='alice', post_id=42
# /user/bob/posts/100      -> username='bob', post_id=100


# ----------------------------------------------------------------------------
# [SYNC] PARAMÈTRES OPTIONNELS
# ----------------------------------------------------------------------------

"""
[REFLEXION] PROBLÈME : Rendre un paramètre optionnel

Cas d'usage : /blog/ -> Tous les posts
              /blog/2024 -> Posts de 2024
"""

# [X] APPROCHE NAÏVE (duplication)
@app.route('/blog/')
def blog_all():
    return "Tous les posts"

@app.route('/blog/<int:year>')
def blog_year(year):
    return f"Posts de {year}"

"""
[OK] MEILLEURE APPROCHE : Deux routes, une fonction
"""

@app.route('/blog/')
@app.route('/blog/<int:year>')
def blog(year=None):
    """
    [IDEE] DEUX DÉCORATEURS @app.route()
    
    Les deux URLs pointent vers la même fonction !
    
    /blog/       -> year = None (valeur par défaut)
    /blog/2024   -> year = 2024
    """
    if year is None:
        return "<h1>Tous les posts</h1>"
    else:
        return f"<h1>Posts de {year}</h1>"

# Exemples :
# /blog/       [OK] year = None
# /blog/2024   [OK] year = 2024
# /blog/2023   [OK] year = 2023

"""
EXEMPLE PLUS COMPLEXE : Pagination optionnelle
"""

@app.route('/posts/')
@app.route('/posts/page/<int:page>')
def posts_list(page=1):
    """
    Liste des posts avec pagination
    
    /posts/           -> page 1 par défaut
    /posts/page/2     -> page 2
    /posts/page/10    -> page 10
    """
    posts_per_page = 10
    start = (page - 1) * posts_per_page
    end = start + posts_per_page
    
    return f"""
    <h1>Page {page}</h1>
    <p>Affichage des posts {start+1} à {end}</p>
    <a href="/posts/page/{page-1}"><- Précédent</a> |
    <a href="/posts/page/{page+1}">Suivant -></a>
    """

"""
MULTIPLE PARAMÈTRES OPTIONNELS
"""

@app.route('/archive/')
@app.route('/archive/<int:year>')
@app.route('/archive/<int:year>/<int:month>')
def archive(year=None, month=None):
    """
    Archive flexible
    
    /archive/            -> Tout
    /archive/2024        -> Année 2024
    /archive/2024/12     -> Décembre 2024
    """
    if year is None:
        return "Toutes les archives"
    elif month is None:
        return f"Archives de {year}"
    else:
        return f"Archives de {month:02d}/{year}"


# ----------------------------------------------------------------------------
# [WEB] MÉTHODES HTTP
# ----------------------------------------------------------------------------

"""
[REFLEXION] QU'EST-CE QUE LES MÉTHODES HTTP ?

HTTP définit plusieurs "verbes" pour différentes actions :

GET    -> Récupérer des données (lecture)
POST   -> Envoyer des données (création)
PUT    -> Modifier des données (mise à jour complète)
PATCH  -> Modifier partiellement
DELETE -> Supprimer des données


PAR DÉFAUT : Routes acceptent SEULEMENT GET
"""

@app.route('/data')
def get_data():
    """Cette route accepte SEULEMENT GET"""
    return "Données"

# GET /data      [OK] OK
# POST /data     [X] 405 Method Not Allowed
# PUT /data      [X] 405 Method Not Allowed
# DELETE /data   [X] 405 Method Not Allowed

"""
AUTORISER PLUSIEURS MÉTHODES
"""

from flask import request

@app.route('/api/user', methods=['GET', 'POST'])
def user_api():
    """
    [IDEE] methods=['GET', 'POST']
    
    Cette route accepte GET ET POST
    """
    if request.method == 'GET':
        # Récupérer utilisateur
        return {'name': 'Alice', 'email': 'alice@example.com'}
    
    elif request.method == 'POST':
        # Créer utilisateur
        data = request.get_json()
        return {'message': 'Utilisateur créé', 'data': data}, 201

"""
[IDEE] request.method

Variable globale qui contient la méthode HTTP de la requête actuelle
- request.method = 'GET' pour requête GET
- request.method = 'POST' pour requête POST
- etc.


EXEMPLE COMPLET : CRUD
"""

@app.route('/api/posts', methods=['GET', 'POST'])
def posts_api():
    """Liste et création de posts"""
    
    if request.method == 'GET':
        # Lister tous les posts
        posts = [
            {'id': 1, 'title': 'Post 1'},
            {'id': 2, 'title': 'Post 2'}
        ]
        return {'posts': posts}
    
    elif request.method == 'POST':
        # Créer un nouveau post
        data = request.get_json()
        # En vrai : sauvegarder en DB
        return {'message': 'Post créé', 'data': data}, 201

@app.route('/api/posts/<int:post_id>', methods=['GET', 'PUT', 'DELETE'])
def post_detail(post_id):
    """Récupération, modification et suppression d'un post"""
    
    if request.method == 'GET':
        # Récupérer un post
        # post = Post.query.get_or_404(post_id)
        return {'id': post_id, 'title': 'Mon post'}
    
    elif request.method == 'PUT':
        # Modifier un post
        data = request.get_json()
        # En vrai : mettre à jour en DB
        return {'message': 'Post modifié', 'data': data}
    
    elif request.method == 'DELETE':
        # Supprimer un post
        # Post.query.filter_by(id=post_id).delete()
        # db.session.commit()
        return '', 204  # 204 No Content

"""
[IDEE] PATTERN REST

Collection :
GET    /api/posts       -> Lister tous
POST   /api/posts       -> Créer nouveau

Élément :
GET    /api/posts/123   -> Récupérer post 123
PUT    /api/posts/123   -> Modifier post 123
DELETE /api/posts/123   -> Supprimer post 123


FORMULAIRES HTML
"""

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    """
    GET : Afficher le formulaire
    POST : Traiter la soumission
    """
    if request.method == 'POST':
        # Récupérer données du formulaire
        name = request.form.get('name')
        email = request.form.get('email')
        message = request.form.get('message')
        
        # Traiter (envoyer email, sauvegarder, etc.)
        return f"Merci {name} ! Message reçu."
    
    # GET : Afficher formulaire
    return """
    <form method="POST">
        <input type="text" name="name" placeholder="Nom" required>
        <input type="email" name="email" placeholder="Email" required>
        <textarea name="message" placeholder="Message" required></textarea>
        <button type="submit">Envoyer</button>
    </form>
    """

"""
[IDEE] request.form vs request.get_json()

request.form :
- Données de formulaire HTML
- Content-Type: application/x-www-form-urlencoded
- <form> standard

request.get_json() :
- Données JSON (APIs)
- Content-Type: application/json
- Requêtes AJAX, fetch(), axios, etc.
"""


# ----------------------------------------------------------------------------
# [LIEN] URL BUILDING AVEC url_for()
# ----------------------------------------------------------------------------

"""
[REFLEXION] PROBLÈME : URLs EN DUR

Imaginez que vous changez vos routes...
"""

# Version initiale
@app.route('/user/<username>')
def profile(username):
    return f"<a href='/user/{username}/posts'>Mes posts</a>"

# 6 mois plus tard, vous changez la route :
@app.route('/profile/<username>')  # <- URL changée !
def profile(username):
    # Lien cassé ! Pointe toujours vers /user/...
    return f"<a href='/user/{username}/posts'>Mes posts</a>"

"""
[OK] SOLUTION : url_for()

url_for() génère des URLs à partir du NOM DE LA FONCTION
"""

from flask import url_for

@app.route('/profile/<username>')
def profile(username):
    """
    [IDEE] url_for('nom_fonction', paramètre=valeur)
    
    Génère l'URL de la fonction spécifiée
    """
    posts_url = url_for('user_posts', username=username)
    return f"<a href='{posts_url}'>Mes posts</a>"

@app.route('/profile/<username>/posts')
def user_posts(username):
    return f"Posts de {username}"

"""
[IDEE] COMMENT ÇA MARCHE ?

url_for('user_posts', username='alice')
         │             │
         │             └─ Paramètres à passer
         └───────────────── Nom de la fonction (pas la route !)

Flask :
1. Trouve la fonction user_posts
2. Regarde sa route : /profile/<username>/posts
3. Remplace <username> par 'alice'
4. Retourne : '/profile/alice/posts'


AVANTAGES [OBJECTIF]

1. URLS CENTRALISÉES
   Changez la route -> url_for() s'adapte automatiquement !

2. ÉVITE LES ERREURS
   Typo dans URL -> Erreur au démarrage (détectable)
   Typo en string -> Lien cassé (pas détectable)

3. PARAMÈTRES AUTOMATIQUES
   Flask construit l'URL correctement


EXEMPLES COMPLETS
"""

# Routes
@app.route('/')
def home():
    return "Accueil"

@app.route('/user/<username>')
def user_profile(username):
    return f"Profil de {username}"

@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f"Post #{post_id}"

@app.route('/blog/<int:year>/<int:month>')
def blog_archive(year, month):
    return f"Archive {month}/{year}"

# Utilisation de url_for()
with app.test_request_context():
    # Sans paramètres
    print(url_for('home'))
    # -> '/'
    
    # Avec paramètres
    print(url_for('user_profile', username='alice'))
    # -> '/user/alice'
    
    print(url_for('show_post', post_id=123))
    # -> '/post/123'
    
    print(url_for('blog_archive', year=2024, month=12))
    # -> '/blog/2024/12'

"""
[IDEE] _external=True : URL ABSOLUE
"""

with app.test_request_context():
    # URL relative (défaut)
    print(url_for('home'))
    # -> '/'
    
    # URL absolue
    print(url_for('home', _external=True))
    # -> 'http://localhost:5000/'
    
    print(url_for('user_profile', username='alice', _external=True))
    # -> 'http://localhost:5000/user/alice'

"""
UTILISATION DANS TEMPLATES

url_for() est aussi disponible dans les templates Jinja2
(on verra ça au chapitre 3)
"""

# template.html
"""
<nav>
    <a href="{{ url_for('home') }}">Accueil</a>
    <a href="{{ url_for('user_profile', username=current_user.username) }}">Profil</a>
    <a href="{{ url_for('show_post', post_id=post.id) }}">Lire</a>
</nav>
"""

"""
REDIRECTIONS AVEC url_for()
"""

from flask import redirect

@app.route('/old-url')
def old_page():
    # Rediriger vers nouvelle route
    return redirect(url_for('new_page'))

@app.route('/new-url')
def new_page():
    return "Nouvelle page"

@app.route('/login')
def login():
    # Après login, rediriger vers profil
    username = 'alice'  # Après authentification
    return redirect(url_for('user_profile', username=username))


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 2 : BLOG AVEC ROUTES DYNAMIQUES
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer un blog simple avec routes dynamiques


CAHIER DES CHARGES :

1. Page d'accueil (/)
   - Liste de tous les posts
   - Liens vers chaque post

2. Page post individuel (/post/<int:post_id>)
   - Titre et contenu du post
   - Lien retour à l'accueil

3. Archive par année (/archive/<int:year>)
   - Posts de l'année spécifiée
   - Lien retour à l'accueil

4. Profil auteur (/author/<username>)
   - Posts de cet auteur
   - Lien retour à l'accueil


SOLUTION :
"""

from flask import Flask, url_for

app = Flask(__name__)

# Données simulées (en vrai : base de données)
POSTS = [
    {
        'id': 1,
        'title': 'Introduction à Flask',
        'content': 'Flask est un micro-framework...',
        'author': 'alice',
        'year': 2024
    },
    {
        'id': 2,
        'title': 'Apprendre Python',
        'content': 'Python est un langage...',
        'author': 'bob',
        'year': 2024
    },
    {
        'id': 3,
        'title': 'Web Development',
        'content': 'Le développement web...',
        'author': 'alice',
        'year': 2023
    },
]

@app.route('/')
def home():
    """Page d'accueil avec liste de posts"""
    posts_html = ""
    for post in POSTS:
        # Utiliser url_for pour générer les URLs
        post_url = url_for('show_post', post_id=post['id'])
        author_url = url_for('author_posts', username=post['author'])
        
        posts_html += f"""
        <article>
            <h2><a href="{post_url}">{post['title']}</a></h2>
            <p>Par <a href="{author_url}">{post['author']}</a> en {post['year']}</p>
            <p>{post['content'][:100]}...</p>
        </article>
        <hr>
        """
    
    return f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Mon Blog</title>
        <style>
            body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
            article {{ margin-bottom: 30px; }}
            a {{ color: #0066cc; text-decoration: none; }}
            a:hover {{ text-decoration: underline; }}
        </style>
    </head>
    <body>
        <h1>Mon Blog</h1>
        {posts_html}
    </body>
    </html>
    """

@app.route('/post/<int:post_id>')
def show_post(post_id):
    """Afficher un post individuel"""
    # Chercher le post
    post = next((p for p in POSTS if p['id'] == post_id), None)
    
    if post is None:
        return "Post non trouvé", 404
    
    author_url = url_for('author_posts', username=post['author'])
    home_url = url_for('home')
    
    return f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>{post['title']}</title>
        <style>
            body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
        </style>
    </head>
    <body>
        <h1>{post['title']}</h1>
        <p>Par <a href="{author_url}">{post['author']}</a> en {post['year']}</p>
        <p>{post['content']}</p>
        <hr>
        <a href="{home_url}"><- Retour à l'accueil</a>
    </body>
    </html>
    """

@app.route('/archive/<int:year>')
def archive(year):
    """Archive par année"""
    # Filtrer posts par année
    year_posts = [p for p in POSTS if p['year'] == year]
    
    if not year_posts:
        return f"Aucun post en {year}", 404
    
    posts_html = ""
    for post in year_posts:
        post_url = url_for('show_post', post_id=post['id'])
        posts_html += f"""
        <li><a href="{post_url}">{post['title']}</a> par {post['author']}</li>
        """
    
    home_url = url_for('home')
    
    return f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Archive {year}</title>
        <style>
            body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
        </style>
    </head>
    <body>
        <h1>Archive {year}</h1>
        <ul>
        {posts_html}
        </ul>
        <a href="{home_url}"><- Retour à l'accueil</a>
    </body>
    </html>
    """

@app.route('/author/<username>')
def author_posts(username):
    """Posts d'un auteur"""
    # Filtrer posts par auteur
    author_posts = [p for p in POSTS if p['author'] == username]
    
    if not author_posts:
        return f"Aucun post de {username}", 404
    
    posts_html = ""
    for post in author_posts:
        post_url = url_for('show_post', post_id=post['id'])
        posts_html += f"""
        <li><a href="{post_url}">{post['title']}</a> ({post['year']})</li>
        """
    
    home_url = url_for('home')
    
    return f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Posts de {username}</title>
        <style>
            body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
        </style>
    </head>
    <body>
        <h1>Posts de {username}</h1>
        <ul>
        {posts_html}
        </ul>
        <a href="{home_url}"><- Retour à l'accueil</a>
    </body>
    </html>
    """

if __name__ == '__main__':
    app.run(debug=True)

"""
[OBJECTIF] TESTEZ VOTRE SOLUTION

1. Lancez : python app.py
2. Visitez http://localhost:5000
3. Cliquez sur les posts
4. Testez les archives
5. Testez les profils auteurs


[IDEE] AMÉLIORATIONS POSSIBLES

- Ajouter pagination
- Tri par date
- Recherche
- Catégories
- Commentaires
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 2
# ----------------------------------------------------------------------------

"""
CE QUE VOUS AVEZ APPRIS

[OK] Routes dynamiques avec <paramètre>
[OK] Types de paramètres (string, int, float, path, uuid)
[OK] Paramètres multiples
[OK] Routes optionnelles
[OK] Méthodes HTTP (GET, POST, PUT, DELETE)
[OK] url_for() pour générer URLs
[OK] Redirections


[CLE] POINTS CLÉS

1. <variable> dans route = paramètre dynamique
2. <type:variable> = conversion automatique
3. methods=['GET', 'POST'] = autoriser méthodes
4. url_for('fonction', param=val) = générer URL
5. request.method = savoir méthode HTTP
6. request.form = données formulaire
7. request.get_json() = données JSON


[OBJECTIF] PATTERNS COURANTS

Blog :
GET /                    -> Liste posts
GET /post/<int:id>       -> Post individuel
GET /archive/<int:year>  -> Archive année

API REST :
GET    /api/posts        -> Lister
POST   /api/posts        -> Créer
GET    /api/posts/<id>   -> Récupérer
PUT    /api/posts/<id>   -> Modifier
DELETE /api/posts/<id>   -> Supprimer

E-commerce :
GET /shop/<category>/<int:product_id>


-> PROCHAINE ÉTAPE : Chapitre 3 - Templates Jinja2 !

Vous allez apprendre :
- Séparer HTML de Python
- Templates réutilisables
- Variables, boucles, conditions
- Héritage de templates
- Filtres Jinja2

Fini le HTML dans les strings Python ! [BRAVO]
"""


# ============================================================================
# [GUIDE] CHAPITRE 3 : TEMPLATES JINJA2
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Séparer HTML et Python avec templates
[OK] Passer des variables aux templates
[OK] Utiliser conditions et boucles dans templates
[OK] Créer template de base avec héritage
[OK] Utiliser filtres Jinja2
[OK] Inclure des sous-templates (partials)
[OK] Gérer les fichiers statiques
[OK] Organiser templates efficacement
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI DES TEMPLATES ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : HTML DANS PYTHON

Jusqu'ici, nous faisions :
"""

@app.route('/user/<username>')
def user_profile(username):
    return f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Profil de {username}</title>
        <style>
            body {{ font-family: Arial; }}
            .profile {{ padding: 20px; }}
        </style>
    </head>
    <body>
        <div class="profile">
            <h1>{username}</h1>
            <p>Email: {username}@example.com</p>
        </div>
    </body>
    </html>
    """

"""
[X] PROBLÈMES MAJEURS

1. ILLISIBLE
   - HTML mélangé avec Python
   - Pas de coloration syntaxique HTML
   - Difficile à déboguer

2. NON MAINTENABLE
   - Modifier design = toucher au code Python
   - Impossible pour designer HTML/CSS
   - Risque de casser la logique

3. RÉPÉTITION
   - Navigation répétée sur chaque page
   - Footer répété partout
   - Impossible de partager des morceaux

4. PAS D'OUTILS
   - Pas d'autocomplétion HTML
   - Pas de validation
   - Pas de formatage

5. SÉPARATION DES RESPONSABILITÉS
   - Backend (Python) ≠ Frontend (HTML/CSS)
   - Équipe frontend/backend compliqué


[OK] SOLUTION : TEMPLATES

Template = Fichier HTML séparé avec placeholders
Flask utilise JINJA2 comme moteur de templates


AVANTAGES [OBJECTIF]

[OK] Séparation claire Python/HTML
[OK] Coloration syntaxique
[OK] Réutilisation (héritage)
[OK] Logique simple dans templates
[OK] Équipe peut travailler séparément
[OK] Outils IDE complets
"""


# ----------------------------------------------------------------------------
# [DOSSIER] STRUCTURE ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
STRUCTURE OBLIGATOIRE
"""

mon_projet/
├── app.py
├── templates/          # <- Dossier OBLIGATOIRE
│   ├── base.html
│   ├── index.html
│   ├── profile.html
│   └── blog/
│       ├── list.html
│       └── post.html
└── static/             # <- Pour CSS, JS, images
    ├── css/
    ├── js/
    └── images/

"""
[IDEE] RÈGLES IMPORTANTES

1. DOSSIER "templates"
   - Doit s'appeler exactement "templates"
   - Au même niveau que app.py
   - Flask cherche automatiquement dedans

2. DOSSIER "static"
   - Pour fichiers CSS, JS, images
   - Accessibles directement via URLs
   - On verra au chapitre 4

3. ORGANISATION
   - Sous-dossiers OK pour organiser
   - templates/blog/list.html -> 'blog/list.html'


FLASK TROUVE AUTOMATIQUEMENT

Quand vous faites Flask(__name__), Flask :
1. Note le chemin du module
2. Cherche templates/ relativement à ce chemin
3. Configure Jinja2 pour ce dossier
"""

# app.py
from flask import Flask

app = Flask(__name__)

# Flask configure automatiquement :
# - app.template_folder = 'templates'
# - app.jinja_loader pointe vers templates/

"""
PERSONNALISATION (rare)
"""

# Changer le dossier templates
app = Flask(__name__, template_folder='mes_templates')

# Plusieurs dossiers templates
from jinja2 import ChoiceLoader, FileSystemLoader
app.jinja_loader = ChoiceLoader([
    FileSystemLoader('templates'),
    FileSystemLoader('other_templates')
])


# ----------------------------------------------------------------------------
# [DEMARRAGE] PREMIER TEMPLATE
# ----------------------------------------------------------------------------

"""
CRÉER templates/index.html
"""

# templates/index.html
"""
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mon Site Flask</title>
</head>
<body>
    <h1>Bienvenue sur mon site Flask !</h1>
    <p>Ceci est mon premier template.</p>
</body>
</html>
"""

"""
UTILISER LE TEMPLATE
"""

# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    """
    [IDEE] render_template('fichier.html')
    
    1. Cherche fichier dans templates/
    2. Lit le contenu
    3. Le traite avec Jinja2
    4. Retourne le HTML final
    """
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True)

"""
[IDEE] QUE FAIT render_template() ?

render_template('index.html')
    │
    ├─ Cherche templates/index.html
    ├─ Lit le fichier
    ├─ Traite avec Jinja2 (remplace {{ }}, {% %})
    ├─ Génère HTML final
    └─ Retourne comme réponse HTTP


TESTER

1. Créer templates/index.html
2. Lancer : python app.py
3. Ouvrir : http://localhost:5000
4. Vous voyez le contenu du template !
"""


# ----------------------------------------------------------------------------
# [NOTE] PASSER DES VARIABLES AUX TEMPLATES
# ----------------------------------------------------------------------------

"""
SYNTAXE DE BASE
"""

# app.py
@app.route('/hello/<name>')
def hello(name):
    """
    [IDEE] PASSER VARIABLES
    
    render_template('fichier.html', variable=valeur, ...)
    
    Vous pouvez passer autant de variables que vous voulez !
    """
    return render_template('hello.html', username=name)

# templates/hello.html
"""
<!DOCTYPE html>
<html>
<body>
    <!-- [IDEE] {{ variable }} = Afficher variable -->
    <h1>Bonjour {{ username }} !</h1>
</body>
</html>
"""

"""
[IDEE] SYNTAXE JINJA2

{{ ... }}  -> Afficher/évaluer expression
{% ... %}  -> Instructions (if, for, etc.)
{# ... #}  -> Commentaire (invisible dans HTML)


EXEMPLES DE {{ }}
"""

# templates/examples.html
"""
<!-- Variables simples -->
<p>Nom: {{ username }}</p>
<p>Age: {{ age }}</p>

<!-- Attributs d'objets -->
<p>Email: {{ user.email }}</p>
<p>Ville: {{ user.address.city }}</p>

<!-- Éléments de liste -->
<p>Premier hobby: {{ hobbies[0] }}</p>

<!-- Éléments de dict -->
<p>Titre: {{ post['title'] }}</p>

<!-- Expressions -->
<p>Double de l'âge: {{ age * 2 }}</p>
<p>Nom complet: {{ first_name + ' ' + last_name }}</p>

<!-- Appels de méthodes -->
<p>Majuscules: {{ name.upper() }}</p>
<p>Longueur: {{ items|length }}</p>
"""

"""
PASSER PLUSIEURS VARIABLES
"""

@app.route('/user/<username>')
def user_profile(username):
    """
    Passer plusieurs variables au template
    """
    user_data = {
        'username': username,
        'email': f'{username}@example.com',
        'age': 25,
        'hobbies': ['Lecture', 'Coding', 'Gaming'],
        'is_active': True
    }
    
    return render_template(
        'profile.html',
        user=user_data,
        page_title='Profil Utilisateur',
        year=2024
    )

# templates/profile.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>{{ page_title }}</title>
</head>
<body>
    <h1>Profil de {{ user.username }}</h1>
    
    <div class="info">
        <p>Email: {{ user.email }}</p>
        <p>Âge: {{ user.age }} ans</p>
        <p>Statut: {{ 'Actif' if user.is_active else 'Inactif' }}</p>
    </div>
    
    <h2>Hobbies</h2>
    <ul>
        {% for hobby in user.hobbies %}
            <li>{{ hobby }}</li>
        {% endfor %}
    </ul>
    
    <footer>
        <p>&copy; {{ year }}</p>
    </footer>
</body>
</html>
"""

"""
PASSER DICTIONNAIRES DIRECTEMENT
"""

@app.route('/product/<int:product_id>')
def product(product_id):
    product = {
        'id': product_id,
        'name': 'Laptop',
        'price': 999.99,
        'in_stock': True,
        'specs': {
            'ram': '16GB',
            'ssd': '512GB'
        }
    }
    
    return render_template('product.html', product=product)

# templates/product.html
"""
<h1>{{ product.name }}</h1>
<p>Prix: {{ product.price }}€</p>
<p>RAM: {{ product.specs.ram }}</p>
<p>SSD: {{ product.specs.ssd }}</p>
"""


# ----------------------------------------------------------------------------
# [MELANGE] CONDITIONS DANS TEMPLATES
# ----------------------------------------------------------------------------

"""
SYNTAXE IF/ELIF/ELSE
"""

# templates/conditions.html
"""
{% if user.is_logged_in %}
    <p>Bienvenue {{ user.name }} !</p>
    <a href="/logout">Se déconnecter</a>
{% else %}
    <p>Veuillez vous connecter</p>
    <a href="/login">Se connecter</a>
{% endif %}
"""

"""
[IDEE] STRUCTURE IF COMPLÈTE
"""

"""
{% if condition1 %}
    <!-- Si condition1 vraie -->
{% elif condition2 %}
    <!-- Sinon si condition2 vraie -->
{% elif condition3 %}
    <!-- Sinon si condition3 vraie -->
{% else %}
    <!-- Sinon -->
{% endif %}
"""

"""
OPÉRATEURS DE COMPARAISON
"""

"""
<!-- Égalité -->
{% if age == 18 %}

<!-- Différent -->
{% if status != 'pending' %}

<!-- Plus grand -->
{% if score > 100 %}

<!-- Plus petit -->
{% if price < 50 %}

<!-- Plus grand ou égal -->
{% if age >= 18 %}

<!-- Plus petit ou égal -->
{% if stock <= 10 %}

<!-- Dans une liste -->
{% if role in ['admin', 'moderator'] %}

<!-- Pas dans une liste -->
{% if status not in ['banned', 'suspended'] %}
"""

"""
OPÉRATEURS LOGIQUES
"""

"""
<!-- ET -->
{% if user.is_active and user.is_verified %}

<!-- OU -->
{% if user.is_admin or user.is_moderator %}

<!-- NON -->
{% if not user.is_banned %}

<!-- Combinaisons -->
{% if (user.is_active and user.is_verified) or user.is_admin %}
"""

"""
TESTS JINJA2
"""

"""
<!-- Vérifier si défini -->
{% if variable is defined %}

<!-- Vérifier si non défini -->
{% if variable is not defined %}

<!-- Vérifier si None -->
{% if value is none %}

<!-- Vérifier si pas None -->
{% if value is not none %}

<!-- Vérifier si vrai (truthy) -->
{% if value %}

<!-- Vérifier si faux (falsy) -->
{% if not value %}

<!-- Vérifier si liste vide -->
{% if not items %}
    <p>Aucun élément</p>
{% endif %}
"""

"""
EXEMPLE COMPLET
"""

# app.py
@app.route('/dashboard')
def dashboard():
    user = {
        'name': 'Alice',
        'role': 'admin',
        'is_verified': True,
        'points': 150
    }
    
    return render_template('dashboard.html', user=user)

# templates/dashboard.html
"""
<!DOCTYPE html>
<html>
<body>
    <h1>Tableau de bord</h1>
    
    {% if user.is_verified %}
        <span class="badge">[OK] Vérifié</span>
    {% else %}
        <span class="warning">[ATTENTION] Non vérifié</span>
        <a href="/verify">Vérifier mon compte</a>
    {% endif %}
    
    <h2>Accès</h2>
    {% if user.role == 'admin' %}
        <a href="/admin">Panneau d'administration</a>
    {% elif user.role == 'moderator' %}
        <a href="/moderate">Modération</a>
    {% else %}
        <p>Accès standard</p>
    {% endif %}
    
    <h2>Niveau</h2>
    {% if user.points >= 200 %}
        <p>* Expert</p>
    {% elif user.points >= 100 %}
        <p>* Intermédiaire</p>
    {% else %}
        <p>[JAPANESE_SYMBOL_FOR_BEGINNER] Débutant</p>
    {% endif %}
</body>
</html>
"""


# ----------------------------------------------------------------------------
# [SYNC] BOUCLES DANS TEMPLATES
# ----------------------------------------------------------------------------

"""
SYNTAXE FOR
"""

# templates/loops.html
"""
<ul>
    {% for item in items %}
        <li>{{ item }}</li>
    {% endfor %}
</ul>
"""

"""
[IDEE] STRUCTURE FOR COMPLÈTE
"""

"""
{% for item in liste %}
    <!-- Corps de la boucle -->
    <!-- item est accessible ici -->
{% endfor %}
"""

"""
EXEMPLE SIMPLE
"""

# app.py
@app.route('/posts')
def posts_list():
    posts = [
        {'title': 'Post 1', 'author': 'Alice'},
        {'title': 'Post 2', 'author': 'Bob'},
        {'title': 'Post 3', 'author': 'Charlie'}
    ]
    
    return render_template('posts.html', posts=posts)

# templates/posts.html
"""
<h1>Liste des posts</h1>
<ul>
    {% for post in posts %}
        <li>
            <strong>{{ post.title }}</strong>
            par {{ post.author }}
        </li>
    {% endfor %}
</ul>
"""

"""
VARIABLE loop

Dans une boucle, Jinja2 fournit une variable spéciale : loop
"""

"""
{% for item in items %}
    <!-- Index (commence à 1) -->
    {{ loop.index }}      -> 1, 2, 3, ...
    
    <!-- Index (commence à 0) -->
    {{ loop.index0 }}     -> 0, 1, 2, ...
    
    <!-- Nombre d'itérations restantes -->
    {{ loop.revindex }}   -> 3, 2, 1
    
    <!-- Nombre d'itérations restantes (à partir de 0) -->
    {{ loop.revindex0 }}  -> 2, 1, 0
    
    <!-- Premier élément ? -->
    {{ loop.first }}      -> True ou False
    
    <!-- Dernier élément ? -->
    {{ loop.last }}       -> True ou False
    
    <!-- Longueur totale -->
    {{ loop.length }}     -> 3
{% endfor %}
"""

"""
EXEMPLE AVEC loop
"""

# templates/posts_advanced.html
"""
<h1>Articles du blog</h1>

{% for post in posts %}
    <article class="{{ 'featured' if loop.first else '' }}">
        <h2>{{ loop.index }}. {{ post.title }}</h2>
        <p>Par {{ post.author }}</p>
        
        {% if loop.first %}
            <span class="badge">Nouveau !</span>
        {% endif %}
        
        {% if not loop.last %}
            <hr>
        {% endif %}
    </article>
{% endfor %}
"""

"""
BOUCLE VIDE (else)
"""

# templates/posts_empty.html
"""
<h1>Articles</h1>

{% for post in posts %}
    <article>
        <h2>{{ post.title }}</h2>
    </article>
{% else %}
    <!-- Exécuté si liste vide -->
    <p>Aucun article disponible.</p>
{% endfor %}
"""

"""
BOUCLES IMBRIQUÉES
"""

# app.py
@app.route('/categories')
def categories():
    categories = [
        {
            'name': 'Électronique',
            'products': ['Laptop', 'Smartphone', 'Tablette']
        },
        {
            'name': 'Livres',
            'products': ['Roman', 'BD', 'Manga']
        }
    ]
    
    return render_template('categories.html', categories=categories)

# templates/categories.html
"""
{% for category in categories %}
    <h2>{{ category.name }}</h2>
    <ul>
        {% for product in category.products %}
            <li>{{ product }}</li>
        {% endfor %}
    </ul>
{% endfor %}
"""

"""
BOUCLE SUR DICTIONNAIRE
"""

# app.py
@app.route('/config')
def config():
    settings = {
        'theme': 'dark',
        'language': 'fr',
        'notifications': True
    }
    
    return render_template('config.html', settings=settings)

# templates/config.html
"""
<h2>Configuration</h2>
<table>
    {% for key, value in settings.items() %}
        <tr>
            <td>{{ key }}</td>
            <td>{{ value }}</td>
        </tr>
    {% endfor %}
</table>
"""

"""
FILTRER DANS BOUCLE
"""

# templates/posts_filtered.html
"""
<!-- Seulement les posts publiés -->
{% for post in posts if post.published %}
    <article>
        <h2>{{ post.title }}</h2>
    </article>
{% endfor %}

<!-- Posts actifs d'Alice -->
{% for post in posts if post.author == 'Alice' and post.active %}
    <h3>{{ post.title }}</h3>
{% endfor %}
"""


# Ce fichier est déjà très long. La suite (Héritage de templates, filtres, etc.)
# sera dans un fichier suivant pour ne pas dépasser les limites.

# ============================================================================
# FIN DE CE FICHIER - CONTINUEZ AVEC flask_partie1_suite2.txt
# ============================================================================
# ============================================================================
# [LIVRE] FLASK - PARTIE 1 (FIN) : HÉRITAGE, FILTRES ET FICHIERS STATIQUES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Suite Chapitre 3 : Héritage de templates, Filtres Jinja2, Partials
# - Chapitre 4 : Fichiers Statiques (CSS, JS, Images)
#
# [TEMPS] TEMPS : ~2-3 heures
# [DOCS] PRÉREQUIS : Chapitres précédents complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 3 (SUITE) : HÉRITAGE DE TEMPLATES
# ============================================================================

"""
[REFLEXION] PROBLÈME : RÉPÉTITION DANS TEMPLATES

Sans héritage, chaque template répète :
"""

# templates/home.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>Accueil</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <a href="/">Accueil</a>
        <a href="/about">À propos</a>
        <a href="/contact">Contact</a>
    </nav>
    
    <main>
        <h1>Bienvenue</h1>
        <p>Contenu de la page d'accueil</p>
    </main>
    
    <footer>
        <p>&copy; 2024 Mon Site</p>
    </footer>
</body>
</html>
"""

# templates/about.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>À propos</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav>
        <a href="/">Accueil</a>
        <a href="/about">À propos</a>
        <a href="/contact">Contact</a>
    </nav>
    
    <main>
        <h1>À propos</h1>
        <p>Contenu de la page à propos</p>
    </main>
    
    <footer>
        <p>&copy; 2024 Mon Site</p>
    </footer>
</body>
</html>
"""

"""
[X] PROBLÈMES

1. CODE DUPLIQUÉ
   - <head>, <nav>, <footer> répétés partout
   
2. MAINTENANCE DIFFICILE
   - Changer navigation = modifier tous les fichiers
   - Oubli facile
   - Erreurs de cohérence

3. ÉVOLUTIVITÉ
   - Ajouter élément = modifier 50+ fichiers
   

[OK] SOLUTION : HÉRITAGE DE TEMPLATES

Créer un template de BASE que les autres étendent
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] CRÉER UN TEMPLATE DE BASE
# ----------------------------------------------------------------------------

"""
TEMPLATE DE BASE (templates/base.html)
"""

# templates/base.html
"""
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
    <!-- [IDEE] BLOCK = Zone remplaçable -->
    <title>{% block title %}Mon Site{% endblock %}</title>
    
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    
    <!-- Block pour CSS supplémentaire -->
    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- Navigation commune à toutes les pages -->
    <nav class="navbar">
        <a href="{{ url_for('home') }}">Accueil</a>
        <a href="{{ url_for('about') }}">À propos</a>
        <a href="{{ url_for('contact') }}">Contact</a>
        <a href="{{ url_for('blog') }}">Blog</a>
    </nav>
    
    <!-- Contenu principal (sera remplacé) -->
    <main class="container">
        <!-- [IDEE] BLOCK content = Zone principale -->
        {% block content %}
            <p>Contenu par défaut</p>
        {% endblock %}
    </main>
    
    <!-- Footer commun à toutes les pages -->
    <footer>
        <p>&copy; {{ year or 2024 }} Mon Site. Tous droits réservés.</p>
        
        <!-- Block pour contenu footer supplémentaire -->
        {% block footer %}{% endblock %}
    </footer>
    
    <!-- Scripts communs -->
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
    
    <!-- Block pour JS supplémentaire -->
    {% block extra_js %}{% endblock %}
</body>
</html>
"""

"""
[IDEE] ANATOMIE D'UN BLOCK

{% block nom_du_block %}
    Contenu par défaut (optionnel)
{% endblock %}


CARACTÉRISTIQUES :

1. NOM UNIQUE
   Chaque block doit avoir un nom unique
   
2. CONTENU PAR DÉFAUT
   Ce qui est affiché si template enfant ne remplace pas
   Peut être vide
   
3. REMPLACEMENT
   Template enfant peut remplacer le contenu
   
4. PLUSIEURS BLOCKS
   Un template peut avoir autant de blocks que nécessaire


BLOCKS COURANTS :

{% block title %}        -> Titre de la page
{% block content %}      -> Contenu principal
{% block extra_css %}    -> CSS spécifique
{% block extra_js %}     -> JS spécifique
{% block sidebar %}      -> Barre latérale
{% block header %}       -> En-tête personnalisé
{% block footer %}       -> Footer personnalisé
"""


# ----------------------------------------------------------------------------
# [ENFANT] TEMPLATES ENFANTS
# ----------------------------------------------------------------------------

"""
TEMPLATE ENFANT (templates/home.html)
"""

# templates/home.html
"""
<!-- [IDEE] HÉRITER DE base.html -->
{% extends "base.html" %}

<!-- [IDEE] REMPLACER block title -->
{% block title %}Accueil - Mon Site{% endblock %}

<!-- [IDEE] REMPLACER block content -->
{% block content %}
    <h1>Bienvenue sur mon site !</h1>
    <p>Ceci est la page d'accueil.</p>
    <p>Découvrez nos services et n'hésitez pas à nous contacter.</p>
    
    <div class="features">
        <div class="feature">
            <h2>Rapide</h2>
            <p>Performance optimale</p>
        </div>
        <div class="feature">
            <h2>Sécurisé</h2>
            <p>Protection de vos données</p>
        </div>
        <div class="feature">
            <h2>Fiable</h2>
            <p>Disponible 24/7</p>
        </div>
    </div>
{% endblock %}
"""

"""
[IDEE] COMMENT ÇA MARCHE ?

1. {% extends "base.html" %}
   - Doit être la PREMIÈRE ligne
   - Indique quel template étendre
   
2. {% block title %}...{% endblock %}
   - Remplace le block "title" de base.html
   
3. RÉSULTAT FINAL
   Jinja2 fusionne les deux :
   - Prend base.html
   - Remplace les blocks par ceux de home.html
   - Garde le reste (nav, footer, etc.)


TEMPLATE ENFANT (templates/about.html)
"""

# templates/about.html
"""
{% extends "base.html" %}

{% block title %}À propos - Mon Site{% endblock %}

{% block content %}
    <h1>À propos de nous</h1>
    <p>Nous sommes une entreprise passionnée par la technologie.</p>
    <p>Notre mission : rendre le web accessible à tous.</p>
    
    <h2>Notre équipe</h2>
    <ul>
        <li>Alice - CEO</li>
        <li>Bob - CTO</li>
        <li>Charlie - Designer</li>
    </ul>
{% endblock %}

<!-- Block optionnel -->
{% block extra_css %}
    <style>
        .team-photo {
            border-radius: 50%;
        }
    </style>
{% endblock %}
"""

"""
TEMPLATE AVEC PLUSIEURS BLOCKS
"""

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

{% block title %}Blog - Mon Site{% endblock %}

{% block extra_css %}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/blog.css') }}">
{% endblock %}

{% block content %}
    <h1>Blog</h1>
    
    {% for post in posts %}
        <article>
            <h2>{{ post.title }}</h2>
            <p class="meta">Par {{ post.author }} le {{ post.date }}</p>
            <p>{{ post.excerpt }}</p>
            <a href="{{ url_for('show_post', post_id=post.id) }}">Lire la suite -></a>
        </article>
    {% endfor %}
{% endblock %}

{% block extra_js %}
    <script>
        // JavaScript spécifique au blog
        console.log('Blog page loaded');
    </script>
{% endblock %}
"""


# ----------------------------------------------------------------------------
# [LIEN] SUPER() - AJOUTER AU LIEU DE REMPLACER
# ----------------------------------------------------------------------------

"""
[IDEE] PROBLÈME : Ajouter du contenu sans tout remplacer

Parfois, on veut AJOUTER au block parent, pas remplacer
"""

# templates/special_page.html
"""
{% extends "base.html" %}

{% block footer %}
    <!-- [IDEE] super() = Contenu du block parent -->
    {{ super() }}
    
    <!-- Ajouter du contenu supplémentaire -->
    <p>Cette page a du contenu footer supplémentaire</p>
{% endblock %}
"""

"""
[IDEE] RÉSULTAT

Footer de base.html :
    <p>&copy; 2024 Mon Site. Tous droits réservés.</p>

Footer de special_page.html :
    <p>&copy; 2024 Mon Site. Tous droits réservés.</p>  <- super()
    <p>Cette page a du contenu footer supplémentaire</p> <- ajout


EXEMPLE : Ajouter scripts
"""

# templates/interactive_page.html
"""
{% extends "base.html" %}

{% block extra_js %}
    <!-- Scripts du parent -->
    {{ super() }}
    
    <!-- Scripts supplémentaires -->
    <script src="{{ url_for('static', filename='js/charts.js') }}"></script>
    <script>
        initializeCharts();
    </script>
{% endblock %}
"""


# ----------------------------------------------------------------------------
# [MODULE] HÉRITAGE MULTI-NIVEAUX
# ----------------------------------------------------------------------------

"""
On peut créer une hiérarchie de templates
"""

# templates/base.html (Niveau 1 : Base générale)
"""
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <nav>{% block nav %}{% endblock %}</nav>
    <main>{% block content %}{% endblock %}</main>
    <footer>{% block footer %}{% endblock %}</footer>
</body>
</html>
"""

# templates/blog_base.html (Niveau 2 : Base pour blog)
"""
{% extends "base.html" %}

{% block nav %}
    <a href="{{ url_for('home') }}">Accueil</a>
    <a href="{{ url_for('blog') }}">Blog</a>
    <a href="{{ url_for('archive') }}">Archives</a>
{% endblock %}

{% block content %}
    <div class="blog-layout">
        <aside class="sidebar">
            {% block sidebar %}
                <h3>Catégories</h3>
                <ul>
                    <li>Tech</li>
                    <li>Science</li>
                    <li>Culture</li>
                </ul>
            {% endblock %}
        </aside>
        
        <div class="blog-content">
            {% block blog_content %}{% endblock %}
        </div>
    </div>
{% endblock %}
"""

# templates/blog_post.html (Niveau 3 : Post individuel)
"""
{% extends "blog_base.html" %}

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

{% block blog_content %}
    <article>
        <h1>{{ post.title }}</h1>
        <p class="meta">{{ post.author }} - {{ post.date }}</p>
        <div class="content">
            {{ post.content }}
        </div>
    </article>
{% endblock %}
"""

"""
[IDEE] HIÉRARCHIE

base.html (tout le site)
    v extends
blog_base.html (section blog)
    v extends
blog_post.html (post individuel)


QUAND UTILISER ?

[OK] Grande application
[OK] Sections avec layouts différents
[OK] Réutilisation maximale

[X] Petite app simple -> 1-2 niveaux suffisent
"""


# ----------------------------------------------------------------------------
# [PACKAGE] INCLUDES (PARTIALS)
# ----------------------------------------------------------------------------

"""
[IDEE] INCLUDES = Réutiliser des morceaux de templates

Différence avec héritage :
- extends : Template complet qui hérite
- include : Petit morceau inséré
"""

# templates/partials/_header.html
"""
<header class="site-header">
    <div class="logo">
        <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
    </div>
    <h1>Mon Site</h1>
</header>
"""

# templates/partials/_user_card.html
"""
<div class="user-card">
    <img src="{{ user.avatar }}" alt="{{ user.name }}">
    <h3>{{ user.name }}</h3>
    <p>{{ user.bio }}</p>
    <a href="{{ url_for('user_profile', username=user.username) }}">
        Voir le profil
    </a>
</div>
"""

"""
UTILISER LES INCLUDES
"""

# templates/home.html
"""
{% extends "base.html" %}

{% block content %}
    <!-- [IDEE] INCLUDE un partial -->
    {% include 'partials/_header.html' %}
    
    <h2>Utilisateurs populaires</h2>
    <div class="users-grid">
        {% for user in users %}
            <!-- Inclure pour chaque utilisateur -->
            {% include 'partials/_user_card.html' %}
        {% endfor %}
    </div>
{% endblock %}
"""

"""
[IDEE] CONVENTIONS

1. DOSSIER partials/
   Organiser les includes dans un dossier
   
2. PREFIX UNDERSCORE (_)
   _header.html, _footer.html
   Indique que c'est un partial
   
3. VARIABLES
   Les partials ont accès aux variables du contexte


PASSER DES VARIABLES AUX INCLUDES
"""

# templates/page.html
"""
{% include 'partials/_alert.html' with context %}

<!-- Ou avec variables spécifiques -->
{% include 'partials/_alert.html' with {'type': 'success', 'message': 'OK'} %}
"""

"""
INCLUDE CONDITIONNEL
"""

# templates/page.html
"""
<!-- Inclure seulement si user admin -->
{% if user.is_admin %}
    {% include 'partials/_admin_panel.html' %}
{% endif %}
"""

"""
IGNORE MISSING (ne pas planter si absent)
"""

# templates/page.html
"""
{% include 'partials/_optional.html' ignore missing %}
"""


# ----------------------------------------------------------------------------
# [DESIGN] FILTRES JINJA2
# ----------------------------------------------------------------------------

"""
[IDEE] FILTRES = Modifier variables dans templates

SYNTAXE : {{ variable|filtre }}


FILTRES TEXTE
"""

# templates/filters_text.html
"""
{% set name = "alice martin" %}

<!-- Majuscules -->
{{ name|upper }}           -> ALICE MARTIN

<!-- Minuscules -->
{{ name|lower }}           -> alice martin

<!-- Title Case -->
{{ name|title }}           -> Alice Martin

<!-- Capitalize (première lettre) -->
{{ name|capitalize }}      -> Alice martin

<!-- Remplacer -->
{{ name|replace("martin", "durand") }}  -> alice durand

<!-- Longueur -->
{{ name|length }}          -> 12

<!-- Tronquer -->
{{ "Texte très long"|truncate(10) }}    -> Texte t...

<!-- Tronquer (sans ...) -->
{{ "Texte long"|truncate(10, False, '') }} -> Texte long
"""

"""
FILTRES NOMBRES
"""

# templates/filters_numbers.html
"""
{% set price = 19.99 %}
{% set count = 1234567 %}

<!-- Arrondir -->
{{ 3.14159|round(2) }}     -> 3.14

<!-- Valeur absolue -->
{{ -42|abs }}              -> 42

<!-- Formater nombre -->
{{ count|int }}            -> 1234567

<!-- Float -->
{{ "3.14"|float }}         -> 3.14
"""

"""
FILTRES LISTES
"""

# templates/filters_lists.html
"""
{% set items = ['pomme', 'banane', 'orange'] %}
{% set numbers = [3, 1, 4, 1, 5, 9, 2, 6] %}

<!-- Longueur -->
{{ items|length }}         -> 3

<!-- Premier élément -->
{{ items|first }}          -> pomme

<!-- Dernier élément -->
{{ items|last }}           -> orange

<!-- Joindre -->
{{ items|join(', ') }}     -> pomme, banane, orange
{{ items|join(' et ') }}   -> pomme et banane et orange

<!-- Trier -->
{{ numbers|sort }}         -> [1, 1, 2, 3, 4, 5, 6, 9]

<!-- Trier (inverse) -->
{{ numbers|sort(reverse=True) }} -> [9, 6, 5, 4, 3, 2, 1, 1]

<!-- Unique (enlever doublons) -->
{{ numbers|unique }}       -> [1, 2, 3, 4, 5, 6, 9]
"""

"""
FILTRES VALEUR PAR DÉFAUT
"""

# templates/filters_default.html
"""
{% set value = None %}
{% set empty = "" %}

<!-- Valeur par défaut si None/False/vide -->
{{ value|default('Non défini') }}        -> Non défini
{{ empty|default('Vide') }}              -> Vide

<!-- Valeur par défaut seulement si vraiment None -->
{{ empty|default('Vide', true) }}        -> (chaîne vide)
"""

"""
FILTRES HTML/SÉCURITÉ
"""

# templates/filters_html.html
"""
{% set html = "<script>alert('XSS')</script>" %}
{% set safe_html = "<strong>Important</strong>" %}

<!-- Échapper HTML (par défaut, automatique) -->
{{ html }}
-> &lt;script&gt;alert('XSS')&lt;/script&gt;

<!-- Ne PAS échapper (DANGEREUX !) -->
{{ safe_html|safe }}
-> <strong>Important</strong>

<!-- Enlever tags HTML -->
{{ safe_html|striptags }}
-> Important
"""

"""
FILTRES DATES (avec extension)
"""

pip install babel

# app.py
from flask_babel import Babel

babel = Babel(app)

# templates/filters_dates.html
"""
{% set now = date %}

<!-- Format date -->
{{ now|datetimeformat('%Y-%m-%d') }}     -> 2024-12-18
{{ now|datetimeformat('%d/%m/%Y') }}     -> 18/12/2024
{{ now|datetimeformat('%H:%M:%S') }}     -> 14:30:00
"""

"""
CHAÎNER LES FILTRES
"""

# templates/filters_chain.html
"""
{% set text = "  hello world  " %}

<!-- Appliquer plusieurs filtres -->
{{ text|trim|upper|replace("WORLD", "PYTHON") }}
-> HELLO PYTHON

Ordre d'application :
text -> trim() -> upper() -> replace() -> résultat
"""

"""
FILTRES PERSONNALISÉS
"""

# app.py
@app.template_filter('reverse')
def reverse_filter(s):
    """Inverser une chaîne"""
    return s[::-1]

@app.template_filter('currency')
def currency_filter(value):
    """Formater en monnaie"""
    return f"{value:.2f}€"

# templates/custom_filters.html
"""
{{ "hello"|reverse }}              -> olleh
{{ 19.99|currency }}               -> 19.99€
"""

"""
LISTE COMPLÈTE DES FILTRES

TEXTE :
upper, lower, title, capitalize
replace, trim, center
truncate, wordcount, striptags

NOMBRES :
round, abs, int, float

LISTES :
first, last, length, join
sort, reverse, unique, sum, max, min

HTML :
safe, escape, striptags, urlize

DIVERS :
default, length, dictsort
select, reject, selectattr, rejectattr

Documentation : https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters
"""


# ============================================================================
# [GUIDE] CHAPITRE 4 : FICHIERS STATIQUES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Organiser fichiers CSS, JS et images
[OK] Servir fichiers statiques avec Flask
[OK] Utiliser url_for pour fichiers statiques
[OK] Optimiser chargement des assets
[OK] Gérer versions et cache
"""


# ----------------------------------------------------------------------------
# [DOSSIER] STRUCTURE DES FICHIERS STATIQUES
# ----------------------------------------------------------------------------

"""
ORGANISATION RECOMMANDÉE
"""

mon_projet/
├── app.py
├── templates/
│   └── ...
└── static/                 # <- Dossier OBLIGATOIRE
    ├── css/
    │   ├── style.css       # CSS principal
    │   ├── blog.css        # CSS spécifique blog
    │   └── vendor/         # Bibliothèques CSS
    │       └── bootstrap.min.css
    ├── js/
    │   ├── main.js         # JS principal
    │   ├── blog.js         # JS spécifique
    │   └── vendor/         # Bibliothèques JS
    │       ├── jquery.min.js
    │       └── chart.js
    ├── images/
    │   ├── logo.png
    │   ├── banner.jpg
    │   └── icons/
    │       ├── home.svg
    │       └── user.svg
    ├── fonts/
    │   ├── roboto.woff2
    │   └── opensans.woff2
    └── downloads/          # Fichiers téléchargeables
        └── guide.pdf

"""
[IDEE] RÈGLES

1. DOSSIER "static"
   - Nom exact : static/
   - Au même niveau que app.py
   - Flask sert automatiquement

2. ORGANISATION
   - Sous-dossiers par type
   - vendor/ pour bibliothèques externes
   - Noms clairs et cohérents

3. TAILLE
   - Optimiser images (compression)
   - Minifier CSS/JS en production
   - Ne pas mettre fichiers énormes
"""


# ----------------------------------------------------------------------------
# [LIEN] UTILISER LES FICHIERS STATIQUES
# ----------------------------------------------------------------------------

"""
SYNTAXE DE BASE : url_for('static')
"""

# templates/base.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>Mon Site</title>
    
    <!-- [IDEE] CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    
    <!-- CSS Bootstrap -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/vendor/bootstrap.min.css') }}">
    
    <!-- Favicon -->
    <link rel="icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
</head>
<body>
    <!-- Image -->
    <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
    
    <!-- Contenu -->
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <!-- [IDEE] JavaScript -->
    <script src="{{ url_for('static', filename='js/vendor/jquery.min.js') }}"></script>
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
"""

"""
[IDEE] POURQUOI url_for('static', filename='...') ?

1. URLS PORTABLES
   Flask génère le bon chemin automatiquement
   
2. CONFIGURATION FLEXIBLE
   On peut changer le dossier static sans toucher aux templates
   
3. VERSIONING
   Facilite ajout de cache-busting (voir plus bas)


ALTERNATIVE ([X] À ÉVITER)
"""

# [X] Mauvais : URLs en dur
"""
<link rel="stylesheet" href="/static/css/style.css">
<img src="/static/images/logo.png">
"""

# [OK] Bon : url_for
"""
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<img src="{{ url_for('static', filename='images/logo.png') }}">
"""


# ----------------------------------------------------------------------------
# [DESIGN] EXEMPLE COMPLET : CSS ET JS
# ----------------------------------------------------------------------------

"""
CRÉER static/css/style.css
"""

# static/css/style.css
"""
/* Reset et base */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Arial', sans-serif;
    line-height: 1.6;
    color: #333;
    background: #f4f4f4;
}

/* Container */
.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

/* Navigation */
.navbar {
    background: #333;
    color: white;
    padding: 15px 0;
}

.navbar a {
    color: white;
    text-decoration: none;
    padding: 10px 15px;
    margin: 0 5px;
}

.navbar a:hover {
    background: #555;
    border-radius: 5px;
}

/* Cards */
.card {
    background: white;
    padding: 20px;
    margin: 20px 0;
    border-radius: 5px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* Buttons */
.btn {
    display: inline-block;
    padding: 10px 20px;
    background: #007bff;
    color: white;
    text-decoration: none;
    border-radius: 5px;
    border: none;
    cursor: pointer;
}

.btn:hover {
    background: #0056b3;
}

/* Footer */
footer {
    background: #333;
    color: white;
    text-align: center;
    padding: 20px 0;
    margin-top: 40px;
}
"""

"""
CRÉER static/js/main.js
"""

# static/js/main.js
"""
// Attendre que le DOM soit chargé
document.addEventListener('DOMContentLoaded', function() {
    console.log('Page chargée !');
    
    // Animation sur les cards
    const cards = document.querySelectorAll('.card');
    cards.forEach((card, index) => {
        setTimeout(() => {
            card.style.opacity = '0';
            card.style.transform = 'translateY(20px)';
            card.style.transition = 'all 0.5s';
            
            setTimeout(() => {
                card.style.opacity = '1';
                card.style.transform = 'translateY(0)';
            }, 10);
        }, index * 100);
    });
    
    // Gestion des liens actifs
    const currentPath = window.location.pathname;
    const navLinks = document.querySelectorAll('.navbar a');
    
    navLinks.forEach(link => {
        if (link.getAttribute('href') === currentPath) {
            link.style.background = '#555';
            link.style.borderRadius = '5px';
        }
    });
});

// Fonction utilitaire
function showAlert(message, type = 'info') {
    const alert = document.createElement('div');
    alert.className = `alert alert-${type}`;
    alert.textContent = message;
    
    document.body.insertBefore(alert, document.body.firstChild);
    
    setTimeout(() => {
        alert.remove();
    }, 3000);
}
"""

"""
UTILISER DANS TEMPLATE
"""

# templates/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 Site{% endblock %}</title>
    
    <!-- CSS Principal -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    
    <!-- CSS spécifique à la page -->
    {% block extra_css %}{% endblock %}
</head>
<body>
    <nav class="navbar">
        <div class="container">
            <a href="{{ url_for('home') }}">Accueil</a>
            <a href="{{ url_for('about') }}">À propos</a>
            <a href="{{ url_for('blog') }}">Blog</a>
            <a href="{{ url_for('contact') }}">Contact</a>
        </div>
    </nav>
    
    <div class="container">
        {% block content %}{% endblock %}
    </div>
    
    <footer>
        <p>&copy; 2024 Mon Site</p>
    </footer>
    
    <!-- JS Principal -->
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
    
    <!-- JS spécifique à la page -->
    {% block extra_js %}{% endblock %}
</body>
</html>
"""


# ----------------------------------------------------------------------------
# [FRAME_WITH_PICTURE] IMAGES ET MÉDIAS
# ----------------------------------------------------------------------------

"""
IMAGES DANS TEMPLATES
"""

# templates/home.html
"""
{% extends "base.html" %}

{% block content %}
    <!-- Image simple -->
    <img src="{{ url_for('static', filename='images/logo.png') }}" 
         alt="Logo" 
         width="200">
    
    <!-- Image de fond en CSS inline -->
    <div style="background-image: url('{{ url_for('static', filename='images/banner.jpg') }}');">
        <h1>Bienvenue</h1>
    </div>
    
    <!-- Galerie d'images -->
    <div class="gallery">
        {% for i in range(1, 6) %}
            <img src="{{ url_for('static', filename='images/gallery/photo' ~ i ~ '.jpg') }}" 
                 alt="Photo {{ i }}">
        {% endfor %}
    </div>
{% endblock %}
"""

"""
FAVICON
"""

# templates/base.html (dans <head>)
"""
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
"""

"""
OPTIMISATION IMAGES

1. COMPRESSION
   - TinyPNG, ImageOptim
   - Réduire qualité JPEG
   - Utiliser WebP

2. RESPONSIVE
   - Différentes tailles
   - srcset et sizes
"""

# templates/responsive_image.html
"""
<img srcset="{{ url_for('static', filename='images/photo-small.jpg') }} 400w,
             {{ url_for('static', filename='images/photo-medium.jpg') }} 800w,
             {{ url_for('static', filename='images/photo-large.jpg') }} 1200w"
     sizes="(max-width: 400px) 400px,
            (max-width: 800px) 800px,
            1200px"
     src="{{ url_for('static', filename='images/photo-medium.jpg') }}"
     alt="Photo responsive">
"""


# ----------------------------------------------------------------------------
# [RAPIDE] OPTIMISATION ET BONNES PRATIQUES
# ----------------------------------------------------------------------------

"""
1. CACHE-BUSTING (versions)
"""

# app.py
import os
import hashlib

def file_hash(filename):
    """Générer hash du fichier pour cache-busting"""
    filepath = os.path.join(app.static_folder, filename)
    
    if not os.path.exists(filepath):
        return ''
    
    with open(filepath, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()[:8]

@app.context_processor
def override_url_for():
    """Ajouter version aux URLs statiques"""
    return dict(url_for=dated_url_for)

def dated_url_for(endpoint, **values):
    """url_for avec versioning automatique"""
    if endpoint == 'static':
        filename = values.get('filename', None)
        if filename:
            file_hash_value = file_hash(filename)
            values['v'] = file_hash_value
    
    return url_for(endpoint, **values)

# Template
"""
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
-> /static/css/style.css?v=a1b2c3d4
"""

"""
2. MINIFICATION

En production, minifier CSS/JS :
"""

pip install Flask-Assets

# app.py
from flask_assets import Environment, Bundle

assets = Environment(app)

# Bundle CSS
css = Bundle(
    'css/style.css',
    'css/blog.css',
    filters='cssmin',
    output='gen/packed.css'
)
assets.register('css_all', css)

# Bundle JS
js = Bundle(
    'js/vendor/jquery.js',
    'js/main.js',
    filters='jsmin',
    output='gen/packed.js'
)
assets.register('js_all', js)

# Template
"""
{% assets "css_all" %}
    <link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %}

{% assets "js_all" %}
    <script src="{{ ASSET_URL }}"></script>
{% endassets %}
"""

"""
3. CDN POUR BIBLIOTHÈQUES
"""

# templates/base.html
"""
<!-- Bootstrap depuis CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" 
      rel="stylesheet">

<!-- jQuery depuis CDN -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- Avec fallback local -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
    window.jQuery || document.write('<script src="{{ url_for('static', filename='js/vendor/jquery.min.js') }}"><\/script>')
</script>
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF PARTIE 1 COMPLÈTE
# ----------------------------------------------------------------------------

"""
[BRAVO] FÉLICITATIONS ! PARTIE 1 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 0 : Introduction
[OK] Qu'est-ce que Flask
[OK] Comparaison frameworks
[OK] Installation et configuration
[OK] Structure de projet

Chapitre 1 : Première Application
[OK] Créer app Flask minimale
[OK] Anatomie ligne par ligne
[OK] Routes basiques
[OK] Lancer serveur
[OK] Cycle requête-réponse

Chapitre 2 : Routes Dynamiques
[OK] Paramètres d'URL (<variable>)
[OK] Types de paramètres (int, float, path, uuid)
[OK] Paramètres multiples
[OK] Routes optionnelles
[OK] Méthodes HTTP (GET, POST, PUT, DELETE)
[OK] url_for() pour générer URLs
[OK] Redirections

Chapitre 3 : Templates Jinja2
[OK] Séparer HTML et Python
[OK] render_template()
[OK] Variables dans templates ({{ }})
[OK] Conditions ({% if %})
[OK] Boucles ({% for %})
[OK] Héritage ({% extends %}, {% block %})
[OK] Includes ({% include %})
[OK] Filtres Jinja2
[OK] Filtres personnalisés

Chapitre 4 : Fichiers Statiques
[OK] Organiser CSS, JS, images
[OK] url_for('static')
[OK] Optimisation
[OK] Cache-busting


[CLE] COMPÉTENCES ACQUISES

Vous pouvez maintenant créer :
[OK] Site web multi-pages
[OK] Blog simple
[OK] Portfolio
[OK] Site vitrine
[OK] Prototype d'application


-> PROCHAINE ÉTAPE : PARTIE 2

La Partie 2 couvrira :
- Formulaires et validation (Flask-WTF)
- Base de données (SQLAlchemy)
- Relations entre tables
- Migrations

Vous allez apprendre à :
- Collecter données utilisateurs
- Valider entrées
- Stocker en base de données
- Créer applications dynamiques


[GUIDE] FICHIER SUIVANT : flask_partie2.txt

Prenez une pause, révisez, pratiquez !
Quand vous êtes prêt, continuez avec la Partie 2 ! [RAPIDE]
"""
# ============================================================================
# [LIVRE] FLASK - PARTIE 2 : FORMULAIRES ET BASES DE DONNÉES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 5 : Formulaires et Validation (Flask-WTF)
# - Chapitre 6 : Base de Données avec SQLAlchemy
# - Chapitre 7 : Migrations de Base de Données
# - Chapitre 8 : Relations entre Tables
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 5 : FORMULAIRES ET VALIDATION (Flask-WTF)
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer formulaires avec Flask-WTF
[OK] Valider données automatiquement
[OK] Afficher erreurs de validation
[OK] Protéger contre CSRF
[OK] Uploader des fichiers
[OK] Créer validateurs personnalisés
[OK] Gérer formulaires complexes
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI Flask-WTF ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : FORMULAIRES EN HTML PUR

Sans Flask-WTF :
"""

# [X] Approche manuelle (NE PAS FAIRE)
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        email = request.form.get('email')
        password = request.form.get('password')
        
        # Validation manuelle... [!]
        errors = []
        
        if not username:
            errors.append("Username requis")
        elif len(username) < 3:
            errors.append("Username trop court")
        
        if not email:
            errors.append("Email requis")
        elif '@' not in email:
            errors.append("Email invalide")
        
        if not password:
            errors.append("Password requis")
        elif len(password) < 8:
            errors.append("Password trop court")
        
        if errors:
            return render_template('register.html', errors=errors)
        
        # Traiter inscription...
        
    return render_template('register.html')

"""
[X] PROBLÈMES

1. CODE RÉPÉTITIF
   - Validation manuelle partout
   - Beaucoup de if/elif

2. PAS DE PROTECTION CSRF
   - Vulnérable aux attaques
   
3. MESSAGES D'ERREUR EN DUR
   - Difficile à internationaliser
   - Pas cohérents

4. HTML MANUEL
   - Répéter structure formulaire
   - Oublis faciles

5. DIFFICILE À TESTER
   - Logique mélangée


[OK] SOLUTION : Flask-WTF

Flask-WTF = Extension pour formulaires basée sur WTForms
- Validation automatique
- Protection CSRF intégrée
- Génération HTML automatique
- Messages d'erreur
- Testabilité


AVANTAGES [OBJECTIF]

[OK] Code propre et lisible
[OK] Validation réutilisable
[OK] Sécurité par défaut
[OK] DRY (Don't Repeat Yourself)
[OK] Facile à étendre
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install flask-wtf
pip install email-validator  # Pour validation d'emails

"""
CONFIGURATION DE BASE
"""

# app.py
from flask import Flask
from flask_wtf import FlaskForm

app = Flask(__name__)

# [ATTENTION] SECRET_KEY OBLIGATOIRE pour CSRF
app.config['SECRET_KEY'] = 'votre-cle-secrete-super-longue-et-aleatoire'

"""
[IDEE] SECRET_KEY

QU'EST-CE QUE C'EST ?
Clé secrète pour signer cryptographiquement les données

UTILISATION :
- Protection CSRF (tokens)
- Sessions sécurisées
- Cookies signés

[ATTENTION] EN PRODUCTION :
- Clé longue et aléatoire
- Ne JAMAIS commiter dans Git
- Utiliser variables d'environnement
"""

# Générer une clé sécurisée
import secrets
print(secrets.token_hex(32))
# -> 'a1b2c3...' (64 caractères)

# Utiliser variable d'environnement
import os
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'dev-key-not-secure'


# ----------------------------------------------------------------------------
# [NOTE] CRÉER UN FORMULAIRE SIMPLE
# ----------------------------------------------------------------------------

"""
ÉTAPE 1 : CRÉER LA CLASSE FORMULAIRE
"""

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class LoginForm(FlaskForm):
    """
    [IDEE] FORMULAIRE DE LOGIN
    
    Hérite de FlaskForm
    Chaque attribut = un champ du formulaire
    """
    
    # Champ texte
    username = StringField(
        'Nom d\'utilisateur',           # Label affiché
        validators=[                     # Validateurs
            DataRequired(message='Ce champ est requis'),
            Length(min=3, max=20, message='Entre 3 et 20 caractères')
        ]
    )
    
    # Champ password
    password = PasswordField(
        'Mot de passe',
        validators=[
            DataRequired(message='Ce champ est requis'),
            Length(min=8, message='Minimum 8 caractères')
        ]
    )
    
    # Bouton submit
    submit = SubmitField('Se connecter')

"""
[IDEE] ANATOMIE D'UN CHAMP

champ = TypeDeChamp(
    'Label',                    # <- Texte affiché
    validators=[...],           # <- Règles de validation
    default='valeur',           # <- Valeur par défaut
    description='Aide',         # <- Texte d'aide
    render_kw={'placeholder': 'Entrez...'} # <- Attributs HTML
)


TYPES DE CHAMPS COURANTS

StringField         -> <input type="text">
PasswordField       -> <input type="password">
EmailField          -> <input type="email">
IntegerField        -> <input type="number">
FloatField          -> <input type="number" step="any">
TextAreaField       -> <textarea>
BooleanField        -> <input type="checkbox">
RadioField          -> <input type="radio">
SelectField         -> <select>
SelectMultipleField -> <select multiple>
FileField           -> <input type="file">
HiddenField         -> <input type="hidden">
SubmitField         -> <button type="submit">
"""


"""
ÉTAPE 2 : UTILISER DANS LA ROUTE
"""

# app.py
from flask import Flask, render_template, flash, redirect, url_for
from forms import LoginForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'votre-cle-secrete'

@app.route('/login', methods=['GET', 'POST'])
def login():
    """
    [IDEE] FLUX COMPLET
    
    GET :
    1. Créer formulaire vide
    2. Afficher le template
    
    POST :
    1. Créer formulaire avec données
    2. Valider automatiquement
    3. Si valide -> traiter
    4. Si invalide -> ré-afficher avec erreurs
    """
    form = LoginForm()
    
    if form.validate_on_submit():
        """
        [IDEE] validate_on_submit() retourne True si :
        - Méthode est POST (ou PUT/PATCH)
        - Token CSRF est valide
        - Tous les validateurs passent
        """
        # Récupérer données validées
        username = form.username.data
        password = form.password.data
        
        # Traiter le login (vérifier en DB, etc.)
        # ... logique d'authentification ...
        
        flash(f'Connexion réussie pour {username}!', 'success')
        return redirect(url_for('home'))
    
    # GET ou validation échouée
    return render_template('login.html', form=form)

"""
[IDEE] DÉTAILS validate_on_submit()

1. VÉRIFIE LA MÉTHODE
   request.method in ('POST', 'PUT', 'PATCH')
   
2. VÉRIFIE LE TOKEN CSRF
   Compare token formulaire avec token session
   
3. EXÉCUTE LES VALIDATEURS
   DataRequired, Email, Length, etc.
   
4. RETOURNE
   True si tout est OK
   False sinon (erreurs stockées dans form.field.errors)
"""


"""
ÉTAPE 3 : CRÉER LE TEMPLATE
"""

# templates/login.html
"""
{% extends "base.html" %}

{% block content %}
<div class="container">
    <h2>Connexion</h2>
    
    <!-- Afficher messages flash -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div class="alert alert-{{ category }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
    {% endwith %}
    
    <!-- Formulaire -->
    <form method="POST" novalidate>
        <!-- [IDEE] Token CSRF (AUTOMATIQUE!) -->
        {{ form.hidden_tag() }}
        
        <!-- Champ username -->
        <div class="form-group">
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
            
            <!-- Afficher erreurs de validation -->
            {% if form.username.errors %}
                <div class="errors">
                    {% for error in form.username.errors %}
                        <span class="error">{{ error }}</span>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        
        <!-- Champ password -->
        <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
            
            {% if form.password.errors %}
                <div class="errors">
                    {% for error in form.password.errors %}
                        <span class="error">{{ error }}</span>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        
        <!-- Bouton submit -->
        <div class="form-group">
            {{ form.submit(class="btn btn-primary") }}
        </div>
    </form>
</div>
{% endblock %}
"""

"""
[IDEE] SYNTAXE TEMPLATE

{{ form.hidden_tag() }}
-> Génère token CSRF et autres champs cachés
-> <input type="hidden" name="csrf_token" value="...">

{{ form.field.label }}
-> Génère le <label>
-> <label for="username">Nom d'utilisateur</label>

{{ form.field() }}
-> Génère l'<input>
-> <input type="text" name="username" id="username">

{{ form.field(class="ma-classe") }}
-> Ajoute attributs HTML
-> <input type="text" class="ma-classe" name="username">

{{ form.field.errors }}
-> Liste des erreurs de validation
-> ['Ce champ est requis', 'Trop court']
"""


# ----------------------------------------------------------------------------
# [OK] VALIDATEURS
# ----------------------------------------------------------------------------

"""
VALIDATEURS INTÉGRÉS
"""

from wtforms.validators import (
    DataRequired,    # Champ obligatoire
    Email,           # Email valide
    Length,          # Longueur min/max
    EqualTo,         # Égal à un autre champ
    NumberRange,     # Plage de nombres
    URL,             # URL valide
    Regexp,          # Expression régulière
    Optional,        # Optionnel (peut être vide)
    InputRequired,   # Présent mais peut être vide
    ValidationError  # Pour exceptions personnalisées
)

"""
EXEMPLES D'UTILISATION
"""

class RegistrationForm(FlaskForm):
    """Formulaire d'inscription complet"""
    
    # Username : obligatoire, 3-20 caractères
    username = StringField(
        'Nom d\'utilisateur',
        validators=[
            DataRequired(),
            Length(min=3, max=20)
        ]
    )
    
    # Email : obligatoire et valide
    email = EmailField(
        'Email',
        validators=[
            DataRequired(),
            Email(message='Email invalide')
        ]
    )
    
    # Password : obligatoire, min 8 caractères
    password = PasswordField(
        'Mot de passe',
        validators=[
            DataRequired(),
            Length(min=8, message='Minimum 8 caractères')
        ]
    )
    
    # Confirmation password : doit correspondre à password
    password2 = PasswordField(
        'Confirmer le mot de passe',
        validators=[
            DataRequired(),
            EqualTo('password', message='Les mots de passe doivent correspondre')
        ]
    )
    
    # Age : optionnel, mais si fourni doit être entre 18 et 120
    age = IntegerField(
        'Âge',
        validators=[
            Optional(),
            NumberRange(min=18, max=120, message='Âge entre 18 et 120')
        ]
    )
    
    # URL : optionnelle, mais si fournie doit être valide
    website = StringField(
        'Site web',
        validators=[
            Optional(),
            URL(message='URL invalide')
        ]
    )
    
    # Code postal : format français
    zipcode = StringField(
        'Code postal',
        validators=[
            DataRequired(),
            Regexp(r'^\d{5}$', message='Format : 5 chiffres (ex: 75001)')
        ]
    )
    
    submit = SubmitField('S\'inscrire')

"""
[IDEE] DÉTAILS DES VALIDATEURS


1. DataRequired()
-----------------
Champ NON VIDE (après .strip())
"""
username = StringField('Username', validators=[DataRequired()])

# [OK] Valide : 'alice', '  bob  '
# [X] Invalide : '', '   ', None

"""
2. Email()
----------
Format email valide (regex complexe)
"""
email = EmailField('Email', validators=[Email()])

# [OK] Valide : 'user@example.com'
# [X] Invalide : 'user', 'user@', '@example.com'

"""
3. Length(min=, max=)
---------------------
Longueur de chaîne
"""
username = StringField('Username', validators=[Length(min=3, max=20)])

# [OK] Valide : 'abc', 'abcdefghijklmnopqrst'
# [X] Invalide : 'ab' (trop court), 'a'*21 (trop long)

"""
4. EqualTo('field_name')
------------------------
Doit être égal à un autre champ
"""
password = PasswordField('Password')
password2 = PasswordField('Confirm', validators=[EqualTo('password')])

# Si password='secret123' et password2='secret123' -> [OK]
# Si password='secret123' et password2='secret456' -> [X]

"""
5. NumberRange(min=, max=)
--------------------------
Nombre dans une plage
"""
age = IntegerField('Age', validators=[NumberRange(min=18, max=120)])

# [OK] Valide : 18, 25, 120
# [X] Invalide : 17, 121

"""
6. URL()
--------
URL valide (avec protocole)
"""
website = StringField('Website', validators=[URL()])

# [OK] Valide : 'https://example.com', 'http://site.fr'
# [X] Invalide : 'example.com', 'not a url'

"""
7. Regexp(pattern)
------------------
Expression régulière
"""
phone = StringField('Phone', validators=[
    Regexp(r'^\+?1?\d{9,15}$', message='Téléphone invalide')
])

# [OK] Valide : '+33612345678', '0612345678'
# [X] Invalide : '123', 'abc'

"""
8. Optional()
-------------
Champ optionnel (peut être vide)
Mais si rempli, autres validateurs s'appliquent
"""
website = StringField('Website', validators=[Optional(), URL()])

# [OK] Valide : '' (vide), 'https://example.com'
# [X] Invalide : 'not a url'


# ----------------------------------------------------------------------------
# [DESIGN] VALIDATEURS PERSONNALISÉS
# ----------------------------------------------------------------------------

"""
MÉTHODE 1 : Méthode validate_<field_name>
"""

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    
    def validate_username(self, username):
        """
        [IDEE] VALIDATEUR PERSONNALISÉ
        
        Nom : validate_<nom_du_champ>
        Appelé automatiquement après validateurs standard
        Lever ValidationError si invalide
        """
        # Vérifier si username déjà pris
        # (en vrai, chercher en base de données)
        forbidden_usernames = ['admin', 'root', 'system', 'user']
        
        if username.data.lower() in forbidden_usernames:
            raise ValidationError('Ce nom d\'utilisateur n\'est pas autorisé')
        
        # Vérifier caractères spéciaux
        if not username.data.isalnum():
            raise ValidationError('Seulement lettres et chiffres autorisés')

"""
MÉTHODE 2 : Fonction validateur réutilisable
"""

from wtforms import ValidationError

def length_between(min_length, max_length):
    """
    Créer un validateur de longueur personnalisé
    """
    def _length(form, field):
        length = len(field.data)
        if length < min_length or length > max_length:
            raise ValidationError(
                f'Doit contenir entre {min_length} et {max_length} caractères'
            )
    return _length

def unique_email(form, field):
    """
    Vérifier que l'email n'existe pas déjà
    """
    # En vrai : chercher en base de données
    # from models import User
    # if User.query.filter_by(email=field.data).first():
    #     raise ValidationError('Cet email est déjà utilisé')
    
    # Simulation
    existing_emails = ['alice@example.com', 'bob@example.com']
    if field.data in existing_emails:
        raise ValidationError('Cet email est déjà utilisé')

# Utilisation
class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        length_between(3, 20)
    ])
    
    email = EmailField('Email', validators=[
        DataRequired(),
        Email(),
        unique_email
    ])

"""
MÉTHODE 3 : Classe validateur réutilisable
"""

class Unique:
    """
    Validateur pour vérifier l'unicité en base de données
    """
    def __init__(self, model, field, message='Cette valeur existe déjà'):
        self.model = model
        self.field = field
        self.message = message
    
    def __call__(self, form, field):
        # Chercher en DB
        # check = self.model.query.filter(self.field == field.data).first()
        # if check:
        #     raise ValidationError(self.message)
        pass

# Utilisation
from models import User

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        Unique(User, User.username, message='Username déjà pris')
    ])
    
    email = EmailField('Email', validators=[
        DataRequired(),
        Email(),
        Unique(User, User.email, message='Email déjà utilisé')
    ])


# ----------------------------------------------------------------------------
# [LISTE] TYPES DE CHAMPS AVANCÉS
# ----------------------------------------------------------------------------

"""
CHAMPS DE SÉLECTION
"""

from wtforms import SelectField, SelectMultipleField, RadioField

class PreferencesForm(FlaskForm):
    """
    Formulaire avec sélections
    """
    
    # Liste déroulante (select)
    country = SelectField(
        'Pays',
        choices=[
            ('fr', 'France'),
            ('uk', 'Royaume-Uni'),
            ('de', 'Allemagne'),
            ('es', 'Espagne')
        ],
        validators=[DataRequired()]
    )
    
    # Sélection multiple
    interests = SelectMultipleField(
        'Centres d\'intérêt',
        choices=[
            ('tech', 'Technologie'),
            ('sport', 'Sport'),
            ('music', 'Musique'),
            ('travel', 'Voyages')
        ]
    )
    
    # Boutons radio
    gender = RadioField(
        'Genre',
        choices=[
            ('m', 'Homme'),
            ('f', 'Femme'),
            ('o', 'Autre')
        ],
        default='o'
    )

"""
[IDEE] CHOICES DYNAMIQUES

Charger depuis base de données :
"""

@app.route('/form')
def show_form():
    form = PreferencesForm()
    
    # Charger pays depuis DB
    # countries = Country.query.all()
    # form.country.choices = [(c.code, c.name) for c in countries]
    
    # Simulation
    form.country.choices = [
        ('fr', 'France'),
        ('uk', 'Royaume-Uni')
    ]
    
    return render_template('form.html', form=form)

"""
CHAMPS CHECKBOX
"""

from wtforms import BooleanField

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    
    # Checkbox simple
    remember_me = BooleanField('Se souvenir de moi')
    
    # Checkbox requis (conditions d'utilisation)
    accept_terms = BooleanField(
        'J\'accepte les conditions d\'utilisation',
        validators=[
            DataRequired(message='Vous devez accepter les conditions')
        ]
    )

"""
CHAMPS TEXTAREA
"""

from wtforms import TextAreaField

class PostForm(FlaskForm):
    title = StringField('Titre', validators=[DataRequired()])
    
    # Zone de texte multi-lignes
    content = TextAreaField(
        'Contenu',
        validators=[DataRequired(), Length(min=10)],
        render_kw={
            'rows': 10,
            'placeholder': 'Écrivez votre article ici...'
        }
    )

"""
CHAMPS CACHÉS
"""

from wtforms import HiddenField

class EditForm(FlaskForm):
    # Stocker l'ID (invisible pour l'utilisateur)
    post_id = HiddenField()
    
    title = StringField('Titre', validators=[DataRequired()])
    content = TextAreaField('Contenu', validators=[DataRequired()])

# Template
"""
<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.post_id() }}  <!-- Caché -->
    {{ form.title() }}     <!-- Visible -->
    {{ form.content() }}   <!-- Visible -->
</form>
"""


# ----------------------------------------------------------------------------
# [SORTIE] UPLOAD DE FICHIERS
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install flask-wtf[file]

"""
FORMULAIRE D'UPLOAD
"""

from flask_wtf.file import FileField, FileAllowed, FileRequired
from werkzeug.utils import secure_filename
import os

class UploadForm(FlaskForm):
    """
    Formulaire d'upload de fichier
    """
    # Champ fichier
    photo = FileField(
        'Photo de profil',
        validators=[
            FileRequired(message='Fichier requis'),
            FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images seulement!')
        ]
    )
    
    description = StringField('Description')
    submit = SubmitField('Uploader')

"""
CONFIGURATION
"""

# app.py
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16 MB max

# Créer le dossier s'il n'existe pas
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

"""
ROUTE D'UPLOAD
"""

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    form = UploadForm()
    
    if form.validate_on_submit():
        """
        [IDEE] TRAITER LE FICHIER UPLOADÉ
        """
        file = form.photo.data
        
        # [IDEE] secure_filename() = Sécuriser le nom
        # Enlève caractères dangereux : /, \, .., etc.
        filename = secure_filename(file.filename)
        
        # Ajouter timestamp pour éviter collisions
        from datetime import datetime
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"{timestamp}_{filename}"
        
        # Chemin complet
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        
        # Sauvegarder
        file.save(filepath)
        
        flash(f'Fichier {filename} uploadé avec succès !', 'success')
        return redirect(url_for('home'))
    
    return render_template('upload.html', form=form)

"""
TEMPLATE D'UPLOAD
"""

# templates/upload.html
"""
{% extends "base.html" %}

{% block content %}
<h2>Upload de fichier</h2>

<!-- [ATTENTION] IMPORTANT : enctype pour fichiers! -->
<form method="POST" enctype="multipart/form-data">
    {{ form.hidden_tag() }}
    
    <div>
        {{ form.photo.label }}
        {{ form.photo() }}
        
        {% if form.photo.errors %}
            {% for error in form.photo.errors %}
                <span class="error">{{ error }}</span>
            {% endfor %}
        {% endif %}
    </div>
    
    <div>
        {{ form.description.label }}
        {{ form.description() }}
    </div>
    
    <div>
        {{ form.submit() }}
    </div>
</form>
{% endblock %}
"""

"""
[IDEE] VALIDATIONS SUPPLÉMENTAIRES

Taille du fichier :
"""

def file_size_limit(max_size_mb):
    """Limiter la taille du fichier"""
    max_bytes = max_size_mb * 1024 * 1024
    
    def _check_file_size(form, field):
        if len(field.data.read()) > max_bytes:
            field.data.seek(0)  # Remettre à zéro
            raise ValidationError(f'Fichier trop grand (max {max_size_mb}MB)')
        field.data.seek(0)
    
    return _check_file_size

class UploadForm(FlaskForm):
    photo = FileField('Photo', validators=[
        FileRequired(),
        FileAllowed(['jpg', 'png']),
        file_size_limit(5)  # Max 5 MB
    ])


# Ce fichier continue...
# La suite (Chapitre 6: Base de données) sera dans le prochain fichier

# ============================================================================
# FIN DE CE FICHIER - SUITE DANS flask_partie2_suite.txt
# ============================================================================
# ============================================================================
# [LIVRE] FLASK - PARTIE 2 (SUITE) : BASE DE DONNÉES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 6 : Base de Données avec SQLAlchemy
# - Chapitre 7 : Migrations de Base de Données
# - Chapitre 8 : Relations entre Tables
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Chapitre 5 complété
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 6 : BASE DE DONNÉES AVEC SQLAlchemy
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi utiliser une base de données
[OK] Qu'est-ce qu'un ORM et SQLAlchemy
[OK] Configurer SQLAlchemy avec Flask
[OK] Créer des modèles (tables)
[OK] Opérations CRUD (Create, Read, Update, Delete)
[OK] Requêtes simples et complexes
[OK] Intégrer avec formulaires
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI UNE BASE DE DONNÉES ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : DONNÉES EN MÉMOIRE

Sans base de données :
"""

# [X] Données stockées dans des variables Python
USERS = []
POSTS = []

@app.route('/register', methods=['POST'])
def register():
    user = {
        'username': request.form['username'],
        'email': request.form['email']
    }
    USERS.append(user)
    return "OK"

"""
[X] PROBLÈMES MAJEURS

1. DONNÉES PERDUES AU REDÉMARRAGE
   Serveur arrêté -> Toutes les données disparaissent
   
2. PAS DE PERSISTENCE
   Impossible de conserver les données
   
3. LIMITE DE MÉMOIRE
   RAM limitée -> Impossible de gérer beaucoup de données
   
4. PAS DE RECHERCHE EFFICACE
   Parcourir toute la liste -> O(n)
   
5. PAS DE RELATIONS
   Difficile de lier users <-> posts
   
6. PAS DE TRANSACTIONS
   Pas de rollback en cas d'erreur
   
7. PAS DE CONCURRENT ACCESS
   Problèmes avec plusieurs utilisateurs


[OK] SOLUTION : BASE DE DONNÉES

Base de données = Système pour stocker et gérer les données

AVANTAGES [OBJECTIF]

[OK] PERSISTENCE
   Données sauvegardées sur disque
   Survivent aux redémarrages
   
[OK] GRANDE CAPACITÉ
   Millions, milliards de lignes
   
[OK] RECHERCHE RAPIDE
   Index, optimisations -> O(log n) ou O(1)
   
[OK] RELATIONS
   Liens entre tables (foreign keys)
   
[OK] TRANSACTIONS
   ACID (Atomicity, Consistency, Isolation, Durability)
   
[OK] CONCURRENT ACCESS
   Plusieurs utilisateurs simultanés
   
[OK] SÉCURITÉ
   Permissions, encryption
"""


# ----------------------------------------------------------------------------
# [ARCHIVE] TYPES DE BASES DE DONNÉES
# ----------------------------------------------------------------------------

"""
BASES DE DONNÉES RELATIONNELLES (SQL)

Structure : Tables avec lignes et colonnes
Langage : SQL (Structured Query Language)

EXEMPLES :
- SQLite (fichier local, simple)
- PostgreSQL (puissant, production)
- MySQL/MariaDB (populaire)
- SQL Server (Microsoft)


┌────────────────────────────────────┐
│          Table : users             │
├─────┬──────────┬───────────────────┤
│ id  │ username │ email             │
├─────┼──────────┼───────────────────┤
│  1  │ alice    │ alice@example.com │
│  2  │ bob      │ bob@example.com   │
│  3  │ charlie  │ charlie@ex.com    │
└─────┴──────────┴───────────────────┘

Requête SQL :
SELECT * FROM users WHERE username = 'alice';


BASES DE DONNÉES NoSQL

Structure : Documents, clés-valeurs, graphes
Pas de schéma fixe

EXEMPLES :
- MongoDB (documents JSON)
- Redis (clé-valeur)
- Cassandra (colonnes)
- Neo4j (graphes)


POUR FLASK : Généralement SQL
- Débutants : SQLite
- Production : PostgreSQL
"""


# ----------------------------------------------------------------------------
# [OUTIL] QU'EST-CE QU'UN ORM ?
# ----------------------------------------------------------------------------

"""
ORM = Object-Relational Mapping

[IDEE] CONCEPT

ORM = Pont entre Python (objets) et SQL (tables)

Sans ORM (SQL brut) :
"""

import sqlite3

# Créer utilisateur
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute(
    "INSERT INTO users (username, email) VALUES (?, ?)",
    ('alice', 'alice@example.com')
)
conn.commit()

# Récupérer utilisateurs
cursor.execute("SELECT * FROM users WHERE username = ?", ('alice',))
result = cursor.fetchone()
# result = (1, 'alice', 'alice@example.com')

"""
[X] PROBLÈMES SQL BRUT

1. CODE VERBEUX
   Beaucoup de boilerplate
   
2. ERREURS SQL
   Typos dans requêtes difficiles à déboguer
   
3. PAS D'AUTOCOMPLÉTION
   IDE ne peut pas aider
   
4. INJECTION SQL
   Risque de sécurité si mal fait
   
5. MANIPULATION MANUELLE
   Convertir tuples <-> objets Python


[OK] AVEC ORM (SQLAlchemy)
"""

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

# Définir modèle
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

# Créer utilisateur
user = User(username='alice', email='alice@example.com')
db.session.add(user)
db.session.commit()

# Récupérer utilisateur
user = User.query.filter_by(username='alice').first()
# user.username -> 'alice'
# user.email -> 'alice@example.com'

"""
[OK] AVANTAGES ORM

1. CODE PYTHONIQUE
   Manipuler objets Python
   
2. AUTOCOMPLÉTION
   IDE aide avec attributs
   
3. SÉCURITÉ
   Protection SQL injection automatique
   
4. ABSTRACTION BD
   Changer de SQLite -> PostgreSQL facilement
   
5. RELATIONS
   user.posts automatique
   
6. VALIDATION
   Types vérifiés


[IDEE] SQLAlchemy = ORM le plus populaire pour Python
Flask-SQLAlchemy = Intégration SQLAlchemy + Flask
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install flask-sqlalchemy

"""
CONFIGURATION DE BASE
"""

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 1. Configuration de la base de données
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

# 2. Désactiver tracking (économie mémoire)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 3. Créer instance SQLAlchemy
db = SQLAlchemy(app)

"""
[IDEE] SQLALCHEMY_DATABASE_URI

Format : dialect+driver://username:password@host:port/database

EXEMPLES :

SQLite (fichier local) :
'sqlite:///app.db'              -> Fichier app.db dans dossier courant
'sqlite:////tmp/test.db'        -> Chemin absolu
'sqlite:///:memory:'            -> Base en mémoire (tests)

PostgreSQL :
'postgresql://user:pass@localhost/mydb'
'postgresql://user:pass@localhost:5432/mydb'

MySQL :
'mysql://user:pass@localhost/mydb'
'mysql+pymysql://user:pass@localhost/mydb'  (avec driver pymysql)

MariaDB :
'mariadb+mariadbconnector://user:pass@localhost/mydb'


[IDEE] POURQUOI SQLite POUR DÉBUTER ?

[OK] Aucune installation
[OK] Fichier unique
[OK] Parfait pour développement
[OK] Facile à réinitialiser
[OK] Portable

[ATTENTION] Limitations SQLite :
- Pas de concurrent writes
- Fonctionnalités limitées
- Pas pour production à forte charge


[IDEE] SQLALCHEMY_TRACK_MODIFICATIONS

Option obsolète qui consomme de la mémoire
Toujours mettre False


STRUCTURE DE PROJET
"""

mon_projet/
├── app.py              # Application Flask
├── models.py           # Modèles de base de données
├── forms.py            # Formulaires
├── routes.py           # Routes (optionnel)
├── templates/
├── static/
├── instance/           # <- Dossier créé automatiquement
│   └── app.db         # <- Base de données SQLite
└── requirements.txt


# ----------------------------------------------------------------------------
# [GRAPHIQUE] CRÉER UN MODÈLE
# ----------------------------------------------------------------------------

"""
MODÈLE = CLASSE PYTHON = TABLE EN BASE DE DONNÉES
"""

# models.py
from datetime import datetime
from app import db

class User(db.Model):
    """
    [IDEE] MODÈLE USER
    
    Hérite de db.Model
    Représente la table 'user' en base de données
    """
    
    # Nom de la table (optionnel)
    __tablename__ = 'users'
    
    # Colonnes
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    is_active = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def __repr__(self):
        """Représentation string (pour debug)"""
        return f'<User {self.username}>'

"""
[IDEE] ANATOMIE D'UN MODÈLE


1. CLASSE
class User(db.Model):
    Hérite de db.Model -> SQLAlchemy sait que c'est un modèle


2. __tablename__ (OPTIONNEL)
__tablename__ = 'users'
    Nom de la table en BD
    Si absent : nom de la classe en minuscules ('user')


3. COLONNES
nom = db.Column(type, option1, option2, ...)
    
    Type : db.Integer, db.String, db.Boolean, etc.
    Options : primary_key, unique, nullable, default, etc.


4. __repr__() (OPTIONNEL mais RECOMMANDÉ)
def __repr__(self):
    return f'<User {self.username}>'
    
    Représentation lisible de l'objet
    Utile pour debug : print(user) -> <User alice>
"""


# ----------------------------------------------------------------------------
# [DESIGN] TYPES DE COLONNES
# ----------------------------------------------------------------------------

"""
TYPES COURANTS
"""

class Example(db.Model):
    # Nombres entiers
    id = db.Column(db.Integer, primary_key=True)
    age = db.Column(db.Integer)
    views = db.Column(db.BigInteger)  # Grand entier
    
    # Texte
    username = db.Column(db.String(80))     # Longueur fixe max
    email = db.Column(db.String(120))
    bio = db.Column(db.Text)                # Texte illimité
    
    # Nombres décimaux
    price = db.Column(db.Float)
    rating = db.Column(db.Numeric(5, 2))   # 5 chiffres, 2 décimales
    
    # Booléen
    is_active = db.Column(db.Boolean, default=False)
    
    # Date et heure
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    birthday = db.Column(db.Date)
    wake_time = db.Column(db.Time)
    
    # Binaire
    avatar = db.Column(db.LargeBinary)      # Fichiers (éviter, utiliser URL)
    
    # JSON (PostgreSQL, SQLite 3.9+)
    settings = db.Column(db.JSON)

"""
[IDEE] DÉTAILS DES TYPES


db.Integer
----------
Entier signé (-2147483648 à 2147483647)
Utilisation : IDs, compteurs, âge
"""
age = db.Column(db.Integer)

"""
db.String(length)
-----------------
Chaîne de caractères avec longueur max
Utilisation : username, email, nom
"""
username = db.Column(db.String(80))

"""
[ATTENTION] IMPORTANT : Toujours spécifier longueur !
db.String(80)  [OK] OK
db.String()    [X] Erreur (sauf si Text)


db.Text
-------
Texte illimité
Utilisation : articles, descriptions, contenu
"""
content = db.Column(db.Text)

"""
db.Boolean
----------
True/False
Base de données : 0/1 ou TRUE/FALSE selon SGBD
"""
is_published = db.Column(db.Boolean, default=False)

"""
db.DateTime
-----------
Date + Heure
Format : datetime.datetime Python
"""
created_at = db.Column(db.DateTime, default=datetime.utcnow)

# [ATTENTION] default=datetime.utcnow (SANS parenthèses !)
# [OK] datetime.utcnow   -> Fonction (appelée à chaque insertion)
# [X] datetime.utcnow() -> Valeur fixe (date de définition du modèle)

"""
db.Float
--------
Nombre décimal (approximatif)
Utilisation : coordonnées, mesures
"""
latitude = db.Column(db.Float)

"""
db.Numeric(precision, scale)
----------------------------
Nombre décimal (exact)
precision : Total de chiffres
scale : Chiffres après virgule

Utilisation : argent, scores
"""
price = db.Column(db.Numeric(10, 2))  # 99999999.99

"""
db.JSON
-------
Données JSON (dict, list)
Nécessite PostgreSQL ou SQLite 3.9+
"""
settings = db.Column(db.JSON)

# Utilisation :
user.settings = {'theme': 'dark', 'lang': 'fr'}


# ----------------------------------------------------------------------------
# [CONFIG] OPTIONS DE COLONNES
# ----------------------------------------------------------------------------

"""
OPTIONS PRINCIPALES
"""

class User(db.Model):
    # PRIMARY KEY (obligatoire sur au moins une colonne)
    id = db.Column(db.Integer, primary_key=True)
    
    # UNIQUE (valeur unique dans toute la table)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)
    
    # NULLABLE (peut être NULL/None)
    phone = db.Column(db.String(20), nullable=True)   # Peut être vide
    password = db.Column(db.String(128), nullable=False)  # Obligatoire
    
    # DEFAULT (valeur par défaut)
    is_active = db.Column(db.Boolean, default=True)
    role = db.Column(db.String(20), default='user')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # INDEX (accélère recherche)
    email_indexed = db.Column(db.String(120), index=True)
    
    # SERVER_DEFAULT (valeur côté BD)
    uuid = db.Column(db.String(36), server_default='uuid_generate_v4()')

"""
[IDEE] DÉTAILS DES OPTIONS


primary_key=True
----------------
- Clé primaire (identifiant unique)
- Auto-increment automatique si Integer
- Une seule par table (ou composite)
"""
id = db.Column(db.Integer, primary_key=True)

# Après insertion :
user = User(username='alice')
db.session.add(user)
db.session.commit()
print(user.id)  # -> 1 (généré automatiquement)

"""
unique=True
-----------
- Valeur doit être unique dans la table
- Crée un index automatiquement
"""
username = db.Column(db.String(80), unique=True)

# Si tentative de doublon -> IntegrityError
user1 = User(username='alice')
user2 = User(username='alice')  # [X] Erreur !

"""
nullable=False
--------------
- Champ obligatoire (NOT NULL en SQL)
- Par défaut : nullable=True
"""
email = db.Column(db.String(120), nullable=False)

# Si omis -> IntegrityError
user = User(username='alice')  # [X] email manquant !

"""
default=value
-------------
- Valeur par défaut si non fournie
- Évaluée côté Python
"""
is_active = db.Column(db.Boolean, default=True)

user = User(username='alice')
print(user.is_active)  # -> True (défaut)

# Avec fonction :
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# [ATTENTION] Sans () : fonction appelée à chaque création

"""
index=True
----------
- Crée un index de base de données
- Accélère les recherches sur cette colonne
- Utiliser pour colonnes recherchées fréquemment
"""
email = db.Column(db.String(120), index=True)

# Recherche optimisée :
User.query.filter_by(email='alice@example.com').first()


# ----------------------------------------------------------------------------
# [ARCHIVE] CRÉER LES TABLES
# ----------------------------------------------------------------------------

"""
MÉTHODE 1 : db.create_all() (Développement)
"""

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# Importer modèles
from models import User, Post

# Créer les tables
with app.app_context():
    db.create_all()
    print("Tables créées !")

"""
[IDEE] db.create_all()

- Crée TOUTES les tables définies
- Si table existe déjà -> ne fait rien
- Ne modifie PAS les tables existantes
- OK pour développement
- [ATTENTION] NE PAS utiliser en production (utiliser migrations)


MÉTHODE 2 : Flask Shell (Interactif)
"""

# Terminal
flask shell

# Dans le shell Python
>>> from app import db
>>> db.create_all()
>>> exit()

"""
MÉTHODE 3 : Script d'initialisation
"""

# init_db.py
from app import app, db

with app.app_context():
    # Supprimer toutes les tables
    db.drop_all()
    
    # Recréer toutes les tables
    db.create_all()
    
    print("Base de données initialisée !")

# Lancer : python init_db.py

"""
[ATTENTION] db.drop_all() SUPPRIME TOUTES LES DONNÉES !
Utiliser seulement en développement


VÉRIFIER LES TABLES
"""

# Dans Flask shell
>>> from app import db
>>> db.engine.table_names()
['users', 'posts']

# Ou avec SQLite
sqlite3 instance/app.db
sqlite> .tables
users  posts

sqlite> .schema users
CREATE TABLE users (
    id INTEGER NOT NULL PRIMARY KEY,
    username VARCHAR(80) NOT NULL UNIQUE,
    email VARCHAR(120) NOT NULL UNIQUE,
    ...
);


# ----------------------------------------------------------------------------
# + OPÉRATIONS CRUD : CREATE (Créer)
# ----------------------------------------------------------------------------

"""
CRÉER UN ENREGISTREMENT
"""

from models import User
from app import db

# 1. Créer l'objet
user = User(
    username='alice',
    email='alice@example.com'
)

# 2. Ajouter à la session
db.session.add(user)

# 3. Sauvegarder en base de données
db.session.commit()

"""
[IDEE] COMPRENDRE LA SESSION


SESSION = "Transaction" en cours
          Panier d'achats avant de payer


ÉTAPES :

1. db.session.add(objet)
   -> Ajoute à la session (pas encore en BD)
   -> Comme mettre article dans panier
   
2. db.session.commit()
   -> Sauvegarde TOUT en BD
   -> Comme payer et finaliser commande
   
3. db.session.rollback()
   -> Annule tous les changements
   -> Comme vider le panier


POURQUOI ?

- TRANSACTIONS : Tout ou rien
- PERFORMANCE : Batch plusieurs opérations
- COHÉRENCE : Valider ensemble


EXEMPLE TRANSACTION
"""

try:
    # Créer utilisateur
    user = User(username='alice', email='alice@example.com')
    db.session.add(user)
    
    # Créer post
    post = Post(title='Hello', content='...', user=user)
    db.session.add(post)
    
    # Sauvegarder ensemble
    db.session.commit()
    print("User et post créés !")
    
except Exception as e:
    # Erreur -> Annuler tout
    db.session.rollback()
    print(f"Erreur : {e}")

"""
CRÉER PLUSIEURS ENREGISTREMENTS
"""

# Méthode 1 : add() multiple fois
user1 = User(username='alice', email='alice@example.com')
user2 = User(username='bob', email='bob@example.com')
user3 = User(username='charlie', email='charlie@example.com')

db.session.add(user1)
db.session.add(user2)
db.session.add(user3)
db.session.commit()

# Méthode 2 : add_all()
users = [
    User(username='alice', email='alice@example.com'),
    User(username='bob', email='bob@example.com'),
    User(username='charlie', email='charlie@example.com')
]
db.session.add_all(users)
db.session.commit()

"""
VALEURS GÉNÉRÉES AUTOMATIQUEMENT
"""

user = User(username='alice', email='alice@example.com')

# Avant commit
print(user.id)          # -> None (pas encore en BD)
print(user.created_at)  # -> None

# Ajouter et commiter
db.session.add(user)
db.session.commit()

# Après commit
print(user.id)          # -> 1 (généré par BD)
print(user.created_at)  # -> datetime(...) (default appliqué)


# ----------------------------------------------------------------------------
# [RECHERCHE] OPÉRATIONS CRUD : READ (Lire)
# ----------------------------------------------------------------------------

"""
REQUÊTES DE BASE
"""

# Tous les utilisateurs
users = User.query.all()
# -> [<User alice>, <User bob>, <User charlie>]

# Premier utilisateur
user = User.query.first()
# -> <User alice>

# Utilisateur par ID (primary key)
user = User.query.get(1)
# -> <User alice> ou None si pas trouvé

# Utilisateur par ID avec 404 si absent
user = User.query.get_or_404(1)
# -> <User alice> ou erreur 404

# Compter
count = User.query.count()
# -> 3

"""
[IDEE] QUERY API

User.query retourne un objet Query
Méthodes chaînables :
"""

User.query.filter(...).order_by(...).limit(10).all()

"""
FILTRER : filter_by()
"""

# Filtre simple (égalité)
user = User.query.filter_by(username='alice').first()
# SQL: SELECT * FROM users WHERE username = 'alice' LIMIT 1

# Plusieurs conditions (AND)
user = User.query.filter_by(username='alice', is_active=True).first()
# SQL: WHERE username = 'alice' AND is_active = 1

# Tous les utilisateurs actifs
users = User.query.filter_by(is_active=True).all()

"""
FILTRER : filter() (Plus puissant)
"""

# Égalité
users = User.query.filter(User.username == 'alice').all()

# Différent
users = User.query.filter(User.username != 'admin').all()

# Comparaison
users = User.query.filter(User.age > 18).all()
users = User.query.filter(User.age >= 18).all()
users = User.query.filter(User.age < 65).all()

# LIKE (correspondance partielle)
users = User.query.filter(User.email.like('%@gmail.com')).all()
# SQL: WHERE email LIKE '%@gmail.com'

# ILIKE (insensible à la casse, PostgreSQL)
users = User.query.filter(User.username.ilike('alice%')).all()

# IN (dans une liste)
users = User.query.filter(User.username.in_(['alice', 'bob'])).all()

# NOT IN
users = User.query.filter(~User.username.in_(['admin', 'root'])).all()

# IS NULL
users = User.query.filter(User.phone == None).all()
users = User.query.filter(User.phone.is_(None)).all()

# IS NOT NULL
users = User.query.filter(User.phone != None).all()
users = User.query.filter(User.phone.isnot(None)).all()

"""
OPÉRATEURS LOGIQUES
"""

from sqlalchemy import and_, or_, not_

# AND
users = User.query.filter(
    and_(
        User.is_active == True,
        User.age >= 18
    )
).all()

# Ou simplement avec plusieurs filter()
users = User.query.filter(
    User.is_active == True
).filter(
    User.age >= 18
).all()

# OR
users = User.query.filter(
    or_(
        User.username == 'alice',
        User.email.like('%@admin.com')
    )
).all()

# NOT
users = User.query.filter(
    not_(User.username == 'admin')
).all()

# Combinaison
users = User.query.filter(
    and_(
        User.is_active == True,
        or_(
            User.role == 'admin',
            User.role == 'moderator'
        )
    )
).all()

"""
TRIER : order_by()
"""

# Ordre croissant (alphabétique)
users = User.query.order_by(User.username).all()
# SQL: ORDER BY username ASC

# Ordre décroissant
users = User.query.order_by(User.created_at.desc()).all()
# SQL: ORDER BY created_at DESC

# Plusieurs colonnes
users = User.query.order_by(User.role, User.username).all()
# SQL: ORDER BY role, username

"""
LIMITER : limit() et offset()
"""

# Premiers 10
users = User.query.limit(10).all()
# SQL: LIMIT 10

# Pagination : page 2 (10 résultats par page)
page = 2
per_page = 10
offset = (page - 1) * per_page

users = User.query.offset(offset).limit(per_page).all()
# SQL: LIMIT 10 OFFSET 10

"""
PAGINATION INTÉGRÉE
"""

# Paginer automatiquement
page = request.args.get('page', 1, type=int)
pagination = User.query.paginate(
    page=page,
    per_page=10,
    error_out=False
)

# Accéder aux données
users = pagination.items            # Liste des users de cette page
total = pagination.total            # Nombre total d'users
pages = pagination.pages            # Nombre total de pages
has_next = pagination.has_next      # Y a-t-il une page suivante ?
has_prev = pagination.has_prev      # Y a-t-il une page précédente ?
next_num = pagination.next_num      # Numéro page suivante
prev_num = pagination.prev_num      # Numéro page précédente

"""
EXEMPLE COMPLET DE RECHERCHE
"""

@app.route('/users')
def users_list():
    # Paramètres de requête
    search = request.args.get('search', '')
    role = request.args.get('role', '')
    page = request.args.get('page', 1, type=int)
    
    # Construire la requête
    query = User.query
    
    # Filtrer par recherche
    if search:
        query = query.filter(
            or_(
                User.username.like(f'%{search}%'),
                User.email.like(f'%{search}%')
            )
        )
    
    # Filtrer par rôle
    if role:
        query = query.filter_by(role=role)
    
    # Seulement actifs
    query = query.filter_by(is_active=True)
    
    # Trier
    query = query.order_by(User.created_at.desc())
    
    # Paginer
    pagination = query.paginate(
        page=page,
        per_page=20,
        error_out=False
    )
    
    return render_template(
        'users.html',
        users=pagination.items,
        pagination=pagination
    )


# ----------------------------------------------------------------------------
# [EDIT] OPÉRATIONS CRUD : UPDATE (Modifier)
# ----------------------------------------------------------------------------

"""
MODIFIER UN ENREGISTREMENT
"""

# 1. Récupérer l'objet
user = User.query.get(1)

# 2. Modifier les attributs
user.email = 'newemail@example.com'
user.is_active = False

# 3. Sauvegarder
db.session.commit()

"""
[IDEE] AUCUN .add() NÉCESSAIRE !

Si objet déjà dans la session (récupéré par query)
-> Modifications trackées automatiquement
-> commit() suffit


MODIFIER PLUSIEURS ATTRIBUTS
"""

user = User.query.get(1)

# Avec setattr
user.username = 'alice2'
user.email = 'alice2@example.com'
user.bio = 'Updated bio'

db.session.commit()

# Ou avec dict
data = {
    'username': 'alice2',
    'email': 'alice2@example.com',
    'bio': 'Updated bio'
}

for key, value in data.items():
    setattr(user, key, value)

db.session.commit()

"""
MISE À JOUR BULK (Plusieurs enregistrements)
"""

# Désactiver tous les utilisateurs inactifs depuis 1 an
from datetime import datetime, timedelta

cutoff_date = datetime.utcnow() - timedelta(days=365)

User.query.filter(
    User.last_login < cutoff_date
).update({
    'is_active': False
})

db.session.commit()

"""
[ATTENTION] ATTENTION : update() ne déclenche pas les événements
Utiliser seulement pour modifications simples en masse
"""


# ----------------------------------------------------------------------------
# [X] OPÉRATIONS CRUD : DELETE (Supprimer)
# ----------------------------------------------------------------------------

"""
SUPPRIMER UN ENREGISTREMENT
"""

# 1. Récupérer l'objet
user = User.query.get(1)

# 2. Supprimer
db.session.delete(user)

# 3. Commiter
db.session.commit()

"""
SUPPRIMER PLUSIEURS ENREGISTREMENTS
"""

# Supprimer tous les utilisateurs inactifs
users = User.query.filter_by(is_active=False).all()

for user in users:
    db.session.delete(user)

db.session.commit()

# Ou en une requête
User.query.filter_by(is_active=False).delete()
db.session.commit()

"""
VÉRIFIER AVANT DE SUPPRIMER
"""

user = User.query.get_or_404(user_id)

# Vérifications
if user.posts.count() > 0:
    flash('Impossible de supprimer : utilisateur a des posts', 'danger')
    return redirect(url_for('users_list'))

db.session.delete(user)
db.session.commit()
flash('Utilisateur supprimé', 'success')


# Ce fichier continue dans le suivant...
# ============================================================================
# FIN - SUITE DANS flask_partie2_suite2.txt
# ============================================================================
# ============================================================================
# [LIVRE] FLASK - PARTIE 2 (FIN) : MIGRATIONS ET RELATIONS
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 7 : Migrations de Base de Données (Flask-Migrate)
# - Chapitre 8 : Relations entre Tables
# - Exercices pratiques complets
#
# [TEMPS] TEMPS : ~4-6 heures
# [DOCS] PRÉREQUIS : Chapitres 5-6 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 7 : MIGRATIONS DE BASE DE DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi les migrations sont nécessaires
[OK] Installer et configurer Flask-Migrate
[OK] Créer des migrations
[OK] Appliquer des migrations
[OK] Gérer l'historique des migrations
[OK] Résoudre les problèmes courants
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI LES MIGRATIONS ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : ÉVOLUTION DU SCHÉMA

Version 1 de votre app :
"""

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))
    email = db.Column(db.String(120))

# Créer tables
db.create_all()

"""
Quelques semaines plus tard, vous voulez ajouter un champ :
"""

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))
    email = db.Column(db.String(120))
    phone = db.Column(db.String(20))  # <- NOUVEAU !

"""
[X] PROBLÈME : db.create_all() ne modifie pas les tables existantes

Option 1 ([X] MAUVAISE) : Supprimer et recréer
"""
db.drop_all()  # [ATTENTION] PERD TOUTES LES DONNÉES !
db.create_all()

"""
Option 2 ([X] MAUVAISE) : SQL manuel
"""
ALTER TABLE user ADD COLUMN phone VARCHAR(20);

"""
[X] PROBLÈMES :

1. PERTE DE DONNÉES avec drop_all()
2. SQL MANUEL
   - Erreurs faciles
   - Pas de traçabilité
   - Difficile à partager
3. PAS D'HISTORIQUE
   - Impossible de revenir en arrière
4. ENVIRONNEMENTS MULTIPLES
   - Dev / Staging / Production tous différents
5. ÉQUIPE
   - Comment synchroniser les changements ?


[OK] SOLUTION : MIGRATIONS

Migration = Script de transformation de schéma
- Historique versionné
- Réversible (up/down)
- Partageable (Git)
- Automatisée


ANALOGIE [CONSTRUCTION]

Migrations = Historique Git pour base de données

Commit Git     ->  Migration
git commit     ->  flask db migrate
git push       ->  flask db upgrade
git revert     ->  flask db downgrade
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install flask-migrate

"""
Flask-Migrate utilise Alembic en interne
Alembic = Outil de migration pour SQLAlchemy


CONFIGURATION
"""

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
migrate = Migrate(app, db)  # <- Ajouter cette ligne

# Importer modèles
from models import User, Post

"""
[IDEE] Migrate(app, db)

Connecte Flask-Migrate à votre app et DB
Ajoute commandes flask db ...


INITIALISER LES MIGRATIONS
"""

# Terminal
flask db init

"""
[IDEE] flask db init

Crée le dossier migrations/ :
"""

mon_projet/
├── app.py
├── models.py
├── migrations/           # <- NOUVEAU !
│   ├── alembic.ini      # Config Alembic
│   ├── env.py           # Script d'environnement
│   ├── script.py.mako   # Template de migration
│   └── versions/        # Dossier des migrations
├── instance/
│   └── app.db
└── requirements.txt

"""
[ATTENTION] FAIRE UNE SEULE FOIS PAR PROJET !

Ensuite, commiter migrations/ dans Git
(sauf versions/ qui sera rempli progressivement)


CONFIGURATION .gitignore
"""

# .gitignore
instance/app.db          # Base de données
*.pyc
__pycache__/
venv/

# Ne PAS ignorer migrations/ !
# migrations/ doit être dans Git


# ----------------------------------------------------------------------------
# [NOTE] CRÉER UNE MIGRATION
# ----------------------------------------------------------------------------

"""
WORKFLOW COMPLET


ÉTAPE 1 : Modifier les modèles
"""

# models.py
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))
    email = db.Column(db.String(120))
    phone = db.Column(db.String(20))  # <- Ajouté
    created_at = db.Column(db.DateTime)  # <- Ajouté

"""
ÉTAPE 2 : Générer la migration
"""

# Terminal
flask db migrate -m "Add phone and created_at to User"

"""
[IDEE] flask db migrate

1. Compare modèles Python avec schéma BD actuel
2. Détecte les différences
3. Génère script de migration automatiquement
4. Crée fichier dans migrations/versions/

Output :
"""
"""
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.autogenerate.compare] Detected added column 'user.phone'
INFO  [alembic.autogenerate.compare] Detected added column 'user.created_at'
  Generating /migrations/versions/abc123_add_phone_and_created_at_to_user.py ...  done
"""

"""
FICHIER GÉNÉRÉ : migrations/versions/abc123_....py
"""

# migrations/versions/abc123_add_phone_and_created_at_to_user.py
"""
\"\"\"Add phone and created_at to User

Revision ID: abc123
Revises: 
Create Date: 2024-12-18 10:30:00.000000

\"\"\"
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'abc123'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic ###
    op.add_column('user', sa.Column('phone', sa.String(length=20), nullable=True))
    op.add_column('user', sa.Column('created_at', sa.DateTime(), nullable=True))
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic ###
    op.drop_column('user', 'created_at')
    op.drop_column('user', 'phone')
    # ### end Alembic commands ###
"""

"""
[IDEE] ANATOMIE D'UNE MIGRATION

revision = 'abc123'
    ID unique de cette migration
    
down_revision = None
    Migration précédente (None si première)
    
upgrade()
    Appliquer les changements (en avant)
    
downgrade()
    Annuler les changements (en arrière)


ÉTAPE 3 : Vérifier la migration
"""

# Ouvrir le fichier et vérifier
# migrations/versions/abc123_....py

# Modifications possibles si nécessaire

"""
ÉTAPE 4 : Appliquer la migration
"""

# Terminal
flask db upgrade

"""
[IDEE] flask db upgrade

1. Lit la dernière migration non appliquée
2. Exécute upgrade()
3. Modifie la base de données
4. Enregistre dans table alembic_version

Output :
"""
"""
INFO  [alembic.runtime.migration] Running upgrade  -> abc123, Add phone and created_at to User
"""

"""
VÉRIFIER
"""

# Flask shell
>>> from models import User
>>> User.query.first()
<User alice>
>>> user = User.query.first()
>>> user.phone
None
>>> user.created_at
None

# Les colonnes existent maintenant !


# ----------------------------------------------------------------------------
# [SYNC] COMMANDES FLASK-MIGRATE
# ----------------------------------------------------------------------------

"""
COMMANDES PRINCIPALES


flask db init
-------------
Initialiser les migrations (une fois)
"""
flask db init

"""
flask db migrate -m "message"
-----------------------------
Créer une nouvelle migration

-m : Message descriptif
"""
flask db migrate -m "Add user profile fields"

"""
[IDEE] MESSAGES DE MIGRATION

Bonnes pratiques :
[OK] "Add email to User"
[OK] "Create Post model"
[OK] "Add foreign key user_id to Post"
[OK] "Rename column username to user_name"

Mauvais :
[X] "update"
[X] "changes"
[X] "fix"


flask db upgrade
----------------
Appliquer migration(s) en attente
"""
flask db upgrade

# Appliquer jusqu'à une révision spécifique
flask db upgrade abc123

# Appliquer N migrations
flask db upgrade +2

"""
flask db downgrade
------------------
Annuler migration(s)
"""
flask db downgrade

# Revenir à révision spécifique
flask db downgrade abc123

# Annuler N migrations
flask db downgrade -1

"""
flask db current
----------------
Afficher révision actuelle
"""
flask db current

"""
flask db history
----------------
Afficher historique des migrations
"""
flask db history

# Output :
"""
abc123 -> xyz789 (head), Add profile fields
  -> abc123, Add phone and created_at
<base> -> , empty database
"""

"""
flask db show
-------------
Afficher détails d'une migration
"""
flask db show abc123

"""
flask db stamp
--------------
Marquer comme appliqué sans exécuter
(Avancé, pour synchroniser)
"""
flask db stamp head


# ----------------------------------------------------------------------------
# [OUTIL] MODIFICATIONS COMPLEXES
# ----------------------------------------------------------------------------

"""
AJOUTER UNE COLONNE AVEC VALEUR PAR DÉFAUT
"""

# models.py
class User(db.Model):
    # ...
    role = db.Column(db.String(20), default='user')

# Migration
flask db migrate -m "Add role to User"

# Fichier généré
"""
def upgrade():
    op.add_column('user', sa.Column('role', sa.String(20), nullable=True))

def downgrade():
    op.drop_column('user', 'role')
"""

# [ATTENTION] PROBLÈME : nullable=True mais on veut default='user'

# Modifier la migration manuellement :
"""
def upgrade():
    op.add_column('user', sa.Column('role', sa.String(20), 
                  server_default='user', nullable=False))

def downgrade():
    op.drop_column('user', 'role')
"""

"""
RENOMMER UNE COLONNE
"""

# models.py (avant)
class User(db.Model):
    username = db.Column(db.String(80))

# models.py (après)
class User(db.Model):
    user_name = db.Column(db.String(80))  # Renommé

# Migration autogénérée ([X] INCORRECT) :
"""
def upgrade():
    op.drop_column('user', 'username')
    op.add_column('user', sa.Column('user_name', sa.String(80)))
    # [ATTENTION] Perd les données !
"""

# Migration correcte ([OK] MODIFIER MANUELLEMENT) :
"""
def upgrade():
    op.alter_column('user', 'username', new_column_name='user_name')

def downgrade():
    op.alter_column('user', 'user_name', new_column_name='username')
"""

"""
MIGRATION DE DONNÉES
"""

# Ajouter colonne full_name basée sur first_name et last_name

# Migration personnalisée
"""
def upgrade():
    # 1. Ajouter colonne
    op.add_column('user', sa.Column('full_name', sa.String(200)))
    
    # 2. Migrer données
    connection = op.get_bind()
    connection.execute(\"\"\"
        UPDATE user 
        SET full_name = first_name || ' ' || last_name
        WHERE first_name IS NOT NULL AND last_name IS NOT NULL
    \"\"\")
    
    # 3. Supprimer anciennes colonnes
    op.drop_column('user', 'first_name')
    op.drop_column('user', 'last_name')

def downgrade():
    op.add_column('user', sa.Column('first_name', sa.String(80)))
    op.add_column('user', sa.Column('last_name', sa.String(80)))
    
    # Extraire données (approximatif)
    connection = op.get_bind()
    connection.execute(\"\"\"
        UPDATE user 
        SET first_name = SUBSTR(full_name, 1, INSTR(full_name, ' ') - 1),
            last_name = SUBSTR(full_name, INSTR(full_name, ' ') + 1)
        WHERE full_name IS NOT NULL
    \"\"\")
    
    op.drop_column('user', 'full_name')
"""


# ----------------------------------------------------------------------------
# [BUG] PROBLÈMES COURANTS
# ----------------------------------------------------------------------------

"""
PROBLÈME 1 : Migration ne détecte pas changements

Causes :
- Modèles pas importés dans app.py
- Table créée avec db.create_all() après flask db init

Solution :
"""

# app.py
from models import User, Post  # <- Importer TOUS les modèles

# OU créer __init__.py dans dossier models/
# models/__init__.py
from .user import User
from .post import Post

__all__ = ['User', 'Post']

"""
PROBLÈME 2 : "Can't locate revision identified by 'xyz'"

Cause : Base de données et migrations désynchronisées

Solution :
"""

# Supprimer table alembic_version
# SQLite
sqlite3 instance/app.db "DROP TABLE alembic_version;"

# Réinitialiser
flask db stamp head

"""
PROBLÈME 3 : Migrations en conflit (équipe)

Scénario :
- Alice crée migration A
- Bob crée migration B en parallèle
- Conflit lors du merge Git

Solution :
"""

# Fusionner les branches de migrations
flask db merge -m "Merge migrations" <revision1> <revision2>

"""
PROBLÈME 4 : Erreur lors de upgrade

Solution :
"""

# 1. Revenir en arrière
flask db downgrade

# 2. Corriger la migration
# Éditer le fichier dans migrations/versions/

# 3. Réappliquer
flask db upgrade


# ============================================================================
# [GUIDE] CHAPITRE 8 : RELATIONS ENTRE TABLES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer relations One-to-Many
[OK] Créer relations Many-to-Many
[OK] Créer relations One-to-One
[OK] Utiliser backref et lazy loading
[OK] Faire des requêtes avec relations
[OK] Gérer la suppression en cascade
"""


# ----------------------------------------------------------------------------
# [LIEN] TYPES DE RELATIONS
# ----------------------------------------------------------------------------

"""
RELATIONS COURANTES


1. ONE-TO-MANY (1-N)
-------------------

Un parent -> Plusieurs enfants

Exemples :
- Un user -> plusieurs posts
- Un auteur -> plusieurs livres
- Une catégorie -> plusieurs produits

Schéma :
User (1) <--> (N) Post
  id              id
  username        title
                  content
                  user_id  <- Foreign Key


2. MANY-TO-MANY (N-N)
--------------------

Plusieurs <-> Plusieurs

Exemples :
- Plusieurs étudiants <-> plusieurs cours
- Plusieurs posts <-> plusieurs tags
- Plusieurs users <-> plusieurs roles

Nécessite TABLE D'ASSOCIATION

Schéma :
Student (N) <--> Enrollment <--> (N) Course
  id               student_id           id
  name             course_id            name


3. ONE-TO-ONE (1-1)
------------------

Un <-> Un (rare)

Exemples :
- Un user -> un profil
- Un pays -> une capitale
- Un produit -> une fiche technique

Schéma :
User (1) <--> (1) Profile
  id              id
  username        bio
                  avatar
                  user_id  <- Foreign Key UNIQUE
"""


# ----------------------------------------------------------------------------
# [LIEN] RELATION ONE-TO-MANY
# ----------------------------------------------------------------------------

"""
EXEMPLE : USER -> POSTS
"""

# models.py
from datetime import datetime
from app import db

class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    
    # [IDEE] RELATION : Un user a plusieurs posts
    posts = db.relationship('Post', backref='author', lazy=True)

class Post(db.Model):
    __tablename__ = 'posts'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # [IDEE] FOREIGN KEY : Référence vers user
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

"""
[IDEE] DÉCORTIQUONS


FOREIGN KEY (Côté ENFANT - Post)
"""
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

"""
- db.Integer : Type de colonne
- db.ForeignKey('users.id') : Référence vers users.id
- nullable=False : Obligatoire (chaque post a un auteur)

[ATTENTION] 'users.id' = Nom de TABLE (pas de classe!)


RELATIONSHIP (Côté PARENT - User)
"""
posts = db.relationship('Post', backref='author', lazy=True)

"""
- 'Post' : Nom de la CLASSE (pas de table)
- backref='author' : Crée attribut author sur Post
- lazy=True : Chargement différé (voir plus bas)

[IDEE] PAS UNE COLONNE EN BD !
C'est une propriété Python pour faciliter l'accès


COMMENT ÇA MARCHE ?

user.posts
    -> SQLAlchemy fait automatiquement :
      SELECT * FROM posts WHERE user_id = user.id
    -> Retourne liste de Posts

post.author
    -> SQLAlchemy fait automatiquement :
      SELECT * FROM users WHERE id = post.user_id
    -> Retourne User


UTILISATION
"""

# Créer user
user = User(username='alice', email='alice@example.com')
db.session.add(user)
db.session.commit()

# Créer post
post = Post(
    title='Mon premier post',
    content='Contenu du post...',
    author=user  # <- Assigner directement !
)
db.session.add(post)
db.session.commit()

# Ou avec user_id
post = Post(
    title='Deuxième post',
    content='...',
    user_id=user.id  # <- Ou assigner l'ID
)
db.session.add(post)
db.session.commit()

# Accéder aux posts d'un user
user = User.query.first()
for post in user.posts:
    print(post.title)

# Accéder à l'auteur d'un post
post = Post.query.first()
print(post.author.username)

"""
[IDEE] LAZY LOADING

lazy=True (défaut) :
"""
user = User.query.first()
# Pas de requête SQL pour posts encore

print(user.posts)
# Maintenant : SELECT * FROM posts WHERE user_id = ?

"""
lazy='dynamic' :
"""
posts = db.relationship('Post', backref='author', lazy='dynamic')

# user.posts est maintenant une Query, pas une liste
user.posts.count()           # Compter
user.posts.filter_by(...).all()  # Filtrer
user.posts.order_by(...).all()   # Trier

"""
lazy='joined' :
"""
posts = db.relationship('Post', backref='author', lazy='joined')

# User et posts chargés ensemble (JOIN SQL)
user = User.query.first()
# SELECT users.*, posts.* FROM users LEFT JOIN posts ...

"""
lazy='subquery' :
"""
posts = db.relationship('Post', backref='author', lazy='subquery')

# Posts chargés dans une sous-requête séparée


# ----------------------------------------------------------------------------
# [LIEN] RELATION MANY-TO-MANY
# ----------------------------------------------------------------------------

"""
EXEMPLE : POSTS <-> TAGS

Un post peut avoir plusieurs tags
Un tag peut être sur plusieurs posts
"""

# models.py

# Table d'association (pas une classe !)
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id'), primary_key=True),
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
)

class Post(db.Model):
    __tablename__ = 'posts'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    content = db.Column(db.Text)
    
    # [IDEE] RELATION Many-to-Many
    tags = db.relationship(
        'Tag',
        secondary=post_tags,  # <- Table d'association
        lazy='subquery',
        backref=db.backref('posts', lazy=True)
    )

class Tag(db.Model):
    __tablename__ = 'tags'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)

"""
[IDEE] DÉCORTIQUONS


TABLE D'ASSOCIATION
"""
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id'), primary_key=True),
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
)

"""
- db.Table (pas db.Model!) : Juste une table, pas une classe
- Deux foreign keys : post_id et tag_id
- Les deux = primary_key composite


STRUCTURE EN BD :

posts               post_tags           tags
id  title           post_id  tag_id     id  name
1   "Post 1"        1        1          1   "python"
2   "Post 2"        1        2          2   "flask"
3   "Post 3"        2        1          3   "web"
                    2        3

Post 1 a tags: python, flask
Post 2 a tags: python, web


RELATIONSHIP
"""
tags = db.relationship(
    'Tag',
    secondary=post_tags,    # Table d'association
    lazy='subquery',
    backref=db.backref('posts', lazy=True)
)

"""
- secondary=post_tags : Table intermédiaire
- backref : Accès bidirectionnel


UTILISATION
"""

# Créer tags
python_tag = Tag(name='python')
flask_tag = Tag(name='flask')
web_tag = Tag(name='web')

db.session.add_all([python_tag, flask_tag, web_tag])
db.session.commit()

# Créer post avec tags
post = Post(title='Apprendre Flask', content='...')
post.tags.append(python_tag)
post.tags.append(flask_tag)

db.session.add(post)
db.session.commit()

# Ou assigner liste complète
post.tags = [python_tag, flask_tag]

# Accéder aux tags d'un post
post = Post.query.first()
for tag in post.tags:
    print(tag.name)

# Accéder aux posts d'un tag
tag = Tag.query.filter_by(name='python').first()
for post in tag.posts:
    print(post.title)

# Retirer un tag
post.tags.remove(flask_tag)
db.session.commit()


# ----------------------------------------------------------------------------
# [LIEN] RELATION ONE-TO-ONE
# ----------------------------------------------------------------------------

"""
EXEMPLE : USER <-> PROFILE
"""

# models.py

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))
    
    # [IDEE] ONE-TO-ONE
    profile = db.relationship('Profile', backref='user', uselist=False)

class Profile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    bio = db.Column(db.Text)
    avatar = db.Column(db.String(200))
    
    # Foreign key avec UNIQUE
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True)

"""
[IDEE] DIFFÉRENCE AVEC ONE-TO-MANY

uselist=False sur relationship
unique=True sur foreign key


UTILISATION
"""

# Créer user et profile
user = User(username='alice')
profile = Profile(bio='Développeuse Python', user=user)

db.session.add(user)
db.session.add(profile)
db.session.commit()

# Accès
user = User.query.first()
print(user.profile.bio)  # Un seul profile (pas une liste)

profile = Profile.query.first()
print(profile.user.username)


# ----------------------------------------------------------------------------
# [SUPPRIMER] SUPPRESSION EN CASCADE
# ----------------------------------------------------------------------------

"""
PROBLÈME : Que faire des posts si user supprimé ?

Option 1 : Empêcher suppression
Option 2 : Supprimer posts aussi (CASCADE)
Option 3 : Mettre user_id à NULL
"""

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))
    
    # CASCADE : Supprimer posts si user supprimé
    posts = db.relationship(
        'Post',
        backref='author',
        lazy=True,
        cascade='all, delete-orphan'
    )

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

"""
[IDEE] CASCADE OPTIONS

'all, delete-orphan' :
    Supprimer posts si user supprimé
    Supprimer posts si retirés de user.posts

'all, delete' :
    Supprimer posts si user supprimé
    Garder posts si retirés de user.posts

'save-update' (défaut) :
    Rien de spécial

'delete' :
    Juste suppression

None :
    Pas de cascade


UTILISATION
"""

user = User.query.get(1)
db.session.delete(user)
db.session.commit()
# Tous ses posts sont automatiquement supprimés !


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF PARTIE 2 COMPLÈTE
# ----------------------------------------------------------------------------

"""
[BRAVO] FÉLICITATIONS ! PARTIE 2 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 5 : Formulaires (Flask-WTF)
[OK] Créer formulaires avec classes
[OK] Validateurs intégrés et personnalisés
[OK] Protection CSRF automatique
[OK] Upload de fichiers
[OK] Affichage d'erreurs

Chapitre 6 : Base de Données (SQLAlchemy)
[OK] Configuration SQLAlchemy
[OK] Créer modèles (tables)
[OK] Types de colonnes
[OK] Options (primary_key, unique, nullable, etc.)
[OK] CRUD complet (Create, Read, Update, Delete)
[OK] Requêtes simples et complexes
[OK] Pagination

Chapitre 7 : Migrations (Flask-Migrate)
[OK] Pourquoi les migrations
[OK] Initialiser migrations
[OK] Créer et appliquer migrations
[OK] Historique et rollback
[OK] Résoudre problèmes courants

Chapitre 8 : Relations
[OK] One-to-Many (user -> posts)
[OK] Many-to-Many (posts <-> tags)
[OK] One-to-One (user <-> profile)
[OK] Foreign keys et backref
[OK] Lazy loading
[OK] Cascade delete


[OBJECTIF] VOUS POUVEZ MAINTENANT CRÉER :

[OK] Blog complet avec :
  - Inscription/connexion
  - Créer/modifier/supprimer posts
  - Commentaires
  - Tags et catégories
  
[OK] Application e-commerce avec :
  - Produits et catégories
  - Panier et commandes
  - Utilisateurs et adresses
  
[OK] Réseau social avec :
  - Profils utilisateurs
  - Posts et likes
  - Followers/following
  - Messages


-> PROCHAINE ÉTAPE : PARTIE 3

La Partie 3 couvrira :
- Authentification (Flask-Login)
- Blueprints (organisation)
- Sessions et cookies
- API REST
- Upload avancé
- Email et tâches asynchrones

Vous allez apprendre à :
- Gérer connexion utilisateurs
- Organiser grande application
- Créer APIs
- Envoyer emails
- Tâches en arrière-plan


[GUIDE] FICHIER SUIVANT : flask_partie3.txt

Excellente progression ! Continuez ! [RAPIDE]
"""
# ============================================================================
# [LIVRE] FLASK - PARTIE 3 : FONCTIONNALITÉS AVANCÉES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 9 : Authentification (Flask-Login)
# - Chapitre 10 : Blueprints et Organisation
# - Chapitre 11 : Sessions et Cookies
# - Chapitre 12 : API REST
# - Chapitre 13 : Upload Avancé
# - Chapitre 14 : Email et Tâches Asynchrones
#
# [TEMPS] TEMPS : ~10-12 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 9 : AUTHENTIFICATION (Flask-Login)
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter système d'authentification complet
[OK] Hasher les mots de passe (bcrypt)
[OK] Gérer sessions utilisateurs
[OK] Protéger routes avec @login_required
[OK] Créer inscription/connexion/déconnexion
[OK] Gérer "Remember Me"
[OK] Réinitialisation de mot de passe
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI Flask-Login ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : AUTHENTIFICATION MANUELLE

Sans Flask-Login :
"""

# [X] Approche naïve (NE PAS FAIRE)
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    
    # Vérifier en base de données (simplifié)
    user = User.query.filter_by(username=username).first()
    
    if user and user.password == password:  # [ATTENTION] Password en clair !
        session['user_id'] = user.id  # [ATTENTION] Session manuelle
        return redirect('/dashboard')
    
    return "Login failed"

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:  # [ATTENTION] Vérification manuelle
        return redirect('/login')
    
    user_id = session['user_id']
    user = User.query.get(user_id)
    return f"Welcome {user.username}"

"""
[X] PROBLÈMES MAJEURS

1. MOTS DE PASSE EN CLAIR
   Jamais stocker passwords sans hashage !
   
2. GESTION SESSION MANUELLE
   Code répétitif partout
   
3. PAS DE SÉCURITÉ
   Facile d'oublier vérifications
   
4. PAS DE "REMEMBER ME"
   
5. PAS DE GESTION LOGOUT
   
6. DIFFICILE À MAINTENIR


[OK] SOLUTION : Flask-Login

Extension qui gère :
- Sessions utilisateurs
- Protection de routes
- Chargement automatique de l'utilisateur
- Remember me
- Redirection après login
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install flask-login
pip install flask-bcrypt  # Pour hasher passwords

"""
CONFIGURATION
"""

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_bcrypt import Bcrypt

app = Flask(__name__)
app.config['SECRET_KEY'] = 'votre-cle-secrete'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
bcrypt = Bcrypt(app)

# Initialiser Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)

# Configuration de la page de login
login_manager.login_view = 'login'  # Route de login
login_manager.login_message = 'Veuillez vous connecter pour accéder à cette page.'
login_manager.login_message_category = 'info'

"""
[IDEE] CONFIGURATION LOGIN_MANAGER

login_view = 'login'
    Route vers laquelle rediriger si non authentifié
    
login_message = '...'
    Message flash affiché lors de la redirection
    
login_message_category = 'info'
    Catégorie du message flash
"""


# ----------------------------------------------------------------------------
# [UTILISATEUR] MODÈLE USER
# ----------------------------------------------------------------------------

"""
CRÉER LE MODÈLE
"""

# models.py
from datetime import datetime
from flask_login import UserMixin
from app import db, bcrypt

class User(UserMixin, db.Model):
    """
    [IDEE] UserMixin
    
    Ajoute les méthodes requises par Flask-Login :
    - is_authenticated : True si authentifié
    - is_active : True si compte actif
    - is_anonymous : False (True pour AnonymousUser)
    - get_id() : Retourne ID unique (string)
    """
    
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    is_active = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def set_password(self, password):
        """
        [IDEE] HASHER LE MOT DE PASSE
        
        JAMAIS stocker password en clair !
        bcrypt génère un hash sécurisé
        """
        self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
    
    def check_password(self, password):
        """
        [IDEE] VÉRIFIER LE MOT DE PASSE
        
        Compare password avec hash
        """
        return bcrypt.check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f'<User {self.username}>'

"""
[IDEE] POURQUOI UserMixin ?

Flask-Login nécessite ces méthodes :

is_authenticated
    True si utilisateur authentifié
    False sinon

is_active
    True si compte actif (pas banni)
    False si désactivé

is_anonymous
    False pour utilisateurs réels
    True pour AnonymousUser

get_id()
    Retourne ID unique (converti en string)
    Utilisé pour stocker dans session


UserMixin fournit implémentations par défaut
Vous pouvez override si besoin :
"""

class User(UserMixin, db.Model):
    # ...
    
    # Override is_active si logique personnalisée
    @property
    def is_active(self):
        # Compte actif si pas banni et email vérifié
        return self.active and self.email_verified


# ----------------------------------------------------------------------------
# [SECURISE] USER LOADER
# ----------------------------------------------------------------------------

"""
CONFIGURER LE USER LOADER
"""

# app.py
from models import User

@login_manager.user_loader
def load_user(user_id):
    """
    [IDEE] USER LOADER CALLBACK
    
    Flask-Login appelle cette fonction pour :
    - Recharger l'utilisateur depuis la session
    - À chaque requête authentifiée
    
    DOIT retourner :
    - Objet User si trouvé
    - None si pas trouvé
    
    Flask-Login stocke seulement l'ID dans la session
    Cette fonction charge l'objet complet
    """
    return User.query.get(int(user_id))

"""
[IDEE] COMMENT ÇA MARCHE ?

1. USER LOGIN :
   login_user(user) -> Stocke user.get_id() dans session

2. REQUÊTES SUIVANTES :
   - Flask-Login lit user_id de la session
   - Appelle load_user(user_id)
   - Charge objet User complet
   - Disponible via current_user

3. USER LOGOUT :
   logout_user() -> Supprime user_id de session
"""


# ----------------------------------------------------------------------------
# [NOTE] FORMULAIRES D'AUTHENTIFICATION
# ----------------------------------------------------------------------------

"""
FORMULAIRE DE CONNEXION
"""

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from models import User

class LoginForm(FlaskForm):
    """Formulaire de connexion"""
    
    username = StringField(
        'Nom d\'utilisateur',
        validators=[DataRequired()]
    )
    
    password = PasswordField(
        'Mot de passe',
        validators=[DataRequired()]
    )
    
    remember_me = BooleanField('Se souvenir de moi')
    
    submit = SubmitField('Se connecter')


class RegistrationForm(FlaskForm):
    """Formulaire d'inscription"""
    
    username = StringField(
        'Nom d\'utilisateur',
        validators=[
            DataRequired(),
            Length(min=3, max=20, message='Entre 3 et 20 caractères')
        ]
    )
    
    email = StringField(
        'Email',
        validators=[
            DataRequired(),
            Email(message='Email invalide')
        ]
    )
    
    password = PasswordField(
        'Mot de passe',
        validators=[
            DataRequired(),
            Length(min=8, message='Minimum 8 caractères')
        ]
    )
    
    password2 = PasswordField(
        'Confirmer le mot de passe',
        validators=[
            DataRequired(),
            EqualTo('password', message='Les mots de passe doivent correspondre')
        ]
    )
    
    submit = SubmitField('S\'inscrire')
    
    def validate_username(self, username):
        """Vérifier que username n'existe pas déjà"""
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('Ce nom d\'utilisateur est déjà pris.')
    
    def validate_email(self, email):
        """Vérifier que email n'existe pas déjà"""
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('Cet email est déjà utilisé.')


# ----------------------------------------------------------------------------
# [SORTIE] ROUTES D'AUTHENTIFICATION
# ----------------------------------------------------------------------------

"""
ROUTE D'INSCRIPTION
"""

# routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from app import app, db
from models import User
from forms import LoginForm, RegistrationForm

@app.route('/register', methods=['GET', 'POST'])
def register():
    """
    [IDEE] INSCRIPTION
    
    Si déjà connecté -> Rediriger vers home
    Sinon -> Afficher formulaire
    """
    # Si déjà connecté
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    
    form = RegistrationForm()
    
    if form.validate_on_submit():
        # Créer nouvel utilisateur
        user = User(
            username=form.username.data,
            email=form.email.data
        )
        user.set_password(form.password.data)  # Hasher le password
        
        # Sauvegarder
        db.session.add(user)
        db.session.commit()
        
        flash('Compte créé avec succès ! Vous pouvez maintenant vous connecter.', 'success')
        return redirect(url_for('login'))
    
    return render_template('register.html', form=form)

"""
ROUTE DE CONNEXION
"""

@app.route('/login', methods=['GET', 'POST'])
def login():
    """
    [IDEE] CONNEXION
    
    1. Vérifier credentials
    2. login_user() si OK
    3. Rediriger vers page demandée ou home
    """
    # Si déjà connecté
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    
    form = LoginForm()
    
    if form.validate_on_submit():
        # Chercher utilisateur
        user = User.query.filter_by(username=form.username.data).first()
        
        # Vérifier password
        if user and user.check_password(form.password.data):
            # [IDEE] LOGIN USER
            login_user(user, remember=form.remember_me.data)
            
            # Rediriger vers page demandée (ou home)
            next_page = request.args.get('next')
            
            # Sécurité : Vérifier que next_page est relatif
            if next_page and next_page.startswith('/'):
                return redirect(next_page)
            
            return redirect(url_for('home'))
        
        # Credentials invalides
        flash('Nom d\'utilisateur ou mot de passe incorrect.', 'danger')
    
    return render_template('login.html', form=form)

"""
[IDEE] login_user() EN DÉTAIL

login_user(user, remember=False, duration=None, force=False, fresh=True)

Paramètres :
- user : Objet User à authentifier
- remember : Si True, cookie persistant (Remember Me)
- duration : Durée du cookie Remember Me (défaut: 1 an)
- force : Forcer même si user.is_active = False
- fresh : Marquer session comme "fresh" (sécurité)


Que fait login_user() ?
1. Appelle user.get_id()
2. Stocke ID dans session
3. Si remember=True, crée cookie persistant
4. Émet signal "user_logged_in"


ROUTE DE DÉCONNEXION
"""

@app.route('/logout')
@login_required  # <- Seulement si connecté
def logout():
    """
    [IDEE] DÉCONNEXION
    
    logout_user() supprime utilisateur de la session
    """
    logout_user()
    flash('Vous avez été déconnecté.', 'info')
    return redirect(url_for('home'))

"""
[IDEE] logout_user()

- Supprime user_id de la session
- Supprime cookie Remember Me si existant
- Émet signal "user_logged_out"


# ----------------------------------------------------------------------------
# [VERROUILLE] PROTÉGER DES ROUTES
# ----------------------------------------------------------------------------

"""
DÉCORATEUR @login_required
"""

from flask_login import login_required

@app.route('/dashboard')
@login_required  # <- Utilisateur doit être authentifié
def dashboard():
    """
    [IDEE] ROUTE PROTÉGÉE
    
    Si utilisateur pas authentifié :
    -> Redirigé vers login_view ('login')
    -> URL d'origine sauvegardée dans ?next=
    
    Si authentifié :
    -> Route s'exécute normalement
    -> current_user disponible
    """
    return render_template('dashboard.html', user=current_user)

@app.route('/profile')
@login_required
def profile():
    """Profil utilisateur"""
    return render_template('profile.html', user=current_user)

@app.route('/settings')
@login_required
def settings():
    """Paramètres utilisateur"""
    return render_template('settings.html', user=current_user)

"""
VÉRIFICATION MANUELLE
"""

@app.route('/optional-protected')
def optional_protected():
    """
    Route accessible à tous
    Mais contenu différent si authentifié
    """
    if current_user.is_authenticated:
        return f"Bienvenue {current_user.username} !"
    else:
        return "Bienvenue visiteur ! <a href='/login'>Se connecter</a>"

"""
[IDEE] current_user

Variable globale disponible partout :
- Routes (fonctions Python)
- Templates (HTML)

Contient :
- Si authentifié : Objet User chargé
- Si non authentifié : AnonymousUser

Propriétés utiles :
- current_user.is_authenticated : Booléen
- current_user.username : Attributs du User
- current_user.id : ID
"""


# ----------------------------------------------------------------------------
# [DESIGN] TEMPLATES AVEC AUTHENTIFICATION
# ----------------------------------------------------------------------------

"""
TEMPLATE DE BASE
"""

# templates/base.html
"""
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Mon Site{% endblock %}</title>
</head>
<body>
    <nav>
        <a href="{{ url_for('home') }}">Accueil</a>
        
        <!-- [IDEE] Affichage conditionnel -->
        {% if current_user.is_authenticated %}
            <!-- Utilisateur connecté -->
            <span>Bonjour, {{ current_user.username }} !</span>
            <a href="{{ url_for('dashboard') }}">Tableau de bord</a>
            <a href="{{ url_for('profile') }}">Profil</a>
            <a href="{{ url_for('logout') }}">Déconnexion</a>
        {% else %}
            <!-- Utilisateur non connecté -->
            <a href="{{ url_for('login') }}">Connexion</a>
            <a href="{{ url_for('register') }}">Inscription</a>
        {% endif %}
    </nav>
    
    <!-- Messages flash -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div class="alert alert-{{ category }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
    {% endwith %}
    
    <!-- Contenu -->
    {% block content %}{% endblock %}
</body>
</html>
"""

"""
TEMPLATE DE LOGIN
"""

# templates/login.html
"""
{% extends "base.html" %}

{% block title %}Connexion{% endblock %}

{% block content %}
<div class="container">
    <h2>Connexion</h2>
    
    <form method="POST" novalidate>
        {{ form.hidden_tag() }}
        
        <!-- Username -->
        <div class="form-group">
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
            {% if form.username.errors %}
                {% for error in form.username.errors %}
                    <span class="error">{{ error }}</span>
                {% endfor %}
            {% endif %}
        </div>
        
        <!-- Password -->
        <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
            {% if form.password.errors %}
                {% for error in form.password.errors %}
                    <span class="error">{{ error }}</span>
                {% endfor %}
            {% endif %}
        </div>
        
        <!-- Remember Me -->
        <div class="form-group">
            {{ form.remember_me() }}
            {{ form.remember_me.label }}
        </div>
        
        <!-- Submit -->
        <div class="form-group">
            {{ form.submit(class="btn btn-primary") }}
        </div>
    </form>
    
    <p>Pas encore de compte ? <a href="{{ url_for('register') }}">S'inscrire</a></p>
</div>
{% endblock %}
"""


# ----------------------------------------------------------------------------
# [SECURISE] PERMISSIONS PERSONNALISÉES
# ----------------------------------------------------------------------------

"""
DÉCORATEUR PERSONNALISÉ
"""

from functools import wraps
from flask import abort

def admin_required(f):
    """
    [IDEE] DÉCORATEUR ADMIN
    
    Vérifie que l'utilisateur est admin
    """
    @wraps(f)
    def decorated_function(*args, **kwargs):
        # Vérifier authentification
        if not current_user.is_authenticated:
            return redirect(url_for('login'))
        
        # Vérifier rôle admin
        if not hasattr(current_user, 'is_admin') or not current_user.is_admin:
            abort(403)  # Forbidden
        
        return f(*args, **kwargs)
    
    return decorated_function

"""
UTILISATION
"""

@app.route('/admin')
@admin_required
def admin_panel():
    """Panneau d'administration"""
    return render_template('admin.html')

"""
DÉCORATEUR GÉNÉRIQUE POUR RÔLES
"""

def role_required(role):
    """Décorateur pour vérifier un rôle spécifique"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated:
                return redirect(url_for('login'))
            
            if current_user.role != role:
                abort(403)
            
            return f(*args, **kwargs)
        
        return decorated_function
    return decorator

# Utilisation
@app.route('/moderator')
@role_required('moderator')
def moderator_panel():
    return render_template('moderator.html')


# ----------------------------------------------------------------------------
# [SYNC] RÉINITIALISATION DE MOT DE PASSE
# ----------------------------------------------------------------------------

"""
GÉNÉRER TOKEN
"""

from itsdangerous import URLSafeTimedSerializer

def generate_reset_token(email):
    """Générer token de réinitialisation"""
    serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
    return serializer.dumps(email, salt='password-reset-salt')

def verify_reset_token(token, expiration=3600):
    """Vérifier token (expire après expiration secondes)"""
    serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
    try:
        email = serializer.loads(
            token,
            salt='password-reset-salt',
            max_age=expiration
        )
        return email
    except:
        return None

"""
FORMULAIRE DE RÉINITIALISATION
"""

class ResetPasswordRequestForm(FlaskForm):
    """Demander réinitialisation"""
    email = StringField('Email', validators=[DataRequired(), Email()])
    submit = SubmitField('Réinitialiser le mot de passe')

class ResetPasswordForm(FlaskForm):
    """Nouveau mot de passe"""
    password = PasswordField('Nouveau mot de passe', validators=[
        DataRequired(), Length(min=8)
    ])
    password2 = PasswordField('Confirmer', validators=[
        DataRequired(), EqualTo('password')
    ])
    submit = SubmitField('Changer le mot de passe')

"""
ROUTES
"""

@app.route('/reset-password-request', methods=['GET', 'POST'])
def reset_password_request():
    """Demander réinitialisation"""
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    
    form = ResetPasswordRequestForm()
    
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        
        if user:
            token = generate_reset_token(user.email)
            reset_url = url_for('reset_password', token=token, _external=True)
            
            # Envoyer email (voir chapitre 14)
            # send_email(user.email, 'Réinitialisation', reset_url)
            
            flash('Un email a été envoyé avec les instructions.', 'info')
        else:
            flash('Email non trouvé.', 'danger')
        
        return redirect(url_for('login'))
    
    return render_template('reset_password_request.html', form=form)

@app.route('/reset-password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    """Réinitialiser avec token"""
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    
    # Vérifier token
    email = verify_reset_token(token)
    if not email:
        flash('Token invalide ou expiré.', 'danger')
        return redirect(url_for('login'))
    
    form = ResetPasswordForm()
    
    if form.validate_on_submit():
        user = User.query.filter_by(email=email).first()
        if user:
            user.set_password(form.password.data)
            db.session.commit()
            flash('Mot de passe changé avec succès !', 'success')
            return redirect(url_for('login'))
    
    return render_template('reset_password.html', form=form)


# ============================================================================
# [GUIDE] CHAPITRE 10 : BLUEPRINTS ET ORGANISATION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi utiliser les Blueprints
[OK] Créer et enregistrer des Blueprints
[OK] Organiser grande application
[OK] Préfixes d'URL
[OK] Templates et static par Blueprint
[OK] Application Factory pattern
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI LES BLUEPRINTS ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : app.py GÉANT

Petite application :
"""

# app.py (TOUT dans un fichier)
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('home.html')

@app.route('/about')
def about():
    return render_template('about.html')

@app.route('/login')
def login():
    return render_template('login.html')

@app.route('/register')
def register():
    return render_template('register.html')

@app.route('/post/<int:id>')
def post(id):
    return render_template('post.html')

@app.route('/admin')
def admin():
    return render_template('admin.html')

# ... 100+ routes plus tard ...

"""
[X] PROBLÈMES

1. FICHIER ÉNORME
   Impossible à lire et maintenir
   
2. TOUT MÉLANGÉ
   Auth, blog, admin dans le même fichier
   
3. COLLABORATION DIFFICILE
   Conflits Git constants
   
4. PAS DE RÉUTILISATION
   Impossible de réutiliser modules
   
5. TESTS COMPLIQUÉS
   Tout testé ensemble


[OK] SOLUTION : BLUEPRINTS

Blueprint = Module d'application
- Groupe routes par fonctionnalité
- Séparation claire
- Réutilisable


ANALOGIE [CONSTRUCTION]

Application Flask = Maison
Blueprints = Pièces (cuisine, salon, chambre)

Chaque pièce :
- Fonction spécifique
- Indépendante
- Assemblée pour former la maison
"""


# ----------------------------------------------------------------------------
# [DOSSIER] STRUCTURE AVEC BLUEPRINTS
# ----------------------------------------------------------------------------

"""
STRUCTURE RECOMMANDÉE
"""

project/
├── run.py                  # Point d'entrée
├── config.py               # Configuration
├── requirements.txt
├── instance/
│   └── app.db
├── migrations/
└── app/                    # <- Package principal
    ├── __init__.py         # Créer app (factory)
    ├── models.py           # Tous les modèles
    ├── extensions.py       # Extensions (db, login_manager)
    ├── main/               # <- Blueprint main
    │   ├── __init__.py
    │   └── routes.py
    ├── auth/               # <- Blueprint auth
    │   ├── __init__.py
    │   ├── forms.py
    │   └── routes.py
    ├── blog/               # <- Blueprint blog
    │   ├── __init__.py
    │   ├── forms.py
    │   └── routes.py
    ├── admin/              # <- Blueprint admin
    │   ├── __init__.py
    │   └── routes.py
    ├── templates/
    │   ├── base.html
    │   ├── main/
    │   ├── auth/
    │   ├── blog/
    │   └── admin/
    └── static/
        ├── css/
        ├── js/
        └── images/


# ----------------------------------------------------------------------------
# [OUTIL] CRÉER UN BLUEPRINT
# ----------------------------------------------------------------------------

"""
ÉTAPE 1 : Créer le Blueprint
"""

# app/main/__init__.py
from flask import Blueprint

# [IDEE] Créer Blueprint
bp = Blueprint('main', __name__)

# Importer routes (à la fin pour éviter imports circulaires)
from app.main import routes

"""
[IDEE] Blueprint(name, import_name)

Paramètres :
- name : Nom du blueprint ('main', 'auth', 'blog')
- import_name : Généralement __name__

Options :
- url_prefix : Préfixe pour toutes les routes
- template_folder : Dossier templates spécifique
- static_folder : Dossier static spécifique


ÉTAPE 2 : Définir les routes
"""

# app/main/routes.py
from flask import render_template
from app.main import bp

@bp.route('/')
def home():
    """Page d'accueil"""
    return render_template('main/home.html')

@bp.route('/about')
def about():
    """Page à propos"""
    return render_template('main/about.html')

@bp.route('/contact')
def contact():
    """Page contact"""
    return render_template('main/contact.html')

"""
[IDEE] DIFFÉRENCES AVEC app.route()

@app.route('/')       <- Application
@bp.route('/')        <- Blueprint

Routes de blueprint sont "enregistrées" plus tard
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] APPLICATION FACTORY
# ----------------------------------------------------------------------------

"""
CRÉER LES EXTENSIONS
"""

# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_bcrypt import Bcrypt

# Créer instances (sans app)
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
bcrypt = Bcrypt()

"""
APPLICATION FACTORY
"""

# app/__init__.py
from flask import Flask
from config import Config
from app.extensions import db, login_manager, migrate, bcrypt

def create_app(config_class=Config):
    """
    [IDEE] APPLICATION FACTORY
    
    Pattern pour créer app Flask
    Avantages :
    - Plusieurs instances possibles (tests, prod)
    - Configuration flexible
    - Blueprints proprement enregistrés
    """
    # Créer app
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    # Initialiser extensions
    db.init_app(app)
    login_manager.init_app(app)
    migrate.init_app(app, db)
    bcrypt.init_app(app)
    
    # Configurer login_manager
    login_manager.login_view = 'auth.login'
    login_manager.login_message = 'Veuillez vous connecter.'
    
    # User loader
    from app.models import User
    
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    
    # Enregistrer blueprints
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    from app.blog import bp as blog_bp
    app.register_blueprint(blog_bp, url_prefix='/blog')
    
    from app.admin import bp as admin_bp
    app.register_blueprint(admin_bp, url_prefix='/admin')
    
    return app

"""
[IDEE] register_blueprint()

app.register_blueprint(bp, url_prefix='/prefix')

Options :
- url_prefix : Toutes les routes préfixées
- subdomain : Sous-domaine spécifique
- url_defaults : Valeurs par défaut URL
"""


# ----------------------------------------------------------------------------
# [SECURISE] BLUEPRINT AUTH
# ----------------------------------------------------------------------------

"""
CRÉER BLUEPRINT AUTH
"""

# app/auth/__init__.py
from flask import Blueprint

bp = Blueprint('auth', __name__)

from app.auth import routes

# app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from app.models import User

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(3, 20)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired(), Length(8)])
    password2 = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('Username already taken.')
    
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('Email already registered.')

# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
from app.extensions import db

@bp.route('/login', methods=['GET', 'POST'])
def login():
    """Route de connexion"""
    if current_user.is_authenticated:
        return redirect(url_for('main.home'))
    
    form = LoginForm()
    
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember_me.data)
            
            next_page = request.args.get('next')
            if next_page and next_page.startswith('/'):
                return redirect(next_page)
            
            return redirect(url_for('main.home'))
        
        flash('Invalid username or password', 'danger')
    
    return render_template('auth/login.html', form=form)

@bp.route('/register', methods=['GET', 'POST'])
def register():
    """Route d'inscription"""
    if current_user.is_authenticated:
        return redirect(url_for('main.home'))
    
    form = RegistrationForm()
    
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        
        db.session.add(user)
        db.session.commit()
        
        flash('Account created! You can now log in.', 'success')
        return redirect(url_for('auth.login'))
    
    return render_template('auth/register.html', form=form)

@bp.route('/logout')
def logout():
    """Route de déconnexion"""
    logout_user()
    flash('You have been logged out.', 'info')
    return redirect(url_for('main.home'))

"""
[IDEE] url_for() AVEC BLUEPRINTS

Syntaxe : url_for('blueprint.fonction')

url_for('main.home')       -> /
url_for('auth.login')      -> /auth/login
url_for('auth.register')   -> /auth/register
url_for('blog.post', id=1) -> /blog/post/1
"""


# ----------------------------------------------------------------------------
# [RUNNER] LANCER L'APPLICATION
# ----------------------------------------------------------------------------

"""
POINT D'ENTRÉE
"""

# run.py
from app import create_app
from app.extensions import db
from app.models import User, Post

app = create_app()

@app.shell_context_processor
def make_shell_context():
    """
    Ajouter variables au Flask shell
    
    flask shell
    >>> db
    >>> User
    >>> Post
    """
    return {'db': db, 'User': User, 'Post': Post}

if __name__ == '__main__':
    app.run(debug=True)

"""
LANCER
"""

# Développement
python run.py

# Ou avec Flask CLI
export FLASK_APP=run.py  # Mac/Linux
set FLASK_APP=run.py     # Windows
flask run --debug

"""
MIGRATIONS
"""

flask db init
flask db migrate -m "Initial migration"
flask db upgrade


# Ce fichier continue...
# ============================================================================
# FIN - SUITE DANS flask_partie3_suite.txt
# ============================================================================
# ============================================================================
# [LIVRE] FLASK - PARTIE 4 : PRODUCTION ET OPTIMISATION
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 15 : Testing
# - Chapitre 16 : Sécurité
# - Chapitre 17 : Performance et Caching
# - Chapitre 18 : Deployment (Production)
# - Chapitre 19 : Logging et Monitoring
# - Chapitre 20 : Best Practices Finales
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Parties 1, 2 et 3 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 15 : TESTING
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi tester est crucial
[OK] Configurer pytest pour Flask
[OK] Tests unitaires de modèles
[OK] Tests d'intégration de routes
[OK] Tests avec base de données
[OK] Coverage et CI/CD
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI TESTER ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : CODE NON TESTÉ

Sans tests :
"""

# [X] Développement sans tests
@app.route('/user/<int:user_id>')
def get_user(user_id):
    user = User.query.get(user_id)
    return jsonify({'username': user.username})

"""
[X] PROBLÈMES :

1. BUG CACHÉ
   Que se passe-t-il si user_id n'existe pas ?
   -> Erreur 500 en production !

2. RÉGRESSION
   Modification future -> casse l'existant
   
3. REFACTORING DANGEREUX
   Peur de changer le code
   
4. PAS DE DOCUMENTATION
   Tests = documentation vivante
   
5. DÉPLOIEMENT STRESSANT
   Aucune confiance


[OK] AVEC TESTS

def test_get_user_success():
    response = client.get('/user/1')
    assert response.status_code == 200
    assert b'alice' in response.data

def test_get_user_not_found():
    response = client.get('/user/999')
    assert response.status_code == 404

[OK] AVANTAGES :

1. CONFIANCE
   Code marche comme prévu
   
2. REFACTORING SAFE
   Tests cassent si régression
   
3. DOCUMENTATION
   Tests montrent comment utiliser le code
   
4. MEILLEURE CONCEPTION
   Code testable = code bien conçu
   
5. DÉPLOIEMENT SEREIN
   CI/CD automatisé
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install pytest
pip install pytest-cov  # Coverage
pip install pytest-flask  # Helpers Flask

"""
STRUCTURE DE TESTS
"""

project/
├── app/
│   ├── __init__.py
│   ├── models.py
│   └── routes.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py      # Fixtures partagées
│   ├── test_models.py   # Tests modèles
│   ├── test_routes.py   # Tests routes
│   └── test_auth.py     # Tests authentification
├── pytest.ini           # Configuration pytest
└── requirements-dev.txt

"""
CONFIGURATION PYTEST
"""

# pytest.ini
"""
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
"""

"""
REQUIREMENTS DEV
"""

# requirements-dev.txt
"""
pytest==7.4.0
pytest-cov==4.1.0
pytest-flask==1.2.0
faker==19.3.0  # Données de test
factory-boy==3.3.0  # Factories
"""


# ----------------------------------------------------------------------------
# [OUTIL] FIXTURES
# ----------------------------------------------------------------------------

"""
FIXTURES DE BASE
"""

# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
from app.models import User, Post

@pytest.fixture
def app():
    """
    [IDEE] FIXTURE APP
    
    Crée application de test avec config spéciale
    """
    app = create_app('testing')
    
    # Config pour tests
    app.config.update({
        'TESTING': True,
        'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',  # DB en mémoire
        'WTF_CSRF_ENABLED': False,  # Désactiver CSRF pour tests
        'SERVER_NAME': 'localhost.localdomain'
    })
    
    # Créer tables
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture
def client(app):
    """
    [IDEE] FIXTURE CLIENT
    
    Client de test pour faire requêtes HTTP
    """
    return app.test_client()

@pytest.fixture
def runner(app):
    """
    [IDEE] FIXTURE RUNNER
    
    Test CLI commands
    """
    return app.test_cli_runner()

@pytest.fixture
def auth(client):
    """
    [IDEE] FIXTURE AUTH
    
    Helper pour login/logout
    """
    class AuthActions:
        def login(self, username='test', password='test'):
            return client.post('/auth/login', data={
                'username': username,
                'password': password
            })
        
        def logout(self):
            return client.get('/auth/logout')
    
    return AuthActions()

"""
FIXTURES DE DONNÉES
"""

@pytest.fixture
def user(app):
    """Créer utilisateur de test"""
    user = User(username='test', email='test@example.com')
    user.set_password('test')
    
    with app.app_context():
        db.session.add(user)
        db.session.commit()
        return user

@pytest.fixture
def users(app):
    """Créer plusieurs utilisateurs"""
    users_list = []
    for i in range(5):
        user = User(
            username=f'user{i}',
            email=f'user{i}@example.com'
        )
        user.set_password('test')
        users_list.append(user)
    
    with app.app_context():
        db.session.add_all(users_list)
        db.session.commit()
        return users_list


# ----------------------------------------------------------------------------
# [TEST] TESTS UNITAIRES
# ----------------------------------------------------------------------------

"""
TESTER LES MODÈLES
"""

# tests/test_models.py
import pytest
from app.models import User, Post

def test_user_password_hashing(app):
    """
    [IDEE] TEST : Password hashé correctement
    """
    with app.app_context():
        user = User(username='alice', email='alice@example.com')
        user.set_password('secret')
        
        # Password ne doit PAS être stocké en clair
        assert user.password_hash != 'secret'
        
        # Vérification doit fonctionner
        assert user.check_password('secret')
        assert not user.check_password('wrong')

def test_user_repr(app):
    """Test représentation string"""
    with app.app_context():
        user = User(username='alice')
        assert repr(user) == '<User alice>'

def test_user_relationship(app, user):
    """
    [IDEE] TEST : Relations entre modèles
    """
    with app.app_context():
        # Créer post
        post = Post(title='Test Post', content='Content', author=user)
        db.session.add(post)
        db.session.commit()
        
        # Vérifier relation
        assert len(user.posts) == 1
        assert user.posts[0].title == 'Test Post'
        assert post.author.username == 'test'


"""
TESTER LES FONCTIONS UTILITAIRES
"""

# app/utils.py
def slugify(text):
    """Convertir texte en slug"""
    import re
    text = text.lower()
    text = re.sub(r'[^\w\s-]', '', text)
    text = re.sub(r'[-\s]+', '-', text)
    return text.strip('-')

# tests/test_utils.py
from app.utils import slugify

def test_slugify():
    """Test slugification"""
    assert slugify('Hello World') == 'hello-world'
    assert slugify('Python & Flask!') == 'python-flask'
    assert slugify('  Spaces  ') == 'spaces'
    assert slugify('Café Français') == 'caf-franais'


# ----------------------------------------------------------------------------
# [WEB] TESTS D'INTÉGRATION
# ----------------------------------------------------------------------------

"""
TESTER LES ROUTES
"""

# tests/test_routes.py
import pytest

def test_home_page(client):
    """
    [IDEE] TEST : Page d'accueil accessible
    """
    response = client.get('/')
    assert response.status_code == 200
    assert b'Welcome' in response.data

def test_404_page(client):
    """Test page 404"""
    response = client.get('/nonexistent')
    assert response.status_code == 404

def test_user_profile(client, user):
    """Test profil utilisateur"""
    response = client.get(f'/user/{user.id}')
    assert response.status_code == 200
    assert user.username.encode() in response.data

def test_user_profile_not_found(client):
    """Test profil inexistant"""
    response = client.get('/user/999')
    assert response.status_code == 404

"""
TESTER L'AUTHENTIFICATION
"""

# tests/test_auth.py
from app.models import User
from app.extensions import db

def test_register(client, app):
    """
    [IDEE] TEST : Inscription
    """
    response = client.post('/auth/register', data={
        'username': 'newuser',
        'email': 'new@example.com',
        'password': 'password123',
        'password2': 'password123'
    }, follow_redirects=True)
    
    assert response.status_code == 200
    
    # Vérifier que user existe
    with app.app_context():
        user = User.query.filter_by(username='newuser').first()
        assert user is not None
        assert user.email == 'new@example.com'

def test_login(client, user, app):
    """Test connexion"""
    response = client.post('/auth/login', data={
        'username': 'test',
        'password': 'test'
    }, follow_redirects=True)
    
    assert response.status_code == 200
    assert b'Logged in' in response.data

def test_login_invalid_credentials(client, user):
    """Test connexion avec mauvais credentials"""
    response = client.post('/auth/login', data={
        'username': 'test',
        'password': 'wrong'
    })
    
    assert b'Invalid' in response.data

def test_logout(client, auth):
    """Test déconnexion"""
    auth.login()
    response = auth.logout()
    
    assert response.status_code == 302  # Redirection

def test_protected_route(client, auth):
    """
    [IDEE] TEST : Route protégée
    """
    # Sans login
    response = client.get('/dashboard')
    assert response.status_code == 302  # Redirigé vers login
    
    # Avec login
    auth.login()
    response = client.get('/dashboard')
    assert response.status_code == 200

"""
TESTER LES FORMULAIRES
"""

def test_form_validation(client):
    """Test validation formulaire"""
    # Email invalide
    response = client.post('/auth/register', data={
        'username': 'test',
        'email': 'not-an-email',
        'password': 'test',
        'password2': 'test'
    })
    
    assert b'Invalid email' in response.data
    
    # Passwords ne correspondent pas
    response = client.post('/auth/register', data={
        'username': 'test',
        'email': 'test@example.com',
        'password': 'pass1',
        'password2': 'pass2'
    })
    
    assert b'must match' in response.data


# ----------------------------------------------------------------------------
# [GRAPHIQUE] COVERAGE
# ----------------------------------------------------------------------------

"""
MESURER LA COUVERTURE
"""

# Lancer tests avec coverage
pytest --cov=app --cov-report=html

"""
[IDEE] COVERAGE

Coverage = % de code exécuté par les tests

Objectif : > 80%

Rapport HTML :
htmlcov/index.html -> Ouvrir dans navigateur
Lignes rouges = Non testées


CONFIGURATION
"""

# .coveragerc
"""
[run]
source = app
omit =
    */tests/*
    */venv/*
    */__init__.py

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:
"""


# ----------------------------------------------------------------------------
# [RAPIDE] CI/CD
# ----------------------------------------------------------------------------

"""
GITHUB ACTIONS
"""

# .github/workflows/tests.yml
"""
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.10'
    
    - name: Install dependencies
      run: |
        pip install -r requirements.txt
        pip install -r requirements-dev.txt
    
    - name: Run tests
      run: |
        pytest --cov=app --cov-report=xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v2
"""


# ============================================================================
# [GUIDE] CHAPITRE 16 : SÉCURITÉ
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Protéger contre vulnérabilités courantes
[OK] CSRF, XSS, SQL Injection
[OK] Sécuriser sessions et cookies
[OK] HTTPS et headers de sécurité
[OK] Rate limiting
[OK] Best practices sécurité
"""


# ----------------------------------------------------------------------------
# [VERROUILLE] PROTECTION CSRF
# ----------------------------------------------------------------------------

"""
CSRF = Cross-Site Request Forgery

[IDEE] ATTAQUE CSRF :

1. Victime connectée sur site.com
2. Visite site-malveillant.com
3. Site malveillant fait requête vers site.com
4. Navigateur envoie cookies automatiquement
5. Action non voulue exécutée !


[OK] PROTECTION AVEC Flask-WTF
"""

# Flask-WTF protège automatiquement
from flask_wtf import FlaskForm

class DeleteForm(FlaskForm):
    # Token CSRF ajouté automatiquement
    pass

# Template
"""
<form method="POST">
    {{ form.hidden_tag() }}  <!-- Token CSRF -->
    <button type="submit">Delete</button>
</form>
"""

"""
CSRF POUR API
"""

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect()
csrf.init_app(app)

# Exempter routes API
@app.route('/api/data')
@csrf.exempt
def api_data():
    return jsonify({'data': 'value'})


# ----------------------------------------------------------------------------
# [SECURITE] PROTECTION XSS
# ----------------------------------------------------------------------------

"""
XSS = Cross-Site Scripting

[IDEE] ATTAQUE XSS :

Utilisateur entre : <script>alert('XSS')</script>
Sans échappement -> Script exécuté !


[OK] PROTECTION AVEC JINJA2
"""

# Jinja2 échappe automatiquement
"""
{{ user_input }}  <!-- Échappé automatiquement -->
"""

# Exemple
user_input = "<script>alert('XSS')</script>"

# Dans template, affiché comme :
# &lt;script&gt;alert('XSS')&lt;/script&gt;

"""
[X] NE PAS UTILISER |safe SANS VÉRIFICATION
"""

# [X] DANGEREUX
"""
{{ user_input|safe }}  <!-- Script exécuté ! -->
"""

# [OK] BON : Sanitizer HTML si nécessaire
from bleach import clean

allowed_tags = ['b', 'i', 'u', 'a', 'p']
clean_html = clean(user_input, tags=allowed_tags, strip=True)


# ----------------------------------------------------------------------------
# [SYRINGE] PROTECTION SQL INJECTION
# ----------------------------------------------------------------------------

"""
SQL Injection = Insertion de code SQL malveillant

[IDEE] ATTAQUE :

Username : admin' OR '1'='1
Query : SELECT * FROM users WHERE username='admin' OR '1'='1'
Résultat : Tous les users !


[OK] PROTECTION AVEC SQLAlchemy
"""

# [OK] SQLAlchemy protège automatiquement
username = request.form['username']
user = User.query.filter_by(username=username).first()

# SQLAlchemy utilise parameterized queries
# SQL généré : SELECT * FROM users WHERE username=?
# Valeur échappée automatiquement

"""
[X] NE JAMAIS CONSTRUIRE SQL À LA MAIN
"""

# [X] DANGEREUX
query = f"SELECT * FROM users WHERE username='{username}'"
db.session.execute(query)

# [OK] BON : Utiliser paramètres
query = "SELECT * FROM users WHERE username=:username"
db.session.execute(query, {'username': username})


# ----------------------------------------------------------------------------
# [SECURISE] MOTS DE PASSE SÉCURISÉS
# ----------------------------------------------------------------------------

"""
RÈGLES DE BASE
"""

# [OK] Toujours hasher avec bcrypt
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt()

class User(db.Model):
    password_hash = db.Column(db.String(128))
    
    def set_password(self, password):
        self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
    
    def check_password(self, password):
        return bcrypt.check_password_hash(self.password_hash, password)

"""
VALIDATION MOT DE PASSE
"""

import re

def validate_password(password):
    """
    Valider force du mot de passe
    
    Règles :
    - Min 8 caractères
    - Au moins 1 majuscule
    - Au moins 1 minuscule
    - Au moins 1 chiffre
    - Au moins 1 caractère spécial
    """
    if len(password) < 8:
        return False, "Minimum 8 caractères"
    
    if not re.search(r'[A-Z]', password):
        return False, "Au moins 1 majuscule"
    
    if not re.search(r'[a-z]', password):
        return False, "Au moins 1 minuscule"
    
    if not re.search(r'\d', password):
        return False, "Au moins 1 chiffre"
    
    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        return False, "Au moins 1 caractère spécial"
    
    return True, "OK"


# ----------------------------------------------------------------------------
# [COOKIE] SÉCURISER SESSIONS ET COOKIES
# ----------------------------------------------------------------------------

"""
CONFIGURATION SÉCURISÉE
"""

app.config.update(
    # Secret key fort
    SECRET_KEY=os.environ.get('SECRET_KEY') or secrets.token_hex(32),
    
    # Session sécurisée
    SESSION_COOKIE_SECURE=True,      # HTTPS seulement
    SESSION_COOKIE_HTTPONLY=True,    # Pas accessible en JS
    SESSION_COOKIE_SAMESITE='Lax',   # Protection CSRF
    
    # Durée de vie
    PERMANENT_SESSION_LIFETIME=timedelta(hours=1),
    
    # Remember Me sécurisé
    REMEMBER_COOKIE_SECURE=True,
    REMEMBER_COOKIE_HTTPONLY=True,
    REMEMBER_COOKIE_DURATION=timedelta(days=30)
)

"""
[IDEE] OPTIONS EXPLIQUÉES

SECURE=True
    Cookie envoyé SEULEMENT via HTTPS
    [ATTENTION] Mettre False en développement local

HTTPONLY=True
    Cookie pas accessible via JavaScript
    Protection contre XSS

SAMESITE='Lax'
    Protection CSRF
    Options : 'Strict', 'Lax', 'None'
"""


# ----------------------------------------------------------------------------
# [SECURISE] HEADERS DE SÉCURITÉ
# ----------------------------------------------------------------------------

"""
AJOUTER HEADERS DE SÉCURITÉ
"""

from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)

# Talisman force HTTPS et ajoute headers
talisman = Talisman(
    app,
    force_https=True,
    strict_transport_security=True,
    content_security_policy={
        'default-src': "'self'",
        'script-src': "'self' 'unsafe-inline'",
        'style-src': "'self' 'unsafe-inline'"
    }
)

"""
HEADERS MANUELS
"""

@app.after_request
def set_security_headers(response):
    """Ajouter headers de sécurité"""
    
    # Empêcher clickjacking
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'
    
    # Désactiver sniffing MIME
    response.headers['X-Content-Type-Options'] = 'nosniff'
    
    # Activer XSS protection
    response.headers['X-XSS-Protection'] = '1; mode=block'
    
    # Politique de référents
    response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
    
    # Permissions
    response.headers['Permissions-Policy'] = 'geolocation=(), microphone=()'
    
    return response


# ----------------------------------------------------------------------------
# [TEMPS] RATE LIMITING
# ----------------------------------------------------------------------------

"""
LIMITER REQUÊTES
"""

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

# Limiter route spécifique
@app.route('/api/search')
@limiter.limit("10 per minute")
def search():
    return jsonify({'results': []})

# Limiter login
@app.route('/auth/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
    # Empêche brute force
    pass


# ----------------------------------------------------------------------------
# [LISTE] CHECKLIST SÉCURITÉ
# ----------------------------------------------------------------------------

"""
[OK] CHECKLIST AVANT PRODUCTION

GÉNÉRAL :
[ ] SECRET_KEY aléatoire et long
[ ] DEBUG=False
[ ] Pas de credentials dans code
[ ] Variables d'environnement pour secrets

AUTHENTIFICATION :
[ ] Passwords hashés (bcrypt)
[ ] Validation password forte
[ ] Rate limiting sur login
[ ] Protection brute force

SESSIONS :
[ ] SECURE=True (HTTPS)
[ ] HTTPONLY=True
[ ] SAMESITE='Lax'
[ ] Timeout raisonnable

PROTECTION ATTAQUES :
[ ] CSRF activé (Flask-WTF)
[ ] XSS : Échappement Jinja2
[ ] SQL Injection : SQLAlchemy
[ ] Headers de sécurité

HTTPS :
[ ] Certificat SSL valide
[ ] Redirection HTTP -> HTTPS
[ ] HSTS activé

DÉPENDANCES :
[ ] Packages à jour
[ ] pip-audit pour vulnérabilités
[ ] Dependabot activé

LOGS :
[ ] Pas de données sensibles loguées
[ ] Monitoring actif
[ ] Alertes configurées
"""


# ============================================================================
# [GUIDE] CHAPITRE 17 : PERFORMANCE ET CACHING
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Mesurer performance
[OK] Optimiser requêtes DB
[OK] Implémenter caching
[OK] Compression
[OK] CDN et assets
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] MESURER LA PERFORMANCE
# ----------------------------------------------------------------------------

"""
PROFILING AVEC FLASK-DEBUGTOOLBAR
"""

pip install flask-debugtoolbar

from flask_debugtoolbar import DebugToolbarExtension

app.config['DEBUG_TB_ENABLED'] = True
app.config['SECRET_KEY'] = 'secret'

toolbar = DebugToolbarExtension(app)

"""
TIMING DES REQUÊTES
"""

import time
from flask import g

@app.before_request
def before_request():
    g.start = time.time()

@app.after_request
def after_request(response):
    diff = time.time() - g.start
    print(f"Request took {diff:.4f} seconds")
    response.headers['X-Response-Time'] = str(diff)
    return response


# ----------------------------------------------------------------------------
# [ARCHIVE] OPTIMISER BASE DE DONNÉES
# ----------------------------------------------------------------------------

"""
PROBLÈME N+1
"""

# [X] MAUVAIS (N+1 requêtes)
users = User.query.all()  # 1 requête
for user in users:
    print(user.posts)  # N requêtes (une par user)

# [OK] BON (Eager loading)
users = User.query.options(db.joinedload(User.posts)).all()  # 1 requête
for user in users:
    print(user.posts)  # Pas de requête supplémentaire

"""
INDEX SUR COLONNES
"""

class User(db.Model):
    username = db.Column(db.String(80), index=True)  # Index
    email = db.Column(db.String(120), index=True)

# Index composite
__table_args__ = (
    db.Index('idx_user_email', 'username', 'email'),
)

"""
PAGINATION EFFICACE
"""

# [OK] Pagination
@app.route('/posts')
def posts():
    page = request.args.get('page', 1, type=int)
    posts = Post.query.paginate(page=page, per_page=20)
    return render_template('posts.html', posts=posts)


# ----------------------------------------------------------------------------
# [SAUVEGARDE] CACHING
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install Flask-Caching

"""
CONFIGURATION
"""

from flask_caching import Cache

cache = Cache(config={
    'CACHE_TYPE': 'SimpleCache',  # Développement
    # 'CACHE_TYPE': 'RedisCache',  # Production
    # 'CACHE_REDIS_URL': 'redis://localhost:6379/0'
})

cache.init_app(app)

"""
CACHE DE ROUTES
"""

@app.route('/expensive')
@cache.cached(timeout=300)  # 5 minutes
def expensive_operation():
    # Calcul coûteux
    result = perform_heavy_computation()
    return render_template('result.html', result=result)

"""
CACHE DE FONCTIONS
"""

@cache.memoize(timeout=300)
def get_user_stats(user_id):
    """Statistiques utilisateur (cachées)"""
    user = User.query.get(user_id)
    posts_count = user.posts.count()
    # ... calculs ...
    return stats

"""
INVALIDER CACHE
"""

# Supprimer tout le cache
cache.clear()

# Supprimer cache spécifique
cache.delete_memoized(get_user_stats, user_id=1)


# ----------------------------------------------------------------------------
# [COMPRESSION] COMPRESSION
# ----------------------------------------------------------------------------

"""
GZIP COMPRESSION
"""

from flask_compress import Compress

Compress(app)

# Configure
app.config['COMPRESS_ALGORITHM'] = 'gzip'
app.config['COMPRESS_LEVEL'] = 6
app.config['COMPRESS_MIN_SIZE'] = 500


# ----------------------------------------------------------------------------
# [RAPIDE] OPTIMISATION ASSETS
# ----------------------------------------------------------------------------

"""
MINIFICATION
"""

pip install Flask-Assets

from flask_assets import Environment, Bundle

assets = Environment(app)

# Bundle CSS
css = Bundle(
    'css/style.css',
    'css/blog.css',
    filters='cssmin',
    output='gen/packed.css'
)
assets.register('css_all', css)

# Bundle JS
js = Bundle(
    'js/jquery.js',
    'js/main.js',
    filters='jsmin',
    output='gen/packed.js'
)
assets.register('js_all', js)

# Template
"""
{% assets "css_all" %}
    <link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %}
"""


# ============================================================================
# [GUIDE] CHAPITRE 18 : DEPLOYMENT (PRODUCTION)
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Préparer app pour production
[OK] Déployer sur Heroku, AWS, DigitalOcean
[OK] Configurer Gunicorn et Nginx
[OK] Variables d'environnement
[OK] HTTPS et domaine
"""


# ----------------------------------------------------------------------------
# [PACKAGE] PRÉPARER L'APPLICATION
# ----------------------------------------------------------------------------

"""
CONFIGURATION PAR ENVIRONNEMENT
"""

# config.py
import os

class Config:
    """Configuration de base"""
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    """Configuration développement"""
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'

class ProductionConfig(Config):
    """Configuration production"""
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    
    # Sécurité
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    REMEMBER_COOKIE_SECURE = True

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

"""
REQUIREMENTS
"""

# requirements.txt (production)
"""
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Flask-WTF==1.2.1
Flask-Migrate==4.0.5
Flask-Bcrypt==1.0.1
psycopg2-binary==2.9.9  # PostgreSQL
gunicorn==21.2.0
"""

# requirements-dev.txt
"""
-r requirements.txt
pytest==7.4.0
pytest-cov==4.1.0
Flask-DebugToolbar==0.14.1
"""


# ----------------------------------------------------------------------------
# [RAPIDE] DÉPLOYER SUR HEROKU
# ----------------------------------------------------------------------------

"""
PRÉPARATION
"""

# Procfile
"""
web: gunicorn run:app
"""

# runtime.txt
"""
python-3.11.0
"""

"""
COMMANDES HEROKU
"""

# Installer Heroku CLI
# https://devcenter.heroku.com/articles/heroku-cli

# Login
heroku login

# Créer app
heroku create mon-app-flask

# Ajouter PostgreSQL
heroku addons:create heroku-postgresql:hobby-dev

# Variables d'environnement
heroku config:set SECRET_KEY=votre-secret-key
heroku config:set FLASK_ENV=production

# Déployer
git push heroku main

# Migrations
heroku run flask db upgrade

# Logs
heroku logs --tail


# ----------------------------------------------------------------------------
# [DOCKER] DOCKER
# ----------------------------------------------------------------------------

"""
DOCKERFILE
"""

# Dockerfile
"""
FROM python:3.11-slim

WORKDIR /app

# Dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Code
COPY . .

# Variables d'environnement
ENV FLASK_APP=run.py
ENV FLASK_ENV=production

# Port
EXPOSE 5000

# Lancer avec Gunicorn
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "run:app"]
"""

# docker-compose.yml
"""
version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - db
  
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
"""

"""
COMMANDES DOCKER
"""

# Build
docker build -t mon-app-flask .

# Run
docker run -p 5000:5000 mon-app-flask

# Avec docker-compose
docker-compose up -d


# ----------------------------------------------------------------------------
# [WEB] NGINX + GUNICORN
# ----------------------------------------------------------------------------

"""
GUNICORN
"""

# Lancer Gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 run:app

# Config avancée
# gunicorn_config.py
"""
workers = 4
bind = "0.0.0.0:8000"
timeout = 30
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"
"""

# Lancer avec config
gunicorn -c gunicorn_config.py run:app

"""
NGINX
"""

# /etc/nginx/sites-available/mon-app
"""
server {
    listen 80;
    server_name example.com www.example.com;
    
    # Redirection HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    # SSL
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # Headers sécurité
    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    
    # Static files
    location /static {
        alias /var/www/mon-app/static;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # Proxy vers Gunicorn
    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/mon-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx


# ============================================================================
# [GUIDE] CHAPITRE 19 : LOGGING ET MONITORING
# ============================================================================

"""
LOGGING
"""

import logging
from logging.handlers import RotatingFileHandler
import os

if not app.debug:
    # Fichier de log
    if not os.path.exists('logs'):
        os.mkdir('logs')
    
    file_handler = RotatingFileHandler(
        'logs/app.log',
        maxBytes=10240000,  # 10 MB
        backupCount=10
    )
    
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)
    
    app.logger.setLevel(logging.INFO)
    app.logger.info('Application startup')

"""
MONITORING AVEC SENTRY
"""

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration

sentry_sdk.init(
    dsn=os.environ.get('SENTRY_DSN'),
    integrations=[FlaskIntegration()],
    traces_sample_rate=1.0
)


# ============================================================================
# [GUIDE] CHAPITRE 20 : BEST PRACTICES FINALES
# ============================================================================

"""
[OK] ARCHITECTURE :
- Application Factory
- Blueprints pour organisation
- Modèles séparés
- Configuration par environnement

[OK] SÉCURITÉ :
- HTTPS obligatoire
- Headers de sécurité
- CSRF activé
- Passwords hashés
- Rate limiting
- Variables d'environnement pour secrets

[OK] PERFORMANCE :
- Caching (Redis)
- Index DB
- Pagination
- Compression
- CDN pour assets

[OK] QUALITÉ :
- Tests (coverage > 80%)
- Linting (flake8, black)
- Type hints
- Documentation

[OK] PRODUCTION :
- Gunicorn + Nginx
- PostgreSQL
- Monitoring (Sentry)
- Logging
- Backups automatiques
- CI/CD

[BRAVO] VOUS ÊTES MAINTENANT UN EXPERT FLASK ! [BRAVO]
"""

# ============================================================================
# [LIVRE] FLASK - GUIDE ULTRA-DÉTAILLÉ COMPLET
# INDEX ET TABLE DES MATIÈRES
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET : 11,000+ LIGNES
# [DOCS] 20 CHAPITRES COUVRANT TOUT FLASK
# [TEMPS] TEMPS DE LECTURE : ~25-30 HEURES
# 
# Ce guide est LE guide ultime pour maîtriser Flask de A à Z
# Vous n'aurez PLUS BESOIN de chercher ailleurs !
#
# ============================================================================


# ============================================================================
# [GUIDE] COMMENT UTILISER CE GUIDE
# ============================================================================

"""
[OBJECTIF] PARCOURS D'APPRENTISSAGE RECOMMANDÉ

DÉBUTANT (Semaines 1-2) :
-> Partie 1 complète (Chapitres 0-4)
-> Focus : Bases, Routes, Templates
-> Projet : Site portfolio simple

INTERMÉDIAIRE (Semaines 3-4) :
-> Partie 2 complète (Chapitres 5-8)
-> Focus : Formulaires, Base de données
-> Projet : Blog avec commentaires

AVANCÉ (Semaines 5-6) :
-> Partie 3 complète (Chapitres 9-14)
-> Focus : Auth, Blueprints, APIs
-> Projet : Réseau social simple

EXPERT (Semaines 7-8) :
-> Partie 4 complète (Chapitres 15-20)
-> Focus : Tests, Sécurité, Production
-> Projet : Déploiement en production


[NOTE] CONSEILS PRATIQUES

1. PRATIQUEZ QUOTIDIENNEMENT
   - 1-2h par jour > 10h le weekend
   - Tapez TOUS les exemples
   - Créez vos variations

2. SUIVEZ L'ORDRE
   - Chaque chapitre s'appuie sur le précédent
   - Ne sautez pas d'étapes

3. FAITES LES EXERCICES
   - Exercices marqués [COURS]
   - Essentiels pour comprendre

4. CRÉEZ DES PROJETS
   - Appliquez immédiatement
   - Projets suggérés à chaque partie

5. REVENEZ RÉGULIÈREMENT
   - Guide = Référence permanente
   - Annotez, ajoutez vos notes
"""


# ============================================================================
# [DOCS] TABLE DES MATIÈRES COMPLÈTE
# ============================================================================

"""
═══════════════════════════════════════════════════════════════════════════
PARTIE 1 : FONDAMENTAUX DE FLASK
═══════════════════════════════════════════════════════════════════════════

[GUIDE] CHAPITRE 0 : INTRODUCTION À FLASK
────────────────────────────────────
├─ Qu'est-ce que Flask ?
│  ├─ Définition et philosophie
│  ├─ Micro-framework expliqué
│  └─ Composants de base
│
├─ Flask vs Autres Frameworks
│  ├─ Comparaison avec Django
│  ├─ Comparaison avec FastAPI
│  └─ Tableau comparatif détaillé
│
├─ Quand Utiliser Flask ?
│  ├─ Cas d'usage idéaux
│  ├─ Projets adaptés
│  └─ Quand NE PAS utiliser Flask
│
└─ Installation et Configuration
   ├─ Installation Python
   ├─ Environnement virtuel (venv)
   ├─ Installation Flask
   └─ Structure de projet


[GUIDE] CHAPITRE 1 : PREMIÈRE APPLICATION FLASK
───────────────────────────────────────────
├─ Hello World Décrypté
│  ├─ Ligne par ligne expliquée
│  ├─ Flask(__name__) en détail
│  ├─ Décorateurs @app.route()
│  └─ app.run() et debug mode
│
├─ Lancer l'Application
│  ├─ python app.py
│  ├─ flask run
│  └─ Options de lancement
│
├─ Routes Multiples
│  ├─ Plusieurs pages
│  ├─ Navigation
│  └─ Gestion 404
│
├─ Retourner Différents Contenus
│  ├─ HTML
│  ├─ JSON
│  ├─ Status codes
│  └─ Headers personnalisés
│
└─ Cycle Requête-Réponse
   ├─ Flux complet
   ├─ Composants HTTP
   └─ Visualisation détaillée

[COURS] EXERCICE PRATIQUE 1 : Site Multi-Pages


[GUIDE] CHAPITRE 2 : ROUTES ET URLs DYNAMIQUES
──────────────────────────────────────────
├─ Routes Dynamiques
│  ├─ Paramètres <variable>
│  ├─ Pourquoi et comment
│  └─ Exemples pratiques
│
├─ Types de Paramètres
│  ├─ string (défaut)
│  ├─ int (entiers)
│  ├─ float (décimaux)
│  ├─ path (avec slashes)
│  └─ uuid (identifiants uniques)
│
├─ Paramètres Multiples
│  ├─ Plusieurs dans une URL
│  ├─ Validation
│  └─ Exemples e-commerce, blog
│
├─ Paramètres Optionnels
│  ├─ Deux routes, une fonction
│  ├─ Valeurs par défaut
│  └─ Pagination optionnelle
│
├─ Méthodes HTTP
│  ├─ GET, POST, PUT, DELETE
│  ├─ request.method
│  ├─ request.form vs request.get_json()
│  └─ Pattern REST
│
└─ URL Building (url_for)
   ├─ Pourquoi url_for()
   ├─ Syntaxe et exemples
   ├─ Paramètres et _external
   └─ Redirections

[COURS] EXERCICE PRATIQUE 2 : Blog avec Routes Dynamiques


[GUIDE] CHAPITRE 3 : TEMPLATES JINJA2
─────────────────────────────────
├─ Pourquoi des Templates ?
│  ├─ Problèmes HTML dans Python
│  ├─ Avantages séparation
│  └─ Structure dossier templates/
│
├─ Premier Template
│  ├─ render_template()
│  ├─ Passer variables
│  └─ Syntaxe Jinja2 ({{ }}, {% %}, {# #})
│
├─ Variables et Expressions
│  ├─ Afficher variables
│  ├─ Attributs d'objets
│  ├─ Éléments de listes
│  └─ Expressions Python
│
├─ Conditions
│  ├─ if/elif/else
│  ├─ Opérateurs de comparaison
│  ├─ Opérateurs logiques (and, or, not)
│  └─ Tests Jinja2 (is defined, is none)
│
├─ Boucles
│  ├─ for...in
│  ├─ Variable loop (index, first, last)
│  ├─ Boucles imbriquées
│  ├─ Boucles sur dictionnaires
│  └─ else pour liste vide
│
├─ Filtres Jinja2
│  ├─ Filtres texte (upper, lower, title)
│  ├─ Filtres nombres (round, abs)
│  ├─ Filtres listes (length, first, last, join)
│  ├─ Valeurs par défaut (default)
│  ├─ Filtres HTML (safe, escape)
│  ├─ Chaîner filtres
│  └─ Filtres personnalisés
│
├─ Héritage de Templates
│  ├─ Template de base (base.html)
│  ├─ {% extends %} et {% block %}
│  ├─ super() pour ajouter
│  └─ Hiérarchie multi-niveaux
│
├─ Includes (Partials)
│  ├─ {% include %}
│  ├─ Organisation partials/
│  ├─ Passer variables
│  └─ ignore missing
│
└─ URLs dans Templates
   ├─ url_for() dans Jinja2
   └─ Bonnes pratiques

[COURS] EXERCICE PRATIQUE 3 : Blog Complet avec Templates


[GUIDE] CHAPITRE 4 : FICHIERS STATIQUES
───────────────────────────────────
├─ Organisation
│  ├─ Structure dossier static/
│  ├─ css/, js/, images/
│  └─ Conventions de nommage
│
├─ Utiliser Fichiers Statiques
│  ├─ url_for('static', filename='...')
│  ├─ CSS
│  ├─ JavaScript
│  └─ Images
│
├─ Exemple Complet
│  ├─ style.css
│  ├─ main.js
│  └─ Intégration templates
│
├─ Images et Médias
│  ├─ Favicon
│  ├─ Images responsive
│  └─ Optimisation
│
└─ Optimisation Assets
   ├─ Cache-busting
   ├─ Minification (Flask-Assets)
   ├─ CDN pour bibliothèques
   └─ Compression


═══════════════════════════════════════════════════════════════════════════
PARTIE 2 : FORMULAIRES ET BASE DE DONNÉES
═══════════════════════════════════════════════════════════════════════════

[GUIDE] CHAPITRE 5 : FORMULAIRES ET VALIDATION (Flask-WTF)
──────────────────────────────────────────────────────
├─ Pourquoi Flask-WTF ?
│  ├─ Problèmes formulaires manuels
│  ├─ Avantages Flask-WTF
│  └─ Installation et configuration
│
├─ Créer un Formulaire
│  ├─ Classe FlaskForm
│  ├─ Types de champs
│  ├─ Anatomie d'un champ
│  └─ Exemple LoginForm
│
├─ Utiliser dans Routes
│  ├─ validate_on_submit()
│  ├─ Flux GET/POST
│  ├─ Récupérer données (form.field.data)
│  └─ Flash messages
│
├─ Template de Formulaire
│  ├─ form.hidden_tag() (CSRF)
│  ├─ Afficher champs
│  ├─ Afficher erreurs
│  └─ Exemple complet
│
├─ Validateurs
│  ├─ DataRequired
│  ├─ Email
│  ├─ Length
│  ├─ EqualTo
│  ├─ NumberRange
│  ├─ URL
│  ├─ Regexp
│  └─ Optional
│
├─ Validateurs Personnalisés
│  ├─ Méthode validate_<field>
│  ├─ Fonction validateur
│  └─ Classe validateur
│
├─ Types de Champs Avancés
│  ├─ SelectField
│  ├─ SelectMultipleField
│  ├─ RadioField
│  ├─ BooleanField
│  ├─ TextAreaField
│  └─ HiddenField
│
└─ Upload de Fichiers
   ├─ FileField
   ├─ FileAllowed, FileRequired
   ├─ secure_filename()
   ├─ Configuration upload
   └─ Validation taille

[COURS] EXERCICE PRATIQUE 4 : Formulaire de Contact Complet


[GUIDE] CHAPITRE 6 : BASE DE DONNÉES (SQLAlchemy)
─────────────────────────────────────────────
├─ Pourquoi une Base de Données ?
│  ├─ Problèmes données en mémoire
│  ├─ Avantages DB
│  └─ Types de DB (SQL vs NoSQL)
│
├─ Qu'est-ce qu'un ORM ?
│  ├─ Concept ORM
│  ├─ SQL brut vs ORM
│  ├─ Avantages SQLAlchemy
│  └─ Flask-SQLAlchemy
│
├─ Installation et Configuration
│  ├─ pip install
│  ├─ SQLALCHEMY_DATABASE_URI
│  ├─ Différents SGBD
│  └─ Structure projet
│
├─ Créer un Modèle
│  ├─ Classe héritant db.Model
│  ├─ __tablename__
│  ├─ Colonnes (db.Column)
│  └─ __repr__()
│
├─ Types de Colonnes
│  ├─ Integer, BigInteger
│  ├─ String, Text
│  ├─ Boolean
│  ├─ Float, Numeric
│  ├─ DateTime, Date, Time
│  ├─ LargeBinary
│  └─ JSON
│
├─ Options de Colonnes
│  ├─ primary_key
│  ├─ unique
│  ├─ nullable
│  ├─ default
│  ├─ index
│  └─ server_default
│
├─ Créer les Tables
│  ├─ db.create_all()
│  ├─ Flask shell
│  ├─ Script d'initialisation
│  └─ Vérifier tables
│
├─ CRUD : Create
│  ├─ Créer objet
│  ├─ db.session.add()
│  ├─ db.session.commit()
│  ├─ Session expliquée
│  └─ Créer plusieurs enregistrements
│
├─ CRUD : Read
│  ├─ query.all()
│  ├─ query.first()
│  ├─ query.get()
│  ├─ query.get_or_404()
│  ├─ filter_by() (égalité simple)
│  ├─ filter() (conditions complexes)
│  ├─ Opérateurs (==, !=, >, <, like, in)
│  ├─ Opérateurs logiques (and_, or_, not_)
│  ├─ order_by()
│  ├─ limit() et offset()
│  ├─ Pagination avec paginate()
│  └─ Exemple recherche complète
│
├─ CRUD : Update
│  ├─ Modifier attributs
│  ├─ Commit automatique
│  └─ Update bulk
│
└─ CRUD : Delete
   ├─ db.session.delete()
   ├─ Supprimer plusieurs
   └─ Vérifications avant suppression


[GUIDE] CHAPITRE 7 : MIGRATIONS (Flask-Migrate)
───────────────────────────────────────────
├─ Pourquoi les Migrations ?
│  ├─ Problème évolution schéma
│  ├─ Sans migrations (perte données)
│  └─ Avec migrations (historique)
│
├─ Installation et Configuration
│  ├─ pip install flask-migrate
│  ├─ Migrate(app, db)
│  └─ flask db init
│
├─ Créer une Migration
│  ├─ Modifier modèles
│  ├─ flask db migrate -m "message"
│  ├─ Fichier généré expliqué
│  └─ upgrade() et downgrade()
│
├─ Appliquer Migration
│  ├─ flask db upgrade
│  ├─ flask db downgrade
│  └─ Vérifications
│
├─ Commandes Flask-Migrate
│  ├─ flask db init
│  ├─ flask db migrate
│  ├─ flask db upgrade
│  ├─ flask db downgrade
│  ├─ flask db current
│  ├─ flask db history
│  └─ flask db show
│
├─ Modifications Complexes
│  ├─ Ajouter colonne avec défaut
│  ├─ Renommer colonne
│  └─ Migration de données
│
└─ Problèmes Courants
   ├─ Migration ne détecte pas changements
   ├─ Désynchronisation
   ├─ Conflits (équipe)
   └─ Erreurs lors de upgrade


[GUIDE] CHAPITRE 8 : RELATIONS ENTRE TABLES
───────────────────────────────────────
├─ Types de Relations
│  ├─ One-to-Many (1-N)
│  ├─ Many-to-Many (N-N)
│  └─ One-to-One (1-1)
│
├─ Relation One-to-Many
│  ├─ Foreign Key (db.ForeignKey)
│  ├─ Relationship (db.relationship)
│  ├─ backref
│  ├─ lazy loading
│  ├─ Exemple User -> Posts
│  └─ Utilisation
│
├─ Relation Many-to-Many
│  ├─ Table d'association (db.Table)
│  ├─ secondary
│  ├─ Exemple Posts <-> Tags
│  └─ Utilisation
│
├─ Relation One-to-One
│  ├─ uselist=False
│  ├─ unique=True sur FK
│  ├─ Exemple User <-> Profile
│  └─ Utilisation
│
└─ Suppression en Cascade
   ├─ Options cascade
   ├─ 'all, delete-orphan'
   └─ Comportement


═══════════════════════════════════════════════════════════════════════════
PARTIE 3 : FONCTIONNALITÉS AVANCÉES
═══════════════════════════════════════════════════════════════════════════

[GUIDE] CHAPITRE 9 : AUTHENTIFICATION (Flask-Login)
───────────────────────────────────────────────
├─ Pourquoi Flask-Login ?
│  ├─ Problèmes auth manuelle
│  └─ Avantages Flask-Login
│
├─ Installation et Configuration
│  ├─ pip install
│  ├─ LoginManager
│  └─ Configuration (login_view, etc.)
│
├─ Modèle User
│  ├─ UserMixin
│  ├─ Méthodes requises
│  ├─ set_password() (bcrypt)
│  └─ check_password()
│
├─ User Loader
│  ├─ @login_manager.user_loader
│  ├─ Comment ça marche
│  └─ Fonctionnement session
│
├─ Formulaires Auth
│  ├─ LoginForm
│  ├─ RegistrationForm
│  └─ Validateurs personnalisés
│
├─ Routes Authentification
│  ├─ Route inscription
│  ├─ Route connexion (login_user)
│  ├─ Route déconnexion (logout_user)
│  └─ Redirection next_page
│
├─ Protéger Routes
│  ├─ @login_required
│  ├─ current_user
│  └─ Vérification manuelle
│
├─ Templates avec Auth
│  ├─ current_user.is_authenticated
│  ├─ Navigation conditionnelle
│  └─ Messages flash
│
├─ Permissions Personnalisées
│  ├─ Décorateur admin_required
│  └─ Décorateur role_required
│
└─ Réinitialisation Mot de Passe
   ├─ Générer token
   ├─ Vérifier token
   ├─ Formulaires reset
   └─ Routes reset


[GUIDE] CHAPITRE 10 : BLUEPRINTS ET ORGANISATION
────────────────────────────────────────────
├─ Pourquoi les Blueprints ?
│  ├─ Problème app.py géant
│  └─ Avantages modularité
│
├─ Structure avec Blueprints
│  ├─ Organisation dossiers
│  ├─ app/ package principal
│  └─ Blueprints séparés
│
├─ Créer un Blueprint
│  ├─ Blueprint(name, import_name)
│  ├─ Options (url_prefix, etc.)
│  └─ Définir routes
│
├─ Application Factory
│  ├─ Créer extensions (extensions.py)
│  ├─ create_app()
│  ├─ Initialiser extensions
│  └─ Enregistrer blueprints
│
├─ Blueprint Auth
│  ├─ Structure auth/
│  ├─ Forms auth
│  └─ Routes auth
│
├─ url_for avec Blueprints
│  ├─ Syntaxe 'blueprint.fonction'
│  └─ Exemples
│
└─ Lancer Application
   ├─ run.py
   ├─ shell_context_processor
   └─ Commandes Flask


[GUIDE] CHAPITRE 11 : SESSIONS ET COOKIES
─────────────────────────────────────
├─ Sessions
│  ├─ Qu'est-ce qu'une session ?
│  ├─ session dict-like
│  ├─ Stocker données
│  ├─ Lire données
│  └─ Supprimer données
│
└─ Cookies
   ├─ make_response()
   ├─ set_cookie()
   ├─ Options (max_age, secure, httponly)
   ├─ Lire cookies
   └─ Supprimer cookies


[GUIDE] CHAPITRE 12 : API REST
──────────────────────────
├─ Principes REST
│  ├─ Ressources + Verbes HTTP
│  └─ Conventions
│
├─ API Simple
│  ├─ GET (lister, récupérer)
│  ├─ POST (créer)
│  ├─ PUT (modifier)
│  └─ DELETE (supprimer)
│
├─ Réponses JSON
│  ├─ jsonify()
│  └─ Status codes
│
└─ CORS
   ├─ Installation flask-cors
   └─ Configuration


[GUIDE] CHAPITRE 13 : UPLOAD AVANCÉ
───────────────────────────────
(Couvert dans Chapitre 5)


[GUIDE] CHAPITRE 14 : EMAIL ET TÂCHES ASYNCHRONES
─────────────────────────────────────────────
├─ Email
│  ├─ Flask-Mail
│  └─ Envoi emails
│
└─ Tâches Asynchrones
   ├─ Celery
   └─ Redis


═══════════════════════════════════════════════════════════════════════════
PARTIE 4 : PRODUCTION ET OPTIMISATION
═══════════════════════════════════════════════════════════════════════════

[GUIDE] CHAPITRE 15 : TESTING
─────────────────────────
├─ Pourquoi Tester ?
│  ├─ Problèmes code non testé
│  └─ Avantages tests
│
├─ Configuration pytest
│  ├─ Installation
│  ├─ Structure tests/
│  └─ pytest.ini
│
├─ Fixtures
│  ├─ @pytest.fixture
│  ├─ Fixture app
│  ├─ Fixture client
│  ├─ Fixture auth
│  └─ Fixtures données
│
├─ Tests Unitaires
│  ├─ Tester modèles
│  ├─ Tester fonctions
│  └─ Assertions
│
├─ Tests d'Intégration
│  ├─ Tester routes
│  ├─ Tester authentification
│  ├─ Tester formulaires
│  └─ follow_redirects
│
├─ Coverage
│  ├─ pytest --cov
│  ├─ Rapport HTML
│  └─ Configuration .coveragerc
│
└─ CI/CD
   └─ GitHub Actions


[GUIDE] CHAPITRE 16 : SÉCURITÉ
──────────────────────────
├─ Protection CSRF
│  ├─ Attaque CSRF expliquée
│  ├─ Flask-WTF protection
│  └─ @csrf.exempt pour APIs
│
├─ Protection XSS
│  ├─ Attaque XSS expliquée
│  ├─ Jinja2 échappement auto
│  ├─ Danger de |safe
│  └─ Sanitizer HTML (bleach)
│
├─ Protection SQL Injection
│  ├─ Attaque SQL Injection
│  ├─ SQLAlchemy protection auto
│  └─ Parameterized queries
│
├─ Mots de Passe Sécurisés
│  ├─ Bcrypt hashage
│  └─ Validation force password
│
├─ Sessions et Cookies Sécurisés
│  ├─ Configuration SECURE
│  ├─ HTTPONLY
│  └─ SAMESITE
│
├─ Headers de Sécurité
│  ├─ Flask-Talisman
│  ├─ X-Frame-Options
│  ├─ X-Content-Type-Options
│  ├─ X-XSS-Protection
│  └─ CSP (Content Security Policy)
│
├─ Rate Limiting
│  ├─ Flask-Limiter
│  └─ Configuration limites
│
└─ Checklist Sécurité
   └─ Liste complète avant production


[GUIDE] CHAPITRE 17 : PERFORMANCE ET CACHING
────────────────────────────────────────
├─ Mesurer Performance
│  ├─ Flask-DebugToolbar
│  └─ Timing requêtes
│
├─ Optimiser Base de Données
│  ├─ Problème N+1
│  ├─ Eager loading (joinedload)
│  ├─ Index sur colonnes
│  └─ Pagination efficace
│
├─ Caching
│  ├─ Flask-Caching
│  ├─ Configuration (SimpleCache, Redis)
│  ├─ @cache.cached()
│  ├─ @cache.memoize()
│  └─ Invalider cache
│
├─ Compression
│  └─ Flask-Compress (gzip)
│
└─ Optimisation Assets
   ├─ Flask-Assets
   ├─ Minification CSS/JS
   └─ Bundling


[GUIDE] CHAPITRE 18 : DEPLOYMENT (PRODUCTION)
─────────────────────────────────────────
├─ Préparer Application
│  ├─ Configuration par environnement
│  ├─ config.py
│  └─ requirements.txt
│
├─ Déployer sur Heroku
│  ├─ Procfile
│  ├─ runtime.txt
│  ├─ Commandes heroku
│  └─ Variables d'environnement
│
├─ Docker
│  ├─ Dockerfile
│  ├─ docker-compose.yml
│  └─ Commandes Docker
│
└─ Nginx + Gunicorn
   ├─ Configuration Gunicorn
   ├─ Configuration Nginx
   ├─ HTTPS (Let's Encrypt)
   └─ Fichiers statiques


[GUIDE] CHAPITRE 19 : LOGGING ET MONITORING
───────────────────────────────────────
├─ Logging
│  ├─ Configuration logging
│  ├─ RotatingFileHandler
│  └─ Niveaux de log
│
└─ Monitoring
   ├─ Sentry
   └─ Configuration


[GUIDE] CHAPITRE 20 : BEST PRACTICES FINALES
────────────────────────────────────────
├─ Architecture
│  ├─ Application Factory
│  ├─ Blueprints
│  └─ Configuration propre
│
├─ Sécurité Récap
│  └─ Checklist complète
│
├─ Performance Récap
│  └─ Optimisations essentielles
│
├─ Qualité Code
│  ├─ Tests (coverage)
│  ├─ Linting
│  └─ Documentation
│
└─ Production Récap
   └─ Checklist déploiement


═══════════════════════════════════════════════════════════════════════════
"""


# ============================================================================
# [OBJECTIF] PROJETS SUGGÉRÉS PAR NIVEAU
# ============================================================================

"""
NIVEAU 1 : DÉBUTANT (Après Partie 1)
─────────────────────────────────────

Projet 1 : PORTFOLIO PERSONNEL
├─ Page d'accueil
├─ À propos
├─ Projets (liste statique)
├─ Contact (formulaire simple)
└─ Templates + fichiers statiques

Projet 2 : SITE VITRINE
├─ Plusieurs pages
├─ Navigation
├─ Images et CSS
└─ Déploiement basique


NIVEAU 2 : INTERMÉDIAIRE (Après Partie 2)
──────────────────────────────────────────

Projet 3 : BLOG PERSONNEL
├─ Base de données (Posts, Users)
├─ CRUD complet sur posts
├─ Formulaires (création, édition)
├─ Commentaires (relation)
└─ Tags et catégories

Projet 4 : TODO APP
├─ Authentification utilisateurs
├─ CRUD tâches
├─ Filtres (terminées, en cours)
├─ Migrations DB
└─ Tests unitaires


NIVEAU 3 : AVANCÉ (Après Partie 3)
───────────────────────────────────

Projet 5 : RÉSEAU SOCIAL SIMPLE
├─ Inscription/Connexion
├─ Profils utilisateurs
├─ Posts et likes
├─ Followers/Following (many-to-many)
├─ Upload photos
├─ API REST pour mobile
└─ Blueprints (auth, posts, users)

Projet 6 : E-COMMERCE
├─ Produits et catégories
├─ Panier d'achat (session)
├─ Commandes (relations complexes)
├─ Paiement (Stripe API)
├─ Admin panel
└─ Email confirmations


NIVEAU 4 : EXPERT (Après Partie 4)
───────────────────────────────────

Projet 7 : APPLICATION COMPLÈTE EN PRODUCTION
├─ Toutes features précédentes
├─ Tests complets (>80% coverage)
├─ Sécurité renforcée
├─ Performance optimisée
├─ Caching Redis
├─ Monitoring Sentry
├─ CI/CD GitHub Actions
├─ Déploiement production
└─ Documentation complète
"""


# ============================================================================
# [GUIDE] RESSOURCES COMPLÉMENTAIRES
# ============================================================================

"""
DOCUMENTATION OFFICIELLE
────────────────────────
Flask               : https://flask.palletsprojects.com/
Jinja2              : https://jinja.palletsprojects.com/
SQLAlchemy          : https://www.sqlalchemy.org/
WTForms             : https://wtforms.readthedocs.io/
Flask-Login         : https://flask-login.readthedocs.io/
Flask-SQLAlchemy    : https://flask-sqlalchemy.palletsprojects.com/
Flask-Migrate       : https://flask-migrate.readthedocs.io/


TUTORIELS RECOMMANDÉS
─────────────────────
Flask Mega-Tutorial : https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
Real Python Flask   : https://realpython.com/tutorials/flask/
Corey Schafer       : https://www.youtube.com/c/Coreyms (YouTube)


COMMUNAUTÉ
──────────
Reddit              : r/flask
Discord             : Flask Community Server
Stack Overflow      : [flask] tag
GitHub Discussions  : https://github.com/pallets/flask/discussions


OUTILS ET EXTENSIONS
────────────────────
Extensions List     : https://github.com/humiaozuzu/awesome-flask
Flask Snippets      : https://flask.palletsprojects.com/en/stable/patterns/
Cookiecutter Flask  : https://github.com/cookiecutter-flask/cookiecutter-flask


LIVRES
──────
"Flask Web Development" by Miguel Grinberg
"Mastering Flask Web Development" by Jack Stouffer  
"""


# ============================================================================
# [IDEE] CONSEILS FINAUX
# ============================================================================

"""
[OBJECTIF] POUR RÉUSSIR AVEC FLASK

1. PRATIQUEZ RÉGULIÈREMENT
   -> 1-2h par jour mieux que 10h le weekend
   -> Codez TOUS les exemples
   -> Ne copiez-collez pas, tapez !

2. CRÉEZ DES PROJETS
   -> Application personnelle dès le début
   -> Itérez et améliorez
   -> Partagez sur GitHub

3. LISEZ LE CODE DES AUTRES
   -> GitHub projets Flask
   -> Extensions Flask (code source)
   -> Projets open-source

4. REJOIGNEZ LA COMMUNAUTÉ
   -> Posez des questions (intelligemment)
   -> Aidez les débutants
   -> Contribuez à l'open-source

5. RESTEZ À JOUR
   -> Changelog Flask
   -> Blog Miguel Grinberg
   -> Newsletter Python

6. NE VOUS DÉCOURAGEZ PAS
   -> Normal de bloquer
   -> Erreurs = apprentissage
   -> Persévérez !


* VOUS ÊTES PRÊT !

Ce guide contient TOUT ce dont vous avez besoin pour maîtriser Flask.
Avec 11,000+ lignes de documentation, exemples et exercices,
vous n'aurez PLUS BESOIN de chercher ailleurs.

Commencez maintenant et devenez un expert Flask ! [RAPIDE]


═══════════════════════════════════════════════════════════════════════════
[EMAIL] FEEDBACK ET CONTRIBUTIONS

Ce guide est vivant et peut être amélioré.
Si vous trouvez des erreurs ou avez des suggestions,
n'hésitez pas à contribuer !

Bon apprentissage et bon code ! [CODE]*
═══════════════════════════════════════════════════════════════════════════
"""
