Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b304bcaa4 | ||
|
|
7e251ec951 |
@@ -0,0 +1,705 @@
|
||||
# Projet FlexOffice - Mini-cas 2 - Adam GABRIEL, Louis LEULIETTE, Gauvain BOICHÉ
|
||||
|
||||
## Gestion de projet - Mini cas 2 - Simulation pratique de projet agile
|
||||
|
||||
**Projet FlexOffice : application interne de réservation de salles, de matériel informatique et de véhicules de service**
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Ecole** | LiveCampus |
|
||||
| **Promotion** | MCS P 2027.2 |
|
||||
| **Cours** | Gestion de projet |
|
||||
| **Professeur** | Yannick AUMAGY |
|
||||
| **Groupe** | Adam GABRIEL - Louis LEULIETTE - Gauvain BOICHÉ |
|
||||
| **Date de remise** | [07/10/2026] |
|
||||
|
||||
> L'entreprise FlexOffice veut lancer une application interne permettant aux salariés de réserver des salles de réunion, du matériel informatique et des véhicules de service.
|
||||
> La direction souhaite disposer rapidement d'une première version afin de tester le service sur un site pilote.
|
||||
> Vous travaillez en équipe agile.
|
||||
> Votre objectif est de produire un plan de sprint cohérent, suivre l'avancement et préparer une restitution finale.
|
||||
|
||||
## Backlog de départ
|
||||
|
||||
| Fonctionnalité | Priorité initiale |
|
||||
|---|---|
|
||||
| Connexion utilisateur | Must |
|
||||
| Consulter les ressources disponibles | Must |
|
||||
| Réserver une salle | Must |
|
||||
| Annuler une réservation | Must |
|
||||
| Réserver du matériel informatique | Should |
|
||||
| Réserver un véhicule | Should |
|
||||
| Recevoir une confirmation par courriel | Should |
|
||||
| Afficher l'historique des réservations | Could |
|
||||
| Administration des ressources | Must |
|
||||
| Statistiques d'utilisation | Could |
|
||||
| Synchronisation avec un agenda externe | Could |
|
||||
| Notifications mobiles | Could |
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
## Tâches demandées
|
||||
|
||||
**Phase 1 - Préparation du backlog**
|
||||
- Reformuler les besoins en histoires utilisateur.
|
||||
- Ajouter des **Critères d'acceptation**.
|
||||
- Prioriser les éléments selon la valeur et le risque.
|
||||
|
||||
**Phase 2 - Estimation et Sprint Planning**
|
||||
- Estimer les histoires utilisateur en points d'histoire.
|
||||
- Définir une capacité réaliste pour le sprint.
|
||||
- Sélectionner les éléments à réaliser.
|
||||
- Découper les stories retenues en tâches techniques ou fonctionnelles.
|
||||
|
||||
**Phase 3 - Mise en place du suivi**
|
||||
- Créer un tableau Kanban avec au minimum À faire / En cours / À valider / Terminé.
|
||||
- Définir 3 indicateurs de suivi.
|
||||
- Identifier les principaux risques initiaux.
|
||||
|
||||
**Phase 4 - Qualité et livraison**
|
||||
- Définir la Definition of Done.
|
||||
- Préciser les tests nécessaires avant validation.
|
||||
- Décrire comment Git et une chaîne CI/CD seraient utilisés.
|
||||
- Préparer la liste des livrables de fin de sprint.
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# Sommaire
|
||||
|
||||
1. [Synthèse du plan de projet](#1-synthèse-du-plan-de-projet)
|
||||
2. [Phase 1 - Préparation du backlog](#2-phase-1--préparation-du-backlog)
|
||||
3. [Phase 2 - Estimation et Sprint Planning](#3-phase-2--estimation-et-sprint-planning)
|
||||
4. [Phase 3 - Mise en place du suivi](#4-phase-3--mise-en-place-du-suivi)
|
||||
5. [Phase 4 - Qualité et livraison](#5-phase-4--qualité-et-livraison)
|
||||
6. [Planning et diagramme de Gantt](#6-planning-et-diagramme-de-gantt)
|
||||
7. [Estimation des ressources et des délais](#7-estimation-des-ressources-et-des-délais)
|
||||
8. [Plan de gestion des risques](#8-plan-de-gestion-des-risques)
|
||||
9. [Hypothèses retenues](#9-hypothèses-retenues)
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 1. Synthèse du plan de projet
|
||||
|
||||
## 1.1 Vision et objectifs
|
||||
|
||||
**Vision.** Offrir aux salariés de FlexOffice un point d'entrée unique pour la réservation de salles, de matériel informatique et de véhicules de service, afin d'éviter les doublons de réservation et les échanges informels non-traçables (courriels, tableurs, etc).
|
||||
|
||||
**Objectif court terme.** Livrer rapidement une première version utilisable (MVP) pour la tester sur **un site pilote**, puis décider de la généralisation à partir des retours réels.
|
||||
|
||||
**Objectifs mesurables du pilote** *(à valider avec la direction)* :
|
||||
- 100 % des réservations de salles du site pilote passent par l'application ;
|
||||
- aucune double-réservation constatée ;
|
||||
- satisfaction utilisateur supérieure à 4/5 lors de l'enquête de fin de pilote.
|
||||
|
||||
## 1.2 Périmètre
|
||||
|
||||
| Dans le périmètre (pilote) | Hors périmètre (reporté après le pilote) |
|
||||
|---|---|
|
||||
| Connexion, rôles employé / administrateur | Synchronisation avec un agenda externe |
|
||||
| Administration et consultation des ressources | Notifications mobiles |
|
||||
| Réservation et annulation (salles, matériel, véhicules) | Statistiques avancées (selon retours du pilote) |
|
||||
| Confirmation par courriel, historique | Déploiement multi-sites |
|
||||
|
||||
## 1.3 Équipe et organisation
|
||||
|
||||
| Rôle | Effectif | Mission |
|
||||
|---|---|---|
|
||||
| Product Owner (PO) | 1 (50 %) | Porte la vision, priorise le backlog, valide les stories |
|
||||
| Scrum Master (SM) | 1 (50 %) | Facilite les cérémonies, lève les obstacles |
|
||||
| Développeurs | 3 (100 %) | Front-end, back-end, full-stack |
|
||||
| Testeur / QA | 1 (50 %) | Stratégie de test, recette, automatisation |
|
||||
|
||||
**Cadre agile :** Scrum avec sprints de **2 semaines**, suivi visuel Kanban, Git et CI/CD dès le premier sprint.
|
||||
|
||||
## 1.4 Calendrier et jalons
|
||||
|
||||
| Jalon | Échéance | Contenu |
|
||||
|---|---|---|
|
||||
| M1 - Backlog prêt | Fin S1 (semaine 1) | Backlog raffiné, socle et outils prêts |
|
||||
| M2 - Démo Sprint 1 | Fin semaine 3 | Connexion, ressources, réservation de salle |
|
||||
| M3 - Version pilote candidate | Fin semaine 5 | Toutes les stories Must et Should du pilote |
|
||||
| M4 - Version validée (recette) | Fin semaine 7 | Recette utilisateur validée, correctifs livrés |
|
||||
| M5 - Mise en service du pilote | Semaine 8 | Déploiement et formation du site pilote |
|
||||
| M6 - Bilan du pilote | Semaine 12 | Retours, décision Go / No-Go généralisation |
|
||||
|
||||
## 1.5 Chiffres clés
|
||||
|
||||
| Indicateur | Valeur |
|
||||
|---|---|
|
||||
| Durée jusqu'à la mise en service du pilote | 8 semaines |
|
||||
| Nombre de sprints de développement | 3 (+ 1 semaine de cadrage) |
|
||||
| Capacité d'un sprint | 24 point d'histoires |
|
||||
| Backlog total estimé | 68 point d'histoires (13 items), dont 42 pour le périmètre pilote |
|
||||
| Charge totale estimée (jusqu'à la fin du pilote) | ≈ 173 jours-personnes |
|
||||
|
||||
## 1.6 Principaux risques
|
||||
|
||||
Les trois risques les plus critiques sont le **double réservations**, l'**intégration de l'authentification** d'entreprise et une **capacité surestimée** en l'absence de vélocité historique. Le détail et les stratégies d'atténuation figurent en [section 8](#8-plan-de-gestion-des-risques).
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 2. Phase 1 - Préparation du backlog
|
||||
|
||||
## 2.1 Histoires Utilisateurs et **Critères d'acceptation**
|
||||
|
||||
### HU-00 - Socle technique et chaîne CI/CD
|
||||
En tant que dev, je veux du versionnage avec une CI afin de livrer des version fiables et identifiées dès le premier sprint.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Après un push, quand la CI s'exécute, alors build, lint et tests unitaires sont lancés automatiquement.
|
||||
- Après un merge sur la branche main, quand la CI passe, alors l'application est déployée.
|
||||
- Le dépôt contient un README décrivant l'installation locale.
|
||||
|
||||
### HU-01 - Connexion utilisateur
|
||||
En tant que salarié, je veux me connecter avec mon compte d'entreprise afin d'accéder à mes réservations.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Le salarié disposant d'un compte valide, à saisie de ses identifiants, il accède à la page d'accueil.
|
||||
- En cas d'identifiants invalides, un message d'erreur générique s'affiche et aucun accès n'est accordé.
|
||||
- Les rôles « employé » et « administrateur » donnent accès à des fonctions différentes ; la session expire après une période d'inactivité définie.
|
||||
|
||||
### HU-02 - Consulter les ressources disponibles
|
||||
En tant que salarié, je veux consulter les ressources disponibles sur un créneau donné afin de choisir celle qui correspond à mon besoin.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Avec une date et un créneau, en cas de recherche, seules les ressources libres sur ce créneau sont affichées.
|
||||
- Je peux filtrer par type (salle, matériel, véhicule), par site et par capacité.
|
||||
- Le temps d'affichage des résultats est inférieur à 2 secondes pour 200 ressources.
|
||||
|
||||
### HU-03 - Réserver une salle
|
||||
En tant que salarié, je veux réserver une salle de réunion sur un créneau.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Avec une salle libre, je valide la réservation, et elle est enregistrée. La salle devient indisponible sur ce créneau.
|
||||
- Si une salle est déjà réservée, quand je tente de la réserver, la demande est refusée avec un message explicite.
|
||||
- Une réservation ne peut pas être créée dans le passé ni en dehors des horaires d'ouverture.
|
||||
|
||||
### HU-04 - Annuler une réservation
|
||||
En tant que salarié, je veux annuler ma réservation.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Quand j'annule une réservation dans le futur et de mon fait, la ressource redevient disponible immédiatement.
|
||||
- Je ne peux pas annuler la réservation d'un autre salarié (sauf administrateur).
|
||||
- Une confirmation m'est demandée avant l'annulation définitive.
|
||||
|
||||
### HU-05 - Réserver du matériel informatique
|
||||
En tant que salarié, je veux réserver un ordinateur portable (ou autre équipement).
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Si un équipement est disponible, quand je le réserve, il est bloqué sur la période choisie.
|
||||
- Le matériel peut être réservé sur plusieurs jours consécutifs.
|
||||
- Un équipement marqué « hors service » n'est pas proposé à la réservation.
|
||||
|
||||
### HU-06 - Réserver un véhicule
|
||||
En tant que salarié, je veux réserver un véhicule de service pour un déplacement professionnel.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Si un véhicule est libre, je le réserve en indiquant un motif et une durée.
|
||||
- Le formulaire demande la destination et la durée estimée du déplacement.
|
||||
- Un véhicule en maintenance ou déjà réservé n'est pas proposé.
|
||||
|
||||
### HU-07 - Recevoir une confirmation par courriel
|
||||
En tant que salarié, je veux recevoir un courriel à chaque réservation ou annulation afin d'avoir une trace écrite et un rappel.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Si une réservation est créée, quand elle est validée, un courriel récapitulatif (ressource, date, horaires) est envoyé dans les 2 minutes.
|
||||
- Un courriel d'annulation est envoyé lors de toute annulation.
|
||||
- Un échec d'envoi ne bloque pas la réservation et est consigné dans les journaux.
|
||||
|
||||
### HU-08 - Afficher l'historique des réservations
|
||||
En tant que salarié, je veux consulter l'historique de mes réservations passées et à venir.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Quand j'ouvre « Mes réservations », la liste est affichée du plus récent au plus ancien.
|
||||
- Je peux filtrer par statut (à venir, passée, annulée) et par type de ressource.
|
||||
- Je ne vois que mes propres réservations.
|
||||
|
||||
### HU-09 - Administration des ressources
|
||||
En tant qu'administrateur, je veux créer, modifier et désactiver des ressources afin de maintenir un catalogue à jour.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- En tant qu'admin, quand je crée une ressource (type, nom, site, capacité), elle apparaît dans le catalogue.
|
||||
- La désactivation d'une ressource la retire de la recherche sans supprimer l'historique existant.
|
||||
- Un salarié non administrateur n'a pas accès aux écrans d'administration.
|
||||
|
||||
### HU-10 - Statistiques d'utilisation
|
||||
En tant que responsable des services généraux, je veux visualiser des statistiques d'utilisation.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Un tableau de bord affiche le taux d'occupation par type de ressource et par période.
|
||||
- Les données peuvent être exportées au format CSV.
|
||||
- Les statistiques sont réservées aux administrateurs.
|
||||
|
||||
### HU-11 - Synchronisation avec un agenda externe
|
||||
En tant que salarié, je veux que mes réservations apparaissent dans mon agenda.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Avec un compte agenda lié, je crée une réservation, et un événement est créé dans mon agenda.
|
||||
- L'annulation d'une réservation supprime l'événement associé.
|
||||
- Le salarié peut dissocier son agenda à tout moment.
|
||||
|
||||
### HU-12 - Notifications mobiles
|
||||
En tant que salarié, je veux recevoir des notifications sur mon téléphone afin d'être rappelé de mes réservations.
|
||||
|
||||
**Critères d'acceptation**
|
||||
- Un rappel est envoyé 30 minutes avant le début d'une réservation.
|
||||
- L'utilisateur peut activer ou désactiver les notifications.
|
||||
- Aucune notification n'est envoyée pour une réservation annulée.
|
||||
|
||||
## 2.2 Priorisation selon la valeur et le risque
|
||||
|
||||
**Méthode.** Chaque objet reçoit une note de **valeur métier** (1-5) et une note de **risque** (1-5 : complexité technique, incertitude, dépendance externe). Le **score = valeur + risque** favorise les éléments à forte valeur et ceux dont l'incertitude doit être levée tôt. Le rang final tient aussi compte des **dépendances** (on ne consulte pas des ressources qui n'ont pas pu être créées) et de l'objectif du pilote.
|
||||
|
||||
| Rang | ID | Fonctionnalité | Priorité initiale | Valeur | Risque | Score | Priorité révisée | Sprint cible |
|
||||
|:-:|---|---|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| 1 | HU-00 | Socle technique et CI/CD | - | 3 | 3 | 6 | Must | S1 |
|
||||
| 2 | HU-01 | Connexion utilisateur | Must | 5 | 4 | 9 | Must | S1 |
|
||||
| 3 | HU-09 | Administration des ressources | Must | 4 | 2 | 6 | Must | S1 |
|
||||
| 4 | HU-02 | Consulter les ressources | Must | 5 | 2 | 7 | Must | S1 |
|
||||
| 5 | HU-03 | Réserver une salle | Must | 5 | 4 | 9 | Must | S1 |
|
||||
| 6 | HU-04 | Annuler une réservation | Must | 4 | 2 | 6 | Must | S2 |
|
||||
| 7 | HU-05 | Réserver du matériel | Should | 3 | 3 | 6 | Should | S2 |
|
||||
| 8 | HU-07 | Confirmation par courriel | Should | 3 | 3 | 6 | Should | S2 |
|
||||
| 9 | HU-06 | Réserver un véhicule | Should | 3 | 4 | 7 | Should | S2 |
|
||||
| 10 | HU-08 | Historique des réservations | Could | 2 | 1 | 3 | Should *(↑)* | S2 |
|
||||
| 11 | HU-10 | Statistiques d'utilisation | Could | 2 | 2 | 4 | Could | S3 si capacité |
|
||||
| 12 | HU-11 | Synchronisation agenda externe | Could | 2 | 5 | 7 | Won't *(version pilote)* | Après pilote |
|
||||
| 13 | HU-12 | Notifications mobiles | Could | 2 | 4 | 6 | Won't *(version pilote)* | Après pilote |
|
||||
|
||||
**Décisions de priorisation justifiées**
|
||||
- **HU-00 ajoutée** : sans socle ni CI/CD, aucun incrément fiable n'est possible ; c'est un prérequis technique.
|
||||
- **Administration (Must) avancée avant la consultation** : sans ressources dans le catalogue, la consultation et la réservation ne peuvent être testées. Son périmètre est volontairement réduit au minimum (créer / modifier / désactiver).
|
||||
- **Connexion et réservation de salle traitées dans le premier sprint** : ce sont les deux items les plus risqués (SSO, gestion des conflits). Les traiter tôt réduit l'incertitude.
|
||||
- **Historique remonté en Should** : forte valeur perçue pour un coût faible (score de risque de 1) ; il complète naturellement l'annulation.
|
||||
- **Synchronisation agenda et notifications mobiles repoussées** : risque élevé (API externes, canal mobile) pour une valeur faible sur un pilote ; elles seront réévaluées avec les retours du site pilote.
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 3. Phase 2 - Estimation et Sprint Planning
|
||||
|
||||
## 3.1 Estimation en points d'histoire
|
||||
|
||||
**Méthode.** Planning Poker avec la suite de Fibonacci (1, 2, 3, 5, 8, 13), qui est appliqué pour éviter les hésitation entre 5 et 6 (points d'estimation) pour les tâches dures à estimer. L'histoire de référence est **HU-02 « Consulter les ressources » = 3 points** (moyenne complexité : lecture de données, filtres, un écran). Les autres stories sont estimées par comparaison relative, en intégrant effort, complexité et incertitude.
|
||||
|
||||
| ID | Histoire Utilisateur | Points | Justification |
|
||||
|---|---|:-:|---|
|
||||
| HU-00 | Socle technique et CI/CD | 3 | Mise en place connue, mais nécessite plusieurs briques (dépôt, pipeline, recette) |
|
||||
| HU-01 | Connexion utilisateur | 5 | Intégration à l'authentification d'entreprise, gestion des rôles et sessions |
|
||||
| HU-02 | Consulter les ressources | 3 | **Référence** : lecture, filtres, un écran |
|
||||
| HU-03 | Réserver une salle | 8 | Règles métier, contrôle des conflits et concurrence, interface de sélection de créneau |
|
||||
| HU-04 | Annuler une réservation | 3 | Règles de droits simples, remise à disposition de la ressource |
|
||||
| HU-05 | Réserver du matériel | 5 | Réutilise la logique de réservation, ajoute la durée multi-jours et les statuts |
|
||||
| HU-06 | Réserver un véhicule | 5 | Réutilise la logique de réservation, ajoute motif, destination et maintenance |
|
||||
| HU-07 | Confirmation par courriel | 3 | Service d'envoi, modèles de message, gestion des échecs |
|
||||
| HU-08 | Historique des réservations | 2 | Liste filtrée sur des données existantes |
|
||||
| HU-09 | Administration des ressources | 5 | CRUD complet, droits, désactivation sans perte d'historique |
|
||||
| HU-10 | Statistiques d'utilisation | 5 | Agrégations, tableau de bord, export |
|
||||
| HU-11 | Synchronisation agenda externe | 13 | API tierce, authentification déléguée, cas d'erreur nombreux (à découper) |
|
||||
| HU-12 | Notifications mobiles | 8 | Canal push, gestion des appareils et des préférences |
|
||||
| | **Total backlog** | **68** | |
|
||||
|
||||
> Le backlog complet représente **68 points** (13 items) ; le périmètre de la version pilote (HU-00 à HU-09) en représente **42**, le reste étant reporté après le pilote.
|
||||
|
||||
## 3.2 Capacité réaliste du sprint
|
||||
|
||||
L'équipe n'a pas encore d'historique de vélocité : la capacité est donc **calculée à partir de la disponibilité**, puis recalibrée après le Sprint 1.
|
||||
|
||||
| Étape | Calcul | Résultat |
|
||||
|---|---|:-:|
|
||||
| Jours disponibles (3 développeurs × 10 j + QA 0,5 × 10 j) | 30 + 5 | 35 j-p |
|
||||
| Cérémonies Scrum (planning, daily, review, rétro, raffinement) | ≈ 11 % | − 4 j-p |
|
||||
| Absences et congés prévus | | − 1 j-p |
|
||||
| Jours disponibles pour le développement | | 30 j-p |
|
||||
| Facteur de focus (imprévus, support, revues de code) | × 0,8 | **24 j-p** |
|
||||
|
||||

|
||||
|
||||
Hypothèse de calibrage : **1 point d'histoire ≈ 1 jour-personne idéal**. **Capacité retenue pour le Sprint 1 : 24 point d'histoires.** Cette valeur sera ajustée selon la vélocité réellement constatée.
|
||||
|
||||
## 3.3 Sélection des éléments : Sprint 1
|
||||
|
||||
**Objectif du sprint (Sprint Goal)** : *« Un salarié authentifié peut consulter les ressources de son site et réserver une salle sans double réservation ; un administrateur peut alimenter le catalogue. »*
|
||||
|
||||
| Rang | ID | Histoire Utilisateur | Points |
|
||||
|:-:|---|---|:-:|
|
||||
| 1 | HU-00 | Socle technique et CI/CD | 3 |
|
||||
| 2 | HU-01 | Connexion utilisateur | 5 |
|
||||
| 3 | HU-09 | Administration des ressources | 5 |
|
||||
| 4 | HU-02 | Consulter les ressources | 3 |
|
||||
| 5 | HU-03 | Réserver une salle | 8 |
|
||||
| | | **Total engagé** | **24** |
|
||||
|
||||

|
||||
|
||||
L'engagement égale la capacité calculée (24 points) : le périmètre est ambitieux mais réaliste. Si le Sprint 1 est en retard, **HU-03 est l'histoire à risque** ; le PO accepterait alors de réduire son périmètre (par exemple, sans gestion fine des horaires d'ouverture) plutôt que de dégrader la qualité.
|
||||
|
||||
**Aperçu des sprints suivants** *(prévisionnel, à ajuster après chaque sprint)*
|
||||
|
||||
| Sprint | Objectif | Contenu | Points |
|
||||
|---|---|---|:-:|
|
||||
| Sprint 2 | Compléter la version pilote | HU-04 (3), HU-05 (5), HU-07 (3), HU-06 (5), HU-08 (2) | 18 *(+ 6 de réserve pour correctifs du S1)* |
|
||||
| Sprint 3 | Stabiliser et préparer le pilote | Recette utilisateur, correctifs, tests de charge, documentation, formation, déploiement ; HU-10 si capacité | 24 *(dont 12 de stabilisation)* |
|
||||
|
||||
## 3.4 Découpage des stories du Sprint 1 en tâches
|
||||
|
||||
1 point = 8 heures. Total : **192 heures**, soit 24 jours-personnes.
|
||||
|
||||

|
||||
|
||||
### HU-00 - Socle technique et CI/CD (3 pts = 24 h)
|
||||
|
||||
| ID tâche | Tâche | Type | Estimation |
|
||||
|---|---|---|:-:|
|
||||
| T-00.1 | Créer le dépôt Git, les règles de branches et le README | Technique | 2 h |
|
||||
| T-00.2 | Initialiser le squelette front-end et back-end | Technique | 6 h |
|
||||
| T-00.3 | Mettre en place la base de données et les migrations | Technique | 6 h |
|
||||
| T-00.4 | Configurer le pipeline CI (build, lint, tests) | Technique | 6 h |
|
||||
| T-00.5 | Provisionner l'environnement de recette et le déploiement automatique | Technique | 4 h |
|
||||
|
||||
### HU-01 - Connexion utilisateur (5 pts = 40 h)
|
||||
|
||||
| ID tâche | Tâche | Type | Estimation |
|
||||
|---|---|---|:-:|
|
||||
| T-01.1 | Spike : analyser l'annuaire / SSO de l'entreprise | Technique | 4 h |
|
||||
| T-01.2 | Développer l'API d'authentification (jeton / session) | Technique | 12 h |
|
||||
| T-01.3 | Développer l'écran de connexion | Fonctionnel | 8 h |
|
||||
| T-01.4 | Implémenter la gestion des rôles (employé / administrateur) | Technique | 6 h |
|
||||
| T-01.5 | Écrire les tests unitaires et d'intégration | Test | 6 h |
|
||||
| T-01.6 | Revue de sécurité (mots de passe, expiration de session) | Technique | 4 h |
|
||||
|
||||
### HU-09 - Administration des ressources (5 pts = 40 h)
|
||||
|
||||
| ID tâche | Tâche | Type | Estimation |
|
||||
|---|---|---|:-:|
|
||||
| T-09.1 | Concevoir le modèle de données « ressource » | Technique | 6 h |
|
||||
| T-09.2 | Développer l'API CRUD des ressources | Technique | 12 h |
|
||||
| T-09.3 | Développer les écrans d'administration (liste, formulaire) | Fonctionnel | 14 h |
|
||||
| T-09.4 | Gérer la désactivation / l'archivage | Technique | 4 h |
|
||||
| T-09.5 | Écrire les tests | Test | 4 h |
|
||||
|
||||
### HU-02 - Consulter les ressources (3 pts = 24 h)
|
||||
|
||||
| ID tâche | Tâche | Type | Estimation |
|
||||
|---|---|---|:-:|
|
||||
| T-02.1 | Développer l'API de recherche et de filtrage par disponibilité | Technique | 8 h |
|
||||
| T-02.2 | Développer l'écran de liste et les filtres | Fonctionnel | 10 h |
|
||||
| T-02.3 | Écrire les tests | Test | 6 h |
|
||||
|
||||
### HU-03 - Réserver une salle (8 pts = 64 h)
|
||||
|
||||
| ID tâche | Tâche | Type | Estimation |
|
||||
|---|---|---|:-:|
|
||||
| T-03.1 | Concevoir le modèle de réservation et la règle anti-chevauchement | Technique | 10 h |
|
||||
| T-03.2 | Développer l'API de création (contrôle des conflits, transaction) | Technique | 16 h |
|
||||
| T-03.3 | Développer l'écran de sélection de créneau et le formulaire | Fonctionnel | 16 h |
|
||||
| T-03.4 | Gérer les horaires d'ouverture et le fuseau horaire | Fonctionnel | 6 h |
|
||||
| T-03.5 | Écrire les tests unitaires, d'intégration et de concurrence | Test | 10 h |
|
||||
| T-03.6 | Recette fonctionnelle et corrections | Test | 6 h |
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 4. Phase 3 - Mise en place du suivi
|
||||
|
||||
## 4.1 Tableau Kanban
|
||||
|
||||
**Colonnes et règles de gestion**
|
||||
|
||||
| Colonne | Règle d'entrée | Limite de travail en cours (WIP) |
|
||||
|---|---|:-:|
|
||||
| À faire | Tâche issue du Sprint Backlog, estimée et comprise | - |
|
||||
| En cours | Un développeur prend la tâche et crée une branche Git | 5 |
|
||||
| À valider | Code terminé, merge request ouverte, tests passés en CI | 3 |
|
||||
| Terminé | Revue faite, Definition of Done respectée, mergée | - |
|
||||
|
||||
**Exemple d'état du tableau au jour 5 du Sprint 1** *(situation fictive illustrative)*
|
||||
|
||||

|
||||
|
||||
**Règles d'usage :** une tâche ne passe en « Terminé » qu'après validation par une personne autre que son auteur ; la limite WIP est respectée (on termine avant de commencer) ; le tableau est mis à jour avant chaque daily.
|
||||
|
||||
## 4.2 Trois indicateurs de suivi
|
||||
|
||||
| # | Indicateur | Définition | Cible | Seuil d'alerte | Fréquence |
|
||||
|:-:|---|---|---|---|---|
|
||||
| 1 | **Burndown du sprint** | Story points restants par jour, comparés à la droite idéale | Suivre la droite idéale | Écart inférieur à 20 % à partir du jour 4 | Quotidienne |
|
||||
| 2 | **Vélocité et taux d'atteinte de l'engagement** | Points terminés (selon la DoD) divisés par les points engagés | supérieur ou égal 85 % de l'engagement | supérieur à 70 % sur un sprint | Fin de sprint |
|
||||
| 3 | **Cycle time moyen** | Nombre de jours entre « En cours » et « Terminé » pour une histoire | supérieur ou égal à 4 jours par histoire | Inférieur à 6 jours ou colonne « À valider » saturée | Hebdomadaire |
|
||||
|
||||
**Trajectoire idéale du burndown (Sprint 1, 24 points)**
|
||||
|
||||
| Jour | J0 | J2 | J4 | J6 | J8 | J10 |
|
||||
|---|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| Points restants idéaux | 24 | 19,2 | 14,4 | 9,6 | 4,8 | 0 |
|
||||
|
||||

|
||||
|
||||
## 4.3 Principaux risques initiaux
|
||||
|
||||
| Rang | Risque | Criticité |
|
||||
|:-:|---|:-:|
|
||||
| 1 | Double réservation lors de réservations simultanées | 15 |
|
||||
| 2 | Authentification d'entreprise plus complexe que prévu | 12 |
|
||||
| 3 | Capacité surestimée | 12 |
|
||||
| 4 | Faible disponibilité du PO et des utilisateurs du site pilote | 12 |
|
||||
| 5 | Dérive du périmètre | 9 |
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 5. Phase 4 - Qualité et livraison
|
||||
|
||||
## 5.1 Definition of Done (DoD)
|
||||
|
||||
Une histoire utilisateur est **terminée** uniquement si **tous** les points suivants sont vérifiés :
|
||||
|
||||
**Développement**
|
||||
- [ ] Le code est écrit, relu par au moins un autre dev (MR approuvée) et fusionné sur la branche principale.
|
||||
- [ ] Les standards de code sont respectés.
|
||||
- [ ] Aucune dette technique critique n'est introduite sans ticket associé.
|
||||
|
||||
**Tests**
|
||||
- [ ] Les tests unitaires et d'intégration sont écrits et passent en CI.
|
||||
- [ ] La couverture de nouveau code est supérieure ou égale à 70 %.
|
||||
- [ ] Tous les **Critères d'acceptation** sont vérifiés.
|
||||
- [ ] Aucun bug bloquant ou majeur ouvert.
|
||||
|
||||
**Livraison**
|
||||
- [ ] La fonctionnalité est déployée sur l'env ``staging`` par la CI/CD.
|
||||
- [ ] La documentation est mise à jour (README, API, notes de version).
|
||||
- [ ] L'histoire est démontrée et acceptée par le PO.
|
||||
|
||||
**Sécurité et conformité**
|
||||
- [ ] Aucune faille critique détectée par l'analyse des dépendances.
|
||||
- [ ] Les données manipulées respectent les règles RGPD.
|
||||
|
||||
## 5.2 Tests nécessaires avant validation
|
||||
|
||||
| Type de test | Objectif | Responsable | Moment | Exemple pour FlexOffice |
|
||||
|---|---|---|---|---|
|
||||
| **Tests unitaires** | Vérifier la logique métier isolée | Développeurs | À chaque commit (CI) | Règle de détection de chevauchement de créneaux |
|
||||
| **Tests d'intégration** | Vérifier l'interaction entre composants (API, base de données) | Développeurs | À chaque merge request (CI) | Création d'une réservation => enregistrement en base |
|
||||
| **Tests de concurrence** | Garantir l'absence de double réservation | Développeurs / QA | Sprint 1 puis à chaque évolution | Deux utilisateurs réservent la même salle au même instant |
|
||||
| **Tests de bout en bout (E2E)** | Valider les parcours complets | QA | Avant la fin de chaque sprint | Connexion => recherche => réservation => annulation |
|
||||
| **Tests de non-régression** | Vérifier que l'existant fonctionne toujours | QA (automatisés en CI) | À chaque livraison | Rejouer les parcours clés après chaque histoire |
|
||||
| **Tests de sécurité** | Détecter les failles courantes | Développeurs / QA | À chaque build + revue avant pilote | Contrôle des droits, injection, gestion de session |
|
||||
| **Tests de performance** | Vérifier la tenue en charge du site pilote | QA | Sprint 3 | 100 utilisateurs simultanés, recherche en moins de 2 s |
|
||||
| **Tests de compatibilité / accessibilité** | Garantir l'usage sur les navigateurs et écrans courants | QA | Sprint 2 et 3 | Navigateurs principaux, affichage tablette |
|
||||
| **Recette utilisateur (UAT)** | Valider que le produit répond au besoin réel | PO + utilisateurs du site pilote | Sprint 3 (semaine 7) | Scénarios métier réalisés par de vrais salariés |
|
||||
|
||||
## 5.3 Utilisation de Git et d'une chaîne CI/CD
|
||||
|
||||
### Stratégie Git
|
||||
|
||||
- **Dépôt central unique** hébergé sur une plateforme de gestion de code (GitLab, GitHub ou équivalent).
|
||||
- **Modèle de branches simplifié** (trunk-based léger) :
|
||||
- `main` : branche stable, toujours déployable ;
|
||||
- `HU-xx` : une branche courte par histoire ou par tâche ;
|
||||
- `HU-xx-fix` : correctifs urgents sur la version pilote.
|
||||
- **Conventions de commit** : `feat:`, `fix:`, `test:`, `docs:`, `refactor:` (par exemple `feat: contrôle des conflits de réservation`).
|
||||
- **Merge Request obligatoire** : aucun commit direct sur `main` ; au moins une approbation d'un pair et une CI verte sont exigées.
|
||||
- **Versionnement** : les versions livrées sont taguées (`v0.1.0` à la fin du Sprint 1, `v0.2.0` à la fin du Sprint 2, `v1.0.0-pilote` pour la mise en service).
|
||||
|
||||
### Chaîne CI/CD
|
||||
|
||||
| Étape | Déclencheur | Actions | Résultat attendu |
|
||||
|---|---|---|---|
|
||||
| **1. Build** | Chaque push | Installation des dépendances, compilation | Application construite sans erreur |
|
||||
| **2. Qualité** | Chaque push | Lint, analyse statique du code | Aucune erreur bloquante |
|
||||
| **3. Tests** | Chaque push | Tests unitaires et d'intégration, mesure de couverture | Tous les tests passent, couverture supérieur ou égal à 70 % |
|
||||
| **4. Sécurité** | Chaque merge request | Analyse des dépendances et du code | Aucune faille critique |
|
||||
| **5. Déploiement en recette** | Merge sur `main` | Déploiement automatique, migrations de base, tests de fumée | Version disponible pour le PO et le QA |
|
||||
| **6. Tests E2E** | Après déploiement en recette | Parcours automatisés | Parcours clés validés |
|
||||
| **7. Déploiement pilote** | Création d'un tag de version, **validation manuelle** du PO | Déploiement sur l'environnement du site pilote, possibilité de retour arrière | Version en production pilote |
|
||||
|
||||
**Bénéfices :** retour d'information en quelques minutes pour les développeurs, livraisons reproductibles, réduction des erreurs humaines, incrément disponible en continu pour la démonstration.
|
||||
|
||||
## 5.4 Livrables de fin de sprint
|
||||
|
||||
| # | Livrable | Responsable | Format |
|
||||
|:-:|---|---|---|
|
||||
| 1 | **Incrément logiciel** fonctionnel déployé en recette (version taguée) | Équipe de développement | Application + tag Git |
|
||||
| 2 | **Sprint Backlog** à jour (stories terminées / non terminées) | PO / SM | Tableau Kanban |
|
||||
| 3 | **Product Backlog** raffiné et repriorisé | PO | Backlog |
|
||||
| 4 | **Notes de version** (fonctionnalités, correctifs, limites connues) | Équipe | Document |
|
||||
| 5 | **Rapport de tests** (résultats, couverture, anomalies ouvertes) | QA | Rapport |
|
||||
| 6 | **Indicateurs du sprint** (burndown, vélocité, cycle time) | SM | Tableau de bord |
|
||||
| 7 | **Compte rendu de la Sprint Review** (démo, retours des parties prenantes) | PO / SM | Compte rendu |
|
||||
| 8 | **Compte rendu de la rétrospective** et plan d'actions d'amélioration | SM | Compte rendu |
|
||||
| 9 | **Documentation technique** à jour (installation, API, architecture) | Développeurs | README / wiki |
|
||||
| 10 | **Registre des risques** mis à jour | SM | Registre |
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 6. Planning et diagramme de Gantt
|
||||
|
||||
## 6.1 Diagramme de Gantt
|
||||
|
||||

|
||||
|
||||
## 6.2 Données de planification
|
||||
|
||||
Les semaines sont numérotées à partir du démarrage du projet (S1 = première semaine).
|
||||
|
||||
| ID | Activité | Début | Fin | Durée | Prédécesseur |
|
||||
|:-:|---|:-:|:-:|:-:|:-:|
|
||||
| 1 | Cadrage et Sprint 0 (backlog, architecture, outils) | S1 | S1 | 1 sem. | - |
|
||||
| 2 | **Sprint 1** : socle, connexion, ressources, réservation de salle | S2 | S3 | 2 sem. | 1 |
|
||||
| 3 | Revue et rétrospective du Sprint 1 | S3 | S3 | 1 j | 2 |
|
||||
| 4 | **Sprint 2** : annulation, matériel, véhicule, courriel, historique | S4 | S5 | 2 sem. | 2 |
|
||||
| 5 | Revue et rétrospective du Sprint 2 | S5 | S5 | 1 j | 4 |
|
||||
| 6 | **Sprint 3** : stabilisation, tests de performance, correctifs | S6 | S7 | 2 sem. | 4 |
|
||||
| 7 | Recette utilisateur (UAT) | S7 | S7 | 1 sem. | 4 |
|
||||
| 8 | Préparation du déploiement et formation du site pilote | S7 | S8 | 1,5 sem. | 6 |
|
||||
| 9 | Mise en service du site pilote | S8 | S8 | 1 j | 7, 8 |
|
||||
| 10 | Exploitation du pilote et suivi des retours | S8 | S11 | 4 sem. | 9 |
|
||||
| 11 | Bilan du pilote et décision Go / No-Go | S12 | S12 | 1 sem. | 10 |
|
||||
|
||||
**Jalons**
|
||||
|
||||
| Jalon | Semaine | Prédécesseur |
|
||||
|---|:-:|---|
|
||||
| M1 - Backlog prêt, socle défini | S1 | Activité 1 |
|
||||
| M2 - Démo Sprint 1 | S3 | Activité 3 |
|
||||
| M3 - Version pilote candidate | S5 | Activité 5 |
|
||||
| M4 - Version validée en recette | S7 | Activité 7 |
|
||||
| M5 - Mise en service du pilote | S8 | Activité 9 |
|
||||
| M6 - Bilan et décision | S12 | Activité 11 |
|
||||
|
||||
**Chemin critique :** Cadrage => Sprint 1 => Sprint 2 => Sprint 3 / UAT => Mise en service => Exploitation du pilote => Bilan.
|
||||
|
||||
**Cérémonies par sprint (2 semaines)**
|
||||
|
||||
| Cérémonie | Durée | Moment |
|
||||
|---|:-:|---|
|
||||
| Sprint Planning | 4 h | Jour 1 |
|
||||
| Daily Scrum | 15 min | Chaque jour |
|
||||
| Raffinement du backlog | 2 h | Milieu de sprint |
|
||||
| Sprint Review (démo) | 2 h | Jour 10 |
|
||||
| Rétrospective | 1 h 30 | Jour 10 |
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 7. Estimation des ressources et des délais
|
||||
|
||||
## 7.1 Ressources humaines
|
||||
|
||||
Les charges sont exprimées en **jours-personnes (j-p)**.
|
||||
|
||||
| Rôle | Effectif | Allocation | Cadrage (S1) | Sprint 1 | Sprint 2 | Sprint 3 | Pilote (S8-S11) | **Total** |
|
||||
|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| Product Owner | 1 | 50 % | 3 | 5 | 5 | 5 | 5 | **23** |
|
||||
| Scrum Master | 1 | 50 % | 3 | 5 | 5 | 5 | 2 | **20** |
|
||||
| Développeurs | 3 | 100 % | 9 | 30 | 30 | 30 | 12 | **111** |
|
||||
| Testeur / QA | 1 | 50 % | 1 | 5 | 5 | 5 | 3 | **19** |
|
||||
| **Total** | **6** | | **16** | **45** | **45** | **45** | **22** | **173** |
|
||||
|
||||
*Pendant le pilote, les développeurs sont mobilisés à environ 20 % pour le support et les correctifs.*
|
||||
|
||||
## 7.2 Répartition de l'effort de développement par histoire (Sprint 1)
|
||||
|
||||
| Story | Points | Heures | Jours-personnes |
|
||||
|---|:-:|:-:|:-:|
|
||||
| HU-00 Socle technique et CI/CD | 3 | 24 | 3 |
|
||||
| HU-01 Connexion utilisateur | 5 | 40 | 5 |
|
||||
| HU-09 Administration des ressources | 5 | 40 | 5 |
|
||||
| HU-02 Consulter les ressources | 3 | 24 | 3 |
|
||||
| HU-03 Réserver une salle | 8 | 64 | 8 |
|
||||
| **Total** | **24** | **192** | **24** |
|
||||
|
||||
## 7.3 Prévision d'avancement par sprint
|
||||
|
||||
| Sprint | Points planifiés | Vélocité cumulée prévue | Reste du périmètre pilote (42 pts) |
|
||||
|---|:-:|:-:|:-:|
|
||||
| Sprint 1 | 24 | 24 | 18 |
|
||||
| Sprint 2 | 18 *(+ 6 de réserve)* | 42 | 0 |
|
||||
| Sprint 3 | 12 de stabilisation + 12 selon retours | - | - |
|
||||
|
||||

|
||||
|
||||
## 7.4 Ressources techniques et matérielles
|
||||
|
||||
| Ressource | Usage | Disponibilité requise |
|
||||
|---|---|---|
|
||||
| Dépôt Git et plateforme CI/CD | Versionnement, intégration et déploiement continus | Sprint 0 |
|
||||
| Environnement de développement (local) | Développement individuel | Sprint 0 |
|
||||
| Environnement de recette | Démonstrations, tests QA et UAT | Fin du Sprint 1 |
|
||||
| Environnement du site pilote (production) | Utilisation réelle | Semaine 7 |
|
||||
| Base de données | Stockage des ressources et réservations | Sprint 0 |
|
||||
| Accès à l'annuaire / SSO d'entreprise | Authentification (HU-01) | Début du Sprint 1 |
|
||||
| Service d'envoi d'courriels (SMTP) | Confirmations (HU-07) | Début du Sprint 2 |
|
||||
| Outil de suivi (tableau Kanban) | Gestion du sprint | Sprint 0 |
|
||||
| Données initiales des ressources du site pilote | Alimentation du catalogue | Fin du Sprint 1 |
|
||||
|
||||
## 7.5 Synthèse des délais
|
||||
|
||||
| Étape | Durée | Cumul |
|
||||
|---|:-:|:-:|
|
||||
| Cadrage (Sprint 0) | 1 semaine | S1 |
|
||||
| Développement (3 sprints) | 6 semaines | S7 |
|
||||
| Déploiement et formation | 1 semaine | S8 |
|
||||
| Pilote | 4 semaines | S11 |
|
||||
| Bilan | 1 semaine | S12 |
|
||||
|
||||
**Délai pour disposer d'une première version utilisable sur le site pilote : 8 semaines.** Une première démo (réservation de salle) est disponible dès la fin de la **semaine 3**.
|
||||
|
||||
**Marges de sécurité :** réserve de 6 points au Sprint 2 et 12 points de capacité libre au Sprint 3, qui absorbent les aléas sans remettre en cause la date de mise en service.
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 8. Plan de gestion des risques
|
||||
|
||||
## 8.1 Méthode d'évaluation
|
||||
|
||||
- **Probabilité (P)** : 1 = très improbable, 3 = possible, 5 = quasi certain.
|
||||
- **Impact (I)** : 1 = négligeable, 3 = modéré, 5 = bloquant pour le pilote.
|
||||
- **Criticité = P × I** : supérieure ou égale à 12 = **élevée**, 6 à 11 = **moyenne**, supérieur ou égal à 5 = **faible**.
|
||||
- **Stratégies :** Éviter · Réduire · Transférer · Accepter.
|
||||
|
||||
## 8.2 Registre des risques
|
||||
|
||||
| ID | Risque | P | I | Criticité | Stratégie | Actions d'atténuation | Plan de secours (si le risque survient) | Responsable |
|
||||
|:-:|---|:-:|:-:|:-:|---|---|---|---|
|
||||
| R1 | **Double réservation** lors de réservations simultanées | 3 | 5 | **15** | Éviter | Verrou ou contrainte d'unicité en base ; tests de concurrence automatisés dès le Sprint 1 | Verrouiller temporairement la création côté serveur ; correctif prioritaire | Développeurs |
|
||||
| R2 | **Authentification d'entreprise** plus complexe que prévu | 3 | 4 | **12** | Réduire | Spike dès le début du Sprint 1 ; contact avec l'équipe SI identifié en amont | Authentification locale temporaire avec comptes importés | Tech lead |
|
||||
| R3 | **Capacité surestimée** (pas de vélocité historique) | 4 | 3 | **12** | Réduire | Marge incluse dans la capacité ; recalibrage après le Sprint 1 ; réserve de 6 pts au Sprint 2 | Retirer les stories Should du périmètre pilote | Scrum Master |
|
||||
| R4 | **Faible disponibilité du PO** et des utilisateurs du site pilote | 3 | 4 | **12** | Réduire | Créneaux de validation réservés à l'agenda ; désignation d'un suppléant du PO ; utilisateurs relais sur site | Décisions déléguées au suppléant ; démos enregistrées | Product Owner |
|
||||
| R5 | **Dérive du périmètre** (ajout de fonctionnalités Could) | 3 | 3 | **9** | Éviter | Périmètre pilote figé ; toute nouvelle demande passe par le backlog et la priorisation du PO | Report systématique au backlog post-pilote | Product Owner |
|
||||
| R6 | **Données de ressources** incomplètes ou inexactes | 3 | 3 | **9** | Réduire | Modèle de données d'import fourni au site pilote ; responsable des données désigné ; contrôle de qualité avant la mise en service | Saisie manuelle assistée par l'équipe | Product Owner |
|
||||
| R7 | **Faible adoption** par les salariés du site pilote | 2 | 4 | **8** | Réduire | Communication et formation avant la mise en service ; interface simple ; ambassadeurs sur site | Sessions de support supplémentaires, ajustement de l'ergonomie | Product Owner |
|
||||
| R8 | **Sécurité et conformité RGPD** (données personnelles des salariés) | 2 | 4 | **8** | Réduire | Minimisation des données ; contrôle des accès par rôle ; analyse automatique des failles ; validation du DPO | Correctif de sécurité prioritaire ; suspension du service si nécessaire | Tech lead |
|
||||
| R9 | **Dette technique** et baisse de la qualité sous pression | 2 | 3 | **6** | Réduire | Definition of Done stricte ; revues de code ; temps de refactoring dans chaque sprint | Sprint dédié à la dette avant l'extension | Équipe |
|
||||
| R10 | **Absence** d'un membre clé | 2 | 3 | **6** | Réduire | Polyvalence des développeurs ; revues croisées ; documentation à jour | Réaffectation des tâches, réduction de l'engagement | Scrum Master |
|
||||
| R11 | **Indisponibilité du service d'courriel** | 2 | 2 | **4** | Accepter | Envoi asynchrone avec reprise ; échec journalisé sans bloquer la réservation | Notification dans l'application en attendant | Développeurs |
|
||||
|
||||
## 8.3 Matrice de criticité (probabilité × impact)
|
||||
|
||||
| Probabilité / Impact => | 1 | 2 | 3 | 4 | 5 |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| **5** | | | | | |
|
||||
| **4** | | | R3 | | |
|
||||
| **3** | | | R5, R6 | R2, R4 | R1 |
|
||||
| **2** | | R11 | R9, R10 | R7, R8 | |
|
||||
| **1** | | | | | |
|
||||
|
||||
## 8.4 Suivi des risques
|
||||
|
||||
- Le registre est **revu à chaque Sprint Planning** et lors de chaque rétro par le Scrum Master.
|
||||
- Les risques de criticité supérieurs ou égaux à 12 sont **suivis chaque semaine** ; le dépassement d'un seuil d'alerte déclenche immédiatement le plan de secours.
|
||||
- Tout nouveau risque identifié est ajouté au registre, évalué (P × I) et rattaché à un responsable.
|
||||
- Un risque clos ou sans objet est archivé avec date et raison.
|
||||
|
||||
<div style="page-break-after: always"></div>
|
||||
|
||||
# 9. Hypothèses retenues
|
||||
|
||||
Le cas ne précisant pas certaines données, les hypothèses suivantes ont été retenues. Elles devront être confirmées avec les parties prenantes.
|
||||
|
||||
| # | Hypothèse |
|
||||
|:-:|---|
|
||||
| H1 | Sprints de **2 semaines** (10 jours ouvrés). |
|
||||
| H2 | Équipe de 6 personnes : PO (50 %), Scrum Master (50 %), 3 développeurs (100 %), 1 testeur (50 %). |
|
||||
| H3 | **1 point d'histoire ≈ 1 jour-personne idéal** ; ce ratio sera recalibré après le Sprint 1. |
|
||||
| H4 | L'entreprise dispose d'un annuaire d'entreprise (type SSO) exploitable pour la connexion. |
|
||||
| H5 | Le pilote concerne **un seul site**, avec quelques centaines de salariés au plus. |
|
||||
| H6 | Le pilote dure **4 semaines** avant le bilan et la décision de généralisation. |
|
||||
| H7 | Aucune vélocité historique n'existe : la capacité du Sprint 1 est estimée à partir de la disponibilité. |
|
||||
| H8 | Les stories HU-11 (agenda externe) et HU-12 (notifications mobiles) sont exclues de la version pilote. |
|
||||
| H9 | Les environnements (recette, pilote) et le service d'courriel sont fournis par le service informatique de l'entreprise. |
|
||||
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,509 @@
|
||||
# Atelier — Développer une application web sécurisée en Python
|
||||
|
||||
**Fil rouge : Bloc&Co, salle d'escalade.**
|
||||
La salle veut gérer ses adhérents en ligne : inscription, connexion, espace
|
||||
personnel, et un accès réservé au personnel.
|
||||
|
||||
À la fin de l'atelier, vous saurez construire **seul** un module de gestion
|
||||
d'utilisateurs sécurisé : c'est la brique de base de presque toutes les
|
||||
applications web, et elle se transpose à n'importe quel domaine métier.
|
||||
|
||||
---
|
||||
|
||||
## Ce qui est fourni
|
||||
|
||||
| Fichier | Contenu |
|
||||
|---|---|
|
||||
| `starter/app.py` | La plomberie de la base, et les fonctions à compléter |
|
||||
| `starter/schema.sql` | La table `adherents`. Ne pas la modifier. |
|
||||
| `starter/templates/` | Tout le HTML, déjà écrit |
|
||||
| `starter/static/style.css` | La feuille de style |
|
||||
| `test_app.py` | Les tests. **Un exercice est réussi quand ses tests passent.** |
|
||||
|
||||
## Installation (10 minutes, à faire en premier)
|
||||
|
||||
```
|
||||
py -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
pip install flask pytest
|
||||
python app.py
|
||||
```
|
||||
|
||||
Sur macOS ou Linux, remplacez les deux premières lignes par :
|
||||
`python3 -m venv .venv` puis `source .venv/bin/activate`
|
||||
|
||||
Ouvrez http://127.0.0.1:5000 — la page d'accueil doit s'afficher.
|
||||
Un compte administrateur existe déjà : `admin@blocandco.fr` / `Admin2026!`
|
||||
|
||||
Pour lancer les tests : `pytest -v`
|
||||
Au départ, la plupart échouent. C'est normal : c'est votre feuille de route.
|
||||
|
||||
---
|
||||
|
||||
# Partie A — Les bases du web en Python
|
||||
|
||||
## Exercice 0 — Monter un projet de zéro
|
||||
|
||||
**Notions** : environnement virtuel, dépendances, arborescence d'un projet Flask.
|
||||
|
||||
> Le jour de l'épreuve, vous ne saurez qu'en lisant le sujet si un dépôt vous
|
||||
> est fourni ou si vous partez de rien. **Entraînez-vous aux deux.**
|
||||
|
||||
**A. Page blanche.** Sans rien copier, dans un dossier neuf, créez un projet
|
||||
Flask qui démarre : environnement virtuel, installation, `app.py`, une route
|
||||
qui affiche un texte. Chronométrez-vous.
|
||||
|
||||
```
|
||||
Objectif : moins de 10 minutes, sans consulter de documentation.
|
||||
Recommencez demain dans un autre dossier, sans regarder vos notes.
|
||||
```
|
||||
|
||||
**B. Projet existant.** Ouvrez le dossier `starter/` et, **avant de coder
|
||||
quoi que ce soit**, remplissez ce tableau :
|
||||
|
||||
| Question | Votre réponse |
|
||||
|---|---|
|
||||
| Combien de routes sont déclarées ? | |
|
||||
| Où le mot de passe est-il haché ? | |
|
||||
| Quelles fonctions sont vides ? | |
|
||||
| Que fait `@app.teardown_appcontext` ? | |
|
||||
| L'application démarre-t-elle en l'état ? | |
|
||||
|
||||
**Pourquoi cet exercice** : reprendre du code écrit par quelqu'un d'autre est
|
||||
la situation normale en entreprise, et la moitié des candidats perdent une
|
||||
heure parce qu'ils ont commencé à taper avant d'avoir lu.
|
||||
|
||||
La fiche `MEMO-JOUR-J.md` récapitule les deux démarches. Gardez-la ouverte
|
||||
pendant l'épreuve.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 1 — Votre première page
|
||||
|
||||
**Notions** : route, fonction de vue, serveur de développement.
|
||||
|
||||
Créez un fichier `essai.py` séparé contenant une application Flask minimale
|
||||
avec une route `/bonjour` qui renvoie le texte `Bonjour Bloc&Co`.
|
||||
Lancez-la, ouvrez-la dans le navigateur.
|
||||
|
||||
Ajoutez ensuite une route `/bonjour/<prenom>` qui affiche `Bonjour <prenom>`.
|
||||
|
||||
```
|
||||
http://127.0.0.1:5000/bonjour → Bonjour Bloc&Co
|
||||
http://127.0.0.1:5000/bonjour/Lea → Bonjour Lea
|
||||
```
|
||||
|
||||
**Erreurs attendues** : oublier `app.run()` · deux routes avec le même nom de
|
||||
fonction → `AssertionError` · modifier le code sans recharger (pensez à
|
||||
`debug=True`).
|
||||
|
||||
---
|
||||
|
||||
## Exercice 2 — Séparer le HTML du Python
|
||||
|
||||
**Notions** : templates Jinja, `render_template`, héritage de gabarit.
|
||||
|
||||
Ouvrez `templates/base.html` et `templates/accueil.html`. Répondez par écrit,
|
||||
en une phrase chacune :
|
||||
|
||||
1. Que fait `{% extends "base.html" %}` ?
|
||||
2. Où atterrit le contenu de `{% block contenu %}` ?
|
||||
3. Pourquoi écrit-on `{{ url_for('profil') }}` plutôt que `/profil` ?
|
||||
4. Dans `base.html`, pourquoi le lien « Administration » n'apparaît-il pas
|
||||
pour tout le monde ?
|
||||
|
||||
**Pourquoi c'est important** : le jury vous demandera de justifier votre
|
||||
architecture. « J'ai séparé la présentation de la logique » est une réponse,
|
||||
encore faut-il pouvoir la montrer dans le code.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 3 — Un formulaire qui parle au serveur
|
||||
|
||||
**Notions** : méthodes GET et POST, `request.form`.
|
||||
|
||||
Dans `essai.py`, créez une route `/echo` qui accepte GET et POST :
|
||||
|
||||
- en GET, elle affiche un formulaire avec un champ `message`
|
||||
- en POST, elle réaffiche la page avec le message reçu
|
||||
|
||||
```
|
||||
Saisie : "salut"
|
||||
Affiché : Vous avez écrit : salut
|
||||
```
|
||||
|
||||
Puis essayez de saisir `<b>salut</b>`. Que s'affiche-t-il à l'écran ?
|
||||
Gardez la réponse de côté : on y revient à l'exercice 12.
|
||||
|
||||
**Erreurs attendues** : oublier `methods=["GET", "POST"]` → erreur 405 ·
|
||||
`request.form["message"]` sur un champ absent → `KeyError` (préférez
|
||||
`request.form.get("message", "")`).
|
||||
|
||||
---
|
||||
|
||||
# Partie B — Enregistrer les données
|
||||
|
||||
## Exercice 4 — Parler à la base
|
||||
|
||||
**Notions** : SQLite, `SELECT`, `INSERT`, requêtes paramétrées.
|
||||
|
||||
Dans un fichier d'essai, ouvrez `blocandco.db` et :
|
||||
|
||||
1. affichez tous les adhérents
|
||||
2. affichez uniquement ceux dont le rôle est `admin`
|
||||
3. comptez le nombre total d'adhérents
|
||||
4. insérez un adhérent de test, puis supprimez-le
|
||||
|
||||
**Règle non négociable, à appliquer dès maintenant :** une valeur variable ne
|
||||
s'écrit **jamais** dans le texte de la requête.
|
||||
|
||||
```python
|
||||
# NON — faille d'injection SQL
|
||||
db.execute("SELECT * FROM adherents WHERE email = '" + email + "'")
|
||||
|
||||
# OUI — requête paramétrée
|
||||
db.execute("SELECT * FROM adherents WHERE email = ?", (email,))
|
||||
```
|
||||
|
||||
**Erreurs attendues** : oublier la virgule dans `(email,)` → le tuple n'en est
|
||||
pas un · oublier `commit()` après un `INSERT` → rien n'est enregistré ·
|
||||
`fetchone()` renvoie `None` quand il n'y a pas de résultat, et le code plante
|
||||
trois lignes plus loin.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 5 — L'inscription
|
||||
|
||||
**Notions** : validation des entrées, hachage de mot de passe, contrainte
|
||||
d'unicité.
|
||||
|
||||
Complétez `valider_inscription()` puis la route `/inscription`.
|
||||
|
||||
**Règles de validation** :
|
||||
|
||||
| Champ | Règle |
|
||||
|---|---|
|
||||
| nom, prénom | non vides |
|
||||
| email | contient `@` et un point après le `@` |
|
||||
| mot de passe | 12 caractères minimum, une majuscule, un chiffre |
|
||||
| confirmation | identique au mot de passe |
|
||||
| email | pas déjà présent en base |
|
||||
|
||||
**Le mot de passe ne doit jamais arriver en clair dans la base.** Utilisez
|
||||
`generate_password_hash()` de Werkzeug.
|
||||
|
||||
```
|
||||
Entrée : Léa / Dupont / lea@test.fr / "Court1A"
|
||||
Sortie : « Le mot de passe doit faire au moins 12 caractères. »
|
||||
|
||||
Entrée : Léa / Dupont / lea@test.fr / "Grimpe2026ABC"
|
||||
Sortie : « Compte créé. Vous pouvez vous connecter. »
|
||||
En base : $scrypt$... (et non "Grimpe2026ABC")
|
||||
```
|
||||
|
||||
**Vérification** : `pytest -k inscription` et `pytest -k mot_de_passe`
|
||||
|
||||
**Erreurs attendues** : enregistrer le mot de passe en clair · hacher la
|
||||
confirmation au lieu du mot de passe · renvoyer le mot de passe dans le
|
||||
formulaire réaffiché · comparer les e-mails sans normaliser la casse, si bien
|
||||
que `Lea@Test.fr` et `lea@test.fr` créent deux comptes.
|
||||
|
||||
---
|
||||
|
||||
# Partie C — Authentification et autorisations
|
||||
|
||||
## Exercice 6 — La connexion
|
||||
|
||||
**Notions** : session, vérification d'empreinte.
|
||||
|
||||
Complétez `utilisateur_courant()` puis la route `/connexion`.
|
||||
|
||||
Le principe : on ne déchiffre **jamais** un mot de passe. On rehache celui qui
|
||||
est saisi et on compare les deux empreintes, avec `check_password_hash()`.
|
||||
|
||||
**Point de sécurité, et il compte** : que l'adresse existe ou non, que le mot
|
||||
de passe soit faux ou non, affichez **exactement le même message**. Sinon un
|
||||
attaquant découvre quelles adresses sont inscrites chez vous — c'est une
|
||||
énumération de comptes.
|
||||
|
||||
```
|
||||
Entrée : lea@test.fr / mauvais → « Identifiants incorrects. »
|
||||
Entrée : inconnu@test.fr / n'importe → « Identifiants incorrects. »
|
||||
```
|
||||
|
||||
**Vérification** : `pytest -k connexion`
|
||||
|
||||
**Erreurs attendues** : messages différents selon le cas · stocker tout
|
||||
l'utilisateur dans la session au lieu de son seul identifiant · oublier
|
||||
`session.clear()` avant de connecter quelqu'un (fixation de session) · oublier
|
||||
`app.secret_key` → Flask refuse d'écrire dans la session.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 7 — Déconnexion et pages protégées
|
||||
|
||||
**Notions** : décorateur, redirection.
|
||||
|
||||
1. Complétez `/deconnexion` : elle vide la session.
|
||||
2. Complétez le décorateur `login_requis`.
|
||||
|
||||
Un décorateur est une fonction qui en enveloppe une autre pour ajouter un
|
||||
contrôle avant son exécution. Ici : « si personne n'est connecté, redirige
|
||||
vers la connexion ; sinon, laisse passer ».
|
||||
|
||||
```
|
||||
Visiteur anonyme sur /profil → redirigé vers /connexion
|
||||
« Connectez-vous pour accéder à cette page. »
|
||||
```
|
||||
|
||||
**Vérification** : `pytest -k protege` et `pytest -k deconnexion`
|
||||
|
||||
**Erreurs attendues** : oublier `@wraps(vue)` → Flask voit deux routes nommées
|
||||
`wrapper` et refuse de démarrer · placer `@login_requis` **au-dessus** de
|
||||
`@app.route` (l'ordre compte : la route en premier) · oublier de renvoyer le
|
||||
résultat de `vue(...)`.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 8 — L'espace personnel
|
||||
|
||||
**Notions** : `UPDATE`, revalidation côté serveur.
|
||||
|
||||
Complétez `/profil` : afficher les informations, et permettre de modifier nom,
|
||||
prénom et niveau.
|
||||
|
||||
**Le piège, et c'est le plus important de l'atelier** : le `<select>` du
|
||||
niveau propose trois valeurs, mais n'importe qui peut modifier le HTML dans
|
||||
son navigateur et envoyer ce qu'il veut. **Tout ce qui vient du client est
|
||||
hostile jusqu'à preuve du contraire.** Revalidez contre `NIVEAUX` côté serveur.
|
||||
|
||||
```
|
||||
Entrée : niveau = "confirme" → « Profil mis à jour. »
|
||||
Entrée : niveau = "super_admin" → « Niveau invalide. »
|
||||
```
|
||||
|
||||
Vérifiez aussi que l'`UPDATE` porte bien sur l'utilisateur **connecté**, et
|
||||
pas sur un identifiant envoyé par le formulaire.
|
||||
|
||||
**Vérification** : `pytest -k profil` et `pytest -k niveau`
|
||||
|
||||
**Erreurs attendues** : faire confiance au `<select>` · oublier le `WHERE` dans
|
||||
l'`UPDATE` → tous les adhérents sont modifiés d'un coup · laisser l'utilisateur
|
||||
changer son propre rôle par ce formulaire.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 9 — Les rôles
|
||||
|
||||
**Notions** : contrôle d'autorisation, séparation authentification /
|
||||
autorisation.
|
||||
|
||||
Complétez le décorateur `admin_requis`, la route `/admin` et `/admin/promouvoir`.
|
||||
|
||||
Retenez la distinction, le jury la pose souvent :
|
||||
|
||||
- **authentification** : qui êtes-vous ? (connexion)
|
||||
- **autorisation** : avez-vous le droit de faire ceci ? (rôle)
|
||||
|
||||
Un utilisateur connecté n'est pas un utilisateur autorisé.
|
||||
|
||||
```
|
||||
Visiteur anonyme sur /admin → « Connectez-vous… »
|
||||
Adhérent connecté sur /admin → « Accès réservé aux administrateurs. »
|
||||
Administrateur sur /admin → la liste des adhérents
|
||||
```
|
||||
|
||||
**Vérification** : `pytest -k admin` et `pytest -k promotion`
|
||||
|
||||
**Erreurs attendues** : masquer le lien dans le menu **sans** protéger la route
|
||||
(l'URL reste accessible en la tapant) · vérifier le rôle avec `=` au lieu de
|
||||
`==` · protéger l'affichage mais pas l'action `/admin/promouvoir` — c'est la
|
||||
faille la plus fréquente et la plus grave.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 10 — La statistique d'accueil
|
||||
|
||||
**Notions** : `COUNT`, passage de variable au template.
|
||||
|
||||
Affichez sur la page d'accueil le nombre réel d'adhérents inscrits.
|
||||
|
||||
```
|
||||
Base vide + compte admin → 1 adhérent inscrit
|
||||
Après une inscription → 2 adhérents inscrits
|
||||
```
|
||||
|
||||
**Vérification** : `pytest -k nombre`
|
||||
|
||||
**Erreurs attendues** : compter en Python avec `len(fetchall())` au lieu d'un
|
||||
`COUNT(*)` en SQL — correct ici, inacceptable sur un million de lignes ·
|
||||
oublier d'accorder le pluriel.
|
||||
|
||||
---
|
||||
|
||||
# Partie D — La sécurité, ce que le jury regarde vraiment
|
||||
|
||||
> Les quatre exercices suivants se font **sur votre propre application**.
|
||||
> C'est légal, c'est le but, et c'est exactement ce qu'on vous demandera de
|
||||
> démontrer.
|
||||
|
||||
## Exercice 11 — Injection SQL
|
||||
|
||||
**Notions** : OWASP A03, requêtes paramétrées.
|
||||
|
||||
1. Dans une copie de votre code, remplacez volontairement la requête de
|
||||
connexion par une concaténation :
|
||||
|
||||
```python
|
||||
db.execute("SELECT * FROM adherents WHERE email = '" + email + "'")
|
||||
```
|
||||
|
||||
2. Connectez-vous avec l'adresse `lea@test.fr' --` et n'importe quel mot de
|
||||
passe. Observez.
|
||||
3. Essayez `' OR '1'='1`. Observez.
|
||||
4. Revenez à la version paramétrée. Rejouez les deux charges.
|
||||
5. **Expliquez par écrit, en trois phrases, pourquoi le `?` règle le problème.**
|
||||
|
||||
**Vérification** : `pytest -k injection`
|
||||
|
||||
La réponse attendue : avec `?`, la requête et les données voyagent séparément.
|
||||
Le moteur a déjà analysé la structure de la requête quand il reçoit la valeur ;
|
||||
celle-ci ne peut donc plus être lue comme du code SQL.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 12 — Injection de script (XSS)
|
||||
|
||||
**Notions** : OWASP A03, échappement automatique.
|
||||
|
||||
1. Modifiez votre prénom en `<script>alert('XSS')</script>` puis affichez
|
||||
votre profil. Que se passe-t-il ? Regardez le code source de la page.
|
||||
2. Dans `profil.html`, remplacez `{{ utilisateur.prenom }}` par
|
||||
`{{ utilisateur.prenom|safe }}`. Rechargez.
|
||||
3. Expliquez ce que fait Jinja par défaut, et ce que `|safe` désactive.
|
||||
4. **Remettez le code dans son état initial.**
|
||||
|
||||
**Vérification** : `pytest -k xss`
|
||||
|
||||
**À retenir** : Jinja échappe automatiquement. Ce filtre `|safe`, vous le
|
||||
croiserez dans du vrai code — c'est souvent là qu'est la faille.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 13 — Contrôle d'accès défaillant
|
||||
|
||||
**Notions** : OWASP A01, le risque numéro 1 du classement.
|
||||
|
||||
Trois attaques à mener contre votre propre application :
|
||||
|
||||
1. **Accès direct par l'URL** — déconnectez-vous, tapez `/admin` à la main.
|
||||
Êtes-vous bloqué ?
|
||||
2. **Élévation de privilège** — connectez-vous comme simple adhérent, puis
|
||||
envoyez une requête POST vers `/admin/promouvoir/1`. Votre rôle change-t-il ?
|
||||
(Le test `test_promotion_interdite_a_un_adherent` fait exactement cela.)
|
||||
3. **Accès aux données d'autrui** — si vous aviez écrit `/profil/<id>` au lieu
|
||||
de `/profil`, que se passerait-il en changeant le numéro dans l'URL ?
|
||||
Expliquez pourquoi la version sans identifiant est plus sûre.
|
||||
|
||||
**Règle** : cacher un bouton n'est pas une mesure de sécurité. Seul le contrôle
|
||||
exécuté sur le serveur en est une.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 14 — Les quatre critères, appliqués à votre code
|
||||
|
||||
**Notions** : disponibilité, intégrité, confidentialité, traçabilité.
|
||||
|
||||
Pour chacun des quatre critères, citez **une ligne précise de votre code** qui
|
||||
y contribue, ou constatez qu'il manque et dites ce que vous ajouteriez.
|
||||
|
||||
| Critère | Dans votre application |
|
||||
|---|---|
|
||||
| Confidentialité | ? |
|
||||
| Intégrité | ? |
|
||||
| Disponibilité | ? |
|
||||
| Traçabilité | ? |
|
||||
|
||||
Indice pour le dernier : votre application n'enregistre aujourd'hui **aucune**
|
||||
trace des connexions. Proposez une table `journal` et dites ce que vous y
|
||||
mettriez — et surtout ce que vous n'y mettriez **jamais**.
|
||||
|
||||
---
|
||||
|
||||
# Partie E — L'épreuve
|
||||
|
||||
## Exercice 15 — Projet de synthèse, en autonomie (4 heures)
|
||||
|
||||
**Nouveau cahier des charges, à traiter seul.**
|
||||
|
||||
> **Rouletabosse**, association de covoiturage domicile-travail, veut une
|
||||
> application web. Elle doit permettre à un salarié de créer un compte
|
||||
> (nom, prénom, e-mail, mot de passe, entreprise), de se connecter, de
|
||||
> consulter et modifier son profil. Deux rôles : `membre` et `referent`.
|
||||
> Seul un référent peut voir la liste des membres et valider un compte.
|
||||
> La page d'accueil affiche le nombre de membres inscrits.
|
||||
> Front : HTML / CSS. Back : Python. Mots de passe hachés, sessions
|
||||
> sécurisées.
|
||||
|
||||
Vous partez d'une page blanche. Vous avez quatre heures.
|
||||
|
||||
**Livrable** : l'application qui tourne en local, plus la présentation de
|
||||
l'exercice 16.
|
||||
|
||||
**Conseil de méthode** : passez les 20 premières minutes à écrire le schéma de
|
||||
la base et la liste de vos routes, **avant** la première ligne de code. Les
|
||||
candidats qui codent immédiatement sont ceux qui ne finissent pas.
|
||||
|
||||
---
|
||||
|
||||
## Exercice 16 — La présentation au jury
|
||||
|
||||
Une application qui marche mais qu'on ne sait pas présenter est une
|
||||
application à moitié rendue. Préparez un support de 12 à 15 diapositives :
|
||||
|
||||
1. **Introduction** — le besoin métier, en deux phrases
|
||||
2. **Schéma de la base de données** — les tables, leurs champs, leurs liens
|
||||
3. **Modélisation de l'application** — les routes, et le parcours d'un
|
||||
utilisateur de l'inscription au profil
|
||||
4. **Choix technologiques** — et leur **justification**. « Flask parce que
|
||||
c'est léger et que le besoin ne demande pas d'ORM » est une justification.
|
||||
« Flask parce qu'on l'a vu en cours » n'en est pas une.
|
||||
5. **Sécurité** — hachage, sessions, requêtes paramétrées, contrôle des rôles,
|
||||
validation des entrées. Une diapositive par mesure, avec l'extrait de code.
|
||||
6. **Tests** — votre stratégie, le nombre de tests, une capture de `pytest`
|
||||
7. **Démonstration** — captures d'écran du parcours complet
|
||||
8. **Conclusion** — ce qui manque, et ce que vous feriez avec deux jours de plus
|
||||
|
||||
**Préparez une réponse à ces trois questions**, elles tombent presque toujours :
|
||||
|
||||
- Pourquoi hacher plutôt que chiffrer un mot de passe ?
|
||||
- Que se passe-t-il si je tape l'URL d'administration sans être connecté ?
|
||||
- Comment savez-vous que votre application fonctionne ?
|
||||
|
||||
---
|
||||
|
||||
# Grille d'évaluation
|
||||
|
||||
| Compétence du référentiel | Vérifiée par |
|
||||
|---|---|
|
||||
| **C15** — procédures d'authentification et d'autorisation intégrées | Exercices 6, 7, 9 |
|
||||
| **C15** — activités autorisées et interdites définies | Exercices 9, 13 |
|
||||
| **C15** — gestion sécurisée des données sur tout leur cycle de vie | Exercices 5, 14 |
|
||||
| **C16** — langages web et logiciel mobilisés (Python, JS…) | Exercices 1 à 10 |
|
||||
| **C16** — standards de développement respectés | `ruff check .` sur le rendu |
|
||||
| **C16** — les 4 critères de sécurité respectés | Exercice 14 |
|
||||
| **C17** — risques identifiés, robustesse aux attaques | Exercices 11, 12, 13 |
|
||||
| **C17** — composants externes connus et éprouvés | Exercice 16, diapositive 4 |
|
||||
|
||||
| Barème indicatif | Points |
|
||||
|---|---|
|
||||
| Partie A — bases web (ex. 1-3) | 2 |
|
||||
| Partie B — données et inscription (ex. 4-5) | 4 |
|
||||
| Partie C — authentification et rôles (ex. 6-10) | 6 |
|
||||
| Partie D — sécurité (ex. 11-14) | 4 |
|
||||
| Partie E — présentation (ex. 16) | 4 |
|
||||
|
||||
**Retraits transversaux** : mot de passe en clair en base (**−5, éliminatoire**)
|
||||
· requête SQL construite par concaténation (−3) · route protégée uniquement par
|
||||
le masquage d'un lien (−3) · `except:` nu (−1) · `app.secret_key` codée en dur
|
||||
dans un rendu présenté comme « prêt pour la production » (−1).
|
||||
@@ -0,0 +1,213 @@
|
||||
# Fiche — installer Python et Flask
|
||||
|
||||
À garder de côté. Vous n'en avez besoin que si `verif_poste.py` affiche une
|
||||
ligne `[ KO ]`, ou si votre terminal ne reconnaît pas la commande `python`.
|
||||
|
||||
**Durée : 10 minutes.** Suivez les étapes dans l'ordre, sans en sauter.
|
||||
|
||||
---
|
||||
|
||||
# Windows
|
||||
|
||||
## Étape 1 — Vérifier ce que vous avez déjà
|
||||
|
||||
Ouvrez un terminal : touche **Windows**, tapez `cmd`, **Entrée**.
|
||||
|
||||
Tapez :
|
||||
|
||||
```
|
||||
py --list
|
||||
```
|
||||
|
||||
| Ce que vous voyez | Ce que ça veut dire |
|
||||
|---|---|
|
||||
| Une liste avec `-V:3.9` ou mieux | **Python est déjà installé.** Passez directement à l'étape 4. |
|
||||
| `'py' n'est pas reconnu…` | Python n'est pas installé, ou mal installé. Étape 2. |
|
||||
| Uniquement des versions `3.8` ou moins | Trop ancien pour Flask 3. Étape 2. |
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 — Télécharger
|
||||
|
||||
Allez sur **https://www.python.org/downloads/**
|
||||
|
||||
Le gros bouton jaune propose la dernière version pour votre système.
|
||||
Cliquez, puis lancez le fichier téléchargé.
|
||||
|
||||
> N'installez pas Python depuis le Microsoft Store. Il fonctionne, mais il
|
||||
> isole les fichiers d'une façon qui pose des problèmes avec les
|
||||
> environnements virtuels — exactement ce qu'on va utiliser.
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 — Installer : l'écran qui compte
|
||||
|
||||
Le premier écran de l'installateur contient **une case à cocher en bas** :
|
||||
|
||||
```
|
||||
☑ Add python.exe to PATH ← COCHEZ-LA
|
||||
```
|
||||
|
||||
**C'est l'étape la plus importante de toute la fiche.** Si vous l'oubliez,
|
||||
Windows ne saura pas où trouver Python et toutes les commandes échoueront.
|
||||
|
||||
Cochez la case, puis cliquez sur **Install Now**.
|
||||
|
||||
Si vous avez déjà cliqué sans cocher : relancez l'installateur, choisissez
|
||||
**Modify**, puis **Repair** — ou désinstallez et recommencez.
|
||||
|
||||
À la fin, s'il propose **« Disable path length limit »**, acceptez.
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 — Vérifier
|
||||
|
||||
**Fermez complètement votre terminal et rouvrez-en un neuf.** Le PATH n'est lu
|
||||
qu'à l'ouverture : sans ça, vous testerez l'ancienne configuration.
|
||||
|
||||
```
|
||||
py --version
|
||||
```
|
||||
|
||||
Vous devez voir un numéro de version, par exemple `Python 3.13.2`.
|
||||
|
||||
---
|
||||
|
||||
## Étape 5 — Installer Flask
|
||||
|
||||
```
|
||||
py -m pip install flask pytest
|
||||
```
|
||||
|
||||
Puis relancez la vérification :
|
||||
|
||||
```
|
||||
py verif_poste.py
|
||||
```
|
||||
|
||||
Vous devez obtenir « Tout est prêt. Rien à faire. »
|
||||
|
||||
---
|
||||
|
||||
# macOS
|
||||
|
||||
## Étape 1 — Vérifier
|
||||
|
||||
Ouvrez **Terminal** (Spotlight : `Cmd + Espace`, tapez `terminal`).
|
||||
|
||||
```
|
||||
python3 --version
|
||||
```
|
||||
|
||||
Si vous obtenez 3.9 ou mieux, passez à l'étape 3.
|
||||
|
||||
## Étape 2 — Installer
|
||||
|
||||
Téléchargez l'installateur macOS sur **https://www.python.org/downloads/**
|
||||
et suivez-le. Aucune case particulière à cocher : sur macOS le PATH est
|
||||
configuré automatiquement.
|
||||
|
||||
## Étape 3 — Installer Flask
|
||||
|
||||
```
|
||||
python3 -m pip install flask pytest
|
||||
```
|
||||
|
||||
Sur macOS et Linux, remplacez partout `py` par `python3` dans les commandes
|
||||
de l'atelier.
|
||||
|
||||
---
|
||||
|
||||
# Dépannage
|
||||
|
||||
## « 'python' n'est pas reconnu en tant que commande interne »
|
||||
|
||||
La case **Add python.exe to PATH** n'a pas été cochée.
|
||||
|
||||
Solution immédiate : utilisez `py` à la place de `python`. Le lanceur `py`
|
||||
est installé séparément et fonctionne presque toujours.
|
||||
|
||||
Solution durable : relancez l'installateur → **Modify** → **Repair**.
|
||||
|
||||
---
|
||||
|
||||
## Taper `python` ouvre le Microsoft Store
|
||||
|
||||
Windows intercepte la commande avec un raccourci publicitaire.
|
||||
|
||||
**Paramètres** → **Applications** → **Paramètres avancés des applications**
|
||||
→ **Alias d'exécution d'application** → désactivez les deux lignes
|
||||
`python.exe` et `python3.exe`.
|
||||
|
||||
En attendant, utilisez `py` : il n'est pas concerné.
|
||||
|
||||
---
|
||||
|
||||
## J'ai plusieurs versions de Python et je ne sais pas laquelle tourne
|
||||
|
||||
```
|
||||
py --list
|
||||
```
|
||||
|
||||
Vous choisissez explicitement laquelle utiliser :
|
||||
|
||||
```
|
||||
py -3.13 verif_poste.py # remplacez 3.13 par VOTRE version
|
||||
py -3.13 -m pip install flask
|
||||
```
|
||||
|
||||
Le numéro après `-` est la version. Utilisez **toujours le même** pendant
|
||||
tout l'atelier, sinon vous installerez Flask dans une version et lancerez le
|
||||
code avec une autre — c'est la cause numéro un des `ModuleNotFoundError`.
|
||||
|
||||
---
|
||||
|
||||
## `pip` échoue avec une erreur de certificat ou de proxy
|
||||
|
||||
Vous êtes probablement sur un réseau d'entreprise ou d'école qui filtre.
|
||||
|
||||
Essayez d'abord :
|
||||
|
||||
```
|
||||
py -m pip install --user flask pytest
|
||||
```
|
||||
|
||||
Si cela échoue encore, utilisez votre connexion personnelle (partage de
|
||||
connexion du téléphone) le temps de l'installation. Une fois Flask installé,
|
||||
le réseau n'a plus d'importance.
|
||||
|
||||
---
|
||||
|
||||
## `ModuleNotFoundError: No module named 'flask'`
|
||||
|
||||
Flask est installé, mais pas pour l'interpréteur qui exécute votre code.
|
||||
|
||||
Vérifiez avec quel Python vous travaillez :
|
||||
|
||||
```
|
||||
py verif_poste.py
|
||||
```
|
||||
|
||||
La ligne **Executable** vous donne le chemin exact. Installez Flask pour
|
||||
celui-là précisément :
|
||||
|
||||
```
|
||||
py -m pip install flask
|
||||
```
|
||||
|
||||
Si vous utilisez un environnement virtuel, vérifiez qu'il est **activé** :
|
||||
votre ligne de commande doit commencer par `(.venv)`.
|
||||
|
||||
---
|
||||
|
||||
## Rien ne fonctionne et le cours a commencé
|
||||
|
||||
**Ne restez pas bloqué seul.** Dans le chat du cours, envoyez :
|
||||
|
||||
1. Le résultat complet de `py verif_poste.py`
|
||||
2. La commande que vous avez tapée
|
||||
3. Le message d'erreur **en entier**
|
||||
|
||||
En attendant la réponse, suivez sur l'écran partagé et binômez avec un
|
||||
camarade. Vous rattraperez le code à la pause : ce qui compte maintenant,
|
||||
c'est de ne pas perdre le fil du raisonnement.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Mémo — les 30 premières minutes de l'épreuve
|
||||
|
||||
> À garder sous les yeux pendant l'examen.
|
||||
> Vous avez 4 heures. Les 30 premières décident des 3 h 30 suivantes.
|
||||
|
||||
Deux situations possibles. Lisez le sujet : on vous donne un dépôt, ou pas.
|
||||
|
||||
---
|
||||
|
||||
## Situation A — on vous fournit un dépôt
|
||||
|
||||
**Ne codez rien avant d'avoir fait ces six étapes.**
|
||||
|
||||
1. **Clonez**, puis **lisez le README en entier.** Cinq minutes de lecture
|
||||
valent une heure de tâtonnement. Il dit comment lancer le projet.
|
||||
|
||||
2. **Cherchez le fichier de dépendances** : `requirements.txt`,
|
||||
`pyproject.toml` ou `Pipfile`. Il vous dit quel framework est imposé.
|
||||
|
||||
3. **Installez et lancez l'application telle quelle**, avant toute
|
||||
modification. Si elle ne démarre pas, c'est votre premier problème à
|
||||
résoudre — et vous devez le savoir maintenant, pas à 15 h.
|
||||
|
||||
4. **Cartographiez le projet en cinq minutes**, par écrit :
|
||||
|
||||
| Question | Réponse |
|
||||
|---|---|
|
||||
| Où sont déclarées les routes ? | |
|
||||
| Où sont les templates ? | |
|
||||
| Y a-t-il déjà une base de données ? un schéma ? | |
|
||||
| Qu'est-ce qui marche déjà ? | |
|
||||
| Qu'est-ce qui est vide ou marqué « à faire » ? | |
|
||||
|
||||
5. **Repérez les conventions existantes** : langue des noms de variables,
|
||||
structure des fonctions, façon d'accéder à la base. Votre code doit
|
||||
ressembler à celui qui est déjà là. Un jury le remarque.
|
||||
|
||||
6. **Faites un premier commit** avant de commencer. Vous aurez un point de
|
||||
retour si vous cassez tout.
|
||||
|
||||
---
|
||||
|
||||
## Situation B — page blanche
|
||||
|
||||
**L'arborescence à créer, dans cet ordre :**
|
||||
|
||||
```
|
||||
monprojet/
|
||||
app.py
|
||||
schema.sql
|
||||
requirements.txt
|
||||
templates/
|
||||
base.html
|
||||
static/
|
||||
style.css
|
||||
```
|
||||
|
||||
**Les commandes (Windows)** :
|
||||
|
||||
```
|
||||
mkdir monprojet
|
||||
cd monprojet
|
||||
py -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
pip install flask
|
||||
pip freeze > requirements.txt
|
||||
```
|
||||
|
||||
Sur macOS ou Linux, remplacez les deux premières lignes par :
|
||||
`python3 -m venv .venv` puis `source .venv/bin/activate`
|
||||
|
||||
**Le `app.py` minimal, à taper de mémoire :**
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "a-changer"
|
||||
|
||||
@app.route("/")
|
||||
def accueil():
|
||||
return "ça tourne"
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
```
|
||||
|
||||
Lancez-le. **Vous devez voir « ça tourne » dans le navigateur avant d'écrire
|
||||
la moindre fonctionnalité.** Tant que ce n'est pas le cas, rien d'autre
|
||||
n'a d'importance.
|
||||
|
||||
---
|
||||
|
||||
## Dans les deux cas : l'ordre de construction
|
||||
|
||||
Ne construisez jamais dans un autre ordre. Chaque étape s'appuie sur la
|
||||
précédente, et chacune doit marcher avant de passer à la suivante.
|
||||
|
||||
| # | Étape | Fini quand… |
|
||||
|---|---|---|
|
||||
| 1 | L'application démarre | une page s'affiche |
|
||||
| 2 | La base et la table existent | vous lisez une ligne en Python |
|
||||
| 3 | L'inscription | un compte apparaît en base, mot de passe **haché** |
|
||||
| 4 | La connexion | la session retient qui vous êtes |
|
||||
| 5 | Le profil | vous voyez et modifiez vos infos |
|
||||
| 6 | Les rôles | un non-admin est refusé sur la page admin |
|
||||
| 7 | La statistique | le compteur s'affiche sur l'accueil |
|
||||
| 8 | **Les captures d'écran** | vous avez de quoi faire la démo |
|
||||
|
||||
**À 14 h, arrêtez de coder.** La présentation vaut autant que le code. Une
|
||||
application à 80 % bien présentée passe mieux qu'une application à 100 %
|
||||
sans support.
|
||||
|
||||
---
|
||||
|
||||
## Les pièges qui coûtent des heures
|
||||
|
||||
| Symptôme | Cause réelle |
|
||||
|---|---|
|
||||
| `ModuleNotFoundError: flask` | environnement virtuel non activé — regardez si `(.venv)` est au début de la ligne |
|
||||
| `Address already in use` | une ancienne instance tourne encore : fermez-la, ou `app.run(port=5001)` |
|
||||
| Les modifications n'apparaissent pas | `debug=True` oublié, ou page en cache (Ctrl+F5) |
|
||||
| `jinja2.TemplateNotFound` | le dossier doit s'appeler exactement `templates` |
|
||||
| `RuntimeError: The session is unavailable` | `app.secret_key` manquante |
|
||||
| Les accents s'affichent en `?` | `encoding="utf-8"` oublié à la lecture du fichier |
|
||||
| L'erreur 405 | la route n'accepte pas POST : `methods=["GET", "POST"]` |
|
||||
|
||||
---
|
||||
|
||||
## Ce qui est éliminatoire
|
||||
|
||||
Quoi qu'il arrive, même à court de temps :
|
||||
|
||||
- **Jamais** de mot de passe en clair dans la base
|
||||
- **Jamais** de requête SQL construite avec `+` ou une f-string
|
||||
- **Toujours** le contrôle de rôle sur le serveur, pas seulement un lien caché
|
||||
|
||||
Si vous devez sacrifier une fonctionnalité, sacrifiez-la. Ne sacrifiez
|
||||
jamais une de ces trois règles.
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask>=3.0
|
||||
pytest>=8.0
|
||||
@@ -0,0 +1,18 @@
|
||||
# Règles de style de l'atelier.
|
||||
# Les étudiants lancent ruff check . avant de rendre.
|
||||
# Ce fichier garantit que tout le monde obtient le même résultat.
|
||||
|
||||
line-length = 88
|
||||
|
||||
[lint]
|
||||
select = ["E", "F", "W", "I", "UP", "B", "RET"]
|
||||
|
||||
ignore = [
|
||||
# On enseigne date.today(), les fuseaux horaires ne sont pas au programme.
|
||||
"DTZ011",
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
# Dans le squelette, ces imports sont fournis d'avance aux étudiants :
|
||||
# ils deviennent utiles au fur et à mesure des exercices.
|
||||
"starter/app.py" = ["F401", "RET504", "RET501"]
|
||||
@@ -0,0 +1 @@
|
||||
3.14
|
||||
@@ -0,0 +1,26 @@
|
||||
# Bloc&Co — squelette de l'atelier
|
||||
|
||||
## Démarrer
|
||||
|
||||
py -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
pip install -r ..\requirements.txt
|
||||
python app.py
|
||||
|
||||
Sur macOS ou Linux, remplacez les deux premières lignes par :
|
||||
`python3 -m venv .venv` puis `source .venv/bin/activate`
|
||||
|
||||
Puis ouvrir http://127.0.0.1:5000
|
||||
|
||||
## Ce qui est fourni
|
||||
|
||||
- `schema.sql` — la table `adherents`, à ne pas modifier
|
||||
- `templates/` — tout le HTML, déjà écrit
|
||||
- `static/style.css` — la feuille de style
|
||||
- `app.py` — la plomberie de la base de données, et des fonctions à compléter
|
||||
- `verif_poste.py` — vérifie que votre poste est prêt (à lancer en premier)
|
||||
|
||||
## Ce que vous écrivez
|
||||
|
||||
Tout ce qui est marqué `# À FAIRE`. Suivez les exercices dans l'ordre :
|
||||
l'application ne fonctionnera entièrement qu'à la fin de l'exercice 10.
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Bloc&Co — gestion des adhérents d'une salle d'escalade.
|
||||
|
||||
Squelette de l'atelier. Complétez tout ce qui est marqué « À FAIRE ».
|
||||
Lancement : python app.py puis http://127.0.0.1:5000
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import date
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, flash, g, redirect, render_template, request, session, url_for
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
BASE = Path(__file__).parent
|
||||
DB = BASE / "blocandco.db"
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "cle-de-developpement-a-changer"
|
||||
|
||||
levelX = ["debutant", "intermediaire", "confirme"]
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# FOURNI — plomberie de la base de données. Ne rien modifier ici.
|
||||
# ==========================================================================
|
||||
|
||||
def get_db():
|
||||
"""Une connexion par requête, rangée dans g et fermée automatiquement."""
|
||||
if "db" not in g:
|
||||
g.db = sqlite3.connect(DB)
|
||||
g.db.row_factory = sqlite3.Row
|
||||
return g.db
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_db(exception):
|
||||
db = g.pop("db", None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Crée la base au premier lancement et sème un compte administrateur."""
|
||||
if DB.exists():
|
||||
return
|
||||
con = sqlite3.connect(DB)
|
||||
con.executescript((BASE / "schema.sql").read_text(encoding="utf-8"))
|
||||
con.execute(
|
||||
"INSERT INTO adherents (lastname, firstname, email, password, level, role,"
|
||||
" sub_date) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
("Martin", "Claire", "admin@blocandco.fr",
|
||||
generate_password_hash("Admin2026!"), "confirme", "admin",
|
||||
date.today().isoformat()),
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
print("Base créée. Compte admin : admin@blocandco.fr / Admin2026!")
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def injecter_utilisateur():
|
||||
"""Rend la variable `utilisateur` disponible dans tous les templates."""
|
||||
return {"utilisateur": utilisateur_courant()}
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# EXERCICE 6 — qui est connecté ?
|
||||
# ==========================================================================
|
||||
|
||||
def utilisateur_courant():
|
||||
"""Renvoie la ligne de l'adhérent connecté, ou None si personne.
|
||||
|
||||
À FAIRE :
|
||||
- si "user_id" n'est pas dans `session`, renvoyer None
|
||||
- sinon, aller chercher la ligne correspondante dans la base
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# EXERCICE 7 — protéger une page
|
||||
# ==========================================================================
|
||||
|
||||
def login_requis(vue):
|
||||
"""Interdit l'accès à la vue si personne n'est connecté.
|
||||
|
||||
À FAIRE : si `utilisateur_courant()` vaut None, afficher un message
|
||||
et rediriger vers la page de connexion. Sinon, appeler la vue.
|
||||
"""
|
||||
@wraps(vue)
|
||||
def wrapper(*args, **kwargs):
|
||||
return vue(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# EXERCICE 9 — réserver une page aux administrateurs
|
||||
# ==========================================================================
|
||||
|
||||
def admin_requis(vue):
|
||||
"""Interdit l'accès à la vue si l'utilisateur n'est pas administrateur.
|
||||
|
||||
À FAIRE : refuser les visiteurs non connectés, PUIS refuser ceux dont
|
||||
le rôle n'est pas "admin". Deux messages différents.
|
||||
"""
|
||||
@wraps(vue)
|
||||
def wrapper(*args, **kwargs):
|
||||
return vue(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# EXERCICE 5 — valider le formulaire d'inscription
|
||||
# ==========================================================================
|
||||
|
||||
def valider_inscription(lastname, firstname, email, mdp, confirmation):
|
||||
"""Renvoie la liste des messages d'erreur. Liste vide = tout est bon.
|
||||
|
||||
À FAIRE, dans cet ordre :
|
||||
- lastname et prélastname non vides
|
||||
- email contenant "@" et un point après le "@"
|
||||
- mot de passe d'au moins 12 caractères
|
||||
- au moins un chiffre, au moins une majuscule
|
||||
- les deux mots de passe identiques
|
||||
"""
|
||||
erreurs = []
|
||||
return erreurs
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# ROUTES
|
||||
# ==========================================================================
|
||||
|
||||
@app.route("/")
|
||||
def accueil():
|
||||
"""EXERCICE 10 — afficher le lastnamebre d'adhérents inscrits.
|
||||
|
||||
À FAIRE : remplacer le 0 par le vrai total, lu dans la base
|
||||
avec un SELECT COUNT(*).
|
||||
"""
|
||||
nb = 0
|
||||
return render_template("accueil.html", nb_adherents=nb)
|
||||
|
||||
|
||||
@app.route("/inscription", methods=["GET", "POST"])
|
||||
def inscription():
|
||||
"""EXERCICE 5 — créer un compte.
|
||||
|
||||
À FAIRE :
|
||||
- en POST, récupérer les champs du formulaire
|
||||
- les valider avec valider_inscription()
|
||||
- refuser une adresse e-mail déjà présente dans la base
|
||||
- hacher le mot de passe avec generate_password_hash()
|
||||
- insérer l'adhérent avec le rôle "adherent"
|
||||
- rediriger vers la page de connexion
|
||||
"""
|
||||
if request.method == "POST":
|
||||
pass
|
||||
return render_template("inscription.html")
|
||||
|
||||
|
||||
@app.route("/connexion", methods=["GET", "POST"])
|
||||
def connexion():
|
||||
"""EXERCICE 6 — se connecter.
|
||||
|
||||
À FAIRE :
|
||||
- chercher l'adhérent par son e-mail
|
||||
- vérifier le mot de passe avec check_password_hash()
|
||||
- en cas d'échec, afficher LE MÊME message que l'adresse existe ou non
|
||||
- en cas de succès, vider la session puis y stocker l'identifiant
|
||||
"""
|
||||
if request.method == "POST":
|
||||
pass
|
||||
return render_template("connexion.html")
|
||||
|
||||
|
||||
@app.route("/deconnexion")
|
||||
def deconnexion():
|
||||
"""EXERCICE 7 — se déconnecter.
|
||||
|
||||
À FAIRE : vider la session et rediriger vers l'accueil.
|
||||
"""
|
||||
return redirect(url_for("accueil"))
|
||||
|
||||
|
||||
@app.route("/profil", methods=["GET", "POST"])
|
||||
@login_requis
|
||||
def profil():
|
||||
"""EXERCICE 8 — consulter et modifier son profil.
|
||||
|
||||
À FAIRE :
|
||||
- en POST, mettre à jour lastname, prélastname et level de l'utilisateur CONNECTÉ
|
||||
- refuser un level qui n'est pas dans levelX
|
||||
"""
|
||||
if request.method == "POST":
|
||||
pass
|
||||
return render_template("profil.html", levelx=levelX)
|
||||
|
||||
|
||||
@app.route("/admin")
|
||||
@admin_requis
|
||||
def admin():
|
||||
"""EXERCICE 9 — lister les adhérents.
|
||||
|
||||
À FAIRE : lire tous les adhérents, les plus récents d'abord.
|
||||
"""
|
||||
adherents = []
|
||||
return render_template("admin.html", adherents=adherents)
|
||||
|
||||
|
||||
@app.route("/admin/promouvoir/<int:adherent_id>", methods=["POST"])
|
||||
@admin_requis
|
||||
def promouvoir(adherent_id):
|
||||
"""EXERCICE 9 (suite) — promouvoir un adhérent administrateur."""
|
||||
return redirect(url_for("admin"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
app.run(debug=True)
|
||||
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "starter"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"flask>=3.1.3",
|
||||
"pytest>=9.1.1",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Schéma de la base « Bloc&Co »
|
||||
-- Exécuté une seule fois, au premier démarrage de l'application.
|
||||
|
||||
DROP TABLE IF EXISTS adherents;
|
||||
|
||||
CREATE TABLE adherents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
lastname TEXT NOT NULL,
|
||||
firstname TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL, -- empreinte, jamais le mot de passe
|
||||
level TEXT NOT NULL DEFAULT 'debutant',
|
||||
role TEXT NOT NULL DEFAULT 'adherent',
|
||||
sub_date TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
:root {
|
||||
--bleu: #0b6e9e;
|
||||
--bleu-fonce: #084f71;
|
||||
--gris: #5c6672;
|
||||
--bord: #dde3e9;
|
||||
--fond: #f6f8fa;
|
||||
--rouge: #a8341f;
|
||||
--vert: #1f6b45;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
color: #1a1f26;
|
||||
background: var(--fond);
|
||||
line-height: 1.55;
|
||||
}
|
||||
header {
|
||||
display: flex; align-items: center; gap: 1.5rem;
|
||||
padding: 0.9rem 1.5rem; background: #fff;
|
||||
border-bottom: 1px solid var(--bord);
|
||||
}
|
||||
.logo { font-weight: 700; font-size: 1.15rem; color: var(--bleu); text-decoration: none; }
|
||||
nav { margin-left: auto; display: flex; gap: 1.1rem; align-items: center; }
|
||||
nav a { color: var(--gris); text-decoration: none; font-size: 0.92rem; }
|
||||
nav a:hover { color: var(--bleu); }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
||||
h1 { font-size: 1.7rem; margin: 0 0 0.4rem; }
|
||||
h2 { font-size: 1.15rem; margin: 2rem 0 0.6rem; }
|
||||
.chapeau { color: var(--gris); margin-top: 0; }
|
||||
|
||||
.carte {
|
||||
background: #fff; border: 1px solid var(--bord); border-radius: 8px;
|
||||
padding: 1.25rem; display: flex; flex-direction: column; gap: 0.85rem;
|
||||
max-width: 420px;
|
||||
}
|
||||
label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.88rem; color: var(--gris); }
|
||||
input, select {
|
||||
padding: 0.5rem 0.65rem; border: 1px solid var(--bord); border-radius: 5px;
|
||||
font-size: 0.95rem; font-family: inherit; color: #1a1f26; background: #fff;
|
||||
}
|
||||
input:focus, select:focus { outline: 2px solid var(--bleu); outline-offset: 1px; border-color: var(--bleu); }
|
||||
.aide { font-size: 0.8rem; color: var(--gris); margin: 0; }
|
||||
|
||||
button, .bouton {
|
||||
background: var(--bleu); color: #fff; border: none; border-radius: 5px;
|
||||
padding: 0.55rem 1rem; font-size: 0.95rem; font-family: inherit;
|
||||
cursor: pointer; text-decoration: none; display: inline-block; text-align: center;
|
||||
}
|
||||
button:hover, .bouton:hover { background: var(--bleu-fonce); }
|
||||
button.lien { background: none; color: var(--bleu); padding: 0; text-decoration: underline; }
|
||||
button.lien:hover { background: none; color: var(--bleu-fonce); }
|
||||
|
||||
.messages { list-style: none; padding: 0; margin: 0 0 1.25rem; display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
.messages li { padding: 0.6rem 0.85rem; border-radius: 5px; font-size: 0.9rem; border-left: 3px solid; }
|
||||
.messages .erreur { background: #fbecea; border-color: var(--rouge); color: var(--rouge); }
|
||||
.messages .succes { background: #e9f3ed; border-color: var(--vert); color: var(--vert); }
|
||||
|
||||
.stat {
|
||||
display: flex; align-items: baseline; gap: 0.6rem;
|
||||
background: #fff; border: 1px solid var(--bord); border-radius: 8px;
|
||||
padding: 1.1rem 1.4rem; margin: 1.5rem 0; max-width: 420px;
|
||||
}
|
||||
.chiffre { font-size: 2.4rem; font-weight: 700; color: var(--bleu); line-height: 1; }
|
||||
.libelle { color: var(--gris); }
|
||||
|
||||
.infos { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1.5rem; }
|
||||
.infos dt { color: var(--gris); font-size: 0.88rem; }
|
||||
.infos dd { margin: 0; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid var(--bord); border-radius: 8px; overflow: hidden; font-size: 0.9rem; }
|
||||
th, td { text-align: left; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--bord); }
|
||||
th { background: #eef2f6; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gris); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block contenu %}
|
||||
<h1>Salle d'escalade Bloc&Co</h1>
|
||||
<p class="chapeau">Blocs, voies et cours collectifs, sept jours sur sept.</p>
|
||||
|
||||
<div class="stat">
|
||||
<span class="chiffre">{{ nb_adherents }}</span>
|
||||
<span class="libelle">adhérent{{ 's' if nb_adherents > 1 else '' }} inscrit{{ 's' if nb_adherents > 1 else '' }}</span>
|
||||
</div>
|
||||
|
||||
{% if not utilisateur %}
|
||||
<p><a class="bouton" href="{{ url_for('inscription') }}">Créer mon compte</a></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Administration — Bloc&Co{% endblock %}
|
||||
{% block contenu %}
|
||||
<h1>Administration</h1>
|
||||
<p>{{ adherents|length }} adhérent{{ 's' if adherents|length > 1 else '' }} enregistré{{ 's' if adherents|length > 1 else '' }}.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>lastname</th><th>E-mail</th><th>level</th><th>Rôle</th><th>Inscription</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in adherents %}
|
||||
<tr>
|
||||
<td>{{ a.firstname }} {{ a.lastname }}</td>
|
||||
<td>{{ a.email }}</td>
|
||||
<td>{{ a.level }}</td>
|
||||
<td>{{ a.role }}</td>
|
||||
<td>{{ a.sub_date }}</td>
|
||||
<td>
|
||||
{% if a.role != 'admin' %}
|
||||
<form method="post" action="{{ url_for('promouvoir', adherent_id=a.id) }}">
|
||||
<button type="submit" class="lien">Promouvoir</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block titre %}Bloc&Co{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a class="logo" href="{{ url_for('accueil') }}">Bloc&Co</a>
|
||||
<nav>
|
||||
{% if utilisateur %}
|
||||
<a href="{{ url_for('profil') }}">Mon profil</a>
|
||||
{% if utilisateur.role == 'admin' %}
|
||||
<a href="{{ url_for('admin') }}">Administration</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('deconnexion') }}">Déconnexion</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('connexion') }}">Connexion</a>
|
||||
<a class="bouton" href="{{ url_for('inscription') }}">Inscription</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for categorie, texte in messages %}
|
||||
<li class="{{ categorie }}">{{ texte }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block contenu %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Connexion — Bloc&Co{% endblock %}
|
||||
{% block contenu %}
|
||||
<h1>Connexion</h1>
|
||||
<form method="post" class="carte">
|
||||
<label>Adresse e-mail
|
||||
<input type="email" name="email" value="{{ email or '' }}" required>
|
||||
</label>
|
||||
<label>Mot de passe
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button type="submit">Se connecter</button>
|
||||
</form>
|
||||
<p>Pas encore de compte ? <a href="{{ url_for('inscription') }}">Inscrivez-vous</a>.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Inscription — Bloc&Co{% endblock %}
|
||||
{% block contenu %}
|
||||
<h1>Créer un compte</h1>
|
||||
<form method="post" class="carte">
|
||||
<label>Prélastname
|
||||
<input type="text" name="firstname" value="{{ firstname or '' }}" required>
|
||||
</label>
|
||||
<label>lastname
|
||||
<input type="text" name="lastname" value="{{ lastname or '' }}" required>
|
||||
</label>
|
||||
<label>Adresse e-mail
|
||||
<input type="email" name="email" value="{{ email or '' }}" required>
|
||||
</label>
|
||||
<label>Mot de passe
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<label>Confirmer le mot de passe
|
||||
<input type="password" name="confirmation" required>
|
||||
</label>
|
||||
<p class="aide">12 caractères minimum, au moins une majuscule et un chiffre.</p>
|
||||
<button type="submit">S'inscrire</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Mon profil — Bloc&Co{% endblock %}
|
||||
{% block contenu %}
|
||||
<h1>Mon profil</h1>
|
||||
|
||||
<dl class="carte infos">
|
||||
<dt>Adresse e-mail</dt><dd>{{ utilisateur.email }}</dd>
|
||||
<dt>Rôle</dt><dd>{{ utilisateur.role }}</dd>
|
||||
<dt>Inscrit depuis le</dt><dd>{{ utilisateur.sub_date }}</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Modifier mes informations</h2>
|
||||
<form method="post" class="carte">
|
||||
<label>Prélastname
|
||||
<input type="text" name="firstname" value="{{ utilisateur.firstname }}" required>
|
||||
</label>
|
||||
<label>lastname
|
||||
<input type="text" name="lastname" value="{{ utilisateur.lastname }}" required>
|
||||
</label>
|
||||
<label>level
|
||||
<select name="level">
|
||||
{% for n in levelx %}
|
||||
<option value="{{ n }}" {{ 'selected' if n == utilisateur.level }}>{{ n }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Tests de l'application Bloc&Co.
|
||||
|
||||
Lancement : pytest -v
|
||||
|
||||
Ces tests valident le comportement attendu à la fin de l'atelier.
|
||||
Donnez-les aux étudiants : un exercice est réussi quand ses tests passent.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import app as application
|
||||
import pytest
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path, monkeypatch):
|
||||
"""Une application neuve, avec une base jetable, pour chaque test."""
|
||||
base = tmp_path / "test.db"
|
||||
monkeypatch.setattr(application, "DB", base)
|
||||
application.init_db()
|
||||
application.app.config["TESTING"] = True
|
||||
with application.app.test_client() as c:
|
||||
yield c
|
||||
|
||||
def subscribe(client, email="lea@test.fr", mdp="Grimpe2026ABC"):
|
||||
return client.post("/inscription", data={
|
||||
"lastname": "Dupont", "firstname": "Lea", "email": email,
|
||||
"password": mdp, "confirmation": mdp,
|
||||
}, follow_redirects=True)
|
||||
|
||||
def connect(client, email="lea@test.fr", mdp="Grimpe2026ABC"):
|
||||
return client.post("/connexion", data={
|
||||
"email": email, "password": mdp,
|
||||
}, follow_redirects=True)
|
||||
|
||||
# --- Exercice 10 : la statistique ----------------------------------------
|
||||
|
||||
def test_accueil_affiche_le_lastnamebre_d_adherents(client):
|
||||
reponse = client.get("/")
|
||||
assert reponse.status_code == 200
|
||||
assert b">1<" in reponse.data # le compte admin seul
|
||||
|
||||
subscribe(client)
|
||||
assert b">2<" in client.get("/").data
|
||||
|
||||
# --- Exercice 5 : l'inscription ------------------------------------------
|
||||
|
||||
def test_inscription_password_trop_court_refuse(client):
|
||||
reponse = subscribe(client, mdp="Court1A")
|
||||
assert b"12 caract" in reponse.data
|
||||
|
||||
def test_inscription_password_sans_chiffre_refuse(client):
|
||||
reponse = subscribe(client, mdp="MotDePasseSansChiffre")
|
||||
assert b"chiffre" in reponse.data
|
||||
|
||||
def test_inscription_email_en_double_refuse(client):
|
||||
subscribe(client)
|
||||
reponse = subscribe(client)
|
||||
assert "déjà utilis".encode() in reponse.data
|
||||
|
||||
def test_inscription_email_insensible_a_la_casse(client):
|
||||
subscribe(client, email="Lea@Test.fr")
|
||||
reponse = subscribe(client, email="lea@test.fr")
|
||||
assert "déjà utilis".encode() in reponse.data
|
||||
|
||||
def test_inscription_password_jamais_en_clair(client):
|
||||
subscribe(client)
|
||||
stocke = sqlite3.connect(application.DB).execute(
|
||||
"SELECT password FROM adherents WHERE email = 'lea@test.fr'"
|
||||
).fetchone()[0]
|
||||
assert stocke != "Grimpe2026ABC"
|
||||
assert len(stocke) > 40
|
||||
|
||||
# --- Exercice 6 : la connexion -------------------------------------------
|
||||
|
||||
def test_mauvais_password_refuse(client):
|
||||
subscribe(client)
|
||||
reponse = connect(client, mdp="MauvaisMotDePasse")
|
||||
assert b"Identifiants incorrects" in reponse.data
|
||||
|
||||
def test_compte_inexistant_donne_le_meme_message(client):
|
||||
"""Le message ne doit pas révéler si l'adresse existe dans la base."""
|
||||
subscribe(client)
|
||||
inconnu = connect(client, email="personne@test.fr", mdp="Grimpe2026ABC")
|
||||
mauvais = connect(client, mdp="MauvaisMotDePasse")
|
||||
assert b"Identifiants incorrects" in inconnu.data
|
||||
assert b"Identifiants incorrects" in mauvais.data
|
||||
|
||||
def test_connexion_reussie(client):
|
||||
subscribe(client)
|
||||
reponse = connect(client)
|
||||
assert b"Mon profil" in reponse.data
|
||||
|
||||
# --- Exercice 7 : les pages protégées ------------------------------------
|
||||
|
||||
def test_profil_protege_sans_connexion(client):
|
||||
reponse = client.get("/profil", follow_redirects=True)
|
||||
assert b"Connectez-vous" in reponse.data
|
||||
|
||||
def test_profil_protege_apres_deconnexion(client):
|
||||
subscribe(client)
|
||||
connect(client)
|
||||
client.get("/deconnexion")
|
||||
reponse = client.get("/profil", follow_redirects=True)
|
||||
assert b"Connectez-vous" in reponse.data
|
||||
|
||||
# --- Exercice 8 : le profil ----------------------------------------------
|
||||
|
||||
def test_profil_modifiable(client):
|
||||
subscribe(client)
|
||||
connect(client)
|
||||
reponse = client.post("/profil", data={
|
||||
"lastname": "Dupont", "firstname": "Léa", "level": "confirme",
|
||||
}, follow_redirects=True)
|
||||
assert "mis à jour".encode() in reponse.data
|
||||
|
||||
def test_level_invalide_rejete(client):
|
||||
"""Le formulaire HTML est modifiable : le serveur doit revalider."""
|
||||
subscribe(client)
|
||||
connect(client)
|
||||
reponse = client.post("/profil", data={
|
||||
"lastname": "Dupont", "firstname": "Lea", "level": "super_admin",
|
||||
}, follow_redirects=True)
|
||||
assert b"level invalide" in reponse.data
|
||||
|
||||
# --- Exercice 9 : les rôles ----------------------------------------------
|
||||
|
||||
def test_admin_interdit_a_un_adherent(client):
|
||||
subscribe(client)
|
||||
connect(client)
|
||||
reponse = client.get("/admin", follow_redirects=True)
|
||||
assert "réservé aux administrateurs".encode() in reponse.data
|
||||
|
||||
def test_admin_accessible_a_l_administrateur(client):
|
||||
subscribe(client)
|
||||
connect(client, email="admin@blocandco.fr", mdp="Admin2026!")
|
||||
reponse = client.get("/admin")
|
||||
assert reponse.status_code == 200
|
||||
assert b"lea@test.fr" in reponse.data
|
||||
|
||||
def test_promotion_interdite_a_un_adherent(client):
|
||||
subscribe(client)
|
||||
connect(client)
|
||||
client.post("/admin/promouvoir/1", follow_redirects=True)
|
||||
role = sqlite3.connect(application.DB).execute(
|
||||
"SELECT role FROM adherents WHERE email = 'lea@test.fr'"
|
||||
).fetchone()[0]
|
||||
assert role == "adherent"
|
||||
|
||||
# --- Exercice 11 : injection SQL -----------------------------------------
|
||||
|
||||
def test_injection_sql_sur_la_connexion(client):
|
||||
"""Avec des requêtes paramétrées, cette charge ne doit rien ouvrir."""
|
||||
subscribe(client)
|
||||
reponse = client.post("/connexion", data={
|
||||
"email": "lea@test.fr' OR '1'='1",
|
||||
"password": "peu importe",
|
||||
}, follow_redirects=True)
|
||||
assert b"Identifiants incorrects" in reponse.data
|
||||
assert b"Mon profil" not in reponse.data
|
||||
|
||||
# --- Exercice 12 : XSS ---------------------------------------------------
|
||||
|
||||
def test_xss_dans_le_firstname_est_echappe(client):
|
||||
subscribe(client)
|
||||
connect(client)
|
||||
client.post("/profil", data={
|
||||
"lastname": "Dupont", "firstname": "<script>alert(1)</script>",
|
||||
"level": "debutant",
|
||||
}, follow_redirects=True)
|
||||
reponse = client.get("/profil")
|
||||
assert b"<script>alert(1)</script>" not in reponse.data
|
||||
assert b"<script>" in reponse.data
|
||||
@@ -0,0 +1,225 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starter"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "flask", specifier = ">=3.1.3" },
|
||||
{ name = "pytest", specifier = ">=9.1.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/34/4dd12fc8bb7d61c91467ec3efe415ffa7d5456f799954b40c5bbaeae470e/werkzeug-3.1.9.tar.gz", hash = "sha256:55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060", size = 940188, upload-time = "2026-09-27T18:33:41.637Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/38/df03f564f43cec2684823f3cccae1a652ee7face1cbaa76fb223096e64d7/werkzeug-3.1.9-py3-none-any.whl", hash = "sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab", size = 228700, upload-time = "2026-09-27T18:33:39.685Z" },
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Vérification du poste avant l'atelier.
|
||||
|
||||
Lancez simplement : python verif_poste.py
|
||||
Puis copiez-collez TOUT le résultat au formateur.
|
||||
|
||||
Ce script ne modifie rien et n'installe rien.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import sys
|
||||
|
||||
MINIMUM = (3, 9)
|
||||
|
||||
|
||||
def version_python():
|
||||
v = sys.version_info
|
||||
actuelle = f"{v.major}.{v.minor}.{v.micro}"
|
||||
if (v.major, v.minor) >= MINIMUM:
|
||||
return True, f"Python {actuelle}"
|
||||
return False, (f"Python {actuelle} — TROP ANCIEN, il faut au moins "
|
||||
f"{MINIMUM[0]}.{MINIMUM[1]}")
|
||||
|
||||
|
||||
def module_present(lastname):
|
||||
try:
|
||||
__import__(lastname)
|
||||
except ImportError:
|
||||
return False, f"{lastname} : ABSENT"
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
return True, f"{lastname} : {version(lastname)}"
|
||||
except Exception:
|
||||
return True, f"{lastname} : présent"
|
||||
|
||||
|
||||
def dans_un_venv():
|
||||
actif = sys.prefix != getattr(sys, "base_prefix", sys.prefix)
|
||||
if actif:
|
||||
return True, "environnement virtuel : actif"
|
||||
return True, "environnement virtuel : aucun (ce n'est pas bloquant)"
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 56)
|
||||
print(" VERIFICATION DU POSTE - ATELIER PYTHON WEB")
|
||||
print("=" * 56)
|
||||
print(f" Systeme : {platform.system()} {platform.release()}")
|
||||
print(f" Executable : {sys.executable}")
|
||||
print("-" * 56)
|
||||
|
||||
resultats = [version_python(), dans_un_venv()]
|
||||
for lastname in ("flask", "pytest"):
|
||||
resultats.append(module_present(lastname))
|
||||
|
||||
for ok, message in resultats:
|
||||
print((" [ OK ] " if ok else " [ KO ] ") + message)
|
||||
|
||||
print("-" * 56)
|
||||
|
||||
bloquant = not resultats[0][0]
|
||||
modules = zip(("flask", "pytest"), resultats[2:], strict=False)
|
||||
manquants = [lastname for lastname, (ok, _) in modules if not ok]
|
||||
|
||||
if bloquant:
|
||||
print(" ACTION : installez une version recente de Python depuis")
|
||||
print(" python.org, en cochant 'Add python.exe to PATH'.")
|
||||
print(" Prevenez le formateur des ce soir.")
|
||||
elif manquants:
|
||||
print(" ACTION : lancez cette commande, puis relancez ce script :")
|
||||
print()
|
||||
print(" python -m pip install " + " ".join(manquants))
|
||||
else:
|
||||
print(" Tout est pret. Rien a faire.")
|
||||
|
||||
print("=" * 56)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||