0% found this document useful (0 votes)
3 views229 pages

Programmation Python Flask

This document outlines a course on web programming with Flask for second-year GL students at the Institut Supérieur d'Informatique for the 2025/2026 academic year. It covers objectives such as understanding web frameworks, creating complete applications, and deploying them professionally, along with a structured course plan. The document also includes technical details about Flask, its ecosystem, and practical steps for setting up a project and routing in Flask.

Uploaded by

nothing
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views229 pages

Programmation Python Flask

This document outlines a course on web programming with Flask for second-year GL students at the Institut Supérieur d'Informatique for the 2025/2026 academic year. It covers objectives such as understanding web frameworks, creating complete applications, and deploying them professionally, along with a structured course plan. The document also includes technical details about Flask, its ecosystem, and practical steps for setting up a project and routing in Flask.

Uploaded by

nothing
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Programmation

Web avec Flask

Licence 2 : GL
M. DIOP ElHadji Mar
Institut Sup rieur d'Informatique — Ann e 2025/2026
é
é
Objectifs du cours
● Comprendre le r le d'un framework web et celui de Flask en particulier
● Cr er une application web compl te avec Python et Flask
● Ma triser le routage, les templates Jinja2 et les fichiers statiques
● Organiser un projet Flask de mani re modulaire (Blueprints, POO)
● Manipuler une base de donn es PostgreSQL avec SQLAlchemy et Flask-Migrate
● G rer les formulaires, la session utilisateur et l'authentification
● Appliquer les bonnes pratiques de s curit (CSRF, hash, en-t tes…)
● D ployer une application Flask de mani re professionnelle

2
é
é
é
î
ô
é
è
è
é
è
é
ê
Plan du cours
● Partie 1 : Introduction et bases (Flask, Jinja2, fichiers statiques)
● Partie 2 : POO avanc e avec Flask (Blueprints, mod les, repositories)
● Partie 3 : Persistance avec PostgreSQL (SQLAlchemy, migrations, requ tes)
● Partie 4 : Formulaires et authentification (Flask-WTF, Flask-Login)
● Partie 5 : Bonnes pratiques et d ploiement
● Annexes : r f rentiel de commandes, glossaire, pour aller plus loin

3
é
é
é
é
è
ê
PARTIE 1

Introduction et bases
Chapitre 1

Introduction Flask
à
Objectifs du chapitre
● Comprendre le r le d'un framework web c t serveur
● D couvrir Flask et son positionnement dans l' cosyst me Python
● Comparer Flask avec Django et choisir le bon outil selon le projet
● Identifier les composants fondamentaux : Werkzeug, Jinja2, WSGI

7
é
ô
ô
é
é
è
1.1 Qu'est-ce qu'un framework web ?
● Un framework web est un ensemble coh rent de biblioth ques et de conventions qui automatisent
les t ches r p titives du d veloppement d'applications HTTP.
● Plut t que de r inventer la roue (routage, requ tes, rendu HTML, sessions...), le d veloppeur
s'appuie sur des briques prouv es.
● On distingue deux familles :
● Frameworks « full-stack » : tout-en-un, architecture impos e (Django).
● Micro-frameworks : noyau minimal, composition libre (Flask).
● Principe d'inversion de contr le : c'est le framework qui appelle votre code, et non l'inverse.

8
ô
â
é
é
é
é
é
ô
é
é
ê
è
é
é
1.2 Pr sentation de Flask
● Flask est un micro-framework web crit en Python, cr par Armin Ronacher en 2010.
● Il s'appuie sur deux briques fondamentales :
● Werkzeug : biblioth que WSGI fournissant routage, Request/Response, serveur de dev.
● Jinja2 : moteur de templates pour g n rer du HTML dynamique.
● L'adjectif « micro » d signe un noyau minimal — non un framework limit .
● Id al pour : APIs REST, prototypes, microservices, applications de taille moyenne.

9
é
é
é
è
é
é
é
é
é
é
Caract ristiques principales
● L ger et minimaliste : moins de 1 000 lignes de code dans le module principal
● Extensible via un cosyst me mature : Flask-SQLAlchemy, Flask-Login, Flask-WTF, Flask-Migrate…
● Compatible WSGI : d ployable sur Gunicorn, uWSGI, mod_wsgi
● Documentation officielle de tr s grande qualit
● Adopt en production par Pinterest, LinkedIn Learning, Netflix (microservices)

10
é
é
é
é
é
è
è
é
1.3 Flask vs Django — comparaison
Crit re Flask Django
Type Micro-framework Framework full-stack
Philosophie Libert , composition Convention sur configuration
ORM Optionnel (SQLAlchemy via ext.) ORM int gr
Admin Aucun ( coder ou via ext.) Admin riche int gr
Authentification Via Flask-Login Syst me int gr
Courbe d'apprentissage Douce Plus raide
Id al pour API, microservices, prototypes CMS, e-commerce, full-stack
Taille du noyau ~ 1 000 lignes ~ 100 000 lignes

11
é
è
è
é
à
é
é
é
é
é
é
Notre choix p dagogique
● Flask est particuli rement adapt l'apprentissage : on voit ce que l'on fait, on construit pierre par
pierre.
● Une fois Flask ma tris , l'apprentissage de Django ou de FastAPI devient bien plus naturel.
● Ce cours adopte une approche progressive : du « Hello World » l'application compl te d ploy e.

12
î
è
é
é
é
à
à
è
é
é
1.4 L' cosyst me Flask
● WSGI (PEP 3333) : standard Python d finissant la communication serveur ↔ application.
● Werkzeug : impl mente WSGI, fournit Request, Response, routage, erreurs HTTP.
● Jinja2 : moteur de templates puissant et s curis .
● Click : cr ation de commandes CLI personnalis es (flask run, flask db…).
● itsdangerous : signature s curis e des cookies de session.

13
é
é
é
è
é
é
é
é
é
é
Architecture conceptuelle

14
Chapitre 2

Installation et premier projet


Objectifs du chapitre
● Installer Python 3 et v rifier sa version
● Cr er un environnement virtuel isol avec venv
● Installer Flask et lancer un serveur de d veloppement
● Mettre en place une structure de projet professionnelle

16
é
é
é
é
2.1 Pr requis
● Python 3.10 ou sup rieur
● pip, le gestionnaire de paquets Python (livr avec Python ≥ 3.4)
● Un diteur de code : Visual Studio Code ou PyCharm Community
● Un terminal : Bash sous Linux/macOS, PowerShell ou Git Bash sous Windows

17
é
é
é
é
V rifier l'installation Python

# V rifier la version de Python


python3 --version
Python 3.11.4

# V rifier pip
pip3 --version
pip 23.2.1

18
é
é
é
2.2 L'environnement virtuel : pourquoi ?
● Un environnement virtuel est un r pertoire isol contenant son propre interpr teur Python et ses
paquets.
● Chaque application a ses propres versions de Flask, SQLAlchemy, etc. — pas de conflit.
● On ne pollue jamais l'installation syst me de Python.
● R gle d'or : on ne fait JAMAIS pip install sans avoir activ un environnement virtuel.

19
è
é
è
é
é
é
Cr ation d'un environnement virtuel
Linux / macOS Windows (PowerShell)

# 1) Cr er un dossier projet et y entrer # 1) Cr er un dossier projet et y entrer


mkdir monblog && cd monblog mkdir monblog ; cd monblog

# 2) Cr er le venv (sous-dossier .venv) # 2) Cr er le venv


python3 -m venv .venv python -m venv .venv

# 3) Activer l'environnement # 3) Activer l'environnement


source .venv/bin/activate .venv\Scripts\Activate.ps1

# 4) ( tout moment) d sactiver # 4) D sactiver


deactivate deactivate

20
é
À
é
é
é
é
é
é
2.3 Installation de Flask
Installation et v rification :
Astuce
(.venv) $ pip install Flask
Figez syst matiquement les d pendances apr s
Collecting Flask
chaque installation :
Downloading [Link] (101 kB)
pip freeze > [Link]
Collecting Werkzeug>=3.1
Collecting Jinja2>=3.1.2
Successfully installed Flask-3.1.0 ... Un coll gue (ou votre futur vous) pourra recr er
l'environnement avec :
(.venv) $ pip show Flask pip install -r [Link]
Name: Flask
Version: 3.1.0

(.venv) $ flask --version


Python 3.11.4
Flask 3.1.0
Werkzeug 3.1.3

21
è
é
é
é
é
è
2.4 Premi re application : Hello, ISI !
[Link]
Trois tapes :
# [Link]
① Importer la classe Flask et l'instancier
from flask import Flask
② D corer une fonction avec @[Link]("...")
# 1) Cr ation de l'objet application ③ Lancer le serveur avec [Link](debug=True)
app = Flask(__name__)
debug=True → rechargement automatique + page
# 2) D finition d'une route d'erreur interactive.
@[Link]("/")
def accueil():
return "Bonjour, ISI ! Bienvenue sur ma premi re application Flask."

# 3) Lancement du serveur en mode debug


if __name__ == "__main__":
[Link](debug=True)

22
é
é
é
é
è
è
Lancer l'application
M thode 1 — ex cution directe : R sultat dans le navigateur :

(.venv) $ python [Link] ←C [Link]:5000


* Serving Flask app 'app'
* Debug mode: on Bonjour, ISI ! Bienvenue sur ma
premi re application Flask.
* Running on [Link]
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
Logs c t serveur :

[Link] - - [04/May/2026 10:42:11]


M thode 2 — via la commande flask (recommand e) : "GET / HTTP/1.1" 200 -
$ export FLASK_APP=[Link] # Linux / macOS
$ export FLASK_DEBUG=1
$ flask run

PS> $env:FLASK_APP = "[Link]" # Windows


PS> $env:FLASK_DEBUG = 1
PS> flask run

23
é
é
é
è
ô
é
é
é
2.5 Structure de projet recommand e
Arborescence professionnelle :

monblog/
├── .venv/ # environnement virtuel (jamais versionné)
├── app/ # paquet de l'application
│ ├── __init__.py # création de l'app (factory)
│ ├── models/ # modèles métier et SQLAlchemy
│ ├── views/ # blueprints (routes)
│ ├── forms/ # formulaires Flask-WTF
│ ├── templates/ # gabarits Jinja2 (HTML)
│ └── static/ # CSS, JS, images
├── migrations/ # généré par Flask-Migrate
├── tests/ # tests unitaires et d'intégration
├── con [Link] # classes de con guration
├── [Link] # dépendances gées
├── .env # variables sensibles (jamais versionné)
├── .gitignore
└── [Link] # point d'entrée

24
fi
fi
fi
é
.gitignore — mettre absolument
● .venv/
● __pycache__/
● *.pyc
● .env
● instance/
● *.sqlite
● Ne JAMAIS commiter de mots de passe, de cl s secr tes ou d'environnement virtuel.

25
à
é
è
Chapitre 3

Routage et URLs dynamiques


Objectifs du chapitre
● D finir des routes statiques et dynamiques
● Utiliser les convertisseurs (string, int, float, path, uuid)
● G rer plusieurs m thodes HTTP (GET, POST, PUT, DELETE)
● Construire des URLs avec url_for() pour ne jamais coder en dur les chemins

27
é
é
é
3.1 Le concept de route
● Une route est une association entre une URL et une fonction Python.
● Quand le navigateur effectue une requ te, Flask appelle la fonction associ e et renvoie au
navigateur ce qu'elle retourne.
● C'est le d corateur @[Link] qui r alise cette association.

28
é
é
ê
é
Routes simples
@[Link]("/")
def accueil():
return "Page d'accueil"

@[Link]("/contact")
def contact():
return "Page de contact"

@[Link]("/a-propos")
def a_propos():
return " propos de l'application"

Une fonction = un endpoint. Le nom de la fonction sert d'identifiant pour url_for().

29
À
3.2 Routes dynamiques
Une route peut contenir des parties variables, d clar es avec des chevrons : <variable>. Flask transmet la valeur
captur e comme argument la vue.

@[Link]("/utilisateur/<nom>") ←C /utilisateur/aminata
def profil(nom): Profil de aminata

return f"Profil de {nom}"


←C /utilisateur/Mamadou
# /utilisateur/aminata → "Profil de aminata" Profil de Mamadou

# /utilisateur/Mamadou → "Profil de Mamadou"

Le nom du param tre dans l'URL doit correspondre exactement au nom de l'argument de la fonction.

30
é
è
à
é
é
3.3 Convertisseurs
Convertisseur Type Python Exemple d'URL Description
string (d faut) str /article/python Toute cha ne sans /
int int /article/42 Entier positif
float float /produit/19.99 Nombre d cimal
path str /fichier/a/sous/[Link] Comme string, accepte le /
uuid [Link] /session/8c3a… Identifiant UUID

31
é
î
é
Exemples de convertisseurs
@[Link]("/article/<int:id_article>")
def voir_article(id_article):
# id_article est d j un int, pas besoin de int(...)
return f"Lecture de l'article num ro {id_article}"

@[Link]("/produit/<float:prix>")
def afficher_prix(prix):
return f"Prix HT : {prix} €, TTC : {prix * 1.18:.2f} €"

@[Link]("/fichier/<path:chemin>")
def voir_fichier(chemin):
return f"Vous demandez : {chemin}"
Erreur 404 : si la valeur ne correspond pas au type du convertisseur (ex. /article/abc avec int), Flask renvoie automatiquement 404 Not Found.

32
é
à
é
3.4 M thodes HTTP
Par d faut, une route ne r pond qu'aux requ tes GET. Pour autoriser d'autres m thodes, on pr cise methods=[...].

from flask import request Convention REST


GET → lire (idempotent)
POST → cr er
@[Link]("/connexion", methods=["GET", "POST"]) PUT/PATCH → modifier
def connexion(): DELETE → supprimer
if [Link] == "POST":
email = [Link]("email")
return f"Tentative de connexion pour {email}"
return """
<form method='post'>
<input name='email' type='email' required>
<input name='mdp' type='password' required>
<button>Se connecter</button>
</form>
"""
33
é
é
é
é
ê
é
é
3.5 url_for() — b tir des URL sans les coder en dur
Coder en dur les URL dans le HTML est une mauvaise pratique : si on change la route, tous les liens cassent.
url_for() g n re l'URL partir du nom de la fonction de vue.

from flask import url_for

@[Link]("/")
def accueil():
lien = url_for("profil", nom="aminata")
return f"<a href='{lien}'>Profil d'Aminata</a>"

@[Link]("/utilisateur/<nom>")
def profil(nom):
return f"Profil de {nom}"

Encore plus pratique dans les templates Jinja2 — plus jamais d'URL en dur dans votre HTML !
34
é
è
à
â
Chapitre 4

Templates Jinja2
Objectifs du chapitre
● S parer la pr sentation (HTML) de la logique (Python)
● Utiliser variables, structures de contr le et filtres dans Jinja2
● Mettre en place l'h ritage de templates avec extends et block
● Ma triser url_for() c t template et le rendu de listes/objets

36
é
î
é
é
ô
é
ô
4.1 Pourquoi des templates ?
● Renvoyer du HTML brut depuis les vues devient vite impraticable.
● Un moteur de templates permet d' crire des fichiers HTML quasi-classiques dans lesquels on
ins re des balises sp ciales pour afficher des variables ou int grer de la logique.
● Jinja2 est le moteur int gr Flask — pas besoin de l'installer s par ment.

37
è
é
é
é
à
é
é
é
é
4.2 Le dossier templates et render_template
Structure : [Link]

monblog/ from flask import Flask, render_template


├── [Link] app = Flask(__name__)

└── templates/ @[Link]("/")


├── [Link] def accueil():
return render_template(
├── [Link] "[Link]",
└── pro [Link] titre="MonBlog",
prenom="Aminata"
templates/[Link] )

{# templates/[Link] #}
<!doctype html>
<html lang="fr">
<head><title>{{ titre }}</title></head>
<body>
<h1>Bienvenue sur {{ titre }} !</h1>
<p>Bonjour, {{ prenom }}.</p>
</body>
</html>
38
fi
4.3 Syntaxe Jinja2 — trois balises
● {{ ... }} : affiche une expression (variable, calcul…).
● {% ... %} : structure de contr le (if, for, block, extends…).
● {# ... #} : commentaire (n'appara t pas dans le HTML produit).

39
ô
î
Conditions et boucles dans Jinja2
Condition : Boucle :

{% if utilisateur %} <ul>
{% for article in articles %}
<p>Bonjour, {{ [Link] }} !</p> <li>
{% else %} <a href="{{ url_for('voir_article', id=[Link]) }}">
<p>Vous n' tes pas connect .</p> {{ [Link] }}
</a>
{% endif %}
— par {{ [Link] }}
</li>
{% else %}
<li>Aucun article publi pour le moment.</li>
{% endfor %}
</ul>

Astuce m connue : le bloc {% else %} d'un {% for %} s'ex cute si la liste est vide. Tr s utile pour afficher « Aucun l ment ».

40
é
ê
é
é
é
è
é
é
4.4 Filtres Jinja2
Les filtres transforment une valeur. On les applique avec le caract re | (pipe).

Filtre Description Exemple


upper / lower Majuscules / minuscules {{ nom|upper }}
title Capitaliser chaque mot {{ titre|title }}
length Longueur d'une s quence {{ articles|length }}
default Valeur par d faut si vide {{ surnom|default('Anonyme') }}
round(n) Arrondi {{ note|round(2) }}
safe D sactive l' chappement HTML {{ contenu|safe }}
truncate(n) Tronquer {{ resume|truncate(120) }}
join(sep) Joindre une liste {{ tags|join(', ') }}

41
é
é
é
é
è
chappement HTML automatique
● Jinja2 chappe par d faut les caract res dangereux : <, >, &,
● C'est une protection contre les attaques XSS (Cross-Site Scripting).
● N'utilisez |safe que sur du contenu dont vous tes absolument certain de l'origine.

42
É
é
é
è
ê
4.5 H ritage — le template parent
templates/[Link]

{# templates/[Link] #}
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{% block titre %}MonBlog{% endblock %}</title>
<link rel="stylesheet"
href="{{ url_for('static', filename='css/[Link]') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{{ url_for('accueil') }}">Accueil</a>
<a href="{{ url_for('liste_articles') }}">Articles</a>
<a href="{{ url_for('contact') }}">Contact</a>
</nav>
</header>
<main>
{% block contenu %}{% endblock %}
</main>
<footer>&copy; 2026 MonBlog — ISI</footer>
</body>
</html>

43
é
H ritage — le template enfant
templates/[Link]
retenir
{% extends "[Link]" %} extends doit tre la premi re instruction du
fichier.
Un block enfant peut appeler {{ super() }}
{% block titre %}Accueil — MonBlog{% endblock %} pour conserver et enrichir le contenu du
parent.

{% block contenu %}
<h1>Bienvenue sur MonBlog</h1>
<p>Le blog technique des tudiants L2 GL de l'ISI.</p>
{% endblock %}

44
À
é
ê
è
é
4.6 Inclusion de fragments
Pour factoriser du code r p t (carte d'article, formulaire de recherche…), on utilise {% include %} :

templates/_carte_article.html templates/[Link]

<article class="carte"> {% for article in articles %}


<h3>{{ [Link] }}</h3> {% include '_carte_article.html' %}
<p>{{ [Link]|truncate(120) }}</ {% endfor %}
p>
</article>

Convention : pr fixer d'un underscore les fragments destin s tre inclus (_carte_article.html, _formulaire.html…).

45
é
é
é
é
é
à
ê
Chapitre 5

Fichiers statiques
Objectifs du chapitre
● Comprendre le r le du dossier static
● R f rencer correctement les ressources avec url_for('static', filename=...)
● Organiser ses sous-dossiers (css/, js/, img/)

47
é
é
ô
5.1 Le dossier static
Comme pour les templates, Flask cherche par d faut un dossier static c t du module de l'application. Tout fichier qui
s'y trouve est servi l'URL /static/....

monblog/

├── [Link]

├── templates/

│ └── [Link]

└── static/

├── css/

│ └── [Link]

├── js/

│ └── [Link]

└── img/

└── [Link]

48
à
é
à
ô
é
5.2 Inclure les fichiers statiques
Plut t que d' crire en dur href="/static/css/[Link]", on utilise url_for — le code est plus robuste si la configuration de l'URL
des statiques change.

<link rel="stylesheet"
href="{{ url_for('static', filename='css/[Link]') }}">

<script src="{{ url_for('static', filename='js/[Link]') }}" defer></script>

<img src="{{ url_for('static', filename='img/[Link]') }}" alt="Logo ISI" width="120">

49
ô
é
5.3 Exemple de feuille de style minimale
static/css/[Link]

:root {
--bleu: #0b5394;
--orange: #e69138;
--texte: #222;
--fond: #fafafa;
}
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
margin: 0; background: var(--fond); color: var(--texte);
}
header { background: var(--bleu); padding: 1rem; }
header a { color: #fff; margin-right: 1rem; text-decoration: none; }
main { max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
.carte {
background: #fff; border-left: 4px solid var(--orange);
padding: 1rem; margin-bottom: 1rem; border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,.08);
}
50
Performance et production
● En production : on minifie CSS/JS, on les regroupe, on les sert via un CDN ou un reverse proxy
(Nginx) avec un cache long.
● En d veloppement : on garde tout en clair pour faciliter le d bogage.
● Le serveur de dev de Flask n'est pas optimis pour les statiques — c'est Nginx qui doit s'en charger
en production.

51
é
é
é
Travaux pratiques

Partie 1
TP 1.1 — Mise en place et premier serveur
● Cr er un dossier monblog, y cr er un environnement virtuel, l'activer
● Installer Flask et figer les d pendances dans [Link]
● crire une application Flask renvoyant « Bienvenue l'ISI » sur la route /
● Lancer en mode debug et v rifier que la modification du message recharge automatiquement la
page

53
É
é
é
é
é
à
TP 1.2 — Routes et convertisseurs
● Ajouter une route /salut/<nom> qui affiche « Bonjour {nom} »
● Ajouter une route /carre/<int:n> qui affiche le carr de n
● Ajouter une route /tva/<float:ht> qui affiche le prix TTC 18 % de TVA, arrondi 2 d cimales
● Tester que /carre/abc renvoie bien une 404

54
é
à
à
é
TP 1.3 — Templates Jinja2
● Cr er un dossier templates et un gabarit [Link] avec en-t te, pied de page et zone de contenu
● Cr er [Link] h ritant de [Link]. La route / devra utiliser render_template
● Pr parer dans une vue une liste d'articles fictifs (titre, auteur, r sum ) et les afficher sous forme de
cartes via une boucle for et un template _carte_article.html inclus
● Ajouter un filtre |truncate(80) sur le r sum

55
é
é
é
é
é
é
ê
é
é
TP 1.4 — Fichiers statiques
● Cr er static/css/[Link] et l'inclure dans [Link] via url_for
● Ajouter un logo en static/img/ et l'afficher dans l'en-t te
● V rifier dans l'onglet R seau du navigateur que les ressources sont bien servies en 200 OK

56
é
é
é
ê
Crit res d' valuation de la Partie 1
● Le code respecte la PEP 8 (indentation, nommage snake_case)
● Aucune URL n'est cod e en dur dans les templates : tout passe par url_for
● Le fichier [Link] et un .gitignore correct sont pr sents
● Le projet est versionn avec Git (au moins 3 commits significatifs)

57
è
é
é
é
é
PARTIE 2

POO avanc e avec Flask


é
Chapitre 6

Application factory et Blueprints


Objectifs du chapitre
● Comprendre les limites du fichier [Link] monolithique
● Mettre en place le pattern « application factory »
● Structurer le code en modules gr ce aux Blueprints
● Pr parer le projet accueillir les mod les, formulaires et services

60
é
à
â
è
6.1 Pourquoi sortir d'[Link] ?
● Tant que l'application reste une page de code, [Link] suffit
● Mais d s que l'on d passe une dizaine de routes :
● Importations circulaires
● Code peu lisible
● Difficult tester
● Configuration m lang e la logique
● Deux outils Flask permettent de structurer proprement : la factory pattern et les Blueprints

61
è
é
à
é
é
é
à
6.2 La factory pattern
● Id e : au lieu de cr er l'objet Flask en variable globale, on l'instancie l'int rieur d'une fonction
create_app(config)
● Avantages :
● Plusieurs instances peuvent coexister (utile pour les tests)
● La configuration (dev/prod/test) est pass e en argument
● Les extensions sont initialis es de façon explicite et testable

62
é
é
é
é
à
é
app/__init__.py — create_app()
from flask import Flask

def create_app(nom_config="dev"):
app = Flask(__name__, instance_relative_config=True)

# 1) Charger la configuration
if nom_config == "prod":
[Link].from_object("[Link]")
elif nom_config == "test":
[Link].from_object("[Link]")
else:
[Link].from_object("[Link]")

# 2) Initialiser les extensions


# from .extensions import db, login_manager
# db.init_app(app)
# login_manager.init_app(app)

# 3) Enregistrer les Blueprints


from .[Link] import bp_main
from .[Link] import bp_articles
app.register_blueprint(bp_main)
app.register_blueprint(bp_articles, url_prefix="/articles")

# 4) Hooks d'erreurs
from .errors import enregistrer_handlers
enregistrer_handlers(app)

return app

63
[Link] — point d'entr e

import os
from app import create_app

app = create_app([Link]("FLASK_CONFIG", "dev"))

if __name__ == "__main__":
[Link]()

$ export FLASK_APP=run:app
$ export FLASK_CONFIG=dev
$ flask run

64
é
6.3 Les Blueprints
● Un Blueprint est un « mini-Flask » : un objet qui regroupe routes, templates, statiques et erreurs.
● On enregistre ensuite le Blueprint sur l'application — avec ou sans pr fixe d'URL.
● C'est le m canisme officiel pour d couper l'application en modules m tier.

65
é
é
é
é
Blueprint « main »
app/views/[Link]

from flask import Blueprint, render_template

bp_main = Blueprint("main", __name__)

@bp_main.route("/")
def accueil():
return render_template("main/[Link]")

@bp_main.route("/contact")
def contact():
return render_template("main/[Link]")

R f rencer une URL d'un Blueprint : url_for("[Link]") — on pr fixe le nom de l'endpoint par le nom du Blueprint.

66
é
é
é
Blueprint « articles »
app/views/[Link]

from flask import Blueprint, render_template, abort

bp_articles = Blueprint("articles", __name__,


template_folder="../templates")

@bp_articles.route("/")
def liste():
articles = [] # remplir plus tard depuis la BDD
return render_template("articles/[Link]", articles=articles)

@bp_articles.route("/<int:id>")
def detail(id):
if id <= 0:
abort(404)
return render_template("articles/[Link]", id=id)

67
à
6.4 Arborescence cible — MonBlog
monblog/
├── app/
│ ├── **init**.py # create_app()
│ ├── [Link] # db, login_manager, migrate, csrf
│ ├── [Link] # gestion d'erreurs globale
│ ├── models/
│ │ ├── [Link] # BaseModel abstraite
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ ├── views/ # blueprints
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ ├── forms/ # formulaires Flask-WTF
│ ├── services/ # logique métier
│ ├── [Link] # exceptions personnalisées
│ ├── templates/ # gabarits Jinja2
│ └── static/
├── con [Link]
├── [Link]
└── tests/

68
fi
Chapitre 7

Classes mod les m tier


è
é
Objectifs du chapitre
● Concevoir des classes Python repr sentant les entit s m tier
● Distinguer mod le m tier et mod le de persistance
● Mettre en œuvre encapsulation et responsabilit s claires

70
è
é
è
é
é
é
é
7.1 Pourquoi des classes ?
● Une application web manipule en permanence des objets : utilisateurs, articles, commentaires,
commandes...
● Les repr senter par des classes Python apporte :
● Un type plut t qu'un dictionnaire flou
● Des m thodes pour le comportement m tier (publier(), valider_age()…)
● Une validation centralis e
● Un code lisible et auto-document
● Note : terme nos mod les h riteront de [Link] (SQLAlchemy). Mais on peut les concevoir
d'abord en pur Python pour bien s parer m tier et persistance.

71
é
é
à
ô
é
è
é
é
é
é
é
7.2 Premi re classe : User
from datetime import datetime
from [Link] import generate_password_hash, check_password_hash

class User:
"""Repr sente un utilisateur du blog."""

def __init__(self, email: str, prenom: str, nom: str):


[Link] = email
[Link] = prenom
[Link] = nom
self.cree_le = [Link]()
self._mdp_hash = None # encapsulation

# --------------------- Mots de passe ---------------------


def definir_mdp(self, mdp_clair: str) -> None:
self._mdp_hash = generate_password_hash(mdp_clair)

def verifier_mdp(self, mdp_clair: str) -> bool:


return check_password_hash(self._mdp_hash, mdp_clair)

@property
def nom_complet(self) -> str:
return f"{[Link]} {[Link]}".strip()

def __repr__(self) -> str:


return f"<User {[Link]}>"
72
é
è
Encapsulation et conventions
● Le caract re _ devant _mdp_hash signale par convention un attribut « priv ».
● Python ne l'interdit pas techniquement — mais cela documente l'intention.
● On n'acc de pas au hash directement : on passe par definir_mdp() et verifier_mdp().
● @property permet d'exposer une lecture calcul e comme un attribut (user.nom_complet).

73
è
è
é
é
7.3 Article et Commentaire
# app/models/[Link]
from datetime import datetime # app/models/[Link]
class Article: from datetime import datetime
def __init__(self, titre, contenu, auteur):
[Link] = None
[Link] = [Link]() class Commentaire:
[Link] = contenu def __init__(self, contenu, auteur):
[Link] = auteur [Link] = contenu
[Link] = False [Link] = auteur
self.cree_le = [Link]()
[Link] = None
[Link] = []
def publier(self): self.cree_le = [Link]()
if not [Link]:
raise ValueError("Un article doit avoir un titre.")
[Link] = True Le commentaire pointe vers son article — r f rence circulaire g r e par
def ajouter_commentaire(self, c): Article.ajouter_commentaire().
[Link] = self
[Link](c)
def resume(self, longueur=120):
return ([Link][:longueur] + "…"
if len([Link]) > longueur
else [Link])
74
é
é
é
é
7.4 Utiliser ces classes dans une vue
# app/views/[Link]
from flask import Blueprint, render_template
from ..[Link] import User
from ..[Link] import Article

bp_main = Blueprint("main", __name__)

# Donn es factices pour d monstration


auteur = User("aminata@[Link]", "Aminata", "Diallo")
art1 = Article("D couvrir Flask", "Flask est un micro-framework…", auteur)
[Link]()
art2 = Article("Jinja2 en 5 minutes", "Jinja2 est le moteur…", auteur)
[Link]()
ARTICLES = [art1, art2]

@bp_main.route("/")
def accueil():
return render_template("main/[Link]", articles=ARTICLES)

75
é
é
é
Template associ
{% extends "[Link]" %}
{% block contenu %}
<h1>Derniers articles</h1>
{% for art in articles %}
<article class="carte">
<h3>{{ [Link] }}</h3>
<p>{{ [Link](150) }}</p>
<small>par {{ [Link].nom_complet }}
— {{ art.cree_le.strftime('%d/%m/%Y') }}</small>
</article>
{% endfor %}
{% endblock %}

76
é
Chapitre 8

H ritage, encapsulation,
polymorphisme
é
Objectifs du chapitre
● Mobiliser les piliers de la POO dans le contexte d'une application web
● Concevoir une classe abstraite BaseModel pour mutualiser le comportement
● Mettre en place un Repository r utilisable

78
é
8.1 Une BaseModel pour mutualiser
● De nombreux mod les partagent les m mes besoins :
● Avoir un id, des dates de cr ation/mise jour
● Une m thode to_dict() pour la s rialisation
● Un __repr__ standardis
● Plut t que de r p ter ce code, on l' crit une seule fois dans une classe parente abstraite.

79
ô
é
é
é
è
é
é
é
é
ê
à
BaseModel abstraite
from datetime import datetime ABC (Abstract Base Class)
from abc import ABC, abstractmethod
marque la classe comme abstraite : on ne peut
pas l'instancier directement.
class BaseModel(ABC):
"""Classe abstraite pour tous les mod les m tier."""
@abstractmethod oblige les classes filles
def __init__(self): fournir une impl mentation.
[Link] = None
self.cree_le = [Link]()
self.maj_le = [Link]() Excellent moyen de d finir un contrat partag .

def toucher(self):
self.maj_le = [Link]()

@abstractmethod
def valider(self):
"""L ve une exception si l'objet n'est pas valide."""
...

def to_dict(self):
return {k: v for k, v in self.__dict__.items()
if not [Link]("_")}

def __repr__(self):
return f"<{self.__class__.__name__} id={[Link]}>"

80
è
é
é
à
è
é
é
8.2 Faire h riter User de BaseModel
from .base import BaseModel
from [Link] import generate_password_hash, check_password_hash

class User(BaseModel):
def __init__(self, email, prenom, nom):
super().__init__()
[Link] = email
[Link] = prenom
[Link] = nom
self._mdp_hash = None

def definir_mdp(self, mdp):


self._mdp_hash = generate_password_hash(mdp)
[Link]()

def verifier_mdp(self, mdp):


return check_password_hash(self._mdp_hash, mdp) if self._mdp_hash else False

def valider(self):
if "@" not in [Link]:
raise ValueError("Email invalide.")
if not [Link] or not [Link]:
raise ValueError("Pr nom et nom sont obligatoires.")

@property
def nom_complet(self):
return f"{[Link]} {[Link]}"
81
é
é
8.3 Polymorphisme — un service pour tous les
mod les
Le polymorphisme se r v le utile lorsqu'on veut crire une fonction qui accepte n'importe quel mod le.
Tant que l'objet expose to_dict() et valider(), il est utilisable.

# app/services/[Link]
from typing import Iterable
from ..[Link] import BaseModel

def valider_tous(objets: Iterable[BaseModel]) -> list[str]:


"""Valide une collection d'objets, renvoie la liste des erreurs."""
erreurs = []
for obj in objets:
try:
[Link]()
except ValueError as e:
[Link](f"{obj!r} : {e}")
return erreurs

82
è
é
è
é
è
Chapitre 9

Le pattern Repository
Objectifs du chapitre
● Comprendre l'int r t de s parer les vues de l'acc s aux donn es
● Mettre en place un Repository g n rique
● Pr parer la transition vers une vraie base de donn es

84
é
é
ê
é
é
é
è
é
é
9.1 Pourquoi un Repository ?
● Un Repository est une classe dont la responsabilit est de stocker, retrouver et lister des objets.
● Si demain on passe d'une liste en m moire PostgreSQL, on ne change que le Repository :
● Les vues continuent d'appeler les m mes m thodes : lister(), get(id), enregistrer(obj).
● C'est un excellent terrain d'entra nement la s paration des couches.

85
î
é
ê
à
à
é
é
é
9.2 Repository en m moire ( tape 1)
# app/repositories/base_repository.py
from typing import TypeVar, Generic
T = TypeVar("T")

class BaseRepository(Generic[T]):
def __init__(self):
self._items: dict[int, T] = {}
self._next_id = 1

def enregistrer(self, obj: T) -> T:


if getattr(obj, "id", None) is None:
[Link] = self._next_id
self._next_id += 1
self._items[[Link]] = obj
return obj

def get(self, id: int) -> T | None:


return self._items.get(id)

def lister(self) -> list[T]:


return list(self._items.values())

def supprimer(self, id: int) -> bool:


return self._items.pop(id, None) is not None

86
é
é
Sp cialisation : ArticleRepository
# app/repositories/article_repository.py
from .base_repository import BaseRepository
from ..[Link] import Article

class ArticleRepository(BaseRepository[Article]):
def lister_publies(self) -> list[Article]:
return [a for a in [Link]() if [Link]]

def chercher_par_auteur(self, email: str) -> list[Article]:


return [a for a in [Link]() if [Link] == email]

def chercher_par_mot_cle(self, mot: str) -> list[Article]:


mot = [Link]()
return [a for a in [Link]()
if mot in [Link]() or mot in [Link]()]

87
é
9.3 Utilisation dans une vue
# app/views/[Link]
from flask import Blueprint, render_template, abort
from ..repositories.article_repository import ArticleRepository

bp_articles = Blueprint("articles", __name__)

# Repository unique pour la d monstration


# (sera inject plus tard via [Link])
repo_articles = ArticleRepository()

@bp_articles.route("/")
def liste():
return render_template("articles/[Link]",
articles=repo_articles.lister_publies())

@bp_articles.route("/<int:id>")
def detail(id):
article = repo_articles.get(id)
if article is None:
abort(404)
return render_template("articles/[Link]", article=article)
88
é
é
Chapitre 10

Exceptions personnalis es

é
Objectifs du chapitre
● Concevoir une hi rarchie d'exceptions coh rente
● Distinguer erreurs m tier et erreurs techniques
● Convertir les exceptions en r ponses HTTP appropri es

90
é
é
é
é
é
10.1 Pourquoi des exceptions personnalis es ?
● Lever ValueError ou Exception partout finit par produire un code difficile diagnostiquer
● D finir ses propres exceptions, regroup es par domaine, permet :
● De distinguer rapidement la nature d'un probl me (UtilisateurInexistant ≠ DonneesInvalides)
● De convertir uniform ment ces exceptions en r ponses HTTP (404, 422, 401…)
● D'enrichir les exceptions avec un code, un message, des d tails de logging

91
é
é
é
è
é
é
à
é
10.2 D finition d'une hi rarchie
app/[Link]
class MonBlogError(Exception):
"""Classe de base pour toutes les exceptions m tier."""
code_http = 400
message = "Erreur applicative."

def __init__(self, message=None, **details):


super().__init__(message or [Link])
[Link] = details

class RessourceIntrouvable(MonBlogError):
code_http = 404
message = "Ressource introuvable."

class DonneesInvalides(MonBlogError):
code_http = 422
message = "Donn es invalides."

class AuthentificationRequise(MonBlogError):
code_http = 401
message = "Authentification requise."

class AccesInterdit(MonBlogError):
code_http = 403
message = "Acc s refus ."
92
è
é
é
é
é
é
10.3 Lever et capter les exceptions
# Dans un service ou un repository
from .exceptions import RessourceIntrouvable, DonneesInvalides

def get_article_ou_lever(repo, id):


article = [Link](id)
if article is None:
raise RessourceIntrouvable(f"Article #{id} introuvable.", id=id)
return article

def publier_article(article):
if not [Link]():
raise DonneesInvalides("Le titre est obligatoire.")
[Link]()

93
10.4 Conversion automatique en r ponse HTTP
app/[Link]

from flask import jsonify, render_template


from .exceptions import MonBlogError

def enregistrer_handlers(app):
@[Link](MonBlogError)
def gerer_erreur_metier(err):
if [Link]("API_MODE"):
return jsonify(message=str(err), details=[Link]), err.code_http
return render_template("erreurs/[Link]",
message=str(err),
code=err.code_http), err.code_http

@[Link](404)
def not_found(_):
return render_template("erreurs/[Link]"), 404

@[Link](500)
def server_error(_):
return render_template("erreurs/[Link]"), 500

94
é
Strat gie globale
● Levez des exceptions m tier dans les services et repositories
● Convertissez-les en r ponses HTTP au niveau le plus haut (handlers globaux)
● Vos vues redeviennent simples et expressives
● B n fice : le code des routes ne contient plus que la logique de pr sentation.

95
é
é
é
é
é
é
Travaux pratiques

Partie 2
TP 2.1 — Refactoring vers la factory
● Convertir le projet du TP 1 en application factory : cr er create_app(), un fichier [Link] et un sous-
paquet app/
● D placer les routes existantes dans un Blueprint bp_main
● V rifier que flask --app run:app run fonctionne toujours

97
é
é
é
TP 2.2 — Mod les m tier
● Cr er la classe BaseModel abstraite avec valider() abstrait
● Impl menter User, Article, Commentaire en h ritant de BaseModel
● Ajouter une route /demo qui cr e 3 utilisateurs et 5 articles factices, puis les affiche

98
é
é
è
é
é
é
TP 2.3 — Repository et exceptions
● Impl menter BaseRepository et ArticleRepository (m moire)
● D finir la hi rarchie d'exceptions personnalis e et un handler global
● Faire en sorte qu'acc der /articles/9999 produise une page d'erreur 404 personnalis e via
RessourceIntrouvable

99
é
é
é
é
à
é
é
é
Pour aller plus loin
● Ajoutez un m canisme de filtres sur ArticleRepository : par mot-cl , par p riode
● Utilisez @property pour exposer une propri t calcul e (nombre de commentaires, est_recent…)
● Ajoutez des tests unitaires pour valider() et les Repository

100
é
é
é
é
é
é
PARTIE 3

Persistance avec PostgreSQL


Chapitre 11

PostgreSQL : installation et
premiers pas
Objectifs du chapitre
● Installer PostgreSQL sur Linux, macOS et Windows
● Cr er une base de donn es et un utilisateur d di l'application
● Se connecter avec psql et les outils graphiques

103
é
é
é
é
à
11.1 Pourquoi PostgreSQL ?
● SGBD relationnel libre, robuste et conforme aux standards SQL
● Fonctionnalit s avanc es :
● Transactions ACID
● Types JSON natifs (JSONB)
● Full-text search
● Fonctions et proc dures
● H ritage de tables
● Base de donn es par d faut de nombreuses soci t s et services cloud (Heroku, Render, Railway…)

104
é
é
é
é
é
é
é
é
11.2 Installation
Ubuntu / Debian

$ sudo apt update


$ sudo apt install -y postgresql postgresql-contrib
$ sudo systemctl status postgresql

macOS (Homebrew)

$ brew install postgresql@16


$ brew services start postgresql@16

Windows
T l chargez l'installeur officiel sur [Link]/download/windows. Il installe galement pgAdmin (interface graphique) et configure le service
Windows automatiquement.

105
é
é
é
11.3 Cr ation d'une base et d'un utilisateur
$ sudo -u postgres psql

postgres=# CREATE USER monblog_user WITH PASSWORD 'monMotDePasse';


CREATE ROLE
postgres=# CREATE DATABASE monblog OWNER monblog_user ENCODING 'UTF8';
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE monblog TO monblog_user;
GRANT
postgres=# \q

$ psql -h localhost -U monblog_user -d monblog


Password for user monblog_user:
psql (16.2)
monblog=>

106
é
11.4 URL de connexion SQLAlchemy
● Format g n ral :
● postgresql+psycopg2://USER:MOTDEPASSE@HOTE:PORT/NOM_BDD
● Pour notre projet :
● postgresql+psycopg2://monblog_user:monMotDePasse@localhost:5432/monblog
● Cette URL sera stock e dans .env, jamais en dur dans le code.

107
é
é
é
Chapitre 12

SQLAlchemy et Flask-SQLAlchemy
Objectifs du chapitre
● Comprendre ce qu'est un ORM et son int r t
● Installer Flask-SQLAlchemy et le pilote psycopg2
● Configurer la connexion PostgreSQL via [Link]
● D finir un premier mod le persistant

109
é
è
à
é
ê
12.1 Qu'est-ce qu'un ORM ?
● Un ORM (Object Relational Mapper) fait correspondre des classes Python des tables d'une base de
donn es.
● Avantages :
● S curit : chappe les param tres et vite les injections SQL
● Portabilit : fonctionne sur PostgreSQL, MySQL, SQLite…
● Expressivit : m thodes filter, join, group_by…
● SQLAlchemy est le standard Python ; Flask-SQLAlchemy en est l'int gration officielle pour Flask.

110
é
é
é
é
é
é
é
è
é
é
à
12.2 Installation

(.venv) $ pip install Flask-SQLAlchemy psycopg2-binary


Collecting Flask-SQLAlchemy
Downloading flask_sqlalchemy-[Link]
Collecting SQLAlchemy>=2.0.16
Downloading sqlalchemy-2.0.27...
Collecting psycopg2-binary
Downloading psycopg2_binary-2.9.9...
Successfully installed Flask-SQLAlchemy-3.1.1 psycopg2-binary-2.9.9

(.venv) $ pip freeze > [Link]

111
12.3 Initialisation c t application
app/[Link] app/__init__.py (extrait)

from flask_sqlalchemy import SQLAlchemy from flask import Flask


db = SQLAlchemy() from .extensions import db

def create_app(nom_config="dev"):
app = Flask(__name__)
[Link].from_object(
f"config.{nom_config.capitalize()}Config")

db.init_app(app)

from .[Link] import bp_main


app.register_blueprint(bp_main)
return app

Pattern : on cr e l'instance db sans application, puis on l'attache avec init_app(app) dans la factory.

112
é
ô
é
12.4 Configuration via [Link]
# [Link]
import os

class BaseConfig:
SECRET_KEY = [Link]("SECRET_KEY", "changez-moi-en-prod")
SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevConfig(BaseConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = [Link](
"DEV_DATABASE_URL",
"postgresql+psycopg2://monblog_user:monMotDePasse@localhost:5432/monblog_dev"
)

class TestingConfig(BaseConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = [Link](
"TEST_DATABASE_URL",
"postgresql+psycopg2://monblog_user:monMotDePasse@localhost:5432/monblog_test"
)

class ProdConfig(BaseConfig):
DEBUG = False
SQLALCHEMY_DATABASE_URI = [Link]["DATABASE_URL"]

113
Variables d'environnement et .env
● Stockez les secrets (SECRET_KEY, mots de passe BDD) dans un fichier .env, JAMAIS dans le code
● Le paquet python-dotenv permet de charger .env automatiquement :
● pip install python-dotenv
● V rifiez que .env est bien dans .gitignore — jamais commit !

114
é
é
Chapitre 13

Mod les, colonnes et relations


è
Objectifs du chapitre
● D finir un mod le SQLAlchemy avec colonnes et types
● Cr er des relations OneToMany et ManyToMany
● Personnaliser la classe Model pour partager du comportement

116
é
é
è
13.1 Premier mod le : User
# app/models/[Link]
from datetime import datetime
from [Link] import generate_password_hash, check_password_hash
from ..extensions import db

class User([Link]):
__tablename__ = "users"

id = [Link]([Link], primary_key=True)
email = [Link]([Link](120), unique=True, nullable=False, index=True)
prenom = [Link]([Link](50), nullable=False)
nom = [Link]([Link](50), nullable=False)
mdp_hash = [Link]([Link](255), nullable=False)
actif = [Link]([Link], default=True, nullable=False)
cree_le = [Link]([Link], default=[Link], nullable=False)

# Relation 1-N : un utilisateur a plusieurs articles


articles = [Link](
"Article", back_populates="auteur",
cascade="all, delete-orphan", lazy="dynamic"
)

def definir_mdp(self, mdp):


self.mdp_hash = generate_password_hash(mdp)

def verifier_mdp(self, mdp):


return check_password_hash(self.mdp_hash, mdp)

def __repr__(self):
return f"<User {[Link]}>"
117
è
13.2 Article et la relation OneToMany
# app/models/[Link]
from datetime import datetime
from ..extensions import db

class Article([Link]):
__tablename__ = "articles"

id = [Link]([Link], primary_key=True)
titre = [Link]([Link](200), nullable=False)
contenu = [Link]([Link], nullable=False)
publie = [Link]([Link], default=False, nullable=False)
cree_le = [Link]([Link], default=[Link], nullable=False)
maj_le = [Link]([Link], default=[Link],
onupdate=[Link], nullable=False)

# Cl trang re vers [Link]


auteur_id = [Link]([Link], [Link]("[Link]"), nullable=False)
auteur = [Link]("User", back_populates="articles")

commentaires = [Link](
"Commentaire", back_populates="article",
cascade="all, delete-orphan"
)

def resume(self, n=120):


return ([Link][:n] + "…") if len([Link]) > n else [Link]

118
é
é
è
13.3 Commentaire
# app/models/[Link]
from datetime import datetime
from ..extensions import db

class Commentaire([Link]):
__tablename__ = "commentaires"

id = [Link]([Link], primary_key=True)
contenu = [Link]([Link], nullable=False)
cree_le = [Link]([Link], default=[Link], nullable=False)

article_id = [Link]([Link], [Link]("[Link]"), nullable=False)


auteur_id = [Link]([Link], [Link]("[Link]"), nullable=False)

article = [Link]("Article", back_populates="commentaires")


auteur = [Link]("User")

119
13.4 Relations Many-to-Many : les tags
Un article peut avoir plusieurs tags, et un tag est partag entre plusieurs articles.
C'est une relation N-N qui n cessite une table d'association.

# app/models/[Link]
from ..extensions import db

# Table d'association
articles_tags = [Link](
"articles_tags",
[Link]("article_id", [Link], [Link]("[Link]"), primary_key=True),
[Link]("tag_id", [Link], [Link]("[Link]"), primary_key=True),
)

class Tag([Link]):
__tablename__ = "tags"
id = [Link]([Link], primary_key=True)
libelle = [Link]([Link](40), unique=True, nullable=False)

articles = [Link](
"Article", secondary=articles_tags, back_populates="tags"
)

# Dans Article, ajouter :


# tags = [Link]("Tag", secondary=articles_tags, back_populates="articles")

120
é
é
13.5 Types de colonnes utiles
Type SQLAlchemy Type Python Type SQL
[Link] int INTEGER
[Link] int BIGINT
[Link](n) str VARCHAR(n)
[Link] str TEXT
[Link] bool BOOLEAN
[Link] float DOUBLE PRECISION
[Link](p, s) Decimal NUMERIC(p, s)
[Link] / [Link] date / datetime DATE / TIMESTAMP
[Link] dict / list JSONB (Postgres)
[Link](...) Enum ENUM

121
Sch ma relationnel de MonBlog

122
é
Chapitre 14

Migrations avec Flask-Migrate


(Alembic)
Objectifs du chapitre
● Comprendre la n cessit des migrations
● Installer Flask-Migrate
● G n rer, relire et appliquer une migration
● Reculer en cas de probl me

124
é
é
é
è
é
14.1 Pourquoi des migrations ?
● chaque modification de mod le (ajout de colonne, suppression de table…), la base doit suivre
● db.create_all() est utile pour d marrer mais ne sait pas modifier une base existante
● Les migrations enregistrent chaque changement dans un script num rot :
● On peut avancer, reculer, synchroniser plusieurs environnements
● Le sch ma est versionn comme le code

125
À
é
é
é
è
é
é
14.2 Installation et configuration
(.venv) $ pip install Flask-Migrate
(.venv) $ pip freeze > [Link]

app/[Link] app/__init__.py

from flask_sqlalchemy import SQLAlchemy from .extensions import db, migrate


from flask_migrate import Migrate from .models import user, article, commentaire,
tag
db = SQLAlchemy()
migrate = Migrate()
def create_app(nom_config="dev"):
app = Flask(__name__)
[Link].from_object(...)
db.init_app(app)
migrate.init_app(app, db)
return app

126
14.3 Cycle de vie d'une migration
# 1) Initialiser le dossier migrations/ (une seule fois)
$ flask db init

# 2) G n rer une migration partir des mod les


$ flask db migrate -m "Cr ation des tables initiales"

# 3) Relire le fichier g n r dans migrations/versions/

# 4) Appliquer la migration sur la base


$ flask db upgrade

# 5) En cas de probl me : reculer d'un cran


$ flask db downgrade

# 6) Voir l'historique
$ flask db history
$ flask db current

127
é
é
è
é
é
é
é
à
è
Toujours relire la migration
● Alembic est puissant mais pas omniscient
● Exemple : il ne d tecte pas le renommage d'une colonne (il propose suppression + ajout)
● Relisez SYST MATIQUEMENT le script avant flask db upgrade
● Un script de migration peut tre dit la main pour ajuster les op rations

128
É
é
ê
é
é
à
é
Chapitre 15

Le CRUD : Create, Read, Update,


Delete
Objectifs du chapitre
● Ma triser les op rations de base sur les mod les persistants
● Comprendre le r le de la session ([Link])
● Construire les vues correspondantes pour MonBlog

130
î
é
ô
è
15.1 La session SQLAlchemy
● La session est l'unit de travail
● On y ajoute, modifie ou supprime des objets, puis on commit() pour valider la transaction
● Si une erreur survient, rollback() annule tout
● Flask-SQLAlchemy g re pour nous une session par requ te HTTP

131
é
è
ê
15.2 Create — cr er un objet
from [Link] import db
from [Link] import User

u = User(email="aminata@[Link]", prenom="Aminata", nom="Diallo")


u.definir_mdp("Secret123!")

[Link](u)
[Link]()
print([Link]) # g n r automatiquement par PostgreSQL

add() place l'objet dans la session ; commit() crit en base. Apr s le commit, l'id est rempli.

132
é
é
é
é
é
è
15.3 Read — lire des objets
# Lire par cl primaire (SQLAlchemy 2.x)
u = [Link](User, 1)

# Lire la premi re occurrence selon un filtre


u = [Link].filter_by(email="aminata@[Link]").first()

# Lever 404 si absent (pratique dans une vue)


u = [Link].filter_by(email="x").first_or_404()

# Tous les utilisateurs actifs, tri s par nom


users = ([Link]
.filter_by(actif=True)
.order_by([Link]())
.all())

133
é
è
é
15.4 / 15.5 — Update et Delete
Update Delete

u = [Link](User, 1) u = [Link](User, 1)
[Link] = "Amy" [Link](u)
[Link]()
[Link]() # cascade supprime les articles
# si configur ainsi

La cascade « all, delete-orphan » sur la relation Articles assure que les articles d'un utilisateur supprim sont aussi effac s.

134
é
é
é
15.6 Vue compl te : cr er un article
# app/views/[Link]
from flask import Blueprint, request, redirect, url_for, render_template, flash
from ..extensions import db
from ..[Link] import Article

bp_articles = Blueprint("articles", __name__)

@bp_articles.route("/nouveau", methods=["GET", "POST"])


def creer():
if [Link] == "POST":
titre = [Link]["titre"].strip()
contenu = [Link]["contenu"].strip()
if not titre or not contenu:
flash("Titre et contenu sont obligatoires.", "danger")
return redirect(url_for("[Link]"))

article = Article(titre=titre, contenu=contenu, auteur_id=1)


[Link](article)
[Link]()
flash("Article cr avec succ s.", "success")
return redirect(url_for("[Link]", id=[Link]))

return render_template("articles/[Link]")

135
é
é
è
è
é
Chapitre 16

Requ tes avanc es


ê
é
Objectifs du chapitre
● Utiliser filter, filter_by, like, in_, between
● Joindre des tables avec join()
● Grouper, compter et trier les r sultats
● Paginer une liste pour l'affichage

137
é
16.1 Filtres
from sqlalchemy import or_, and_

# galit simple
[Link].filter_by(publie=True).all()

# Expression riche (LIKE insensible la casse)


[Link]([Link]("%flask%")).all()

# Combiner conditions
[Link](
and_([Link] == True,
or_([Link]("%flask%"),
[Link]("%python%")))
).all()

# IN
[Link]([Link].in_([1, 2, 3])).all()

# BETWEEN
from datetime import datetime
debut = datetime(2026, 1, 1)
fin = datetime(2026, 6, 30)
[Link](Article.cree_le.between(debut, fin)).all()

138
É
é
à
16.2 Jointures
from [Link] import User
from [Link] import Article

# Articles publi s par les utilisateurs actifs, tri s par auteur


q = ([Link](Article)
.join(User, Article.auteur_id == [Link])
.filter([Link] == True, [Link] == True)
.order_by([Link](), Article.cree_le.desc()))

for art in [Link]():


print([Link], "—", [Link])

139
é
é
16.3 Agr gations
from sqlalchemy import func

# Nombre d'articles par utilisateur


stats = ([Link]([Link], [Link]([Link]))
.join(Article, Article.auteur_id == [Link])
.group_by([Link])
.order_by([Link]([Link]).desc())
.all())

for email, n in stats:


print(f"{email}: {n} article(s)")

140
é
16.4 Pagination
@bp_articles.route("/")
def liste():
page = [Link]("page", 1, type=int)
pagination = ([Link]
.filter_by(publie=True)
.order_by(Article.cree_le.desc())
.paginate(page=page, per_page=10, error_out=False))
return render_template("articles/[Link]",
articles=[Link],
pagination=pagination)

{# templates/articles/[Link] (extrait) #}
{% if pagination.has_prev %}
<a href="{{ url_for('[Link]', page=pagination.prev_num) }}">« Pr c dent</a>
{% endif %}
Page {{ [Link] }} / {{ [Link] }}
{% if pagination.has_next %}
<a href="{{ url_for('[Link]', page=pagination.next_num) }}">Suivant »</a>
{% endif %}

141
é
é
Chapitre 17

Configuration multi-
environnements
Objectifs du chapitre
● S parer configuration de d veloppement, de test et de production
● S curiser les secrets via .env
● Initialiser la base au d marrage en mode dev

143
é
é
é
é
17.1 Le fichier .env
.env ( NE PAS commiter)

FLASK_APP=[Link]
FLASK_CONFIG=dev
FLASK_DEBUG=1
SECRET_KEY=ma-cle-tres-secrete-en-production
DEV_DATABASE_URL=postgresql+psycopg2://monblog_user:pass@localhost:5432/monblog_dev
TEST_DATABASE_URL=postgresql+psycopg2://monblog_user:pass@localhost:5432/monblog_test

(.venv) $ pip install python-dotenv


(.venv) $ echo ".env" >> .gitignore

# [Link]
import os
from dotenv import load_dotenv
load_dotenv()
from app import create_app
app = create_app([Link]("FLASK_CONFIG", "dev"))
144
à
17.2 Commande personnalis e pour initialiser la BDD
app/[Link]

import click
from [Link] import with_appcontext
from .extensions import db
from .[Link] import User

@[Link]("init-db")
@with_appcontext
def init_db():
"""Cr e les tables et un utilisateur admin de d monstration."""
db.create_all()
if not [Link].filter_by(email="admin@[Link]").first():
admin = User(email="admin@[Link]", prenom="Admin", nom="ISI")
admin.definir_mdp("Admin1234!")
[Link](admin)
[Link]()
[Link]("Base initialis e.")

(.venv) $ flask init-db


Base initialis e.

145
é
é
é
é
é
Travaux pratiques

Partie 3
TP 3.1 — Cr ation de la base et des mod les
● Cr er la base de donn es monblog_dev sous PostgreSQL et l'utilisateur d di
● Configurer Flask-SQLAlchemy via [Link] et un fichier .env
● D finir les mod les User, Article, Commentaire, Tag
● Initialiser Flask-Migrate, g n rer la premi re migration, l'appliquer

147
é
é
è
é
é
é
é
è
é
é
è
TP 3.2 — Donn es et CRUD
● crire une commande Flask seed qui cr e 3 utilisateurs et 10 articles r partis entre eux
● Construire une page /articles pagin e par 5 articles, du plus r cent au plus ancien
● Ajouter le d tail /articles/<id> avec contenu, auteur et commentaires
● Ajouter un formulaire de cr ation (Chapitre 15) — sans authentification pour l'instant

148
É
é
é
é
é
é
é
é
TP 3.3 — Requ tes avanc es
● Impl menter une recherche /articles?q=mot qui filtre les titres et r sum s (ilike)
● Ajouter un filtre par tag : /articles?tag=python
● Afficher en page d'accueil le top 5 des auteurs (par nombre d'articles publi s)

149
é
ê
é
é
é
é
Conseils de d bogage
● Activez SQLALCHEMY_ECHO = True en d veloppement pour voir les requ tes SQL g n r es
● Utilisez psql et la commande \d articles pour visualiser le sch ma r ellement cr
● En cas de migration rat e, supprimez la BDD de dev et recr ez-la — c'est une BDD jetable

150
é
é
é
é
é
é
ê
é
é
é
é
é
PARTIE 4

Formulaires et authentification
Chapitre 18

Formulaires avec Flask-WTF et


WTForms
Objectifs du chapitre
● Comprendre l'int r t d'une biblioth que de formulaires
● D finir un formulaire en Python avec WTForms
● Le rendre dans un template Jinja2
● Activer la protection CSRF int gr e

153
é
é
ê
é
é
è
18.1 Pourquoi pas un simple <form> ?
● crire la main les formulaires avec [Link] fonctionne, mais :
● Code r p titif
● Pas de validation
● Pas de protection CSRF
● Pas de gestion fine des erreurs
● WTForms apporte une couche d'abstraction propre
● Flask-WTF y greffe la protection CSRF et l'int gration avec Flask

154
É
é
à
é
é
18.2 Installation
(.venv) $ pip install Flask-WTF email-validator
(.venv) $ pip freeze > [Link]

app/[Link] app/__init__.py (extrait)

from flask_wtf.csrf import CSRFProtect


csrf = CSRFProtect()
from .extensions import db, migrate, csrf

def create_app(nom_config="dev"):
app = Flask(__name__)
[Link].from_object(...)
db.init_app(app)
migrate.init_app(app, db)
csrf.init_app(app)
return app

155
CSRF (Cross-Site Request Forgery)
● Attaque consistant faire ex cuter une requ te un utilisateur authentifi son insu
● Flask-WTF injecte un jeton secret dans chaque formulaire
● la soumission, le jeton est v rifi — un attaquant ne peut pas le deviner
● Toujours laisser CSRFProtect activ en production.

156
À
à
é
é
é
é
ê
à
é
à
18.3 Premier formulaire : ArticleForm
app/forms/[Link]

from flask_wtf import FlaskForm


from wtforms import StringField, TextAreaField, BooleanField, SubmitField
from [Link] import DataRequired, Length

class ArticleForm(FlaskForm):
titre = StringField("Titre", validators=[
DataRequired(message="Le titre est obligatoire."),
Length(min=3, max=200, message="Entre 3 et 200 caract res.")
])
contenu = TextAreaField("Contenu", validators=[
DataRequired(message="Le contenu est obligatoire."),
Length(min=10, message="Au moins 10 caract res.")
])
publier = BooleanField("Publier imm diatement")
submit = SubmitField("Enregistrer")

157
é
è
è
18.4 Utilisation dans une vue
from flask import Blueprint, render_template, redirect, url_for, flash
from ..[Link] import ArticleForm
from ..extensions import db
from ..[Link] import Article

bp_articles = Blueprint("articles", __name__)

@bp_articles.route("/nouveau", methods=["GET", "POST"])


def creer():
form = ArticleForm()
if form.validate_on_submit(): # POST + validation OK
article = Article(
titre=[Link],
contenu=[Link],
publie=[Link],
auteur_id=1, # provisoire (cf. Flask-Login)
)
[Link](article)
[Link]()
flash("Article enregistr .", "success")
return redirect(url_for("[Link]", id=[Link]))
return render_template("articles/[Link]", form=form)
158
é
18.5 Affichage du formulaire dans le template
{# templates/articles/[Link] #}
{% extends "[Link]" %}
{% block contenu %}
<h1>Nouvel article</h1>

<form method="post" novalidate>


{{ form.hidden_tag() }} {# jeton CSRF + champs cach s #}

<div>
{{ [Link] }}
{{ [Link](class_="champ", placeholder="Titre de l'article") }}
{% for err in [Link] %}
<p class="erreur">{{ err }}</p>
{% endfor %}
</div>

<div>
{{ [Link] }}
{{ [Link](rows=10, class_="champ") }}
{% for err in [Link] %}
<p class="erreur">{{ err }}</p>
{% endfor %}
</div>

<div>{{ [Link]() }} {{ [Link] }}</div>


{{ [Link](class_="btn btn-primaire") }}
</form>
{% endblock %}

159
é
18.6 Validateurs int gr s
Validateur R le
DataRequired() Champ non vide
Length(min, max) Longueur de la cha ne
Email() Adresse email valide (n cessite email-validator)
EqualTo('autre_champ') Doit tre gal un autre champ (confirmation de mot de passe)
NumberRange(min, max) Plage num rique
Regexp(motif) Doit matcher une expression r guli re
URL() URL valide
Optional() D sactive les autres validateurs si vide

160
ô
é
ê
é
é
à
î
é
é
è
é
é
Validateur personnalis

from [Link] import ValidationError

class InscriptionForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
# ...
def validate_email(self, field):
from ..[Link] import User
if [Link].filter_by(email=[Link]).first():
raise ValidationError("Cet email est d j utilis .")

Convention : une m thode validate_<nom_du_champ> est automatiquement appel e par WTForms la validation.

161
é
é
é
à
é
é
à
Chapitre 19

Authentification : concepts et
inscription
Objectifs du chapitre
● Distinguer authentification et autorisation
● Hasher proprement les mots de passe avec Werkzeug
● Cr er un formulaire et une vue d'inscription

163
é
19.1 Authentification vs Autorisation
● Authentification : prouver son identit (« qui tes-vous ? ») via un email et un mot de passe.
● Autorisation : accorder ou refuser une action (« avez-vous le droit ? ») — par exemple, seul l'auteur
d'un article peut le supprimer.
● Les deux m canismes sont distincts mais souvent trait s ensemble dans une application.

164
é
é
ê
é
19.2 Pourquoi hasher les mots de passe ?
● Si la base de donn es est compromise, l'attaquant ne doit pas pouvoir lire les mots de passe
● On stocke une empreinte cryptographique (hash) avec un sel al atoire
● Werkzeug fournit generate_password_hash et check_password_hash :
● Utilise PBKDF2-SHA256 avec un sel par d faut
● Co t ajustable pour rester s r face l' volution du mat riel

165
û
é
û
à
é
é
é
é
NE JAMAIS faire
● Stocker les mots de passe en clair en base
● Utiliser md5() ou sha1() seuls (trop rapides brute-forcer)
● R utiliser le m me sel pour tout le monde
● Loguer un mot de passe, m me en debug

166
À
é
ê
ê
à
19.3 Formulaires d'inscription et de connexion
app/forms/[Link]
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from [Link] import DataRequired, Email, Length, EqualTo, ValidationError

class InscriptionForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
prenom = StringField("Pr nom", validators=[DataRequired(), Length(2, 50)])
nom = StringField("Nom", validators=[DataRequired(), Length(2, 50)])
mdp = PasswordField("Mot de passe", validators=[
DataRequired(), Length(min=8, message="8 caract res minimum.")
])
mdp_confirm = PasswordField("Confirmation", validators=[
DataRequired(),
EqualTo("mdp", message="Les mots de passe ne correspondent pas.")
])
submit = SubmitField("Cr er mon compte")

def validate_email(self, field):


from ..[Link] import User
if [Link].filter_by(email=[Link]()).first():
raise ValidationError("Cet email est d j utilis .")

class ConnexionForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
mdp = PasswordField("Mot de passe", validators=[DataRequired()])
se_souvenir = BooleanField("Se souvenir de moi")
submit = SubmitField("Se connecter")

167
é
é
é
à
é
è
19.4 Vue d'inscription
from flask import Blueprint, render_template, redirect, url_for, flash
from ..[Link] import InscriptionForm
from ..extensions import db
from ..[Link] import User

bp_auth = Blueprint("auth", __name__, url_prefix="/auth")

@bp_auth.route("/inscription", methods=["GET", "POST"])


def inscription():
form = InscriptionForm()
if form.validate_on_submit():
user = User(email=[Link](),
prenom=[Link],
nom=[Link])
user.definir_mdp([Link])
[Link](user)
[Link]()
flash("Compte cr . Vous pouvez vous connecter.", "success")
return redirect(url_for("[Link]"))
return render_template("auth/[Link]", form=form)

168
é
é
Chapitre 20

Connexion et sessions avec Flask-


Login
Objectifs du chapitre
● Installer et configurer Flask-Login
● Impl menter login / logout
● Utiliser current_user dans les vues et templates
● Prot ger des routes avec @login_required

170
é
é
20.1 Installation
(.venv) $ pip install Flask-Login
(.venv) $ pip freeze > [Link]

app/[Link]

from flask_login import LoginManager

login_manager = LoginManager()
login_manager.login_view = "[Link]"
login_manager.login_message = "Veuillez vous connecter pour continuer."
login_manager.login_message_category = "warning"

# app/__init__.py (extrait)
from .extensions import db, migrate, csrf, login_manager

def create_app(...):
# ...
login_manager.init_app(app)
return app
171
20.2 Pr parer le mod le User
Pour tre compatible avec Flask-Login, User doit h riter de UserMixin (qui fournit is_authenticated, is_active, get_id…) et l'application
doit d clarer un user_loader.

# app/models/[Link] (ajouts)
from flask_login import UserMixin
from ..extensions import db, login_manager

class User(UserMixin, [Link]):


__tablename__ = "users"
# ... colonnes existantes ...

@login_manager.user_loader
def charger_user(user_id):
return [Link](User, int(user_id))

172
ê
é
é
é
è
20.3 Vues login et logout
from flask_login import login_user, logout_user, login_required, current_user
from flask import request, flash, redirect, url_for, render_template
from ..[Link] import ConnexionForm

@bp_auth.route("/connexion", methods=["GET", "POST"])


def connexion():
if current_user.is_authenticated:
return redirect(url_for("[Link]"))

form = ConnexionForm()
if form.validate_on_submit():
user = [Link].filter_by(email=[Link]()).first()
if user and user.verifier_mdp([Link]):
login_user(user, remember=form.se_souvenir.data)
flash(f"Bonjour {[Link]} !", "success")
next_url = [Link]("next") or url_for("[Link]")
return redirect(next_url)
flash("Identifiants invalides.", "danger")
return render_template("auth/[Link]", form=form)

@bp_auth.route("/deconnexion")
@login_required
def deconnexion():
logout_user()
flash(" bient t !", "info")
return redirect(url_for("[Link]"))

173
À
ô
Le param tre ?next=...
● Quand un utilisateur non connect tente d'acc der une page prot g e, Flask-Login le redirige vers
la page de connexion avec ?next=URL
● R cup rez ce param tre pour le ramener sur la page d'origine apr s login
● S curit : validez que next pointe bien vers une URL interne (pas vers un site externe) pour viter
les attaques de redirection ouverte.

174
é
é
é
é
è
è
é
é
à
è
é
é
é
20.4 current_user dans les templates
[Link]

<nav>
<a href="{{ url_for('[Link]') }}">Accueil</a>
{% if current_user.is_authenticated %}
<span>Bonjour, {{ current_user.prenom }}</span>
<a href="{{ url_for('[Link]') }}">Nouvel article</a>
<a href="{{ url_for('[Link]') }}">D connexion</a>
{% else %}
<a href="{{ url_for('[Link]') }}">Connexion</a>
<a href="{{ url_for('[Link]') }}">Inscription</a>
{% endif %}
</nav>

current_user est disponible globalement dans tous les templates — pas besoin de le passer explicitement.

175
é
20.5 Prot ger des routes
from flask_login import login_required, current_user

@bp_articles.route("/nouveau", methods=["GET", "POST"])


@login_required
def creer():
form = ArticleForm()
if form.validate_on_submit():
article = Article(
titre=[Link],
contenu=[Link],
publie=[Link],
auteur_id=current_user.id, # plus besoin de hardcoder !
)
[Link](article)
[Link]()
return redirect(url_for("[Link]", id=[Link]))
return render_template("articles/[Link]", form=form)

176
é
Chapitre 21

D corateurs et autorisation fine


é
Objectifs du chapitre
● Comprendre la m canique d'un d corateur Python
● crire un d corateur @admin_required
● Impl menter une logique d'autorisation par r le

178
É
é
é
é
é
ô
21.1 Rappel sur les d corateurs
● Un d corateur est une fonction qui prend une autre fonction en argument et renvoie une nouvelle
fonction enrichie
● @login_required en est un exemple — Flask-Login v rifie l'utilisateur, puis appelle votre vue
● Voyons comment en cr er un nous-m mes pour ajouter une v rification de r le

179
é
é
ê
é
é
é
ô
21.2 Ajouter un r le au mod le User
# app/models/[Link] (ajout)
class User(UserMixin, [Link]):
# ...
role = [Link]([Link](20), default="utilisateur", nullable=False)
# r les possibles : "utilisateur", "auteur", "admin"

@property
def est_admin(self) -> bool:
return [Link] == "admin"

@property
def est_auteur(self) -> bool:
return [Link] in ("auteur", "admin")

N'oubliez pas de cr er une migration : flask db migrate -m "Ajout du r le" && flask db upgrade

180
ô
é
ô
ô
è
21.3 D corateur @admin_required
app/[Link]

from functools import wraps


from flask import abort
from flask_login import current_user

def admin_required(view):
@wraps(view)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
abort(401)
if not current_user.est_admin:
abort(403)
return view(*args, **kwargs)
return wrapper

@wraps(view) pr serve le nom et la docstring de la vue d'origine — indispensable pour le bon fonctionnement de url_for().

181
é
é
21.4 Utilisation et v rification de propri t
Page admin :

from ..decorators import admin_required

@bp_admin.route("/utilisateurs")
@login_required
@admin_required
def lister_utilisateurs():
users = [Link].order_by(User.cree_le.desc()).all()
return render_template("admin/[Link]", users=users)

Propri t d'une ressource :


@bp_articles.route("/<int:id>/modifier", methods=["GET", "POST"])
@login_required
def modifier(id):
article = [Link].get_or_404(id)
if article.auteur_id != current_user.id and not current_user.est_admin:
abort(403)
form = ArticleForm(obj=article)
if form.validate_on_submit():
form.populate_obj(article)
[Link]()
flash("Article mis jour.", "success")
return redirect(url_for("[Link]", id=[Link]))
return render_template("articles/[Link]", form=form)
182
é
é
à
é
é
é
Travaux pratiques

Partie 4
TP 4.1 — Formulaires Flask-WTF
● Convertir le formulaire de cr ation d'article du TP 3 en ArticleForm WTForms
● Ajouter un formulaire d'ajout de commentaire (CommentaireForm) sur la page de d tail
● V rifier que la protection CSRF fonctionne (essayer de soumettre sans token : doit chouer en 400)

184
é
é
é
é
TP 4.2 — Inscription et connexion
● Impl menter la vue d'inscription compl te avec validation
● Impl menter la connexion/d connexion avec Flask-Login
● Adapter [Link] pour afficher dynamiquement les liens selon current_user

185
é
é
é
è
TP 4.3 — Autorisations
● Ajouter le champ role User et cr er une migration
● crire le d corateur @admin_required et l'utiliser sur une page /admin/utilisateurs
● Emp cher la modification d'un article par quelqu'un d'autre que son auteur ou un admin

186
É
ê
é
à
é
Crit res d' valuation de la Partie 4
● Les mots de passe sont stock s hash s en base (v rifiable avec psql)
● Aucun secret n'appara t dans le code source : tout passe par .env
● Toutes les routes sensibles sont prot g es par @login_required
● Les messages flash s'affichent avec leur cat gorie (success, danger, info, warning)

187
è
é
î
é
é
é
é
é
é
PARTIE 5

Bonnes pratiques et avanc

é
Chapitre 22

Structure de projet
professionnelle
Objectifs du chapitre
● R capituler une structure de projet Flask de qualit production
● Comprendre la responsabilit de chaque dossier
● Mettre en place les outils compl mentaires : tests, qualit , logs

190
é
é
é
é
é
22.1 Arborescence cible finale
monblog/
├── app/
│ ├── **init**.py # create_app()
│ ├── [Link] # db, migrate, login_manager, csrf
│ ├── [Link] # MonBlogError, RessourceIntrouvable…
│ ├── [Link] # handlers globaux
│ ├── [Link] # admin_required, etc.
│ ├── [Link] # commandes Flask personnalisées
│ ├── models/
│ ├── views/ # blueprints
│ ├── forms/
│ ├── services/ # logique métier réutilisable
│ ├── repositories/ # accès aux données
│ ├── templates/
│ └── static/
├── migrations/
├── tests/
│ ├── [Link]
│ ├── test_models.py
│ └── test_routes.py
├── con [Link]
├── [Link]
├── [Link]

191
fi
22.2 Responsabilit s par dossier
Dossier / Fichier R le
app/__init__.py Application factory ; assemblage des extensions et blueprints
app/[Link] Instances partag es (db, migrate, login_manager…) sans application
app/models/ Repr sentation des entit s persist es ([Link])
app/views/ Blueprints — orchestration HTTP (formulaires, redirections, templates)
app/forms/ Formulaires WTForms (validation, rendu)
app/services/ Logique m tier complexe, ind pendante du web
app/repositories/ Acc s et requ tes BDD sp cifiques
tests/ Tests automatis s (pytest)
[Link] Classes de configuration (Dev, Test, Prod)
[Link] Point d'entr e pour le serveur de dev

192
ô
è
é
é
é
ê
é
é
é
é
é
é
é
R gle de d pendance entre couches
● views → services → repositories → models
● Les imports doivent toujours suivre ce sens
● Une vue n' crit JAMAIS de SQL
● Un service n'importe JAMAIS request ou render_template
● Le respect strict de ces r gles facilite norm ment les tests et la maintenance

193
è
é
é
è
é
é
22.3 Logging
# app/__init__.py (extrait)
import logging
from [Link] import RotatingFileHandler
import os

def configurer_logs(app):
if [Link]("DEBUG"):
return # le serveur de dev fait d j l'affaire
[Link]("logs", exist_ok=True)
handler = RotatingFileHandler("logs/[Link]",
maxBytes=1_000_000, backupCount=5)
[Link]([Link](
"%(asctime)s [%(levelname)s] %(name)s : %(message)s"
))
[Link]([Link])
[Link](handler)
[Link]([Link])
[Link]("MonBlog d marre.")

194
é
é
à
22.4 Tests automatis s — [Link]
tests/[Link]

import pytest
from app import create_app
from [Link] import db

@[Link]()
def app():
app = create_app("test")
with app.app_context():
db.create_all()
yield app
[Link]()
db.drop_all()

@[Link]()
def client(app):
return app.test_client()

tests/test_routes.py

def test_accueil_repond_200(client):
rep = [Link]("/")
assert rep.status_code == 200
assert b"MonBlog" in [Link]
195
é
Lancer les tests
(.venv) $ pip install pytest pytest-cov

(.venv) $ pytest -v
============================ test session starts =============================
tests/test_routes.py::test_accueil_repond_200 PASSED [ 20%]
tests/test_models.py::test_user_definir_mdp PASSED [ 40%]
tests/test_models.py::test_article_publier PASSED [ 60%]
...
============================== 5 passed in 0.42s =============================

(.venv) $ pytest --cov=app --cov-report=term-missing

196
Chapitre 23

Gestion des erreurs HTTP


Objectifs du chapitre
● D finir des pages d'erreur personnalis es (404, 403, 500)
● Distinguer erreurs c t client et c t serveur
● Logger correctement les exceptions impr vues

198
é
ô
é
ô
é
é
é
23.1 Pages d'erreur personnalis es
app/[Link]

from flask import render_template


from .exceptions import MonBlogError

def enregistrer_handlers(app):
@[Link](404)
def page_404(_):
return render_template("erreurs/[Link]"), 404

@[Link](403)
def page_403(_):
return render_template("erreurs/[Link]"), 403

@[Link](500)
def page_500(err):
from .extensions import db
[Link]()
[Link]("Erreur 500 : %s", err)
return render_template("erreurs/[Link]"), 500

@[Link](MonBlogError)
def erreur_metier(err):
return render_template("erreurs/[Link]",
code=err.code_http,
message=str(err)), err.code_http

199
é
Template d'erreur 404

{# templates/erreurs/[Link] #}
{% extends "[Link]" %}
{% block contenu %}
<h1>Oups, page introuvable</h1>
<p>La page que vous cherchez n'existe pas ou a t d plac e.</p>
<a href="{{ url_for('[Link]') }}">Retour l'accueil</a>
{% endblock %}

200
à
é
é
é
é
23.2 Le pi ge du rollback
● Si une erreur se produit pendant une transaction, la session reste « cass e » jusqu' un rollback
explicite
● Toujours appeler [Link]() dans le handler 500
● Sans cela, toutes les requ tes suivantes choueront en cascade jusqu'au prochain red marrage du
worker

201
è
ê
é
é
à
é
23.3 D tection pr coce avec abort()
abort() permet de court-circuiter une vue avec un code HTTP donn , et de d clencher automatiquement le handler associ .

from flask import abort

@bp_articles.route("/<int:id>/supprimer", methods=["POST"])
@login_required
def supprimer(id):
article = [Link].get_or_404(id)
if article.auteur_id != current_user.id and not current_user.est_admin:
abort(403) # d clenche page_403
[Link](article)
[Link]()
flash("Article supprim .", "success")
return redirect(url_for("[Link]"))

202
é
é
é
é
é
é
é
Chapitre 24

Bonnes pratiques de s curit

é
é
Objectifs du chapitre
● Identifier les principales failles web (OWASP Top 10)
● S curiser cookies, sessions et entr es utilisateur
● Configurer HTTPS et les en-t tes de s curit

204
é
ê
é
é
é
24.1 Le top 10 OWASP, version courte
Vuln rabilit Comment Flask vous aide
Injection SQL Toujours via SQLAlchemy ; jamais de f-string avec entr e utilisateur
XSS (script inject ) Jinja2 chappe le HTML par d faut. M fiez-vous de |safe
CSRF Flask-WTF / CSRFProtect ins re un jeton
Mauvaise authentification Flask-Login + Werkzeug pour les hash
Exposition de donn es Ne pas commiter .env ; s parer dev/prod
Mauvaise configuration D sactiver DEBUG en production. SECRET_KEY robuste
Composants vuln rables pip-audit ; mise jour r guli re

205
é
é
é
é
à
é
é
é
é
é
è
è
é
é
é
24.2 Cookies et sessions
[Link] — production

class ProdConfig(BaseConfig):
SESSION_COOKIE_SECURE = True # uniquement HTTPS
SESSION_COOKIE_HTTPONLY = True # pas accessible en JS
SESSION_COOKIE_SAMESITE = "Lax" # protection CSRF suppl mentaire
REMEMBER_COOKIE_SECURE = True
REMEMBER_COOKIE_HTTPONLY = True
En-t tes de s curit avec Flask-Talisman :

from flask_talisman import Talisman

def create_app(...):
if not [Link]:
Talisman(app, content_security_policy={
"default-src": "'self'",
"img-src": "'self' data:",
"style-src": "'self' 'unsafe-inline'",
})

206
ê
é
é
é
24.3 Validation c t serveur — toujours
● Ne jamais faire confiance aux donn es reçues, m me valid es en JavaScript
● La validation c t client am liore l'exp rience utilisateur mais n'est PAS une s curit
● La validation c t serveur (WTForms, v rifications applicatives) est la seule barri re fiable
● « Never trust the client » — principe fondamental.

207
ô
ô
é
é
é
ô
é
é
é
é
ê
é
é
è
é
24.4 Limitation de d bit
(.venv) $ pip install Flask-Limiter

from flask_limiter import Limiter


from flask_limiter.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@bp_auth.route("/connexion", methods=["GET", "POST"])


@[Link]("5 per minute") # max 5 tentatives / minute / IP
def connexion():
...
Sans rate-limiting, un attaquant peut tester des millions de mots de passe. Limiter les tentatives est une mesure peu co teuse mais tr s efficace.

208
é
û
è
Chapitre 25

D ploiement
é
Objectifs du chapitre
● Comprendre la cha ne Nginx + Gunicorn + Flask + PostgreSQL
● D ployer sur Render ou Railway en quelques minutes
● Configurer un VPS (Ubuntu) de A Z (notions)

210
é
î
à
25.1 Le serveur de dev n'est PAS pour la production
● Le serveur de flask run est mono-thread, lent et non s curis
● En production, on utilise :
● Un serveur WSGI comme Gunicorn ou uWSGI
● Un reverse proxy comme Nginx qui sert les fichiers statiques et termine SSL
● Cette architecture permet de scaler horizontalement (plusieurs workers) et de g rer le HTTPS
proprement

211
é
é
é
25.2 Sch ma de d ploiement classique

212
é
é
25.3 Pr parer l'application
(.venv) $ pip install gunicorn
(.venv) $ pip freeze > [Link]
(.venv) $ echo "web: gunicorn 'run:app'" > Procfile

[Link]

import os
from dotenv import load_dotenv
load_dotenv()
from app import create_app
app = create_app([Link]("FLASK_CONFIG", "prod"))

213
é
25.4 D ploiement sur Render
● Pousser le projet sur GitHub
● Sur [Link], cr er un Web Service partir du d p t
● Configurer Build Command : pip install -r [Link]
● Configurer Start Command : gunicorn run:app
● Ajouter les variables d'environnement (DATABASE_URL fourni par le service Postgres de Render)
● Lancer la migration via le shell distant : flask db upgrade

214
é
é
à
é
ô
25.5 D ploiement sur Railway
● La logique est tr s proche de Render
● Cr er un projet, ajouter un service Postgres, connecter le d p t GitHub
● Configurer la commande de d marrage : gunicorn run:app
● Railway d tecte automatiquement Python via [Link]
● Avantage : interface simple et tarification claire

215
é
é
é
è
é
é
ô
25.6 Notions sur un VPS Ubuntu
# Sur un VPS fra chement provisionn
$ sudo apt update && sudo apt install -y python3-venv nginx postgresql

# 1) Cloner le projet, cr er le venv, installer les d pendances


$ git clone [Link] && cd monblog
$ python3 -m venv .venv && source .venv/bin/activate
$ pip install -r [Link] gunicorn

# 2) Service systemd : /etc/systemd/system/[Link]


# [Unit] Description=MonBlog Gunicorn
# [Service]
# User=ubuntu
# WorkingDirectory=/home/ubuntu/monblog
# EnvironmentFile=/home/ubuntu/monblog/.env
# ExecStart=/home/ubuntu/monblog/.venv/bin/gunicorn -w 3 -b [Link]:8000 run:app
$ sudo systemctl enable --now monblog

216
î
é
é
é
Nginx + HTTPS via Let's Encrypt
# /etc/nginx/sites-available/monblog
server {
listen 80;
server_name [Link];

location /static/ {
alias /home/ubuntu/monblog/app/static/;
expires 30d;
}

location / {
proxy_pass [Link]
proxy_set_header X-Forwarded-For $remote_addr;
}
}

# HTTPS gratuit (Let's Encrypt)


$ sudo certbot --nginx -d [Link]

217
Avant la mise en production
● Mettre DEBUG = False et une SECRET_KEY de 32+ caract res al atoires
● Sauvegarder r guli rement la base avec pg_dump
● Surveiller les logs et l'utilisation CPU/m moire (htop, journalctl)
● Documenter le processus de mise jour
● Tester en staging (environnement identique la production) avant chaque release importante.

218
é
è
à
é
à
è
é
Travaux pratiques

Projet final
Sujet : MonBlog — application compl te
● l'issue de ce cours, vous livrez une application Flask compl te qui combine TOUS les concepts vus
● valuation sur : qualit du code, s curit , coh rence de l'architecture, exp rience utilisateur

220
À
É
é
é
é
é
è
è
é
Cahier des charges (1/2)
● Inscription et connexion s curis es (Flask-WTF, Flask-Login, Werkzeug)
● Trois r les : visiteur (non connect ), utilisateur, admin
● CRUD complet sur les articles : seul l'auteur ou un admin peut modifier/supprimer
● Syst me de commentaires sur chaque article (par les utilisateurs connect s uniquement)
● Tags pour les articles avec relation N-N et page /tags/<libelle>
● Pagination de la liste d'articles (10 par page)

221
è
ô
é
é
é
é
Cahier des charges (2/2)
● Recherche par mot-cl sur le titre et le contenu
● Pages d'erreurs personnalis es (404, 403, 500)
● Configuration multi-environnements (dev, test, prod) via .env
● Migrations Flask-Migrate maintenues jour
● Au moins 5 tests automatis s (pytest)
● D ploiement effectif sur Render ou Railway
● [Link] d crivant : installation locale, lancement des tests, d ploiement

222
é
é
é
é
é
à
é
Grille d' valuation indicative
Crit re Points
Architecture (factory + blueprints + repositories) 3
Mod les, relations et migrations 3
Authentification et autorisation 3
Formulaires et validation 2
Templates et exp rience utilisateur 2
Gestion des erreurs et logs 2
S curit (CSRF, hash, en-t tes, rate-limit) 2
Tests automatis s 1
D ploiement effectif 1
Qualit du code et README 1
Total 20

223
é
é
è
è
é
é
é
é
é
ê
tapes recommand es
● Reprendre le projet du TP 4.3 et compl ter les manques (tags, recherche, pagination)
● Mettre en place tests/[Link] et crire au moins 5 tests
● Personnaliser les pages 404/403/500
● Activer Flask-Talisman et Flask-Limiter
● Pr parer le projet pour la production : Procfile, gunicorn, variables d'env
● D ployer sur Render et noter l'URL publique dans le README

224
É
é
é
é
é
é
Annexes
Annexe A — R f rentiel de commandes (1/2)
Environnement Python

$ python3 -m venv .venv


$ source .venv/bin/activate # Linux / macOS
$ .venv\Scripts\Activate.ps1 # Windows
$ pip install -r [Link]
$ pip freeze > [Link]
$ deactivate

Flask CLI

$ flask --app run:app run


$ flask --app run:app routes # liste les URLs
$ flask --app run:app shell # REPL avec contexte

Migrations

$ flask db init && flask db migrate -m "..." && flask db upgrade

226
é
é
Annexe A — R f rentiel de commandes (2/2)
PostgreSQL — psql

$ psql -U monblog_user -d monblog_dev


monblog_dev=> \dt # lister les tables
monblog_dev=> \d articles # d tails table articles
monblog_dev=> \q # quitter

$ pg_dump monblog > [Link] # sauvegarde


$ psql monblog < [Link] # restauration

Git essentiel

$ git init && git add . && git commit -m "Initial commit"
$ git remote add origin [Link]
$ git push -u origin main
$ git status / log / diff

227
é
é
é
Annexe B — Glossaire
Terme D finition courte
WSGI Interface standard Python entre serveur HTTP et application
Blueprint Module Flask regroupant routes et templates d'un domaine
Factory Fonction create_app() construisant une application param trable
ORM Outil traduisant classes Python ↔ tables relationnelles
Migration Script versionn modifiant le sch ma de la base
CSRF Attaque consistant ex cuter une action l'insu d'un utilisateur connect
XSS Injection de script malveillant dans une page web
Hash Empreinte unidirectionnelle d'une donn e (mot de passe)
Sel Donn e al atoire ajout e avant le hash
D corateur Fonction qui transforme une autre fonction (@login_required)
Reverse proxy Serveur (Nginx) intercal devant l'application (Gunicorn)

228
é
é
é
é
é
à
é
é
é
é
é
à
é
é
Annexe C — Pour aller plus loin
● API REST avec Flask-RESTful ou Flask-Smorest, s rialisation avec marshmallow
● API GraphQL avec strawberry-graphql ou Ariadne
● T ches asynchrones avec Celery + Redis (envoi d'emails, t ches longues)
● WebSockets avec Flask-SocketIO (notifications en temps r el)
● D couvrir FastAPI (alternative moderne pour les API) et Django (full-stack)
● Conteneurisation avec Docker ; orchestration avec docker-compose
● Int gration continue avec GitHub Actions : tests chaque push
● Observabilit : Sentry pour les erreurs, Prometheus + Grafana pour les m triques

229
â
é
é
é
à
é
â
é
é
Merci de votre attention !
Questions ?

M. DIOP ElHadji Mar — Institut Sup rieur d'Informatique — 2025/2026


é

You might also like