Développement web : Python, HTML/CSS et base MySQL
Tout ce dont vous avez besoin pour ce module, dans un seul document. Cochez au fur et à mesure.
Comment utiliser ce document
- À gauche, une partie à la fois. Cliquez pour changer de partie.
- Les boutons bleus A1 ouvrent une annexe dans le panneau de droite.
- Vos réponses et vos cases cochées sont gardées dans ce navigateur.
Ce qu'il faut savoir avant de commencer
Ce qu'il faut savoir avant de commencer. Lisez-le si vous avez un doute. Environ 15 minutes.
C'est quoi une fonction en Python ?
Une fonction est un bloc de code qui porte un nom. On l'écrit une fois, on l'appelle quand on veut.
Elle reçoit des valeurs entre parenthèses et renvoie un résultat avec return.
def double(nombre):
return nombre * 2
double(4) # renvoie 8
C'est quoi un dictionnaire ?
Un dictionnaire range des valeurs sous des noms, appelés clés. On lit une valeur avec sa clé entre crochets.
chantier = {"id": 3, "nom": "Éclairage des allées", "statut": "ouvert"}
chantier["nom"] # "Éclairage des allées"
"statut" in chantier # True : la clé existe
Une liste de dictionnaires, c'est un tableau : une ligne par dictionnaire.
Comment lire un message d'erreur Python ?
Quand un programme plante, Python affiche une trace. On la lit par le bas.
La dernière ligne donne le type d'erreur et sa cause. Juste au-dessus, on trouve le fichier et le numéro de la ligne fautive.
File "calcul.py", line 4, in moyenne
return total / nombre
ZeroDivisionError: division by zero
Ici, la ligne 4 de calcul.py divise par zéro : la variable nombre vaut 0.
Comment joindre un programme sur le réseau ?
Une machine a une adresse IP. Sur cette machine, chaque programme serveur écoute sur un numéro de port.
Dans le navigateur, on écrit l'adresse puis le port après deux-points : http://127.0.0.1:5000. L'adresse 127.0.0.1 désigne votre propre poste. Le port 5000 est celui qu'on utilise pendant le développement.
C'est quoi une requête SQL ?
Une base de données range les données dans des tables. Chaque table a des colonnes et des lignes, comme un tableur.
On parle à la base en SQL. SELECT lit des lignes, WHERE les filtre, INSERT en ajoute.
SELECT nom, statut FROM chantier WHERE statut = 'ouvert';
INSERT INTO client (nom, ville) VALUES ('Mairie de Lusse', 'Lusse');
En SQL, un texte s'écrit entre apostrophes : 'ouvert'. Un nombre s'écrit sans apostrophes.
C'est quoi une clé étrangère ?
Une colonne qui contient le numéro d'une ligne d'une autre table. Dans la table chantier, la colonne id_client contient le numéro du client.
De la même façon, dans la table intervention, la colonne id_chantier contient le numéro du chantier. C'est elle qui relie une intervention à son chantier.
Pour afficher le nom du client avec le chantier, on relie les deux tables avec JOIN :
SELECT chantier.nom, client.nom FROM chantier JOIN client ON client.id = chantier.id_client;
Quelques outils Python du labo
Les labos vous font lire et compléter du code. Voici les mots qui reviennent.
| Écrit dans le code | Ce que ça veut dire |
|---|---|
if …: puis elif …: |
« si » puis « sinon si » : on teste un autre cas. |
x is None |
vrai si x ne contient aucune valeur. |
"a" not in liste |
vrai si "a" n'est pas dans la liste. |
len(x) |
le nombre de caractères d'un texte, ou d'éléments d'une liste. |
x.strip() |
le texte x sans les espaces au début et à la fin. |
"5".isdigit() |
vrai si le texte ne contient que des chiffres. |
liste.append(valeur) |
ajoute valeur à la fin de la liste. |
try: … except ValueError: … |
« essaie ceci ; si ça rate avec cette erreur, fais cela ». |
un tuple : (3, "atelier") |
des valeurs entre parenthèses. Pour une seule valeur, (nom,) avec une virgule. |
Comment lancer MySQL sur votre poste ?
Sur votre poste, MySQL tourne grâce à XAMPP. On le pilote depuis le panneau de contrôle de XAMPP :
- Start, en face de « MySQL », lance la base. Le nom « MySQL » passe au vert.
- Stop l'arrête.
On parle ensuite à la base dans un terminal, avec la commande mysql -u root.
Vérifiez-vous
- 1. Avec
c = {"id": 6, "statut": "ouvert"}, que renvoiec["statut"]? - 2. Une trace se termine par
NameError: name 'chantier' is not defined. Quel est le problème ? - 3. Écrivez la requête qui lit le nom de tous les chantiers clôturés.
- 4. Dans la table
intervention, quelle colonne relie une intervention à son chantier ? - 5. MySQL ne répond plus. Où vérifiez-vous qu'il est lancé ?
Voir les réponses
- Le texte
"ouvert". - Le code utilise une variable
chantierqui n'a jamais reçu de valeur. Il faut chercher la ligne indiquée juste au-dessus. SELECT nom FROM chantier WHERE statut = 'cloture';id_chantier, qui contient le numéro du chantier.- Dans le panneau de contrôle de XAMPP : « MySQL » doit être en vert. Sinon, cliquez sur « Start ».
Ce que fait le serveur, ce que fait le navigateur
COURS-U6-S2-05
Karim Benali dirige l'atelier de TechnoVert. Chaque soir, ses techniciens rentrent de chantier. Ils ont cinq minutes pour noter leur intervention, pas plus.
Aujourd'hui, ils la notent dans un tableur partagé. Un jour, un technicien a saisi sur un chantier déjà terminé. Personne ne l'a vu pendant trois semaines.
Karim veut une page web : on se connecte, on remplit, on envoie. Et si la saisie est fausse, la page doit refuser.
À la fin de la séance, vous saurez comment un serveur Python construit cette page, reçoit la saisie et la vérifie.
Le plan de la séance
- Qui fait quoi : le serveur ou le navigateur.
- Route, vue, gabarit : le chemin d'une page.
- Recevoir une saisie et la vérifier.
- Se souvenir de l'utilisateur : la session.
- Quand le serveur plante.
La question de départ
Vous remplissez un formulaire sur un site, puis vous cliquez sur « Envoyer ». Qui doit vérifier que ce que vous avez tapé est juste : votre navigateur ou le serveur ?
Capsule 1 sur 5Qui fait quoi : le serveur ou le navigateur
On part de ce que vous faites tous les jours : taper une adresse et voir une page. Mais qui fabrique cette page ?
Le navigateur envoie une REQUÊTE au serveur. C'est un court message qui dit : « donne-moi la page /chantiers ». Le serveur répond avec une page écrite en HTML, le langage qui décrit le contenu d'une page : titres, tableaux, formulaires.
Pensez à un fast-food. Vous passez commande au comptoir, la cuisine prépare, on vous tend le plateau. Vous ne voyez jamais la cuisine.
Le navigateur, c'est vous. Le serveur, c'est la cuisine.
- Le navigateur envoie une requête : une adresse, parfois des données.
- Le serveur exécute du code Python : il lit les données, fait les calculs.
- Le serveur renvoie du HTML : une page toute prête.
- Le navigateur affiche la page. Il applique le CSS, le fichier qui règle les couleurs, les tailles et les marges.
Le HTML contient aussi le formulaire : les champs à remplir et le bouton « Envoyer ». Le navigateur l'affiche. Mais c'est le serveur qui reçoit et traite ce qu'on y tape.
On le fait ensemble — la page de saisie de Karim
1. Qui envoie la requête quand un technicien ouvre la page de saisie ?
2. Où se trouve la liste des chantiers affichée dans le menu déroulant ?
3. Qui choisit la couleur verte du bandeau ?
4. Le technicien clique sur « Enregistrer ». Qui range l'intervention ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | Le serveur lit le fichier des chantiers et met la liste dans la page. | Les données restent sur le serveur. Seul le résultat part vers le navigateur. |
| ✘ | « Le navigateur va chercher les chantiers dans le fichier. » | Le navigateur n'a pas accès aux fichiers du serveur. Il reçoit seulement du HTML. |
| ✘ | « Le CSS vérifie que la durée est un nombre. » | Le CSS règle l'apparence. Il ne vérifie rien et ne calcule rien. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. Vous faites clic droit, puis « Afficher le code source » sur une page. Voyez-vous le code Python du serveur ?
2. Karim veut des lettres plus grandes sur la page. Faut-il changer le Python, le HTML ou le CSS ?
Capsule 2 sur 5Route, vue, gabarit : le chemin d'une page
Le serveur construit donc la page. Reste à savoir comment le code Python sait quelle page construire.
On utilise un cadriciel web, une boîte à outils qui gère les requêtes pour nous. Ici, c'est Flask, en Python. Avec Flask, une page passe par trois pièces : la ROUTE, la VUE et le GABARIT.
C'est comme au lycée. L'emploi du temps dit « mardi 10 h, salle B12 » : c'est la route.
Le professeur fait le cours : c'est la vue. Le manuel donne la mise en page : c'est le gabarit.
- La route relie une adresse à une fonction :
/chantiersmène àliste_chantiers(). - La vue est cette fonction Python. Elle va chercher les données.
- La vue passe les données au gabarit, sous un nom :
chantiers=.... - Le gabarit est un fichier HTML avec des trous. Il remplit les trous avec les données. Flask utilise pour ça Jinja2.
@app.route("/chantiers") # la route
def liste_chantiers(): # la vue
chantiers = donnees.lire_chantiers() # les données
return render_template("chantiers.html", chantiers=chantiers)
Dans le gabarit, {{ c.nom }} affiche une valeur. {% for c in chantiers %} répète une ligne pour chaque chantier.
On le fait ensemble — la page /saisir
1. Quelle ligne relie l'adresse /saisir à une fonction ?
2. Le gabarit saisir.html contient {% for c in chantiers %}. Quel nom la vue doit-elle utiliser pour passer la liste ?
3. La vue écrit return render_template("saisir.html", liste=chantiers). Que voit-on ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | render_template("chantiers.html", chantiers=chantiers) |
Le nom chantiers est celui que le gabarit attend. |
| ✘ | La vue fabrique le HTML avec des + et des print. |
C'est illisible et fragile. Le HTML va dans le gabarit, le Python dans la vue. |
| ✘ | Deux fonctions sous la même route. | Flask n'appelle que la première. Une adresse, une vue. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. Le navigateur demande /connexion. Quelle pièce Flask regarde-t-il en premier ?
2. Un gabarit affiche {{ message }}. Que doit écrire la vue pour afficher « Enregistré » ?
Capsule 3 sur 5Recevoir une saisie et la vérifier
La vue sait maintenant envoyer des données à la page. Il faut faire le chemin inverse : recevoir ce que le technicien a tapé.
Les données voyagent de deux façons. Avec GET, elles sont dans l'adresse, après un point d'interrogation. Avec POST, elles sont dans le corps de la requête, la partie invisible du message.
C'est comme une carte postale et une lettre. Sur la carte postale, tout le monde lit le texte. Dans l'enveloppe, le texte ne se voit pas de l'extérieur.
Attention : l'enveloppe cache, mais elle ne protège pas. Seul le chiffrement protège.
- Choisir la méthode : GET pour chercher ou filtrer, POST pour envoyer une saisie.
- Lire les données dans la vue :
request.argspour GET,request.formpour POST. - Vérifier chaque champ : présent, du bon type, dans les bonnes limites.
- Répondre : s'il y a une erreur, réafficher le formulaire avec un message. Sinon, enregistrer.
GET /chantiers?service=atelier → request.args["service"] vaut "atelier"
POST /saisir (corps : duree=2.5 ...) → request.form["duree"] vaut "2.5"
Tout ce qui arrive est du texte. "2.5" n'est pas encore un nombre : la vue doit le convertir, et refuser s'il n'y arrive pas.
On le fait ensemble — une saisie de durée
1. Le technicien envoie le formulaire de saisie. GET ou POST ?
2. Où la vue lit-elle la durée ?
3. Le champ contient deux heures. Que fait la vue ?
4. Le champ contient 40. Est-ce accepté ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | /chantiers?service=atelier pour filtrer la liste |
Un filtre ne modifie rien. L'adresse peut être gardée en favori. |
| ✘ | Un mot de passe envoyé en GET | Il apparaît dans l'adresse, dans l'historique et dans les journaux du serveur. |
| ✘ | « Le champ a l'attribut required : pas besoin de vérifier sur le serveur. » |
Cet attribut est dans le navigateur. On peut l'enlever en deux clics. Le serveur vérifie toujours. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. L'adresse /chantiers?service=commercial est demandée. Où la vue lit-elle commercial ?
2. Un chantier a un statut : ouvert ou cloture. On ne saisit plus rien sur un chantier clôturé. Le champ « chantier » contient le numéro d'un chantier clôturé. La saisie est-elle acceptée ?
Capsule 4 sur 5Se souvenir de l'utilisateur : la session
La saisie est vérifiée. Mais Karim veut une chose de plus : seuls les salariés connectés peuvent saisir. Et le serveur oublie tout entre deux requêtes.
Pour s'en souvenir, il y a la SESSION. C'est une petite mémoire liée à un visiteur.
Flask la range dans un cookie, un petit fichier que le navigateur renvoie à chaque requête. Le cookie est signé par une clé secrète du serveur : si quelqu'un le modifie, le serveur le refuse.
C'est le bracelet d'un festival. On le reçoit à l'entrée en montrant son billet. Ensuite, on le montre à chaque scène, sans ressortir le billet.
- À la connexion, la vue vérifie l'identifiant et le mot de passe.
- Si c'est bon, elle écrit dans la session :
session["login"] = login. - Sur une page protégée, la vue regarde la session en premier. Pas de login ? Elle renvoie vers la page de connexion.
- À la déconnexion, la vue vide la session :
session.clear().
Renvoyer vers une autre page s'appelle une REDIRECTION. Le serveur ne donne pas de page : il répond « va voir à cette adresse ». Chaque réponse porte un code de réponse :
| Code | Sens | Exemple dans le mini-site |
|---|---|---|
| 200 | Tout va bien, voici la page. | La liste des chantiers s'affiche. |
| 302 | Redirection : va voir ailleurs. | Pas connecté : renvoyé vers /connexion. |
| 404 | Cette adresse n'existe pas. | /chantiers/99 quand il n'y a pas de chantier 99. |
| 500 | Le code du serveur a planté. | Une erreur Python dans une vue. |
On le fait ensemble — Yann Ropars ouvre /saisir sans être connecté
1. Que regarde la vue en premier ?
2. Il n'y en a pas. Quel code le serveur renvoie-t-il ?
3. Yann se connecte avec le bon mot de passe. Qu'écrit la vue ?
4. Il rouvre /saisir. Que se passe-t-il ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | Après une connexion réussie, une redirection vers la liste | L'utilisateur arrive sur une vraie page, et un rechargement ne renvoie pas le mot de passe. |
| ✘ | Protéger la page en cachant le lien dans le menu | L'adresse reste joignable. Il faut vérifier la session dans la vue. |
| ✘ | Mettre le mot de passe dans la session | Inutile et risqué. Le login suffit : le mot de passe a déjà été vérifié. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. Où Flask range-t-il la session ?
2. Karim clique sur « Se déconnecter », puis rouvre l'adresse /saisir à la main. Peut-il saisir ?
Capsule 5 sur 5Quand le serveur plante
Une vue plante : une clé manque, un calcul échoue. Le serveur répond alors avec le code 500. Mais que voit l'utilisateur ?
Tout dépend du mode du serveur. En mode développement, Flask affiche la trace complète dans le navigateur : le code, les fichiers, les variables. C'est pratique pour le développeur.
En mode production, le navigateur reçoit seulement « Internal Server Error ». La trace part dans le journal du serveur, là où le technicien la lit.
| Mode | Ce que voit le navigateur | Où lire la trace |
|---|---|---|
Développement : flask run --debug |
La trace complète, le code source | Dans la page et dans le terminal |
Production : flask run sans --debug |
« Internal Server Error », rien d'autre | Dans le terminal du serveur, ou son journal |
La règle : la trace ne doit jamais atteindre le navigateur d'un utilisateur. Elle montre le code, les chemins des fichiers, parfois des mots de passe. Et le mode développement de Flask peut même laisser exécuter du code Python depuis la page.
Instant histoire — 2015 : Patreon et le mode développement
En 2015, Patreon est un site qui aide des artistes à se faire financer par leur public.
Une version de développement du site est restée joignable depuis Internet, avec le mode de débogage allumé.
Des pirates s'en servent pour entrer. Les données de près de 2,3 millions de comptes sont volées puis publiées.
Qu'est-ce qui aurait évité ça ?
Ce qu'on retient
Si vous ne devez retenir que trois choses
1. Le serveur construit la page en Python et envoie du HTML. Le navigateur affiche, il ne décide rien.
2. Chaque saisie se vérifie dans la vue, sur le serveur, champ par champ. Même si le navigateur a déjà vérifié.
3. Une page protégée vérifie la session en premier. Et en production, la trace d'erreur reste dans le journal.
Revenons à Karim. Son technicien ouvre /saisir : sans session, il est renvoyé vers la connexion.
Connecté, il choisit un chantier et envoie le formulaire en POST. La vue voit que le chantier est clôturé, et elle refuse avec un message. L'erreur du tableur ne peut plus passer.
Ce que fait le serveur, ce que fait le navigateur
COURS-U6-S2-05
L'essentiel
- Serveur et navigateur : le serveur exécute le Python et renvoie du HTML. Le navigateur affiche, avec le CSS.
- Route, vue, gabarit : la route relie une adresse à une vue. La vue lit les données et les passe au gabarit. Le gabarit remplit le HTML.
- GET et POST : GET met les données dans l'adresse, POST dans le corps de la requête.
- Valider sur le serveur : chaque champ, à chaque fois. Le navigateur peut être contourné.
- Session : une mémoire par visiteur, dans un cookie signé. Une page protégée la vérifie en premier.
- Production : pas de mode débogage. La trace va dans le journal, jamais dans le navigateur.
Les mots à connaître
| Mot | En clair |
|---|---|
| Requête | Le message du navigateur au serveur : une adresse, parfois des données. |
| HTML | Le langage qui décrit le contenu d'une page. |
| CSS | Le fichier qui règle couleurs, tailles et marges. |
| Route | Le lien entre une adresse et une fonction Python. |
| Vue | La fonction Python qui prépare la réponse. |
| Gabarit | Un fichier HTML avec des trous {{ … }}, rempli par Jinja2. |
| Session | Ce que le serveur retient d'un visiteur entre deux requêtes. |
| Redirection | Réponse 302 : « va voir à cette adresse ». |
La méthode : écrire une vue qui reçoit un formulaire
- Vérifiez la session. Pas de login : redirection vers
/connexion. - Distinguez GET et POST avec
request.method. - Lisez chaque champ dans
request.form. - Vérifiez chaque champ : présent, bon type, bonnes limites. Notez chaque erreur dans une liste.
- Répondez : erreurs → réafficher avec les messages. Sinon → enregistrer, puis afficher la confirmation.
Un exemple concret
Un technicien envoie une durée de abc sur le chantier 3.
| Étape | Ce qu'on fait | Résultat |
|---|---|---|
| 1 | "login" in session |
Oui : on continue. |
| 2 | request.method |
"POST" : c'est un envoi. |
| 3 | request.form["duree"] |
Le texte "abc". |
| 4 | float("abc") |
Échec : on ajoute « La durée doit être un nombre ». |
| 5 | Réafficher saisir.html avec erreurs |
Le message s'affiche. Rien n'est enregistré. |
Conclusion : la saisie fausse est refusée par le serveur, avec un message clair.
Les pièges
- ✘ Faire confiance au navigateur → toujours vérifier dans la vue.
- ✘ Un nom différent dans la vue et dans le gabarit →
chantiers=des deux côtés. - ✘ Oublier que tout arrive en texte → convertir avec
int()oufloat(), et prévoir l'échec. - ✘ Laisser
--debugen production → le lancer seulement sur votre poste de travail.
Un mini-site à trois pages
LABO-U6-S2-05
Contexte — Vous travaillez pour la société qui reprend l'informatique de TechnoVert. Karim Benali, le responsable d'atelier, veut que ses techniciens saisissent leurs interventions sur une page web, et plus sur un tableur.
Un collègue a préparé le squelette du site : les adresses, les pages HTML et la feuille de style. Il reste à écrire le code Python qui fait tourner chaque page.
Règle IA de la séance
Scénario 2 : l'IA peut vous expliquer une notion ou un message d'erreur. Elle n'écrit pas votre code.
Si vous lui posez une question, dites-la au formateur au point de contrôle suivant.
À la fin de ce labo, vous saurez écrire les vues d'un site Flask. Elles affichent une liste, traitent un formulaire et protègent une page. Vous saurez aussi lire une erreur serveur.
Comment ça se valide — à chaque point de contrôle, vous montrez votre résultat au formateur. Il valide, puis vous passez à la suite.
Les mots de la séance
| Mot | Ce que ça veut dire |
|---|---|
| Route | La ligne @app.route(...) qui relie une adresse à une fonction. |
| Vue | La fonction Python placée sous la route. C'est elle que vous écrivez. |
| Gabarit | Le fichier HTML avec des trous {{ … }}, dans le dossier templates. Il est fourni. |
| GET, POST | Deux façons d'envoyer des données : dans l'adresse, ou dans le corps de la requête. |
| Session | Ce que le serveur retient d'un visiteur, comme son identifiant de connexion. |
| Trace | Le message que Python affiche quand il plante. On le lit par le bas. |
Ce dont vous avez besoin
- Votre poste Windows, avec Python, Flask, VS Code et un navigateur.
- Annexe : le mini-site de départ et son mode d'emploi. Le dossier se télécharge depuis l'annexe , dans le fichier
A1_mini_site.zip. - Annexe : les gabarits du mini-site.
- Annexe : les données de TechnoVert et les comptes de test.
- Annexe : un extrait de la documentation de Flask, avec les conditions dans les gabarits.
- Annexe : des gabarits complets, à prendre comme modèles.
En binôme — le pilote tape, le lecteur lit les consignes et les annexes à voix haute, et vérifie chaque résultat. On inverse à la partie C.
Le plan du labo
| Partie | Étapes | Ce que vous obtenez | Temps |
|---|---|---|---|
| A — Afficher la liste | 1 à 6 | la page des chantiers remplie par le serveur | 25 min |
| B — Recevoir un formulaire | 7 à 12 | une saisie enregistrée par le serveur | 30 min |
| C — Vérifier et protéger | 13 à 18 | des saisies vérifiées, des pages réservées aux salariés connectés | 40 min |
| D — Lire une erreur serveur | 19 à 22 | une erreur lue au bon endroit, et le mode production | 20 min |
PARTIE A — Afficher la liste
Étapes 1 à 6, environ 25 min. À la fin, la page /chantiers affiche les six chantiers de TechnoVert.
Besoin d'un rappel ? Revoir le cours : « Route, vue, gabarit : le chemin d'une page ».
Installer le mini-site sur votre poste
- Téléchargez
A1_mini_site.zipdepuis l'annexe . - Faites un clic droit sur le fichier, puis « Extraire tout ». Avec « Parcourir », choisissez votre dossier « Documents ».
Vous devez voir : un dossier mini-site dans « Documents ». Il contient app.py, chantiers.json, donnees.py, et les dossiers static et templates.
Ouvrir le dossier dans VS Code
VS Code sert à la fois à modifier le code et à lancer le serveur.
- Ouvrez VS Code. Menu « Fichier », puis « Ouvrir le dossier ». Choisissez
Documents\mini-site. - Ouvrez un terminal : menu « Terminal », puis « Nouveau terminal ». Tapez :
ls
Vous devez voir : la liste des fichiers du dossier, dont app.py et templates.
Si vous voyez une liste qui ne contient pas app.py : vous avez ouvert le mauvais dossier. Ouvrez celui qui contient directement app.py.
Lancer le serveur en mode développement
- Dans le terminal, tapez :
python -m flask --app app run --debug
Les options de cette commande sont expliquées dans l'annexe .
Vous devez voir : entre autres ces lignes.
* Running on http://127.0.0.1:5000
* Debugger is active!
Si vous voyez « python n'est pas reconnu » : tapez py à la place de python.
- Dans votre navigateur, ouvrez
http://127.0.0.1:5000/chantiers.
Vous devez voir : le texte « Page des chantiers : à écrire ». La route existe, mais sa vue n'est pas écrite.
Ce terminal reste ouvert jusqu'à la partie D. En mode développement, le serveur recharge seul le code à chaque enregistrement.
Lire ce que le gabarit attend
Un gabarit a des trous. La vue doit lui donner les bonnes données, sous le bon nom.
- Ouvrez l'annexe et lisez le gabarit
chantiers.html. - Notez le nom de la liste que le gabarit parcourt dans sa boucle
for: - Notez les cinq clés que le gabarit lit pour chaque chantier :
Vous devez avoir : un nom de liste, et cinq clés. Vous les retrouverez dans le code de l'étape 5.
Écrire la vue de la liste
Voici la vue complète. Elle lit les chantiers, puis les passe au gabarit sous le nom attendu.
- Dans VS Code, ouvrez
app.pydepuis la liste des fichiers, à gauche. - Remplacez le corps de
liste_chantiers()par ces deux lignes, avec quatre espaces devant chacune :
chantiers = donnees.lire_chantiers()
return render_template("chantiers.html", chantiers=chantiers)
- Enregistrez avec Ctrl+S. Rechargez la page dans le navigateur.
Vous devez voir : le titre « Les chantiers », la phrase « 6 chantier(s). » et un tableau de six lignes. La première est « Arrosage du parc de la mairie ».
Si vous voyez « IndentationError » dans la page : une ligne n'a pas ses quatre espaces devant. Rouvrez le fichier et alignez-la.
Vérifier que la page vient du serveur
- Dans le navigateur, affichez le code source de la page : Ctrl+U.
- Cherchez le mot
lire_chantiersdans le code source avec Ctrl+F.
Vous devez voir : aucun résultat. Le code source contient seulement du HTML, avec une balise <tr> par chantier.
Pourquoi ? Pourquoi le navigateur ne reçoit-il jamais le code Python de la vue ?
Votre réponse :
POINT DE CONTRÔLE A
- Montrez au formateur la liste des six chantiers, et votre réponse à l'étape 6.
- Le formateur a validé. Passez à la partie B.
PARTIE B — Recevoir un formulaire
Étapes 7 à 12, environ 30 min. À la fin, une intervention saisie dans la page est enregistrée par le serveur.
Besoin d'un rappel ? Revoir le cours : « Recevoir une saisie et la vérifier ».
Afficher le formulaire vide
La page /saisir reçoit deux sortes de requêtes. En GET, on affiche le formulaire vide. En POST, on reçoit la saisie.
- Ouvrez l'annexe et lisez la liste des données qu'attend le gabarit
saisir.html. - Dans
app.py, remplacez le corps desaisir()par :
chantiers = donnees.lire_chantiers()
if request.method == "GET":
return render_template("saisir.html", chantiers=chantiers, services=donnees.SERVICES,
erreurs=[], saisie={}, message=None)
- Enregistrez, puis ouvrez
/saisirdans le navigateur.
Vous devez voir : le formulaire, avec six chantiers dans le premier menu et trois services dans le second.
Trouver dans la doc
Il faut maintenant lire ce que le technicien a tapé. La méthode est dans la documentation.
- Ouvrez l'annexe .
- Trouvez l'objet qui contient les champs envoyés dans le corps d'une requête POST. Notez son nom :
- Trouvez l'objet qui contient les données envoyées dans l'adresse. Notez son nom :
Vous devez avoir : deux noms qui commencent par request.. Le premier sert à l'étape 9.
Enregistrer la saisie
Pour l'instant, il n'y a pas de connexion. Le technicien s'appelle donc « test ». La partie C remplacera ce nom par le vrai.
- Ajoutez ces lignes à la fin de
saisir(), sous le blocifde l'étape 7 :
saisie = request.form
numero = donnees.ajouter_intervention(int(saisie["chantier"]), "test", saisie["service"],
saisie["date"], float(saisie["duree"]),
saisie["description"], saisie["blocage"])
return render_template("saisir.html", chantiers=chantiers, services=donnees.SERVICES,
erreurs=[], saisie={}, message=f"Intervention n° {numero} enregistrée.")
- Enregistrez le fichier.
Vous devez voir : la page /saisir s'affiche toujours. Aucune trace d'erreur dans le premier terminal.
Envoyer une saisie juste
- Dans le formulaire, choisissez le chantier 3, le service « atelier », la date
2026-09-29, la durée2.5et la description « Contrôle des bornes ». Laissez le blocage vide. Cliquez sur « Enregistrer ».
Vous devez voir : le message vert « Intervention n° 13 enregistrée. ».
- Ouvrez un second terminal avec le bouton « + » du panneau Terminal. Le premier fait toujours tourner le serveur.
- Vérifiez que le fichier de données a changé :
Select-String "Contrôle des bornes" chantiers.json
Vous devez voir : une ligne qui contient "description": "Contrôle des bornes", précédée du nom du fichier et du numéro de la ligne.
Voir la différence entre GET et POST
- Regardez la barre d'adresse du navigateur, juste après votre envoi. Notez l'adresse affichée :
- Ouvrez maintenant
/chantiers?service=atelier. - Dans le premier terminal, cherchez dans le journal du serveur la ligne
POST /saisiret la ligneGET /chantiers?service=atelier. Ignorez les lignes/static/style.csset/favicon.ico. Elles ressemblent à :
127.0.0.1 - - [29/Sep/2026 14:02:11] "POST /saisir HTTP/1.1" 200 -
127.0.0.1 - - [29/Sep/2026 14:03:40] "GET /chantiers?service=atelier HTTP/1.1" 200 -
Vous devez voir : la saisie n'apparaît nulle part dans l'adresse ni dans le journal. Le mot atelier, lui, apparaît dans les deux.
Pourquoi ? Un collègue propose d'envoyer le mot de passe de connexion en GET. Pourquoi est-ce une mauvaise idée ?
Votre réponse :
Envoyer une saisie fausse
- Remplissez le formulaire avec le chantier 3, le service « atelier », la date
2026-09-29et la duréedeux heures. Cliquez sur « Enregistrer ».
Vous devez voir : une page d'erreur de Flask. Tout en bas, la ligne ValueError: could not convert string to float: 'deux heures'.
- Notez la ligne de
app.pyindiquée juste au-dessus de cette dernière ligne :
Pourquoi ? Le serveur a planté au lieu d'afficher un message. Qu'est-ce qui manque dans la vue ?
Votre réponse :
POINT DE CONTRÔLE B
- Montrez au formateur la ligne trouvée à l'étape 10, et vos réponses aux étapes 11 et 12.
- Le formateur a validé. Passez à la partie C.
PARTIE C — Vérifier et protéger
Étapes 13 à 18, environ 40 min. À la fin, les saisies fausses sont refusées avec un message, et seuls les salariés connectés peuvent saisir.
Besoin d'un rappel ? Revoir le cours : « Se souvenir de l'utilisateur : la session ».
Changez de rôle : le lecteur devient pilote.
Vérifier la durée et le chantier
On vérifie chaque champ avant d'enregistrer. Chaque erreur trouvée va dans une liste. Voici les deux premières vérifications, écrites en entier.
- Dans
saisir(), remplacez les lignes de l'étape 9, desaisie = request.formjusqu'à la fin, par :
saisie = request.form
erreurs = []
chantier = None
if saisie["chantier"].isdigit():
chantier = donnees.lire_chantier(int(saisie["chantier"]))
if chantier is None:
erreurs.append("Choisissez un chantier.")
elif chantier["statut"] == "cloture":
erreurs.append("Ce chantier est clôturé : saisie refusée.")
duree = 0
try:
duree = float(saisie["duree"])
if duree <= 0 or duree > 12:
erreurs.append("La durée doit être entre 0 et 12 heures.")
except ValueError:
erreurs.append("La durée doit être un nombre, par exemple 2.5.")
# étape 14 : vos trois vérifications ici
if erreurs:
return render_template("saisir.html", chantiers=chantiers, services=donnees.SERVICES,
erreurs=erreurs, saisie=saisie, message=None)
numero = donnees.ajouter_intervention(chantier["id"], "test", saisie["service"],
saisie["date"], duree,
saisie["description"], saisie["blocage"])
return render_template("saisir.html", chantiers=chantiers, services=donnees.SERVICES,
erreurs=[], saisie={}, message=f"Intervention n° {numero} enregistrée.")
- Enregistrez, puis renvoyez la saisie fausse de l'étape 12.
Vous devez voir : le formulaire revient, avec le message rouge « La durée doit être un nombre, par exemple 2.5. ». Les champs déjà remplis sont gardés.
Écrire les trois autres vérifications
Chaque vérification tient en deux lignes : un if, puis erreurs.append(...). Voici la première, écrite en entier. Elle refuse un service qui n'est pas dans la liste.
if saisie["service"] not in donnees.SERVICES:
erreurs.append("Choisissez un service dans la liste.")
- Sur ce modèle, écrivez les deux autres. Aidez-vous du tableau « Quelques outils Python du labo » des RAPPELS :
len(x)donne la longueur d'un texte,x.strip()enlève les espaces.
| Champ | Le test à écrire | Message à afficher |
|---|---|---|
| date | le texte ne fait pas 10 caractères : len(saisie["date"]) != 10 |
La date s'écrit AAAA-MM-JJ. |
| description | le texte sans espaces est vide : saisie["description"].strip() == "" |
Décrivez ce qui a été fait. |
- Écrivez vos trois vérifications dans
app.py, à la place du commentaire# étape 14, puis enregistrez.
Vous devez voir : la page /saisir s'affiche toujours, sans trace d'erreur.
Tester les vérifications
- Envoyez chaque saisie du tableau. Pour chacune, notez ce que la page affiche.
| Saisie | Résultat attendu | Ce que vous obtenez |
|---|---|---|
chantier 1, atelier, 2026-09-29, 3, « Contrôle » |
refusée : chantier clôturé | |
chantier 4, aucun service, 2026-09-29, 3, « Contrôle » |
refusée : service | |
chantier 4, atelier, 29/09, 3, « Contrôle » |
refusée : date | |
chantier 4, atelier, 2026-09-29, 40, description vide |
refusée : deux messages | |
chantier 4, atelier, 2026-09-29, 3, « Contrôle des mâts » |
acceptée |
Vous devez voir : exactement le résultat attendu sur chaque ligne. La dernière affiche un numéro d'intervention.
Pourquoi ? Le menu déroulant ne propose que trois services. Pourquoi vérifier quand même le service sur le serveur ?
Votre réponse :
Écrire la connexion
La connexion vérifie le compte, puis écrit le login dans la session. La fonction verifier_compte() est fournie : l'annexe la décrit.
- Remplacez le corps de
connexion()par :
if request.method == "POST":
login = request.form["login"]
if donnees.verifier_compte(login, request.form["mot_de_passe"]):
session["login"] = login
return redirect(url_for("liste_chantiers"))
return render_template("connexion.html", erreur="Identifiant ou mot de passe incorrect.")
return render_template("connexion.html", erreur=None)
- Remplacez le corps de
deconnexion()par ces deux lignes :
session.clear()
return redirect(url_for("connexion"))
- Ouvrez
/connexionet connectez-vous avec un compte de l'annexe .
Vous devez voir : vous arrivez sur la liste des chantiers. Le bandeau affiche « Connecté : » suivi de votre identifiant.
Protéger les deux pages
Une page protégée regarde la session avant tout le reste.
- Ajoutez ces deux lignes au tout début de
liste_chantiers()et au tout début desaisir(), avant toute autre ligne du corps :
if "login" not in session:
return redirect(url_for("connexion"))
- Dans
saisir(), dans l'appel àajouter_intervention, remplacez"test"parsession["login"]. Enregistrez.
Vous devez voir : tant que vous êtes connecté, les deux pages s'affichent comme avant.
Tester la session
- Faites chaque essai du tableau, dans l'ordre. Après chaque essai, cherchez dans le journal du premier terminal la ligne de la requête que vous venez de faire :
GET /saisirouPOST /connexion. Relevez le code de trois chiffres à la fin de cette ligne. Les lignes qui suivent viennent du navigateur, qui charge la page suivante,style.cssetfavicon.ico: ignorez-les. Attention : une connexion ratée renvoie 200, car la page est réaffichée avec le message.
| Essai | Résultat attendu | Code relevé |
|---|---|---|
Cliquez sur « Se déconnecter », puis ouvrez /saisir |
renvoyé vers la connexion | |
| Connectez-vous avec un mauvais mot de passe | message rouge, pas de connexion | |
| Connectez-vous avec le bon mot de passe | arrivée sur la liste | |
Ouvrez /saisir |
le formulaire s'affiche |
- Ouvrez les outils du navigateur avec F12. Dans l'onglet « Application » ou « Stockage », ouvrez « Cookies ».
Vous devez voir : un cookie nommé session, avec une valeur en trois morceaux séparés par des points.
Pourquoi ? Un utilisateur modifie la valeur de ce cookie pour y mettre le login d'un autre salarié. Pourquoi le serveur refuse-t-il ce cookie ?
Votre réponse :
POINT DE CONTRÔLE C
- Montrez au formateur votre grille de l'étape 15 et votre tableau de l'étape 18.
- Le formateur a validé. Passez à la partie D.
PARTIE D — Lire une erreur serveur
Étapes 19 à 22, environ 20 min. À la fin, vous savez où lire une erreur, et ce que voit l'utilisateur en production.
Besoin d'un rappel ? Revoir le cours : « Quand le serveur plante ».
Lire l'erreur dans le navigateur
Un collègue a écrit une page /bilan qui additionne les heures. Elle plante.
- Ouvrez
/bilandans le navigateur. - Lisez la trace par le bas. Notez le type d'erreur et la valeur entre guillemets :
- Notez le numéro de la ligne de
app.pyqui plante :
Vous devez voir : une page d'erreur de Flask, avec des morceaux du code de app.py.
Lire la même erreur dans le journal
- Regardez le premier terminal, celui du serveur.
Vous devez voir : la même trace, et une ligne qui se termine par "GET /bilan HTTP/1.1" 500 -.
- Notez le code de réponse de cette ligne :
Passer en mode production
- Dans le premier terminal, arrêtez le serveur avec Ctrl+C.
- Relancez-le sans le mode développement :
python -m flask --app app run
- Rechargez
/bilandans le navigateur.
Vous devez voir : seulement « Internal Server Error », et une phrase en anglais. Aucune ligne de code. La trace complète est toujours dans le terminal.
Pourquoi ? Pourquoi l'utilisateur ne doit-il pas voir la trace de l'étape 19 ?
Votre réponse :
Trouver dans la doc et corriger
La ligne fautive lit une clé qui n'existe pas. Le nom juste est dans la description des données.
- Ouvrez l'annexe . Trouvez le nom exact de la clé qui contient la durée d'une intervention. Notez-le :
- Corrigez la ligne de
bilan()avec ce nom. Enregistrez. - En mode production, le serveur ne recharge pas le code seul. Arrêtez-le avec Ctrl+C, relancez-le, puis rechargez
/bilan.
Vous devez voir : la phrase « Total des heures saisies : » suivie d'un nombre d'heures.
POINT DE CONTRÔLE D
- Montrez au formateur la page
/bilancorrigée, et vos réponses aux étapes 19 à 21. - Le formateur a validé. Le labo est fini.
POUR ALLER PLUS LOIN
10 à 15 min, si vous avez fini avant la fin de la séance.
Un collègue a demandé à une IA d'écrire une page de connexion. Voici ce qu'elle a produit. Le code marche, mais il contient trois erreurs dangereuses.
@app.route("/connexion")
def connexion():
login = request.args.get("login")
mot_de_passe = request.args.get("mot_de_passe")
if login and donnees.verifier_compte(login, mot_de_passe):
session["login"] = login
session["mot_de_passe"] = mot_de_passe
return redirect(url_for("liste_chantiers"))
return render_template("connexion.html", erreur=None)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
- Trouvez les trois erreurs avant la mise en production. Pour chacune, notez la ligne et le danger.
| Ligne | L'erreur | Le danger |
|---|---|---|
CE QU'IL FAUT RETENIR
- La vue passe les données au gabarit sous le nom exact que le gabarit attend.
- Chaque champ reçu est du texte : on le vérifie, on le convertit, et on prévoit l'échec.
- Une page protégée vérifie la session en premier. En production, pas de
--debug: la trace reste dans le journal.
Brancher l'application sur la base
COURS-U6-S2-06
L'application de suivi des chantiers de TechnoVert marche. Les techniciens saisissent leurs interventions chaque soir. Mais les données sont dans un simple fichier, sur le serveur web.
Hélène Vasseur, la dirigeante, veut qu'elles passent dans la base MySQL de l'entreprise. Elle a une question : « Et si quelqu'un tape n'importe quoi dans une page, il peut abîmer la base ? »
La réponse est oui, si l'application est mal écrite. À la fin de la séance, vous saurez brancher l'application sur la base sans lui ouvrir cette porte.
Le plan de la séance
- Parler à la base depuis Python.
- L'injection SQL et la requête paramétrée.
- Une couche d'accès aux données.
- Les identifiants hors du code.
- Un jeu de données qu'on peut rejouer.
La question de départ
Sur un site, vous tapez une apostrophe dans un champ de recherche. La page affiche une erreur de base de données. Comment un seul caractère peut-il faire planter le site ?
Capsule 1 sur 5Parler à la base depuis Python
Commençons par le branchement lui-même. Comment un programme Python envoie-t-il une requête SQL à MySQL ?
Il passe par un CONNECTEUR : une bibliothèque Python qui sait parler à MySQL. Ici, c'est mysql.connector. Le connecteur ouvre une connexion, puis on lui confie un CURSEUR : l'objet qui envoie les requêtes et ramène les lignes.
Pensez à un appel téléphonique. On compose le numéro, on parle, on écoute la réponse, on raccroche.
Le connecteur, c'est le téléphone. Le curseur, c'est la conversation.
- Ouvrir la connexion : l'adresse du serveur, le compte, le mot de passe, la base.
- Créer un curseur.
- Exécuter la requête avec
execute(). - Lire les lignes avec
fetchall(). Pour une écriture, valider aveccommit(). - Fermer la connexion avec
close().
import mysql.connector
connexion = mysql.connector.connect(host="127.0.0.1", user="tv_appli",
password="…", database="technovert")
curseur = connexion.cursor(dictionary=True)
curseur.execute("SELECT id, nom, statut FROM chantier")
chantiers = curseur.fetchall() # une liste de dictionnaires
connexion.close()
Avec dictionary=True, chaque ligne arrive sous forme de dictionnaire : {"id": 3, "nom": "Éclairage des allées", ...}. C'est la forme qu'attendent les gabarits.
On le fait ensemble — ajouter une intervention
1. Quelle est la première chose à faire ?
2. Quelle requête le curseur exécute-t-il ?
3. Faut-il fetchall() ?
4. Que faut-il ajouter avant de fermer ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | Ouvrir, exécuter, lire, fermer, dans la même fonction | La connexion ne reste pas ouverte pour rien. |
| ✘ | Un INSERT sans commit() |
Aucune erreur ne s'affiche, mais la ligne n'est jamais enregistrée. |
| ✘ | Oublier close() dans une page très visitée |
Les connexions s'accumulent. MySQL finit par refuser les nouvelles. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. Quel objet envoie la requête et ramène les lignes ?
2. Un UPDATE qui clôture un chantier marche, mais le changement disparaît. Qu'est-ce qui manque ?
Capsule 2 sur 5L'injection SQL et la requête paramétrée
On sait maintenant envoyer une requête. Souvent, elle contient une valeur tapée par l'utilisateur : un service, un nom. Comment l'y mettre ?
La mauvaise façon, c'est de coller la saisie dans le texte de la requête. L'utilisateur peut alors taper un morceau de SQL. C'est l'INJECTION SQL : la saisie devient une partie de l'ordre donné à la base.
C'est comme un bon de commande où le client remplit la case « quantité ». S'il écrit « 2, et offrez-moi le reste du magasin », et que personne ne relit, le magasinier exécute tout.
- Le code colle la saisie dans la requête, entre apostrophes.
- L'utilisateur tape une apostrophe : elle ferme le texte trop tôt.
- La suite de la saisie devient du SQL, que la base exécute.
- La base fait ce que l'utilisateur a écrit, pas ce que le développeur voulait.
Code : "... WHERE technicien = '" + login + "' AND service = '" + service + "'"
Saisie : x' OR '1'='1
Requête : ... WHERE technicien = 'ngarcia' AND service = 'x' OR '1'='1'
'1'='1' est toujours vrai. La base renvoie les interventions de tous les techniciens.
La parade s'appelle la REQUÊTE PARAMÉTRÉE. On écrit %s à la place de chaque valeur.
On donne les valeurs à part, dans un tuple. Le connecteur les envoie comme des données, jamais comme du SQL.
curseur.execute("SELECT ... WHERE technicien = %s AND service = %s", (login, service))
Avec la même saisie, la base cherche un service qui s'appelle vraiment x' OR '1'='1. Elle n'en trouve aucun.
On le fait ensemble — la recherche d'un chantier par nom
1. Le code écrit "... WHERE nom = '" + nom + "'". Est-il vulnérable ?
2. Par quoi remplace-t-on '" + nom + "' ?
3. Comment donne-t-on la valeur ?
4. Pourquoi une virgule dans (nom,) ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | execute("... WHERE id = %s", (numero,)) |
La valeur part à part : elle ne peut pas changer la requête. |
| ✘ | execute(f"... WHERE service = '{service}'") |
Une f-string colle la saisie, exactement comme +. |
| ✘ | "... WHERE service = '%s'" avec des apostrophes |
Le connecteur ajoute les siennes. Le texte obtenu est faux. |
Instant histoire — 2015 : TalkTalk et ses vieilles pages
En 2015, TalkTalk est un grand opérateur de téléphone et d'Internet au Royaume-Uni.
Des pirates, dont des adolescents, trouvent d'anciennes pages de son site qui collent la saisie dans leurs requêtes.
Par injection SQL, ils volent les données d'environ 157 000 clients. L'entreprise reçoit une amende record de 400 000 livres.
Qu'est-ce qui aurait évité ça ?
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. Réécrivez "SELECT * FROM client WHERE ville = '" + ville + "'" en requête paramétrée.
2. Un collègue dit : « Mon menu déroulant ne propose que trois services, pas de risque. » A-t-il raison ?
Une règle de droit, enfin. Tester une injection sur un site qui n'est pas le vôtre, sans autorisation écrite, est un délit. On ne s'entraîne que sur ses propres machines.
Capsule 3 sur 5Une couche d'accès aux données
La requête est sûre. Reste à savoir où ranger tout ce SQL. Dans chaque vue, au milieu du code de la page ?
Non. On le range dans une COUCHE D'ACCÈS AUX DONNÉES : un module Python à part, qui contient toutes les requêtes.
Les vues appellent ses fonctions, comme lire_chantiers(). Elles ne voient jamais de SQL.
Au restaurant, le serveur ne va pas en cuisine. Il passe la commande au passe-plat, et le plat revient. Si la cuisine change de four, la salle ne voit aucune différence.
- Toutes les requêtes vont dans un seul fichier, par exemple
acces_bdd.py. - Chaque besoin devient une fonction au nom clair :
lire_chantier(numero). - Les vues appellent ces fonctions et reçoivent des listes ou des dictionnaires.
- La couche traduit les pannes : si MySQL ne répond pas, elle lève une erreur à elle,
BaseIndisponible. - L'application répond proprement à cette erreur : une page « Service indisponible », code 503.
Le code 503 veut dire : le serveur marche, mais il attend un autre service qui ne répond pas. L'utilisateur voit un message clair, pas une trace.
On le fait ensemble — la base s'arrête pendant la nuit
1. Où l'erreur de connexion apparaît-elle en premier ?
2. Que fait ouvrir() de cette erreur ?
3. Que voit le technicien qui ouvre la liste ?
4. Où le technicien de maintenance lit-il la vraie cause ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | On passe du fichier à MySQL en changeant une ligne d'import | Les vues n'ont pas bougé : c'est la preuve que la couche est bien isolée. |
| ✘ | Un SELECT écrit dans la vue liste_chantiers() |
Le SQL se disperse. Pour vérifier les injections, il faut relire tout le site. |
| ✘ | La trace MySQL affichée à l'utilisateur quand la base tombe | Elle montre le nom du serveur et du compte. Et l'utilisateur ne sait pas quoi faire. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. Vous cherchez toutes les requêtes de l'application pour les vérifier. Où regardez-vous ?
2. Que veut dire le code 503 ?
Capsule 4 sur 5Les identifiants hors du code
La couche d'accès se connecte avec un compte et un mot de passe. Où les écrire ?
Surtout pas dans le code. Le code se copie, s'envoie, se range dans un dépôt partagé. Le mot de passe partirait avec.
On le met dans une VARIABLE D'ENVIRONNEMENT : une valeur que le système donne au programme quand il le lance.
C'est comme le code du portail d'une résidence. On ne le grave pas sur le portail. Chaque habitant le connaît, et on le change sans changer le portail.
- On écrit les identifiants dans un fichier d'environnement, hors du dossier du code.
- On le protège : on le range dans son dossier personnel, que les autres comptes de la machine ne lisent pas. Sur un serveur Linux, on ajoute
chmod 600. - L'application le charge au démarrage, avec
load_dotenv(). - Le code les lit avec
os.environ.
Fichier technovert.env (dossier perso) Code de acces_bdd.py
TV_BDD_UTILISATEUR=tv_appli user=os.environ["TV_BDD_UTILISATEUR"]
TV_BDD_MOT_DE_PASSE=… password=os.environ["TV_BDD_MOT_DE_PASSE"]
Autre gain : le même code tourne sur votre poste de test et sur le serveur de production. Seul le fichier d'environnement change.
On le fait ensemble — la clé secrète de la session
1. Le code contient app.secret_key = "cle-de-labo". Est-ce un secret ?
2. Où la mettre ?
3. Qu'écrit-on dans le code ?
| Exemple | Pourquoi | |
|---|---|---|
| ✔ | Un fichier d'exemple livré avec le code, avec a-remplacer comme valeur |
Il montre les noms attendus, sans aucun vrai secret. |
| ✘ | Le fichier d'environnement rangé dans le dossier du code | Il part dans le ZIP ou dans le dépôt, avec le code. |
| ✘ | Le mot de passe écrit en commentaire, « pour mémoire » | Un commentaire se lit aussi bien qu'une ligne de code. |
Checkpoint
À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.
1. On passe de votre poste de test au serveur de production. Que modifie-t-on ?
2. Sur le serveur Linux de production, que fait chmod 600 sur le fichier d'environnement ?
Capsule 5 sur 5Un jeu de données qu'on peut rejouer
Dernier outil, pour tester sans crainte. Pendant un test, on ajoute des lignes, on en casse. Comment revenir à un état connu ?
On garde un jeu de données de test : un script SQL qui efface la base et la recrée, toujours à l'identique. On le rejoue avant chaque série de tests. Un script de vérification compte ensuite les lignes et teste les cas connus.
mysql -u root -e "source technovert.sql" # la base revient à l'état connu
python verifier_base.py # chaque ligne doit dire OK
Le même couple sert à prouver une restauration. On restaure la base sur une machine neuve, puis on lance la vérification. Si tout est OK, la restauration a marché.
Ce qu'on retient
Si vous ne devez retenir que trois choses
1. Une valeur saisie ne se colle jamais dans une requête : on écrit %s et on passe la valeur à part.
2. Tout le SQL vit dans la couche d'accès. Les vues appellent des fonctions, et une base arrêtée donne une page 503 propre.
3. Les identifiants vivent dans un fichier d'environnement protégé, jamais dans le code.
Revenons à Hélène Vasseur. Oui, quelqu'un peut taper n'importe quoi dans une page.
Avec des requêtes paramétrées, sa saisie reste une simple donnée, et la base ne l'exécute pas. Et le mot de passe de la base n'est écrit nulle part dans le code.
Brancher l'application sur la base
COURS-U6-S2-06
L'essentiel
- Connecteur :
mysql.connectorrelie Python à MySQL. Ouvrir, curseur,execute(),fetchall()oucommit(),close(). - Injection SQL : une saisie collée dans la requête devient du SQL.
- Requête paramétrée :
%sdans la requête, les valeurs à part dans un tuple. - Couche d'accès : tout le SQL dans un seul module. Les vues appellent ses fonctions.
- Base indisponible : la couche lève
BaseIndisponible, l'application répond 503 avec une page claire. - Identifiants : dans un fichier d'environnement protégé, lus avec
os.environ. - Jeu de test : un script SQL rejouable, puis un script de vérification.
Les mots à connaître
| Mot | En clair |
|---|---|
| Connecteur | La bibliothèque qui permet à Python de parler à MySQL. |
| Curseur | L'objet qui envoie une requête et ramène les lignes. |
| Injection SQL | Une saisie qui change la requête envoyée à la base. |
| Requête paramétrée | Une requête avec %s, dont les valeurs sont envoyées à part. |
| Couche d'accès | Le module qui contient toutes les requêtes de l'application. |
| Variable d'environnement | Une valeur donnée au programme par le système, hors du code. |
| Bibliothèque, module | Un fichier de code déjà écrit qu'on réutilise. Le connecteur est une bibliothèque ; la couche d'accès est un module. |
127.0.0.1 |
L'adresse de la machine elle-même. La base tourne sur la même machine que l'application. |
chmod 600 |
Sur un serveur Linux, rend un fichier lisible par son seul propriétaire. On l'utilise pour le fichier d'environnement. |
| f-string | Un texte Python écrit f"...{x}...", où {x} est remplacé par la valeur. À ne jamais utiliser pour une requête SQL. |
La méthode : écrire une fonction de la couche d'accès
- Ouvrez la connexion avec
ouvrir(). - Écrivez la requête avec un
%spar valeur, sans apostrophes autour. - Exécutez avec les valeurs dans un tuple :
(valeur,). - Lisez avec
fetchall(), ou validez aveccommit(). - Fermez la connexion, puis renvoyez le résultat.
Un exemple concret
Lire les interventions d'un service, sans risque d'injection.
def interventions_du_service(service):
connexion = ouvrir()
curseur = connexion.cursor(dictionary=True)
curseur.execute("SELECT * FROM intervention WHERE service = %s", (service,))
lignes = curseur.fetchall()
connexion.close()
return lignes
| Saisie | Ce que la base reçoit | Résultat |
|---|---|---|
atelier |
le service atelier |
les interventions de l'atelier |
x' OR '1'='1 |
un service nommé x' OR '1'='1 |
aucune ligne |
Conclusion : la saisie piégée est traitée comme une simple donnée.
Les pièges
- ✘ Une f-string dans
execute()→ toujours%set un tuple. - ✘
'%s'entre apostrophes →%sseul. - ✘ Oublier
commit()après une écriture → l'écriture est perdue. - ✘ Le mot de passe dans le code → fichier d'environnement, rangé dans votre dossier personnel.
Injection, correction, restauration
LABO-U6-S2-06
Contexte — Vous reprenez l'application de suivi des chantiers de TechnoVert. Elle marche, mais elle range ses données dans un fichier. Hélène Vasseur veut qu'elles passent dans la base MySQL de l'entreprise.
Un collègue a repris une vieille page écrite par l'ancien prestataire. Elle colle la saisie dans sa requête. Vous allez le prouver, puis corriger.
Règle IA de la séance
Scénario 2 : l'IA peut vous expliquer une notion ou un message d'erreur. Elle n'écrit pas votre code.
Si vous lui posez une question, dites-la au formateur au point de contrôle suivant.
À la fin de ce labo, vous saurez brancher une application sur MySQL, isoler l'accès aux données, corriger une injection SQL et prouver une restauration.
Comment ça se valide — à chaque point de contrôle, vous montrez votre résultat au formateur. Il valide, puis vous passez à la suite.
Les mots de la séance
| Mot | Ce que ça veut dire |
|---|---|
| Connecteur | La bibliothèque mysql.connector qui relie Python à MySQL. |
| Curseur | L'objet qui envoie une requête et ramène les lignes. |
| Injection SQL | Une saisie qui change la requête envoyée à la base. |
| Requête paramétrée | Une requête avec %s, dont les valeurs sont passées à part. |
| Couche d'accès | Le module acces_bdd.py qui contient toutes les requêtes. |
| Restaurer | Recréer la base à l'identique à partir d'un fichier d'export. |
Ce dont vous avez besoin
- Votre poste Windows, avec XAMPP, Python, VS Code et un navigateur.
- Annexe : l'application de départ et son mode d'emploi. Le dossier se télécharge dans
A5_technovert_web.zip. - Annexe : le jeu de données de la base, fichier
A6_technovert.sql. - Annexe : le compte MySQL de l'application, fichier
A7_compte_appli.sql. - Annexe : le fichier d'environnement à remplir.
- Annexe : un extrait de la documentation du connecteur MySQL.
- Annexe : le script de vérification, fichier
A10_verifier_base.py.
En binôme — le pilote tape, le lecteur lit les consignes et les annexes à voix haute, et vérifie chaque résultat. On inverse à la partie C.
Le plan du labo
| Partie | Étapes | Ce que vous obtenez | Temps |
|---|---|---|---|
| A — Brancher l'application | 1 à 6 | l'application qui lit la base MySQL | 35 min |
| B — Isoler et sortir les secrets | 7 à 11 | une couche d'accès propre, sans mot de passe dans le code | 25 min |
| C — Injecter puis corriger | 12 à 17 | une injection prouvée, puis rendue inoffensive | 35 min |
| D — Exporter et restaurer | 18 à 22 | une base restaurée sur une pile vierge, vérifiée | 25 min |
PARTIE A — Brancher l'application
Étapes 1 à 6, environ 35 min. À la fin, l'application affiche les chantiers lus dans MySQL.
Besoin d'un rappel ? Revoir le cours : « Parler à la base depuis Python ».
Installer l'application et lancer MySQL
- Téléchargez
A5_technovert_web.zipdepuis l'annexe . Faites un clic droit dessus, « Extraire tout », puis avec « Parcourir », choisissez votre dossier « Documents ». - Ouvrez le dossier
Documents\technovert-webdans VS Code. Ouvrez un terminal : menu « Terminal », puis « Nouveau terminal ». - Ouvrez le panneau de contrôle de XAMPP. Cliquez sur « Start », en face de « MySQL ».
Vous devez voir : « MySQL » en vert dans le panneau de XAMPP. Dans VS Code, le dossier contient app.py, acces_bdd.py et les fichiers A6_technovert.sql, A7_compte_appli.sql, A10_verifier_base.py.
Créer la base et son compte
- Dans le terminal, vérifiez que la commande
mysqlrépond :
mysql --version
Si vous voyez « mysql n'est pas reconnu » : suivez l'annexe , « Rendre la commande mysql disponible », puis rouvrez VS Code.
- Chargez le jeu de données, puis le compte de l'application :
mysql -u root -e "source A6_technovert.sql"
mysql -u root -e "source A7_compte_appli.sql"
- Vérifiez que la base existe :
mysql -u root -e "SELECT COUNT(*) FROM technovert.chantier;"
Vous devez voir : aucun message pour les deux premières commandes, puis un petit tableau avec le nombre 6.
Écrire la connexion et la lecture des chantiers
La couche d'accès est le fichier acces_bdd.py. Ses premières lignes, déjà écrites, chargent vos identifiants avec load_dotenv. Deux fonctions sont à écrire.
- Dans
acces_bdd.py, remplacez tout le corps deouvrir(),passcompris, par :
try:
return mysql.connector.connect(
host=os.environ["TV_BDD_HOTE"],
user=os.environ["TV_BDD_UTILISATEUR"],
password=os.environ["TV_BDD_MOT_DE_PASSE"],
database=os.environ["TV_BDD_BASE"],
connection_timeout=3)
except mysql.connector.Error as erreur:
raise BaseIndisponible(str(erreur)) from erreur
- Remplacez tout le corps de
lire_chantiers(),return []compris, par :
connexion = ouvrir()
curseur = connexion.cursor(dictionary=True)
curseur.execute(COLONNES_CHANTIER + "ORDER BY ch.date_debut")
lignes = curseur.fetchall()
connexion.close()
return lignes
- Lisez la ligne
COLONNES_CHANTIER, écrite en haut du fichier. Notez la table reliée àchantierpar leJOIN:
Le + colle ici deux textes fixes, écrits par vous. Ce n'est pas une saisie d'utilisateur : il n'y a donc aucun risque d'injection.
Vous devez avoir : un fichier enregistré avec Ctrl+S, et le nom d'une table relié par le JOIN. On teste le tout à l'étape 5.
Pourquoi ? Pourquoi le SELECT relie-t-il deux tables au lieu de lire la seule table chantier ?
Votre réponse :
Remplir le fichier d'environnement
Le fichier d'environnement se range dans votre dossier personnel, jamais dans le dossier du code. L'application le lit seule au démarrage.
- Copiez le modèle de l'annexe dans votre dossier personnel :
Copy-Item technovert.env.exemple $HOME\technovert.env
- Ouvrez-le avec
notepad $HOME\technovert.env. Mettez le mot de passe du comptetv_applide l'annexe , et une longue valeur au hasard pourTV_CLE_SECRETE. Enregistrez. - Vérifiez son contenu :
Get-Content $HOME\technovert.env
Vous devez voir : quatre lignes de commentaire, puis vos cinq lignes TV_…, sans aucun a-remplacer. Les accents des commentaires peuvent mal s'afficher : ce n'est pas grave.
Basculer l'application sur la base
L'application importe encore l'ancienne couche, celle du fichier. On la fait pointer sur la base.
- Ouvrez
app.py. Repérez la ligneimport donnees. - Remplacez-la par :
import acces_bdd as donnees
- Remplacez la ligne de la clé secrète par :
app.secret_key = os.environ["TV_CLE_SECRETE"]
- Dans
app.py, juste au-dessus defrom flask import …, ajoutez la ligneimport os. Enregistrez, puis lancez le serveur :
python -m flask --app app run --debug
- Connectez-vous sur
/connexionavec le comptengarcia, mot de passeChantier2026. Les autres logins sont dans l'annexe , tablecompte. Le mot de passe de test est le même pour tous. Puis ouvrezhttp://127.0.0.1:5000/chantiers.
Vous devez voir : les six chantiers, comme avant. Cette fois, ils viennent de MySQL.
Si vous voyez la page « Service momentanément indisponible » : MySQL est arrêté dans XAMPP, ou le mot de passe de technovert.env est faux. Corrigez, puis rechargez la page.
Vérifier que les données viennent de la base
- Ouvrez un second terminal avec le bouton « + » du panneau Terminal. Le premier fait tourner le serveur.
- Changez un nom de chantier directement dans la base :
mysql -u root -e "UPDATE technovert.chantier SET nom='Essai base' WHERE id=1;"
- Rechargez
/chantiersdans le navigateur.
Vous devez voir : le premier chantier s'appelle maintenant « Essai base ». L'application lit bien la base en direct.
POINT DE CONTRÔLE A
- Montrez au formateur la liste avec « Essai base », et votre réponse à l'étape 3.
- Le formateur a validé. Passez à la partie B.
PARTIE B — Isoler et sortir les secrets
Étapes 7 à 11, environ 25 min. À la fin, tout le SQL est dans la couche d'accès, et aucun mot de passe n'est dans le code.
Besoin d'un rappel ? Revoir le cours : « Une couche d'accès aux données ».
Vérifier que les vues ne contiennent pas de SQL
- Cherchez le mot
SELECTdans le fichier des vues :
Select-String -Pattern "SELECT", "INSERT", "mysql" -Path app.py
Vous devez voir : aucune ligne. Tout le SQL est dans acces_bdd.py.
Pourquoi ? Pourquoi ranger toutes les requêtes dans un seul fichier, au lieu d'en mettre dans chaque vue ?
Votre réponse :
Provoquer une panne de base
- Laissez le serveur tourner. Dans le panneau de XAMPP, cliquez sur « Stop », en face de « MySQL ».
- Rechargez
/chantiersdans le navigateur.
Vous devez voir : la page « Service momentanément indisponible », pas une trace d'erreur.
Lire la vraie cause dans le journal
- Regardez le terminal du serveur.
Vous devez voir : une ligne qui contient ERROR in app: Base indisponible, suivie de la cause. La ligne juste en dessous se termine par 503 -.
- Dans le panneau de XAMPP, cliquez sur « Start » en face de « MySQL ». Rechargez la page.
Vous devez voir : la liste des chantiers revient.
Trouver dans la doc
- Ouvrez l'annexe .
- Trouvez comment passer une valeur à une requête sans la coller dans le texte. Notez le symbole employé dans la requête :
- Notez sous quelle forme les valeurs sont passées à
execute():
Vous devez avoir : un symbole, et le mot « tuple ». Ils servent en partie C.
Vérifier qu'aucun secret n'est dans le code
- Cherchez un mot de passe en clair dans les fichiers du code :
Select-String -Pattern "Labo-Appli", "password=" -Path app.py, acces_bdd.py
Vous devez voir : la seule ligne trouvée est password=os.environ[...]. Aucun mot de passe en clair.
POINT DE CONTRÔLE B
- Montrez au formateur la page 503, la ligne du journal, et vos réponses aux étapes 7 et 10.
- Le formateur a validé. Passez à la partie C.
PARTIE C — Injecter puis corriger
Étapes 12 à 17, environ 35 min. À la fin, une injection est prouvée, puis rendue inoffensive.
Besoin d'un rappel ? Revoir le cours : « L'injection SQL et la requête paramétrée ».
Changez de rôle : le lecteur devient pilote.
Cadre légal. Vous testez une injection sur votre propre machine, dans un but d'apprentissage. Le faire sur un site qui n'est pas le vôtre, sans autorisation écrite, est un délit.
Lire la page vulnérable
Un collègue a repris la page « Mes interventions » de l'ancien prestataire. Sa fonction est interventions_du_technicien, en bas de acces_bdd.py.
- Ouvrez
acces_bdd.pyet lisez cette fonction. - Notez comment le login et le service entrent dans la requête :
Vous devez avoir : les valeurs collées dans le texte avec des + et des apostrophes.
Voir la page normale
- Si le serveur est arrêté, relancez-le dans le premier terminal :
python -m flask --app app run --debug
- Connectez-vous avec le compte
ngarcia, mot de passeChantier2026. - Ouvrez
/mes-interventions?service=atelier.
Vous devez voir : la phrase 4 intervention(s) pour le service « atelier »., et un tableau de 4 lignes, toutes de ngarcia.
Prouver l'injection
- Dans la barre d'adresse, remplacez le service par une injection. Tapez cette adresse, puis validez :
http://127.0.0.1:5000/mes-interventions?service=x' OR '1'='1
Vous devez voir : un nombre d'interventions bien plus grand que 4, et des interventions d'autres techniciens. Vous voyez des données qui ne sont pas les vôtres.
- Notez le nombre d'interventions affiché :
- Faites tout de suite une capture d'écran de cette page. C'est votre preuve « avant correction » : après l'étape 15, vous ne pourrez plus la refaire.
Pourquoi ? Pourquoi la base a-t-elle renvoyé les interventions de tout le monde ?
Votre réponse :
Corriger avec une requête paramétrée
- Dans
interventions_du_technicien, remplacez les lignes derequete = (jusqu'àcurseur.execute(requete)par :
requete = ("SELECT i.date_intervention, c.nom AS chantier, i.technicien, i.service, "
"i.duree_heures FROM intervention i JOIN chantier c ON c.id = i.id_chantier "
"WHERE i.technicien = %s AND i.service = %s "
"ORDER BY i.date_intervention")
curseur.execute(requete, (login, service))
- Enregistrez le fichier.
Vous devez voir : le fichier s'enregistre sans erreur. On teste l'effet à l'étape 16.
Vérifier que l'attaque échoue
- Rechargez la page normale
/mes-interventions?service=atelier.
Vous devez voir : les 4 interventions de ngarcia, comme avant. La correction n'a rien cassé.
- Rechargez l'adresse d'injection de l'étape 14.
Vous devez voir : « 0 intervention(s) ». La base a cherché un service qui s'appelle vraiment x' OR '1'='1. Il n'existe pas.
Garder la preuve
Vous avez déjà la capture « avant correction », faite à l'étape 14.
- Faites maintenant une capture de la même adresse d'injection après correction : « 0 intervention(s) ».
Vous devez avoir : deux captures, l'une de l'étape 14, l'autre d'ici. Vous les montrez au point de contrôle C.
POINT DE CONTRÔLE C
- Montrez au formateur les deux preuves et votre réponse à l'étape 14.
- Le formateur a validé. Passez à la partie D.
PARTIE D — Exporter et restaurer
Étapes 18 à 22, environ 25 min. À la fin, la base est restaurée sur une pile vierge, et vérifiée.
Besoin d'un rappel ? Revoir le cours : « Un jeu de données qu'on peut rejouer ».
Le jeu de test de l'annexe recrée une base neuve, toujours la même. Un export, lui, sauvegarde la vraie base, avec les saisies des utilisateurs. C'est cet export qu'on restaure quand une machine tombe en panne. On le prouve ici sur une base neuve.
Exporter la base
- Exportez toute la base dans un fichier, dans le dossier de l'application :
mysqldump -u root technovert --result-file=export_technovert.sql
- Cherchez dans le fichier les lignes qui créent les tables et qui ajoutent les données :
Select-String -Pattern "CREATE TABLE", "INSERT INTO" -Path export_technovert.sql
Vous devez voir : des lignes CREATE TABLE pour les quatre tables, et des lignes INSERT INTO. L'export contient la structure et les données.
Simuler une pile vierge
Une pile vierge, c'est une base qui n'existe pas encore. On efface la base pour le simuler.
- Supprimez la base :
mysql -u root -e "DROP DATABASE technovert;"
- Rechargez
/chantiersdans le navigateur.
Vous devez voir : la page « Service momentanément indisponible ». La base n'existe plus.
Restaurer
- Recréez la base vide, puis rechargez l'export dedans :
mysql -u root -e "CREATE DATABASE technovert;"
mysql -u root technovert -e "source export_technovert.sql"
- Rechargez
/chantiers.
Vous devez voir : les six chantiers reviennent.
Vérifier la restauration avec le script
Une page qui s'affiche ne prouve pas que tout est là. Le script de l'annexe compte les lignes et teste les cas connus.
- Dans le second terminal, lancez le script de vérification. Il lit lui aussi
technovert.env:
python A10_verifier_base.py
Vous devez voir : chaque ligne commence par OK, et la dernière ligne dit « 13 vérification(s) sur 13 réussie(s). ».
Si vous voyez un ÉCHEC : la restauration est incomplète, ou le jeu de test n'est pas le bon. Reprenez à l'étape 20.
Trouver dans la doc et conclure
- Ouvrez l'annexe . Trouvez la dernière vérification du script, celle qui teste une injection.
- Notez ce qu'elle envoie comme service, et le résultat attendu :
Vous devez avoir : un service piégé, avec des apostrophes, et zéro ligne attendue.
Pourquoi ? Pourquoi vérifier une injection dans un script de recette, en plus de l'avoir corrigée ?
Votre réponse :
POINT DE CONTRÔLE D
- Montrez au formateur le script qui affiche 13 sur 13, et vos réponses aux étapes 22.
- Le formateur a validé. Le labo est fini.
POUR ALLER PLUS LOIN
10 à 15 min, si vous avez fini avant la fin de la séance.
Une IA a proposé cette fonction pour chercher un client par sa ville. Elle marche, mais elle contient trois défauts.
def clients_par_ville(ville):
connexion = mysql.connector.connect(host="127.0.0.1", user="root",
password="root", database="technovert")
curseur = connexion.cursor(dictionary=True)
curseur.execute(f"SELECT * FROM client WHERE ville = '{ville}'")
return curseur.fetchall()
- Trouvez les trois défauts avant la mise en production. Pour chacun, notez la ligne et le danger.
| Ligne | Le défaut | Le danger |
|---|---|---|
CE QU'IL FAUT RETENIR
- Une valeur saisie ne se colle jamais dans une requête : on écrit
%set on passe la valeur à part. - Tout le SQL vit dans la couche d'accès. Une base arrêtée donne une page 503 propre, jamais une trace.
- Une restauration ne se croit pas : elle se prouve avec un script de vérification.
Un tableau de bord qui tient
MISE EN SITUATION-U6-S2-M6
Contexte — Karim Benali veut un tableau de bord pour suivre les interventions de l'atelier. Il sera exposé aux utilisateurs de TechnoVert.
Hélène Vasseur a une exigence : « Je ne veux pas qu'un petit malin casse la base en tapant dans une case. » Le formateur jouera cet utilisateur malveillant.
Règle IA de la séance
Scénario 2 : l'IA peut vous expliquer une notion ou un message d'erreur. Elle n'écrit ni votre requête ni votre vue.
Si vous avez posé une question à une IA, dites-le pendant l'entretien. C'est pris en compte, pas reproché.
COMMENT VOUS SEREZ NOTÉ
Il n'y a aucun fichier à rendre. Vous êtes noté à l'oral, sur votre poste, pendant un entretien de 5 minutes avec le formateur. Il vient vous voir dès que votre tableau marche.
Pendant l'entretien, le formateur :
- vous demande de montrer votre tableau, sans filtre puis avec un filtre ;
- tape lui-même une saisie piégée dans l'adresse de votre page ;
- vous pose des questions sur votre code ;
- vous demande une petite modification, à faire devant lui en 2 minutes ;
- regarde votre façon de travailler et d'expliquer.
Il suit cette grille. Lisez-la avant de commencer : c'est exactement ce qui sera noté.
| Critère | Insuffisant (0 ou 1) | Fragile (2) | Acquis (3) | Maîtrisé (4) |
|---|---|---|---|---|
| Démonstration · le tableau lit la base | La page ne s'affiche pas, ou plante sur un cas normal. | La page s'affiche, mais le filtre ou le nom du chantier manque. | Toutes les interventions, avec leur chantier, et le filtre par service marche. | Tout marche, et le total d'heures correspond aux lignes montrées. |
| Résistance · la saisie piégée reste sans effet | La saisie affiche des données en plus, ou une trace d'erreur. | La saisie est bloquée, mais seulement par le menu ou par un test fragile. | La requête est paramétrée : la saisie ne change rien. | La requête est paramétrée, et vous le prouvez vous-même devant le formateur. |
| Questions techniques · vous expliquez votre code | Vous ne savez pas dire ce que fait votre requête. | Vous l'expliquez avec de l'aide. | Vous expliquez la requête, le JOIN et le rôle de %s. |
Vous expliquez aussi, avec vos mots, pourquoi l'attaque échoue. |
| Travail personnel · la modification en direct | Vous ne trouvez pas où modifier. | Vous trouvez l'endroit, mais la modification n'aboutit pas. | La modification marche en 2 minutes. | Elle marche, et vous la testez vous-même sans qu'on vous le demande. |
| Professionnalisme · votre façon de travailler | Réponses floues, dossier en désordre, rien n'est dit de ce qui ne marche pas. | Vocabulaire approximatif, explications à reprendre. | Mots du métier, explications claires. Vous dites ce qui ne marche pas et l'aide reçue. | En plus, vous signalez un risque qui reste, ou une amélioration possible. |
La note est la somme des cinq lignes, sur 20.
Ce dont vous disposez
- Votre poste Windows, avec XAMPP, Python, VS Code et un navigateur.
- Annexe : le tableau de bord de départ et son mode d'emploi. Le dossier se télécharge dans
A11_tableau_de_bord.zip. Il contient aussi les scripts de la base. - Annexe : le jeu de données de la base.
- Annexe : le compte MySQL de l'application.
- Annexe : le fichier d'environnement, à ranger dans votre dossier personnel.
- Annexe : un extrait de la documentation du connecteur MySQL. Il rappelle la requête paramétrée avec
%s.
Pour lire le service choisi dans l'adresse (/tableau?service=atelier), la vue utilise request.args.get("service"). Il renvoie None si l'adresse ne contient pas de service, et un texte vide "" si on choisit « Tous les services » dans le menu. Écrivez request.args.get("service") or None : les deux cas donnent alors None.
CE QU'ON VOUS DEMANDE
Le tableau de bord a une seule page, /tableau. Elle affiche toutes les interventions, avec le nom de leur chantier. Un menu filtre les interventions par service.
La fonction interventions_par_service de la couche d'accès est à écrire. La vue qui l'appelle est à compléter. La saisie du service ne doit jamais pouvoir changer la requête envoyée à la base.
Pendant l'entretien, le formateur tapera lui-même un service piégé dans votre page. Votre tableau doit rester sain : aucune donnée qui ne devrait pas s'afficher, aucune trace d'erreur.
PAR OÙ COMMENCER
45 minutes en tout : environ 40 minutes de travail, et l'entretien de 5 minutes dès que votre tableau marche.
- Préparez votre poste avec l'annexe : dossier extrait, MySQL lancé, base chargée,
technovert.envrempli. Lancez le tableau de bord et ouvrez/tableau. Environ 5 min. - Écrivez
interventions_par_serviced'abord sans filtre : la requête relie l'intervention à son chantier avecJOIN, et renvoie tout. Environ 10 min. - Ajoutez le filtre : quand un service est donné, ajoutez
WHERE i.service = %set passez le service en paramètre. Environ 10 min. - Complétez la vue
tableau(): elle lit le service avecrequest.args.get, appelle la couche, et calcule le total des heures. Le gabarit attend aussimessage: c'est un texte à afficher quand le service demandé n'est pas dansSERVICES, sinonNone. Environ 10 min. - Testez avec un service normal, puis avec une injection de votre choix. Environ 5 min.
CE QUI DOIT MARCHER À LA FIN
- Sans filtre, le tableau affiche les 12 interventions, avec le nom de chaque chantier.
- Le filtre « atelier » n'affiche que les interventions de l'atelier.
- Le total des heures affiché correspond aux lignes montrées.
- Une saisie qui contient une apostrophe ne renvoie aucune ligne et ne provoque aucune trace.
- Vous savez expliquer chaque ligne de votre requête et de votre vue.
Bloqué ? Dites-le au formateur pendant l'entretien. Savoir expliquer où l'on bloque fait partie du professionnalisme.
Tous les documents du module
Cliquez sur une annexe pour l'ouvrir dans le panneau de droite. Elle reste ouverte pendant que vous lisez.