From 4ca9cd9a7c414e7cc5c8d3a8539a9f9b09a0b6f0 Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Tue, 4 Feb 2025 16:27:57 +0100 Subject: [PATCH 01/10] Corrige le bug Jinja et ajoute tests pour /showSummary --- .DS_Store | Bin 6148 -> 6148 bytes CACHEDIR.TAG | 4 + app/__init__.py | 0 app/booking_manager.py | 84 ++++++++++++++++++++ app/models.py | 13 +++ app/server.py | 71 +++++++++++++++++ {templates => app/templates}/booking.html | 0 {templates => app/templates}/index.html | 0 {templates => app/templates}/welcome.html | 6 +- clubs.json => data/clubs.json | 0 competitions.json => data/competitions.json | 0 launch.json | 17 ++++ pyvenv.cfg | 8 ++ server.py | 59 -------------- 14 files changed, 200 insertions(+), 62 deletions(-) create mode 100644 CACHEDIR.TAG create mode 100644 app/__init__.py create mode 100644 app/booking_manager.py create mode 100644 app/models.py create mode 100644 app/server.py rename {templates => app/templates}/booking.html (100%) rename {templates => app/templates}/index.html (100%) rename {templates => app/templates}/welcome.html (82%) rename clubs.json => data/clubs.json (100%) rename competitions.json => data/competitions.json (100%) create mode 100644 launch.json create mode 100644 pyvenv.cfg delete mode 100644 server.py diff --git a/.DS_Store b/.DS_Store index 04a508e6f6048834346c5f1769770f2ff01806e6..ed2a65c8bc3d2bd8bb480af7d9ea7b7cbbf7c371 100644 GIT binary patch delta 37 tcmZoMXfc@J&nU1lU^g?Pz-As6WyZ<-SqnCwXR~13*kHi8nVsV=KLFjh3&;Qf delta 333 zcmZoMXfc@J&nU4mU^g?P#AY5AWyX4ThD?S$hGd2uhEj$UhSZ#N!{Frn+yVv&Z~; List[Club]: + """Charge et retourne la liste de clubs depuis un fichier JSON.""" + if not os.path.exists(filepath): + raise FileNotFoundError(f"Fichier introuvable : {filepath}") + with open(filepath, "r") as f: + data = json.load(f) + clubs = [] + for c in data["clubs"]: + clubs.append( + Club(name=c["name"], email=c["email"], points=int(c["points"])) + ) + return clubs + + @staticmethod + def load_competitions(filepath: str) -> List[Competition]: + """Charge et retourne la liste de compétitions depuis un fichier JSON.""" + if not os.path.exists(filepath): + raise FileNotFoundError(f"Fichier introuvable : {filepath}") + with open(filepath, "r") as f: + data = json.load(f) + competitions = [] + for c in data["competitions"]: + competitions.append( + Competition( + name=c["name"], + date=c["date"], + number_of_places=int(c["numberOfPlaces"]) + ) + ) + return competitions + + def find_club_by_email(self, email: str) -> Optional[Club]: + return next((club for club in self.clubs if club.email == email), None) + + def find_club_by_name(self, name: str) -> Optional[Club]: + return next((club for club in self.clubs if club.name == name), None) + + def find_competition_by_name(self, name: str) -> Optional[Competition]: + return next((c for c in self.competitions if c.name == name), None) + + def purchase_places(self, club_name: str, competition_name: str, places_requested: int) -> bool: + """Tente d'acheter `places_requested` places pour le `club_name` dans `competition_name`. + Renvoie True si l’opération réussit, False sinon (règles non respectées). + """ + club = self.find_club_by_name(club_name) + competition = self.find_competition_by_name(competition_name) + + # Vérifications basiques + if not club or not competition: + return False + + # 1) Pas plus de 12 places en une seule fois + if places_requested > 12: + return False + + # 2) Pas plus de places que le club n'a de points + if places_requested > club.points: + return False + + # 3) Pas plus de places que celles disponibles dans la compétition + if places_requested > competition.number_of_places: + return False + + # Si tout est OK, on décrémente les places + competition.number_of_places -= places_requested + club.points -= places_requested + return True diff --git a/app/models.py b/app/models.py new file mode 100644 index 000000000..daff602f2 --- /dev/null +++ b/app/models.py @@ -0,0 +1,13 @@ +# app/models.py +class Competition: + def __init__(self, name, date, number_of_places): + self.name = name + self.date = date + self.number_of_places = number_of_places + + +class Club: + def __init__(self, name, email, points): + self.name = name + self.email = email + self.points = points diff --git a/app/server.py b/app/server.py new file mode 100644 index 000000000..ef99850eb --- /dev/null +++ b/app/server.py @@ -0,0 +1,71 @@ +# app/server.py + +from flask import Flask, render_template, request, redirect, flash, url_for +from app.booking_manager import BookingManager + +app = Flask(__name__) +app.secret_key = "secret_key_xyz" + +# Instanciation du manager avec les chemins de fichiers en paramètre +manager = BookingManager( + clubs_file="data/clubs.json", + competitions_file="data/competitions.json" +) + + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/showSummary", methods=["POST"]) +def show_summary(): + email = request.form.get("email", "") + club = manager.find_club_by_email(email) + if not club: + flash("Email inconnu ou invalide.") + return redirect(url_for("index")) + return render_template("welcome.html", club=club, competitions=manager.competitions) + + +@app.route("/book//") +def book(competition, club): + found_competition = manager.find_competition_by_name(competition) + found_club = manager.find_club_by_name(club) + if not found_competition or not found_club: + flash("Something went wrong-please try again") + return redirect(url_for("index")) + return render_template("booking.html", club=found_club, competition=found_competition) + + +@app.route("/purchasePlaces", methods=["POST"]) +def purchase_places(): + competition_name = request.form.get("competition") + club_name = request.form.get("club") + places_str = request.form.get("places") + + try: + places_requested = int(places_str) + except ValueError: + flash("Le nombre de places est invalide.") + return redirect(url_for("index")) + + success = manager.purchase_places( + club_name, competition_name, places_requested) + if success: + flash("Great-booking complete!") + else: + flash("Impossible de réserver ces places (Règle non respectée).") + + club = manager.find_club_by_name(club_name) + return render_template("welcome.html", club=club, competitions=manager.competitions) + + +@app.route("/clubsPoints") +def clubs_points(): + return render_template("clubs_points.html", clubs=manager.clubs) + + +@app.route("/logout") +def logout(): + return redirect(url_for("index")) diff --git a/templates/booking.html b/app/templates/booking.html similarity index 100% rename from templates/booking.html rename to app/templates/booking.html diff --git a/templates/index.html b/app/templates/index.html similarity index 100% rename from templates/index.html rename to app/templates/index.html diff --git a/templates/welcome.html b/app/templates/welcome.html similarity index 82% rename from templates/welcome.html rename to app/templates/welcome.html index ff6b261a2..928a83c4a 100644 --- a/templates/welcome.html +++ b/app/templates/welcome.html @@ -23,9 +23,9 @@

Competitions:

{{comp['name']}}
Date: {{comp['date']}}
Number of Places: {{comp['numberOfPlaces']}} - {%if comp['numberOfPlaces']|int >0%} - Book Places - {%endif%} + {% if comp.number_of_places|int > 0 %} + {{ comp.number_of_places }} places available +{% endif %}
{% endfor %} diff --git a/clubs.json b/data/clubs.json similarity index 100% rename from clubs.json rename to data/clubs.json diff --git a/competitions.json b/data/competitions.json similarity index 100% rename from competitions.json rename to data/competitions.json diff --git a/launch.json b/launch.json new file mode 100644 index 000000000..a0cc0ba8c --- /dev/null +++ b/launch.json @@ -0,0 +1,17 @@ +{ + "name": "Python Debugger: Flask", + "type": "debugpy", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app/server.py", + "FLASK_DEBUG": "1" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true, + "justMyCode": true +} diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 000000000..9c71c33dd --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,8 @@ +home = /usr/local/bin +implementation = CPython +version_info = 3.12.2.final.0 +virtualenv = 20.28.0 +include-system-site-packages = false +base-prefix = /Library/Frameworks/Python.framework/Versions/3.12 +base-exec-prefix = /Library/Frameworks/Python.framework/Versions/3.12 +base-executable = /Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12 diff --git a/server.py b/server.py deleted file mode 100644 index 4084baeac..000000000 --- a/server.py +++ /dev/null @@ -1,59 +0,0 @@ -import json -from flask import Flask,render_template,request,redirect,flash,url_for - - -def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs - - -def loadCompetitions(): - with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions - - -app = Flask(__name__) -app.secret_key = 'something_special' - -competitions = loadCompetitions() -clubs = loadClubs() - -@app.route('/') -def index(): - return render_template('index.html') - -@app.route('/showSummary',methods=['POST']) -def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] - return render_template('welcome.html',club=club,competitions=competitions) - - -@app.route('/book//') -def book(competition,club): - foundClub = [c for c in clubs if c['name'] == club][0] - foundCompetition = [c for c in competitions if c['name'] == competition][0] - if foundClub and foundCompetition: - return render_template('booking.html',club=foundClub,competition=foundCompetition) - else: - flash("Something went wrong-please try again") - return render_template('welcome.html', club=club, competitions=competitions) - - -@app.route('/purchasePlaces',methods=['POST']) -def purchasePlaces(): - competition = [c for c in competitions if c['name'] == request.form['competition']][0] - club = [c for c in clubs if c['name'] == request.form['club']][0] - placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired - flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) - - -# TODO: Add route for points display - - -@app.route('/logout') -def logout(): - return redirect(url_for('index')) \ No newline at end of file From 194489a16a2c38fc3fb9cf69f29676c5b27e4e8d Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Tue, 4 Feb 2025 17:42:09 +0100 Subject: [PATCH 02/10] Ajout du test pour le chargement du CSS et modifications des templates pour harmoniser le style --- app/server.py | 7 +- app/templates/base.html | 37 ++++++++++ app/templates/booking.html | 33 +++++---- app/templates/clubs_points.html | 24 +++++++ app/templates/index.html | 29 ++++---- app/templates/welcome.html | 54 ++++++-------- static/css/style.css | 120 ++++++++++++++++++++++++++++++++ 7 files changed, 234 insertions(+), 70 deletions(-) create mode 100644 app/templates/base.html create mode 100644 app/templates/clubs_points.html create mode 100644 static/css/style.css diff --git a/app/server.py b/app/server.py index ef99850eb..bd306ca80 100644 --- a/app/server.py +++ b/app/server.py @@ -3,7 +3,9 @@ from flask import Flask, render_template, request, redirect, flash, url_for from app.booking_manager import BookingManager -app = Flask(__name__) +# Indique que le dossier statique se trouve dans ../static (à la racine du projet) +# Les templates seront recherchés par défaut dans "templates" situé dans ce même dossier (ici "app/templates") +app = Flask(__name__, static_folder="../static") app.secret_key = "secret_key_xyz" # Instanciation du manager avec les chemins de fichiers en paramètre @@ -33,7 +35,7 @@ def book(competition, club): found_competition = manager.find_competition_by_name(competition) found_club = manager.find_club_by_name(club) if not found_competition or not found_club: - flash("Something went wrong-please try again") + flash("Something went wrong - please try again") return redirect(url_for("index")) return render_template("booking.html", club=found_club, competition=found_competition) @@ -43,7 +45,6 @@ def purchase_places(): competition_name = request.form.get("competition") club_name = request.form.get("club") places_str = request.form.get("places") - try: places_requested = int(places_str) except ValueError: diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 000000000..3828a02fa --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,37 @@ + + + + + + + {% block title %}GUDLFT{% endblock %} + + + + + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + + + {% block content %}{% endblock %} +
+ + diff --git a/app/templates/booking.html b/app/templates/booking.html index 06ae1156c..be884db6e 100644 --- a/app/templates/booking.html +++ b/app/templates/booking.html @@ -1,17 +1,16 @@ - - - - - Booking for {{competition['name']}} || GUDLFT - - -

{{competition['name']}}

- Places available: {{competition['numberOfPlaces']}} -
- - - - -
- - \ No newline at end of file + +{% extends "base.html" %} + +{% block title %}Réservation | {{ competition['name'] }}{% endblock %} + +{% block content %} +

{{ competition['name'] }}

+

Places disponibles: {{ competition['number_of_places'] }}

+
+ + + + + +
+{% endblock %} diff --git a/app/templates/clubs_points.html b/app/templates/clubs_points.html new file mode 100644 index 000000000..f7a06436c --- /dev/null +++ b/app/templates/clubs_points.html @@ -0,0 +1,24 @@ + +{% extends "base.html" %} + +{% block title %}Points des Clubs{% endblock %} + +{% block content %} +

Points des Clubs

+ + + + + + + + + {% for club in clubs %} + + + + + {% endfor %} + +
ClubPoints
{{ club['name'] }}{{ club['points'] }}
+{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 926526b7d..4ef54b808 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,16 +1,13 @@ - - - - - GUDLFT Registration - - -

Welcome to the GUDLFT Registration Portal!

- Please enter your secretary email to continue: -
- - - -
- - \ No newline at end of file + +{% extends "base.html" %} + +{% block title %}Accueil | GUDLFT{% endblock %} + +{% block content %} +

Bienvenue sur GUDLFT

+
+ + + +
+{% endblock %} diff --git a/app/templates/welcome.html b/app/templates/welcome.html index 928a83c4a..b821d7bb1 100644 --- a/app/templates/welcome.html +++ b/app/templates/welcome.html @@ -1,36 +1,22 @@ - - - - - Summary | GUDLFT Registration - - -

Welcome, {{club['email']}}

Logout + +{% extends "base.html" %} - {% with messages = get_flashed_messages()%} - {% if messages %} -
    - {% for message in messages %} -
  • {{message}}
  • - {% endfor %} -
- {% endif%} - Points available: {{club['points']}} -

Competitions:

-
    - {% for comp in competitions%} -
  • - {{comp['name']}}
    - Date: {{comp['date']}}
    - Number of Places: {{comp['numberOfPlaces']}} - {% if comp.number_of_places|int > 0 %} - {{ comp.number_of_places }} places available -{% endif %} -
  • -
    - {% endfor %} -
- {%endwith%} +{% block title %}Résumé | GUDLFT Registration{% endblock %} - - \ No newline at end of file +{% block content %} +

Welcome, {{ club['email'] }}

+

Points disponibles: {{ club['points'] }}

+ +

Compétitions :

+
    + {% for comp in competitions %} +
  • + {{ comp['name'] }}
    + Date: {{ comp['date'] }}
    + Places disponibles: {{ comp['number_of_places'] }}
    + Réserver +
  • +
    + {% endfor %} +
+{% endblock %} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 000000000..d13d15053 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,120 @@ +/* static/css/style.css */ + +/* Style global */ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #f8f8f8; + margin: 0; + padding: 0; + color: #333; + } + + /* Barre de navigation */ + nav { + background-color: #333; + color: #fff; + padding: 10px 20px; + } + + nav ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + } + + nav ul li { + margin-right: 15px; + } + + nav ul li a { + color: #fff; + text-decoration: none; + font-weight: bold; + } + + nav ul li a:hover { + text-decoration: underline; + } + + /* Conteneur principal */ + .container { + width: 90%; + max-width: 1000px; + margin: 20px auto; + background-color: #fff; + padding: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border-radius: 5px; + } + + /* Titres */ + h1, h2, h3 { + color: #444; + } + + /* Formulaires */ + form label { + display: block; + margin-bottom: 5px; + font-weight: bold; + } + + form input[type="email"], + form input[type="number"], + form input[type="text"] { + width: 100%; + max-width: 300px; + padding: 8px; + margin-bottom: 10px; + border: 1px solid #ddd; + border-radius: 4px; + } + + form button { + background-color: #333; + color: #fff; + border: none; + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; + } + + form button:hover { + background-color: #555; + } + + /* Messages flash */ + .flashes { + list-style: none; + padding: 0; + margin: 10px 0; + } + + .flashes li { + background-color: #e74c3c; + color: #fff; + padding: 10px; + border-radius: 4px; + margin-bottom: 5px; + } + + /* Tableaux */ + table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; + } + + table th, + table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + } + + table th { + background-color: #333; + color: #fff; + } + \ No newline at end of file From d03bd45961e39a620dad214bb972e89c284b9c75 Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Tue, 4 Feb 2025 18:00:58 +0100 Subject: [PATCH 03/10] Ajout du test pour le chargement du CSS et modifications des templates pour harmoniser le style --- Python_Testing | 1 + 1 file changed, 1 insertion(+) create mode 160000 Python_Testing diff --git a/Python_Testing b/Python_Testing new file mode 160000 index 000000000..4ca9cd9a7 --- /dev/null +++ b/Python_Testing @@ -0,0 +1 @@ +Subproject commit 4ca9cd9a7c414e7cc5c8d3a8539a9f9b09a0b6f0 From f35e17287927636945ae6b83e27f70c44a8bb17f Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Tue, 4 Feb 2025 18:10:38 +0100 Subject: [PATCH 04/10] mep --- Python_Testing | 1 + 1 file changed, 1 insertion(+) create mode 160000 Python_Testing diff --git a/Python_Testing b/Python_Testing new file mode 160000 index 000000000..4ca9cd9a7 --- /dev/null +++ b/Python_Testing @@ -0,0 +1 @@ +Subproject commit 4ca9cd9a7c414e7cc5c8d3a8539a9f9b09a0b6f0 From fd8668b0b35077b975dad472891004f16f87088e Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Tue, 4 Feb 2025 20:42:40 +0100 Subject: [PATCH 05/10] mise des test en ligne --- .gitignore | 2 +- Python_Testing | 1 - tests/__init__.py | 0 tests/functional/test_functional.py | 0 tests/integration/__init__.py | 0 tests/integration/test_integration.py | 27 +++++++++++++++++ tests/unit/__init__.py | 0 tests/unit/test_booking_manager.py | 42 +++++++++++++++++++++++++++ tests/unit/test_server.py | 38 ++++++++++++++++++++++++ tests/unit/test_static_files.py | 25 ++++++++++++++++ 10 files changed, 133 insertions(+), 2 deletions(-) delete mode 160000 Python_Testing create mode 100644 tests/__init__.py create mode 100644 tests/functional/test_functional.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_integration.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_booking_manager.py create mode 100644 tests/unit/test_server.py create mode 100644 tests/unit/test_static_files.py diff --git a/.gitignore b/.gitignore index 2cba99d87..cefb4c172 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ bin include lib .Python -tests/ + .envrc __pycache__ \ No newline at end of file diff --git a/Python_Testing b/Python_Testing deleted file mode 160000 index 4ca9cd9a7..000000000 --- a/Python_Testing +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4ca9cd9a7c414e7cc5c8d3a8539a9f9b09a0b6f0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py new file mode 100644 index 000000000..45570f9ec --- /dev/null +++ b/tests/integration/test_integration.py @@ -0,0 +1,27 @@ +import unittest +from app.server import app + + +class TestServerRoutes(unittest.TestCase): + + def setUp(self): + app.config['TESTING'] = True + self.client = app.test_client() + + def test_purchase_places_route(self): + data = { + "club": "Iron Temple", + "competition": "Spring Festival", + "places": "3" + } + response = self.client.post("/purchasePlaces", data=data) + # Vérifier qu'on obtient un code 200 et un message de succès + self.assertEqual(response.status_code, 200) + self.assertIn(b"booking complete", response.data.lower(), + "Doit contenir un message de réservation réussie.") + + # On peut ajouter des checks sur le HTML renvoyé, le nombre de places, etc. + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/test_booking_manager.py b/tests/unit/test_booking_manager.py new file mode 100644 index 000000000..63b83b3e7 --- /dev/null +++ b/tests/unit/test_booking_manager.py @@ -0,0 +1,42 @@ +import unittest +from app.booking_manager import BookingManager + + +class TestBookingManager(unittest.TestCase): + + def setUp(self): + # Instancier un BookingManager avec des JSON de test + self.manager = BookingManager( + clubs_file="data/clubs.json", + competitions_file="data/competitions.json" + ) + + def test_purchase_places_happy_path(self): + """ + Teste le cas normal : + - Club a suffisamment de points + - Compétition a suffisamment de places + - Moins de 12 places demandées + """ + success = self.manager.purchase_places( + "Iron Temple", "Spring Festival", 3) + self.assertTrue(success, "L'achat de 3 places devrait réussir.") + + def test_purchase_places_too_many_places_requested(self): + """ + Teste la limite de 12 places : si on essaie d'en prendre 13, ça doit échouer. + """ + success = self.manager.purchase_places( + "Iron Temple", "Spring Festival", 13) + self.assertFalse( + success, "L'achat de 13 places doit échouer (max 12).") + + # Ajoute d'autres tests pour couvrir : + # - club sans assez de points + # - competition full + # - club ou competition inexistant + # etc. + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py new file mode 100644 index 000000000..a32d405b6 --- /dev/null +++ b/tests/unit/test_server.py @@ -0,0 +1,38 @@ +import unittest +from app.server import app + + +class TestWelcomePage(unittest.TestCase): + + def setUp(self): + """ + Prépare l'application Flask en mode TEST. + """ + app.config['TESTING'] = True + self.client = app.test_client() + + def test_show_summary_no_undefined_error_for_unknown_email(self): + """ + Vérifie que l'application redirige (302) pour un email inconnu + et ne produit pas d'erreur Jinja une fois la redirection suivie. + """ + response = self.client.post( + '/showSummary', data={'email': 'email_inconnu@example.com'}) + + # On s'attend à un code 302 (redirection) + self.assertEqual(response.status_code, 302, + "La route doit rediriger pour un email inconnu.") + + # Optionnel : suivre la redirection pour valider le contenu final + response_followed = self.client.post( + '/showSummary', + data={'email': 'email_inconnu@example.com'}, + follow_redirects=True + ) + # Après la redirection, on devrait avoir un code 200 (page affichée) + self.assertEqual(response_followed.status_code, 200, + "Après redirection, on doit avoir un code 200.") + + # Vérifie qu'on ne voit pas 'UndefinedError' dans la page + self.assertNotIn(b'UndefinedError', response_followed.data, + "Aucune erreur Jinja ne doit être présente dans la page finale.") diff --git a/tests/unit/test_static_files.py b/tests/unit/test_static_files.py new file mode 100644 index 000000000..af9f43d3e --- /dev/null +++ b/tests/unit/test_static_files.py @@ -0,0 +1,25 @@ +import unittest +from app.server import app + + +class TestStaticFiles(unittest.TestCase): + def setUp(self): + app.config['TESTING'] = True + self.client = app.test_client() + + def test_css_loaded(self): + # On envoie une requête GET vers l'URL du CSS + response = self.client.get('/static/css/style.css') + # Vérifie que le serveur renvoie bien un code 200 (OK) + self.assertEqual(response.status_code, 200, + "Le CSS devrait être accessible et retourner un code 200.") + # Vérifie que le contenu du CSS n'est pas vide + self.assertTrue(len(response.data) > 0, + "Le fichier CSS ne doit pas être vide.") + # Optionnel : vérifier qu'une règle CSS connue est présente (ici "body") + self.assertIn(b"body", response.data, + "Le CSS doit contenir la règle 'body'.") + + +if __name__ == '__main__': + unittest.main() From e6b637ff451c3a1b1736eedd10a3a174be611c90 Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Wed, 5 Feb 2025 16:11:32 +0100 Subject: [PATCH 06/10] refactorisation du code --- app/booking_manager.py | 84 ------- app/booking_manager/__init__.py | 4 + app/booking_manager/booking_service.py | 32 +++ app/booking_manager/club_manager.py | 32 +++ app/booking_manager/competition_manager.py | 29 +++ app/booking_manager/data_loader.py | 19 ++ app/models.py | 1 + app/server.py | 27 +- app/templates/base.html | 6 - app/templates/welcome.html | 1 - static/css/style.css | 231 +++++++++--------- tests/unit/test_booking_manager.py | 42 ---- .../test_booking_manager_competition_full.py | 25 ++ tests/unit/test_booking_manager_inexistant.py | 26 ++ ...est_booking_manager_insufficient_points.py | 21 ++ tests/unit/test_points_display.py | 28 +++ tests/unit/test_static_files.py | 6 +- tests/unit/test_welcome_page.py | 28 +++ 18 files changed, 374 insertions(+), 268 deletions(-) delete mode 100644 app/booking_manager.py create mode 100644 app/booking_manager/__init__.py create mode 100644 app/booking_manager/booking_service.py create mode 100644 app/booking_manager/club_manager.py create mode 100644 app/booking_manager/competition_manager.py create mode 100644 app/booking_manager/data_loader.py delete mode 100644 tests/unit/test_booking_manager.py create mode 100644 tests/unit/test_booking_manager_competition_full.py create mode 100644 tests/unit/test_booking_manager_inexistant.py create mode 100644 tests/unit/test_booking_manager_insufficient_points.py create mode 100644 tests/unit/test_points_display.py create mode 100644 tests/unit/test_welcome_page.py diff --git a/app/booking_manager.py b/app/booking_manager.py deleted file mode 100644 index 3ef6047d3..000000000 --- a/app/booking_manager.py +++ /dev/null @@ -1,84 +0,0 @@ -# app/booking_manager.py - -import json -import os -from typing import List, Optional -from app.models import Club, Competition - - -class BookingManager: - """Gère la logique de réservation : chargement des données, vérification des règles, etc.""" - - def __init__(self, clubs_file: str, competitions_file: str): - self.clubs: List[Club] = self.load_clubs(clubs_file) - self.competitions: List[Competition] = self.load_competitions( - competitions_file) - - @staticmethod - def load_clubs(filepath: str) -> List[Club]: - """Charge et retourne la liste de clubs depuis un fichier JSON.""" - if not os.path.exists(filepath): - raise FileNotFoundError(f"Fichier introuvable : {filepath}") - with open(filepath, "r") as f: - data = json.load(f) - clubs = [] - for c in data["clubs"]: - clubs.append( - Club(name=c["name"], email=c["email"], points=int(c["points"])) - ) - return clubs - - @staticmethod - def load_competitions(filepath: str) -> List[Competition]: - """Charge et retourne la liste de compétitions depuis un fichier JSON.""" - if not os.path.exists(filepath): - raise FileNotFoundError(f"Fichier introuvable : {filepath}") - with open(filepath, "r") as f: - data = json.load(f) - competitions = [] - for c in data["competitions"]: - competitions.append( - Competition( - name=c["name"], - date=c["date"], - number_of_places=int(c["numberOfPlaces"]) - ) - ) - return competitions - - def find_club_by_email(self, email: str) -> Optional[Club]: - return next((club for club in self.clubs if club.email == email), None) - - def find_club_by_name(self, name: str) -> Optional[Club]: - return next((club for club in self.clubs if club.name == name), None) - - def find_competition_by_name(self, name: str) -> Optional[Competition]: - return next((c for c in self.competitions if c.name == name), None) - - def purchase_places(self, club_name: str, competition_name: str, places_requested: int) -> bool: - """Tente d'acheter `places_requested` places pour le `club_name` dans `competition_name`. - Renvoie True si l’opération réussit, False sinon (règles non respectées). - """ - club = self.find_club_by_name(club_name) - competition = self.find_competition_by_name(competition_name) - - # Vérifications basiques - if not club or not competition: - return False - - # 1) Pas plus de 12 places en une seule fois - if places_requested > 12: - return False - - # 2) Pas plus de places que le club n'a de points - if places_requested > club.points: - return False - - # 3) Pas plus de places que celles disponibles dans la compétition - if places_requested > competition.number_of_places: - return False - - # Si tout est OK, on décrémente les places - competition.number_of_places -= places_requested - club.points -= places_requested - return True diff --git a/app/booking_manager/__init__.py b/app/booking_manager/__init__.py new file mode 100644 index 000000000..7fa7d72a8 --- /dev/null +++ b/app/booking_manager/__init__.py @@ -0,0 +1,4 @@ +from .booking_service import BookingService +from .club_manager import ClubManager +from .competition_manager import CompetitionManager +from .data_loader import JSONDataLoader diff --git a/app/booking_manager/booking_service.py b/app/booking_manager/booking_service.py new file mode 100644 index 000000000..f43714884 --- /dev/null +++ b/app/booking_manager/booking_service.py @@ -0,0 +1,32 @@ +from app.booking_manager.club_manager import ClubManager +from app.booking_manager.competition_manager import CompetitionManager + + +class BookingService: + """ + Ordonne le processus de réservation en utilisant les gestionnaires de clubs et de compétitions. + """ + + def __init__(self, clubs_file: str, competitions_file: str): + self.club_manager = ClubManager(clubs_file) + self.competition_manager = CompetitionManager(competitions_file) + + def purchase_places(self, club_name: str, competition_name: str, places_requested: int) -> bool: + club = self.club_manager.find_by_name(club_name) + competition = self.competition_manager.find_by_name(competition_name) + + if not club or not competition: + return False + + if places_requested > 12: + return False + + if places_requested > club.points: + return False + + if places_requested > competition.number_of_places: + return False + + competition.number_of_places -= places_requested + club.points -= places_requested + return True diff --git a/app/booking_manager/club_manager.py b/app/booking_manager/club_manager.py new file mode 100644 index 000000000..44755bacd --- /dev/null +++ b/app/booking_manager/club_manager.py @@ -0,0 +1,32 @@ +from typing import List, Optional +from app.models import Club +from .data_loader import JSONDataLoader + + +class ClubManager: + """ + Gère le chargement et la recherche des clubs. + """ + + def __init__(self, clubs_file: str): + loader = JSONDataLoader(clubs_file) + data = loader.load_data() + self.clubs: List[Club] = self._parse_clubs(data) + + def _parse_clubs(self, data: dict) -> List[Club]: + clubs = [] + for c in data.get("clubs", []): + clubs.append( + Club( + name=c["name"], + email=c["email"], + points=int(c["points"]) + ) + ) + return clubs + + def find_by_email(self, email: str): + return next((club for club in self.clubs if club.email == email), None) + + def find_by_name(self, name: str): + return next((club for club in self.clubs if club.name == name), None) diff --git a/app/booking_manager/competition_manager.py b/app/booking_manager/competition_manager.py new file mode 100644 index 000000000..520f16da7 --- /dev/null +++ b/app/booking_manager/competition_manager.py @@ -0,0 +1,29 @@ +from typing import List, Optional +from app.models import Competition +from .data_loader import JSONDataLoader + + +class CompetitionManager: + """ + Gère le chargement et la recherche des compétitions. + """ + + def __init__(self, competitions_file: str): + loader = JSONDataLoader(competitions_file) + data = loader.load_data() + self.competitions: List[Competition] = self._parse_competitions(data) + + def _parse_competitions(self, data: dict) -> List[Competition]: + competitions = [] + for c in data.get("competitions", []): + competitions.append( + Competition( + name=c["name"], + date=c["date"], + number_of_places=int(c["numberOfPlaces"]) + ) + ) + return competitions + + def find_by_name(self, name: str) -> Optional[Competition]: + return next((comp for comp in self.competitions if comp.name == name), None) diff --git a/app/booking_manager/data_loader.py b/app/booking_manager/data_loader.py new file mode 100644 index 000000000..73ec2689f --- /dev/null +++ b/app/booking_manager/data_loader.py @@ -0,0 +1,19 @@ +import json +import os +from typing import Any + + +class JSONDataLoader: + """ + Classe générique pour charger des données depuis un fichier JSON. + """ + + def __init__(self, filepath: str): + self.filepath = filepath + + def load_data(self) -> Any: + if not os.path.exists(self.filepath): + raise FileNotFoundError(f"Fichier introuvable : {self.filepath}") + with open(self.filepath, "r") as f: + data = json.load(f) + return data diff --git a/app/models.py b/app/models.py index daff602f2..9a7d96d8a 100644 --- a/app/models.py +++ b/app/models.py @@ -1,4 +1,5 @@ # app/models.py + class Competition: def __init__(self, name, date, number_of_places): self.name = name diff --git a/app/server.py b/app/server.py index bd306ca80..e119af1bf 100644 --- a/app/server.py +++ b/app/server.py @@ -1,15 +1,11 @@ -# app/server.py - from flask import Flask, render_template, request, redirect, flash, url_for -from app.booking_manager import BookingManager +from app.booking_manager import BookingService -# Indique que le dossier statique se trouve dans ../static (à la racine du projet) -# Les templates seront recherchés par défaut dans "templates" situé dans ce même dossier (ici "app/templates") app = Flask(__name__, static_folder="../static") app.secret_key = "secret_key_xyz" -# Instanciation du manager avec les chemins de fichiers en paramètre -manager = BookingManager( +# Instanciation du service de réservation +booking_service = BookingService( clubs_file="data/clubs.json", competitions_file="data/competitions.json" ) @@ -23,17 +19,18 @@ def index(): @app.route("/showSummary", methods=["POST"]) def show_summary(): email = request.form.get("email", "") - club = manager.find_club_by_email(email) + club = booking_service.club_manager.find_by_email(email) if not club: flash("Email inconnu ou invalide.") return redirect(url_for("index")) - return render_template("welcome.html", club=club, competitions=manager.competitions) + return render_template("welcome.html", club=club, competitions=booking_service.competition_manager.competitions) @app.route("/book//") def book(competition, club): - found_competition = manager.find_competition_by_name(competition) - found_club = manager.find_club_by_name(club) + found_competition = booking_service.competition_manager.find_by_name( + competition) + found_club = booking_service.club_manager.find_by_name(club) if not found_competition or not found_club: flash("Something went wrong - please try again") return redirect(url_for("index")) @@ -51,20 +48,20 @@ def purchase_places(): flash("Le nombre de places est invalide.") return redirect(url_for("index")) - success = manager.purchase_places( + success = booking_service.purchase_places( club_name, competition_name, places_requested) if success: flash("Great-booking complete!") else: flash("Impossible de réserver ces places (Règle non respectée).") - club = manager.find_club_by_name(club_name) - return render_template("welcome.html", club=club, competitions=manager.competitions) + club = booking_service.club_manager.find_by_name(club_name) + return render_template("welcome.html", club=club, competitions=booking_service.competition_manager.competitions) @app.route("/clubsPoints") def clubs_points(): - return render_template("clubs_points.html", clubs=manager.clubs) + return render_template("clubs_points.html", clubs=booking_service.club_manager.clubs) @app.route("/logout") diff --git a/app/templates/base.html b/app/templates/base.html index 3828a02fa..b2b0af466 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,11 +5,9 @@ {% block title %}GUDLFT{% endblock %} - - - -
{% with messages = get_flashed_messages() %} {% if messages %} @@ -29,8 +25,6 @@ {% endif %} {% endwith %} - - {% block content %}{% endblock %}
diff --git a/app/templates/welcome.html b/app/templates/welcome.html index b821d7bb1..b0e3c68ee 100644 --- a/app/templates/welcome.html +++ b/app/templates/welcome.html @@ -6,7 +6,6 @@ {% block content %}

Welcome, {{ club['email'] }}

Points disponibles: {{ club['points'] }}

-

Compétitions :

    {% for comp in competitions %} diff --git a/static/css/style.css b/static/css/style.css index d13d15053..2a656539d 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -2,119 +2,118 @@ /* Style global */ body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - background-color: #f8f8f8; - margin: 0; - padding: 0; - color: #333; - } - - /* Barre de navigation */ - nav { - background-color: #333; - color: #fff; - padding: 10px 20px; - } - - nav ul { - list-style: none; - margin: 0; - padding: 0; - display: flex; - } - - nav ul li { - margin-right: 15px; - } - - nav ul li a { - color: #fff; - text-decoration: none; - font-weight: bold; - } - - nav ul li a:hover { - text-decoration: underline; - } - - /* Conteneur principal */ - .container { - width: 90%; - max-width: 1000px; - margin: 20px auto; - background-color: #fff; - padding: 20px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - border-radius: 5px; - } - - /* Titres */ - h1, h2, h3 { - color: #444; - } - - /* Formulaires */ - form label { - display: block; - margin-bottom: 5px; - font-weight: bold; - } - - form input[type="email"], - form input[type="number"], - form input[type="text"] { - width: 100%; - max-width: 300px; - padding: 8px; - margin-bottom: 10px; - border: 1px solid #ddd; - border-radius: 4px; - } - - form button { - background-color: #333; - color: #fff; - border: none; - padding: 10px 15px; - border-radius: 4px; - cursor: pointer; - } - - form button:hover { - background-color: #555; - } - - /* Messages flash */ - .flashes { - list-style: none; - padding: 0; - margin: 10px 0; - } - - .flashes li { - background-color: #e74c3c; - color: #fff; - padding: 10px; - border-radius: 4px; - margin-bottom: 5px; - } - - /* Tableaux */ - table { - width: 100%; - border-collapse: collapse; - margin-top: 20px; - } - - table th, - table td { - border: 1px solid #ddd; - padding: 8px; - text-align: left; - } - - table th { - background-color: #333; - color: #fff; - } - \ No newline at end of file + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #f8f8f8; + margin: 0; + padding: 0; + color: #333; +} + +/* Barre de navigation */ +nav { + background-color: #333; + color: #fff; + padding: 10px 20px; +} + +nav ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; +} + +nav ul li { + margin-right: 15px; +} + +nav ul li a { + color: #fff; + text-decoration: none; + font-weight: bold; +} + +nav ul li a:hover { + text-decoration: underline; +} + +/* Conteneur principal */ +.container { + width: 90%; + max-width: 1000px; + margin: 20px auto; + background-color: #fff; + padding: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border-radius: 5px; +} + +/* Titres */ +h1, h2, h3 { + color: #444; +} + +/* Formulaires */ +form label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +form input[type="email"], +form input[type="number"], +form input[type="text"] { + width: 100%; + max-width: 300px; + padding: 8px; + margin-bottom: 10px; + border: 1px solid #ddd; + border-radius: 4px; +} + +form button { + background-color: #333; + color: #fff; + border: none; + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; +} + +form button:hover { + background-color: #555; +} + +/* Messages flash */ +.flashes { + list-style: none; + padding: 0; + margin: 10px 0; +} + +.flashes li { + background-color: #e74c3c; + color: #fff; + padding: 10px; + border-radius: 4px; + margin-bottom: 5px; +} + +/* Tableaux */ +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +table th, +table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +table th { + background-color: #333; + color: #fff; +} diff --git a/tests/unit/test_booking_manager.py b/tests/unit/test_booking_manager.py deleted file mode 100644 index 63b83b3e7..000000000 --- a/tests/unit/test_booking_manager.py +++ /dev/null @@ -1,42 +0,0 @@ -import unittest -from app.booking_manager import BookingManager - - -class TestBookingManager(unittest.TestCase): - - def setUp(self): - # Instancier un BookingManager avec des JSON de test - self.manager = BookingManager( - clubs_file="data/clubs.json", - competitions_file="data/competitions.json" - ) - - def test_purchase_places_happy_path(self): - """ - Teste le cas normal : - - Club a suffisamment de points - - Compétition a suffisamment de places - - Moins de 12 places demandées - """ - success = self.manager.purchase_places( - "Iron Temple", "Spring Festival", 3) - self.assertTrue(success, "L'achat de 3 places devrait réussir.") - - def test_purchase_places_too_many_places_requested(self): - """ - Teste la limite de 12 places : si on essaie d'en prendre 13, ça doit échouer. - """ - success = self.manager.purchase_places( - "Iron Temple", "Spring Festival", 13) - self.assertFalse( - success, "L'achat de 13 places doit échouer (max 12).") - - # Ajoute d'autres tests pour couvrir : - # - club sans assez de points - # - competition full - # - club ou competition inexistant - # etc. - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit/test_booking_manager_competition_full.py b/tests/unit/test_booking_manager_competition_full.py new file mode 100644 index 000000000..d45d06c5c --- /dev/null +++ b/tests/unit/test_booking_manager_competition_full.py @@ -0,0 +1,25 @@ +# tests/unit/test_booking_manager_competition_full.py + +import unittest +from app.booking_manager.booking_service import BookingService + + +class TestBookingManagerCompetitionFull(unittest.TestCase): + def setUp(self): + self.service = BookingService( + clubs_file="data/clubs.json", competitions_file="data/competitions.json") + + def test_purchase_places_competition_full(self): + # Forcer la compétition "Spring Festival" à n'avoir que 2 places disponibles + competition = self.service.competition_manager.find_by_name( + "Spring Festival") + if competition: + competition.number_of_places = 2 + success = self.service.purchase_places( + "Iron Temple", "Spring Festival", 3) + self.assertFalse( + success, "L'achat de 3 places doit échouer si la compétition n'a que 2 places disponibles.") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_booking_manager_inexistant.py b/tests/unit/test_booking_manager_inexistant.py new file mode 100644 index 000000000..ecd69d779 --- /dev/null +++ b/tests/unit/test_booking_manager_inexistant.py @@ -0,0 +1,26 @@ +# tests/unit/test_booking_manager_inexistant.py + +import unittest +from app.booking_manager.booking_service import BookingService + + +class TestBookingManagerInexistant(unittest.TestCase): + def setUp(self): + self.service = BookingService( + clubs_file="data/clubs.json", competitions_file="data/competitions.json") + + def test_purchase_places_club_not_found(self): + success = self.service.purchase_places( + "NonExistentClub", "Spring Festival", 3) + self.assertFalse( + success, "L'achat doit échouer si le club n'existe pas.") + + def test_purchase_places_competition_not_found(self): + success = self.service.purchase_places( + "Iron Temple", "NonExistentCompetition", 3) + self.assertFalse( + success, "L'achat doit échouer si la compétition n'existe pas.") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_booking_manager_insufficient_points.py b/tests/unit/test_booking_manager_insufficient_points.py new file mode 100644 index 000000000..c6f9c4dc1 --- /dev/null +++ b/tests/unit/test_booking_manager_insufficient_points.py @@ -0,0 +1,21 @@ +import unittest +from app.booking_manager.booking_service import BookingService + + +class TestBookingManagerInsufficientPoints(unittest.TestCase): + def setUp(self): + self.service = BookingService( + clubs_file="data/clubs.json", competitions_file="data/competitions.json") + + def test_purchase_places_insufficient_points(self): + club = self.service.club_manager.find_by_name("Iron Temple") + if club: + club.points = 2 + success = self.service.purchase_places( + "Iron Temple", "Spring Festival", 3) + self.assertFalse( + success, "L'achat de 3 places doit échouer si le club n'a que 2 points.") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_points_display.py b/tests/unit/test_points_display.py new file mode 100644 index 000000000..cf731bb04 --- /dev/null +++ b/tests/unit/test_points_display.py @@ -0,0 +1,28 @@ +# tests/unit/test_points_display.py + +import unittest +from app.server import app + + +class TestPointsDisplay(unittest.TestCase): + def setUp(self): + app.config['TESTING'] = True + self.client = app.test_client() + + def test_points_display_on_index(self): + """Vérifie que la page d'accueil contient un indice sur l'affichage des points.""" + response = self.client.get("/") + self.assertEqual(response.status_code, 200) + # Par exemple, vérifier que le lien vers "Points Clubs" est présent + self.assertIn(b"Points Clubs", response.data) + + def test_points_display_on_welcome(self): + """Vérifie que la page welcome affiche les points après connexion.""" + response = self.client.post( + "/showSummary", data={"email": "admin@irontemple.com"}, follow_redirects=True) + self.assertEqual(response.status_code, 200) + self.assertIn(b"Points disponibles", response.data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_static_files.py b/tests/unit/test_static_files.py index af9f43d3e..553b15a2c 100644 --- a/tests/unit/test_static_files.py +++ b/tests/unit/test_static_files.py @@ -1,3 +1,5 @@ +# tests/unit/test_static_files.py + import unittest from app.server import app @@ -8,15 +10,11 @@ def setUp(self): self.client = app.test_client() def test_css_loaded(self): - # On envoie une requête GET vers l'URL du CSS response = self.client.get('/static/css/style.css') - # Vérifie que le serveur renvoie bien un code 200 (OK) self.assertEqual(response.status_code, 200, "Le CSS devrait être accessible et retourner un code 200.") - # Vérifie que le contenu du CSS n'est pas vide self.assertTrue(len(response.data) > 0, "Le fichier CSS ne doit pas être vide.") - # Optionnel : vérifier qu'une règle CSS connue est présente (ici "body") self.assertIn(b"body", response.data, "Le CSS doit contenir la règle 'body'.") diff --git a/tests/unit/test_welcome_page.py b/tests/unit/test_welcome_page.py new file mode 100644 index 000000000..ec8b92f98 --- /dev/null +++ b/tests/unit/test_welcome_page.py @@ -0,0 +1,28 @@ +# tests/unit/test_welcome_page.py + +import unittest +from app.server import app + + +class TestWelcomePage(unittest.TestCase): + def setUp(self): + app.config['TESTING'] = True + self.client = app.test_client() + + def test_show_summary_unknown_email_redirects(self): + response = self.client.post( + '/showSummary', data={'email': 'unknown@example.com'}) + self.assertEqual(response.status_code, 302, + "Doit rediriger pour un email inconnu.") + + # Suivre la redirection pour vérifier l'absence d'erreur Jinja + response_followed = self.client.post( + '/showSummary', data={'email': 'unknown@example.com'}, follow_redirects=True) + self.assertEqual(response_followed.status_code, 200, + "Après redirection, le code doit être 200.") + self.assertNotIn(b"UndefinedError", response_followed.data, + "Aucune erreur Jinja ne doit apparaître.") + + +if __name__ == '__main__': + unittest.main() From cb53592999c54cd196d007b61d6a5ee9edc7ca49 Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Wed, 5 Feb 2025 17:11:14 +0100 Subject: [PATCH 07/10] =?UTF-8?q?tests=20unitaires=20suppl=C3=A9mentaires?= =?UTF-8?q?=20pour=20la=20gestion=20de=20la=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/booking_manager/__init__.py | 2 + app/booking_manager/booking_service.py | 10 ++- app/booking_manager/club_manager.py | 31 ++++++- app/booking_manager/competition_manager.py | 23 +++++- app/booking_manager/data_loader.py | 2 + app/models.py | 3 +- app/server.py | 2 + data/clubs.json | 38 +++++---- data/competitions.json | 2 +- .../test_booking_manager_competition_full.py | 3 +- ...est_booking_manager_insufficient_points.py | 4 +- .../unit/test_booking_service_persistence.py | 81 +++++++++++++++++++ tests/unit/test_club_manager_save.py | 53 ++++++++++++ tests/unit/test_welcome_page.py | 2 - 14 files changed, 225 insertions(+), 31 deletions(-) create mode 100644 tests/unit/test_booking_service_persistence.py create mode 100644 tests/unit/test_club_manager_save.py diff --git a/app/booking_manager/__init__.py b/app/booking_manager/__init__.py index 7fa7d72a8..9ec577cc3 100644 --- a/app/booking_manager/__init__.py +++ b/app/booking_manager/__init__.py @@ -1,3 +1,5 @@ +# app/booking_manager/__init__.py + from .booking_service import BookingService from .club_manager import ClubManager from .competition_manager import CompetitionManager diff --git a/app/booking_manager/booking_service.py b/app/booking_manager/booking_service.py index f43714884..3c21d53b8 100644 --- a/app/booking_manager/booking_service.py +++ b/app/booking_manager/booking_service.py @@ -1,3 +1,5 @@ +# app/booking_manager/booking_service.py + from app.booking_manager.club_manager import ClubManager from app.booking_manager.competition_manager import CompetitionManager @@ -17,16 +19,18 @@ def purchase_places(self, club_name: str, competition_name: str, places_requeste if not club or not competition: return False - if places_requested > 12: return False - if places_requested > club.points: return False - if places_requested > competition.number_of_places: return False competition.number_of_places -= places_requested club.points -= places_requested + + # Sauvegarde des modifications dans les fichiers JSON + self.club_manager.save_clubs() + self.competition_manager.save_competitions() + return True diff --git a/app/booking_manager/club_manager.py b/app/booking_manager/club_manager.py index 44755bacd..8376dddb1 100644 --- a/app/booking_manager/club_manager.py +++ b/app/booking_manager/club_manager.py @@ -1,3 +1,6 @@ +# app/booking_manager/club_manager.py + +import json from typing import List, Optional from app.models import Club from .data_loader import JSONDataLoader @@ -5,10 +8,11 @@ class ClubManager: """ - Gère le chargement et la recherche des clubs. + Gère le chargement, la recherche et la sauvegarde des clubs. """ def __init__(self, clubs_file: str): + self.clubs_file = clubs_file # Pour la sauvegarde loader = JSONDataLoader(clubs_file) data = loader.load_data() self.clubs: List[Club] = self._parse_clubs(data) @@ -20,13 +24,32 @@ def _parse_clubs(self, data: dict) -> List[Club]: Club( name=c["name"], email=c["email"], - points=int(c["points"]) + points=int(c["points"]), + id=c.get("id") ) ) return clubs - def find_by_email(self, email: str): + def find_by_email(self, email: str) -> Optional[Club]: return next((club for club in self.clubs if club.email == email), None) - def find_by_name(self, name: str): + def find_by_name(self, name: str) -> Optional[Club]: return next((club for club in self.clubs if club.name == name), None) + + def save_clubs(self, filepath: Optional[str] = None) -> None: + """ + Sauvegarde l'état actuel des clubs dans un fichier JSON. + """ + if filepath is None: + filepath = self.clubs_file + clubs_data = {"clubs": []} + for club in self.clubs: + club_dict = { + "id": club.id, + "name": club.name, + "email": club.email, + "points": str(club.points) + } + clubs_data["clubs"].append(club_dict) + with open(filepath, "w") as f: + json.dump(clubs_data, f, indent=4) diff --git a/app/booking_manager/competition_manager.py b/app/booking_manager/competition_manager.py index 520f16da7..c94579932 100644 --- a/app/booking_manager/competition_manager.py +++ b/app/booking_manager/competition_manager.py @@ -1,3 +1,6 @@ +# app/booking_manager/competition_manager.py + +import json from typing import List, Optional from app.models import Competition from .data_loader import JSONDataLoader @@ -5,10 +8,11 @@ class CompetitionManager: """ - Gère le chargement et la recherche des compétitions. + Gère le chargement, la recherche et la sauvegarde des compétitions. """ def __init__(self, competitions_file: str): + self.competitions_file = competitions_file # Pour la sauvegarde loader = JSONDataLoader(competitions_file) data = loader.load_data() self.competitions: List[Competition] = self._parse_competitions(data) @@ -27,3 +31,20 @@ def _parse_competitions(self, data: dict) -> List[Competition]: def find_by_name(self, name: str) -> Optional[Competition]: return next((comp for comp in self.competitions if comp.name == name), None) + + def save_competitions(self, filepath: Optional[str] = None) -> None: + """ + Sauvegarde l'état actuel des compétitions dans un fichier JSON. + """ + if filepath is None: + filepath = self.competitions_file + competitions_data = {"competitions": []} + for comp in self.competitions: + comp_dict = { + "name": comp.name, + "date": comp.date, + "numberOfPlaces": str(comp.number_of_places) + } + competitions_data["competitions"].append(comp_dict) + with open(filepath, "w") as f: + json.dump(competitions_data, f, indent=4) diff --git a/app/booking_manager/data_loader.py b/app/booking_manager/data_loader.py index 73ec2689f..82da6b023 100644 --- a/app/booking_manager/data_loader.py +++ b/app/booking_manager/data_loader.py @@ -1,3 +1,5 @@ +# app/booking_manager/data_loader.py + import json import os from typing import Any diff --git a/app/models.py b/app/models.py index 9a7d96d8a..1783b24c5 100644 --- a/app/models.py +++ b/app/models.py @@ -8,7 +8,8 @@ def __init__(self, name, date, number_of_places): class Club: - def __init__(self, name, email, points): + def __init__(self, name, email, points, id=None): self.name = name self.email = email self.points = points + self.id = id diff --git a/app/server.py b/app/server.py index e119af1bf..96285b2b4 100644 --- a/app/server.py +++ b/app/server.py @@ -1,3 +1,5 @@ +# app/server.py + from flask import Flask, render_template, request, redirect, flash, url_for from app.booking_manager import BookingService diff --git a/data/clubs.json b/data/clubs.json index 1d7ad1ffe..fff390673 100644 --- a/data/clubs.json +++ b/data/clubs.json @@ -1,16 +1,22 @@ -{"clubs":[ - { - "name":"Simply Lift", - "email":"john@simplylift.co", - "points":"13" - }, - { - "name":"Iron Temple", - "email": "admin@irontemple.com", - "points":"4" - }, - { "name":"She Lifts", - "email": "kate@shelifts.co.uk", - "points":"12" - } -]} \ No newline at end of file +{ + "clubs": [ + { + "id": "1", + "name": "Simply Lift", + "email": "john@simplylift.co", + "points": "1" + }, + { + "id": "2", + "name": "Iron Temple", + "email": "admin@irontemple.com", + "points": "1" + }, + { + "id": "3", + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "points": "2" + } + ] +} \ No newline at end of file diff --git a/data/competitions.json b/data/competitions.json index 039fc61bd..0b66bf807 100644 --- a/data/competitions.json +++ b/data/competitions.json @@ -3,7 +3,7 @@ { "name": "Spring Festival", "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25" + "numberOfPlaces": "0" }, { "name": "Fall Classic", diff --git a/tests/unit/test_booking_manager_competition_full.py b/tests/unit/test_booking_manager_competition_full.py index d45d06c5c..de99f2210 100644 --- a/tests/unit/test_booking_manager_competition_full.py +++ b/tests/unit/test_booking_manager_competition_full.py @@ -10,11 +10,10 @@ def setUp(self): clubs_file="data/clubs.json", competitions_file="data/competitions.json") def test_purchase_places_competition_full(self): - # Forcer la compétition "Spring Festival" à n'avoir que 2 places disponibles competition = self.service.competition_manager.find_by_name( "Spring Festival") if competition: - competition.number_of_places = 2 + competition.number_of_places = 2 # Forcer une compétition avec peu de places success = self.service.purchase_places( "Iron Temple", "Spring Festival", 3) self.assertFalse( diff --git a/tests/unit/test_booking_manager_insufficient_points.py b/tests/unit/test_booking_manager_insufficient_points.py index c6f9c4dc1..d4354a24b 100644 --- a/tests/unit/test_booking_manager_insufficient_points.py +++ b/tests/unit/test_booking_manager_insufficient_points.py @@ -1,3 +1,5 @@ +# tests/unit/test_booking_manager_insufficient_points.py + import unittest from app.booking_manager.booking_service import BookingService @@ -10,7 +12,7 @@ def setUp(self): def test_purchase_places_insufficient_points(self): club = self.service.club_manager.find_by_name("Iron Temple") if club: - club.points = 2 + club.points = 2 # Forcer un club avec peu de points success = self.service.purchase_places( "Iron Temple", "Spring Festival", 3) self.assertFalse( diff --git a/tests/unit/test_booking_service_persistence.py b/tests/unit/test_booking_service_persistence.py new file mode 100644 index 000000000..11f72d00e --- /dev/null +++ b/tests/unit/test_booking_service_persistence.py @@ -0,0 +1,81 @@ +# tests/unit/test_booking_service_persistence.py + +import unittest +import os +import json +from app.booking_manager.booking_service import BookingService + + +class TestBookingServicePersistence(unittest.TestCase): + def setUp(self): + # Créer des fichiers temporaires pour les tests + self.test_clubs_file = "data/test_clubs.json" + self.test_competitions_file = "data/test_competitions.json" + + clubs_data = { + "clubs": [ + {"id": "1", "name": "Simply Lift", + "email": "john@simplylift.co", "points": "13"}, + {"id": "2", "name": "Iron Temple", + "email": "admin@irontemple.com", "points": "4"}, + {"id": "3", "name": "She Lifts", + "email": "kate@shelifts.co.uk", "points": "12"} + ] + } + competitions_data = { + "competitions": [ + {"name": "Spring Festival", "date": "2020-03-27 10:00:00", + "numberOfPlaces": "25"}, + {"name": "Fall Classic", "date": "2020-10-22 13:30:00", + "numberOfPlaces": "13"} + ] + } + with open(self.test_clubs_file, "w") as f: + json.dump(clubs_data, f, indent=4) + with open(self.test_competitions_file, "w") as f: + json.dump(competitions_data, f, indent=4) + + self.service = BookingService( + clubs_file=self.test_clubs_file, + competitions_file=self.test_competitions_file + ) + + def tearDown(self): + if os.path.exists(self.test_clubs_file): + os.remove(self.test_clubs_file) + if os.path.exists(self.test_competitions_file): + os.remove(self.test_competitions_file) + + def test_booking_service_persistence(self): + """ + Vérifie qu'après une réservation de 3 places pour "Iron Temple" dans "Spring Festival", + le nombre de points du club est mis à jour (4 - 3 = 1) et le nombre de places restantes + dans la compétition est mis à jour (25 - 3 = 22). + """ + success = self.service.purchase_places( + "Iron Temple", "Spring Festival", 3) + self.assertTrue(success, "La réservation devrait réussir.") + + # Vérifier la mise à jour du fichier clubs + with open(self.test_clubs_file, "r") as f: + clubs_data = json.load(f) + iron_club = next( + (club for club in clubs_data["clubs"] if club["name"] == "Iron Temple"), None) + self.assertIsNotNone( + iron_club, "Les données du club Iron Temple doivent être présentes.") + self.assertEqual( + iron_club["points"], "1", "Les points doivent être mis à jour (4 - 3 = 1).") + + # Vérifier la mise à jour du fichier competitions + with open(self.test_competitions_file, "r") as f: + competitions_data = json.load(f) + spring_comp = next( + (comp for comp in competitions_data["competitions"] if comp["name"] == "Spring Festival"), None) + self.assertIsNotNone( + spring_comp, "Les données de Spring Festival doivent être présentes.") + self.assertEqual(spring_comp["numberOfPlaces"], "22", + "Les places doivent être mises à jour (25 - 3 = 22).") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_club_manager_save.py b/tests/unit/test_club_manager_save.py new file mode 100644 index 000000000..8755f3f48 --- /dev/null +++ b/tests/unit/test_club_manager_save.py @@ -0,0 +1,53 @@ +# tests/unit/test_club_manager_save.py + +import unittest +import os +import json +from app.booking_manager.club_manager import ClubManager + + +class TestClubManagerSave(unittest.TestCase): + def setUp(self): + # Créez un fichier temporaire de clubs pour les tests + self.test_file = "data/test_clubs.json" + clubs_data = { + "clubs": [ + {"id": "1", "name": "Simply Lift", + "email": "john@simplylift.co", "points": "13"}, + {"id": "2", "name": "Iron Temple", + "email": "admin@irontemple.com", "points": "4"}, + {"id": "3", "name": "She Lifts", + "email": "kate@shelifts.co.uk", "points": "12"} + ] + } + with open(self.test_file, "w") as f: + json.dump(clubs_data, f, indent=4) + self.manager = ClubManager(clubs_file=self.test_file) + + def tearDown(self): + # Supprimez le fichier temporaire après le test + if os.path.exists(self.test_file): + os.remove(self.test_file) + + def test_save_clubs_updates_json(self): + # Modifier les points pour un club + club = self.manager.find_by_name("Iron Temple") + self.assertIsNotNone(club, "Le club 'Iron Temple' doit exister.") + club.points = 2 # On simule une réduction de points + + # Sauvegarder l'état actuel + self.manager.save_clubs() + + # Relire le fichier pour vérifier la mise à jour + with open(self.test_file, "r") as f: + data = json.load(f) + iron_data = next( + (c for c in data["clubs"] if c["name"] == "Iron Temple"), None) + self.assertIsNotNone( + iron_data, "Les données pour 'Iron Temple' doivent être présentes dans le fichier JSON.") + self.assertEqual( + iron_data["points"], "2", "Les points du club doivent être mis à jour dans le fichier JSON.") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_welcome_page.py b/tests/unit/test_welcome_page.py index ec8b92f98..70abc8b95 100644 --- a/tests/unit/test_welcome_page.py +++ b/tests/unit/test_welcome_page.py @@ -14,8 +14,6 @@ def test_show_summary_unknown_email_redirects(self): '/showSummary', data={'email': 'unknown@example.com'}) self.assertEqual(response.status_code, 302, "Doit rediriger pour un email inconnu.") - - # Suivre la redirection pour vérifier l'absence d'erreur Jinja response_followed = self.client.post( '/showSummary', data={'email': 'unknown@example.com'}, follow_redirects=True) self.assertEqual(response_followed.status_code, 200, From 0cb020902b8ec7384f78279ecb776155ea9a3418 Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Sun, 9 Feb 2025 20:27:59 +0100 Subject: [PATCH 08/10] usage test --- .flaskenv | 2 + data/clubs.json | 6 +- data/competitions.json | 4 +- tests/integration/test_integration.py | 53 +++++++++-- .../unit/test_booking_service_persistence.py | 93 ++++++++++++++----- 5 files changed, 124 insertions(+), 34 deletions(-) create mode 100644 .flaskenv diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 000000000..865919fdd --- /dev/null +++ b/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=app.server +FLASK_ENV=development diff --git a/data/clubs.json b/data/clubs.json index fff390673..300c3d907 100644 --- a/data/clubs.json +++ b/data/clubs.json @@ -4,19 +4,19 @@ "id": "1", "name": "Simply Lift", "email": "john@simplylift.co", - "points": "1" + "points": "12" }, { "id": "2", "name": "Iron Temple", "email": "admin@irontemple.com", - "points": "1" + "points": "2" }, { "id": "3", "name": "She Lifts", "email": "kate@shelifts.co.uk", - "points": "2" + "points": "7" } ] } \ No newline at end of file diff --git a/data/competitions.json b/data/competitions.json index 0b66bf807..78cec8e06 100644 --- a/data/competitions.json +++ b/data/competitions.json @@ -3,12 +3,12 @@ { "name": "Spring Festival", "date": "2020-03-27 10:00:00", - "numberOfPlaces": "0" + "numberOfPlaces": "22" }, { "name": "Fall Classic", "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13" + "numberOfPlaces": "12" } ] } \ No newline at end of file diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 45570f9ec..605d7b96a 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1,26 +1,67 @@ +# tests/integration/test_integration.py + import unittest +import os +import json from app.server import app class TestServerRoutes(unittest.TestCase): + """ + Test d'intégration pour vérifier qu'une réservation de 3 places + aboutit à un message de succès ('Great-booking complete!'). + """ def setUp(self): + """ + Prépare l'application Flask en mode TEST et un client de test. + Si nécessaire, réinitialisez ici vos fichiers JSON + (par ex. copy test_clubs.json et test_competitions.json). + """ app.config['TESTING'] = True self.client = app.test_client() + # Exemple (optionnel) : si vous avez un paramètre pour surcharger + # l'emplacement des fichiers clubs/competitions en mode test, + # vous pouvez le faire ici (non détaillé). + def test_purchase_places_route(self): + """ + Envoie un POST à '/purchasePlaces' pour réserver 3 places + pour 'Iron Temple' dans 'Spring Festival'. Vérifie qu'on obtient + un code 200 et un message de succès ('Great-booking complete!'). + """ data = { "club": "Iron Temple", "competition": "Spring Festival", "places": "3" } - response = self.client.post("/purchasePlaces", data=data) - # Vérifier qu'on obtient un code 200 et un message de succès - self.assertEqual(response.status_code, 200) - self.assertIn(b"booking complete", response.data.lower(), - "Doit contenir un message de réservation réussie.") + # On suit la redirection pour voir le contenu final + response = self.client.post( + "/purchasePlaces", + data=data, + follow_redirects=True + ) + + # Vérifie qu'on obtient un code 200 + self.assertEqual( + response.status_code, + 200, + "La requête /purchasePlaces doit retourner un code 200 (OK)." + ) + + # Vérifie qu'on obtient bien le message de succès + # (ex. "Great-booking complete!") + lowercase_data = response.data.lower() + self.assertIn( + b"great-booking complete!", + lowercase_data, + "Doit contenir un message de réservation réussie ('Great-booking complete!')." + ) - # On peut ajouter des checks sur le HTML renvoyé, le nombre de places, etc. + # Vous pouvez ajouter d'autres checks ici, par ex. vérifier que + # le nombre de places du club et/ou le nombre de points restants + # s'affichent correctement dans la page renvoyée. if __name__ == "__main__": diff --git a/tests/unit/test_booking_service_persistence.py b/tests/unit/test_booking_service_persistence.py index 11f72d00e..128d3327e 100644 --- a/tests/unit/test_booking_service_persistence.py +++ b/tests/unit/test_booking_service_persistence.py @@ -7,40 +7,72 @@ class TestBookingServicePersistence(unittest.TestCase): + """ + Teste la persistance des données lors de l'achat de places via BookingService. + """ + def setUp(self): - # Créer des fichiers temporaires pour les tests + """ + Prépare deux fichiers JSON de test (pour clubs et compétitions) + afin de ne pas affecter les fichiers de production. + """ self.test_clubs_file = "data/test_clubs.json" self.test_competitions_file = "data/test_competitions.json" clubs_data = { "clubs": [ - {"id": "1", "name": "Simply Lift", - "email": "john@simplylift.co", "points": "13"}, - {"id": "2", "name": "Iron Temple", - "email": "admin@irontemple.com", "points": "4"}, - {"id": "3", "name": "She Lifts", - "email": "kate@shelifts.co.uk", "points": "12"} + { + "id": "1", + "name": "Simply Lift", + "email": "john@simplylift.co", + "points": "13" + }, + { + "id": "2", + "name": "Iron Temple", + "email": "admin@irontemple.com", + "points": "4" + }, + { + "id": "3", + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "points": "12" + } ] } + competitions_data = { "competitions": [ - {"name": "Spring Festival", "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25"}, - {"name": "Fall Classic", "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13"} + { + "name": "Spring Festival", + "date": "2020-03-27 10:00:00", + "numberOfPlaces": "25" + }, + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "numberOfPlaces": "13" + } ] } + + # Écrit les données JSON dans des fichiers temporaires with open(self.test_clubs_file, "w") as f: json.dump(clubs_data, f, indent=4) with open(self.test_competitions_file, "w") as f: json.dump(competitions_data, f, indent=4) + # Instancie BookingService avec ces fichiers de test self.service = BookingService( clubs_file=self.test_clubs_file, competitions_file=self.test_competitions_file ) def tearDown(self): + """ + Supprime les fichiers temporaires pour ne pas polluer l'environnement. + """ if os.path.exists(self.test_clubs_file): os.remove(self.test_clubs_file) if os.path.exists(self.test_competitions_file): @@ -48,33 +80,48 @@ def tearDown(self): def test_booking_service_persistence(self): """ - Vérifie qu'après une réservation de 3 places pour "Iron Temple" dans "Spring Festival", - le nombre de points du club est mis à jour (4 - 3 = 1) et le nombre de places restantes - dans la compétition est mis à jour (25 - 3 = 22). + Vérifie qu'après une réservation de 3 places pour "Iron Temple" dans "Spring Festival" : + - Le nombre de points du club est mis à jour (4 - 3 = 1). + - Le nombre de places dans la compétition est mis à jour (25 - 3 = 22). + - Les modifications sont bien persistées dans les fichiers JSON. """ success = self.service.purchase_places( "Iron Temple", "Spring Festival", 3) self.assertTrue(success, "La réservation devrait réussir.") - # Vérifier la mise à jour du fichier clubs + # Vérification du fichier JSON des clubs with open(self.test_clubs_file, "r") as f: clubs_data = json.load(f) + iron_club = next( - (club for club in clubs_data["clubs"] if club["name"] == "Iron Temple"), None) + (club for club in clubs_data["clubs"] + if club["name"] == "Iron Temple"), + None + ) self.assertIsNotNone( - iron_club, "Les données du club Iron Temple doivent être présentes.") + iron_club, "Les données du club 'Iron Temple' doivent être présentes.") self.assertEqual( - iron_club["points"], "1", "Les points doivent être mis à jour (4 - 3 = 1).") + iron_club["points"], "1", + "Les points doivent être mis à jour (4 - 3 = 1)." + ) - # Vérifier la mise à jour du fichier competitions + # Vérification du fichier JSON des compétitions with open(self.test_competitions_file, "r") as f: competitions_data = json.load(f) + spring_comp = next( - (comp for comp in competitions_data["competitions"] if comp["name"] == "Spring Festival"), None) + (comp for comp in competitions_data["competitions"] + if comp["name"] == "Spring Festival"), + None + ) self.assertIsNotNone( - spring_comp, "Les données de Spring Festival doivent être présentes.") - self.assertEqual(spring_comp["numberOfPlaces"], "22", - "Les places doivent être mises à jour (25 - 3 = 22).") + spring_comp, + "Les données de 'Spring Festival' doivent être présentes." + ) + self.assertEqual( + spring_comp["numberOfPlaces"], "22", + "Les places doivent être mises à jour (25 - 3 = 22)." + ) if __name__ == '__main__': From f4c6591c4bcd58684ed73b059b8dfc89564c624b Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Sun, 9 Feb 2025 20:40:01 +0100 Subject: [PATCH 09/10] correction sur test_persitence --- app/server.py | 53 ++++++++++++++++++++++++++++++++++++++---- data/clubs.json | 2 +- data/competitions.json | 2 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/server.py b/app/server.py index 96285b2b4..14703de59 100644 --- a/app/server.py +++ b/app/server.py @@ -6,7 +6,9 @@ app = Flask(__name__, static_folder="../static") app.secret_key = "secret_key_xyz" -# Instanciation du service de réservation +# Instanciation du service de réservation, avec les JSON par défaut. +# Si vous souhaitez utiliser des fichiers de test différents, +# configurez-les dans le code ou via des variables d'environnement. booking_service = BookingService( clubs_file="data/clubs.json", competitions_file="data/competitions.json" @@ -15,41 +17,71 @@ @app.route("/") def index(): + """ + Page d'accueil de l'application. + """ return render_template("index.html") @app.route("/showSummary", methods=["POST"]) def show_summary(): + """ + Récupère l'email du club et affiche la page de résumé (welcome.html). + Si le club n'est pas trouvé, renvoie vers la page d'accueil avec un message d'erreur. + """ email = request.form.get("email", "") club = booking_service.club_manager.find_by_email(email) if not club: flash("Email inconnu ou invalide.") return redirect(url_for("index")) - return render_template("welcome.html", club=club, competitions=booking_service.competition_manager.competitions) + + # Affiche la liste des compétitions disponibles + competitions = booking_service.competition_manager.competitions + return render_template("welcome.html", club=club, competitions=competitions) @app.route("/book//") def book(competition, club): + """ + Affiche la page de réservation (booking.html) pour un club et une compétition donnés. + Si le club ou la compétition n'existe pas, renvoie vers l'index. + """ found_competition = booking_service.competition_manager.find_by_name( competition) found_club = booking_service.club_manager.find_by_name(club) if not found_competition or not found_club: flash("Something went wrong - please try again") return redirect(url_for("index")) + return render_template("booking.html", club=found_club, competition=found_competition) @app.route("/purchasePlaces", methods=["POST"]) def purchase_places(): + """ + Tente d'acheter 'places' places pour un club et une compétition. + - Si la réservation réussit, on affiche 'Great-booking complete!' + - Sinon, on affiche 'Impossible de réserver...' + """ competition_name = request.form.get("competition") club_name = request.form.get("club") places_str = request.form.get("places") + try: places_requested = int(places_str) except ValueError: flash("Le nombre de places est invalide.") return redirect(url_for("index")) + # Vérification de l'existence du club et de la compétition + found_competition = booking_service.competition_manager.find_by_name( + competition_name) + found_club = booking_service.club_manager.find_by_name(club_name) + if not found_competition or not found_club: + flash("Something went wrong - please try again") + return redirect(url_for("index")) + + # Appel à la logique métier success = booking_service.purchase_places( club_name, competition_name, places_requested) if success: @@ -57,15 +89,26 @@ def purchase_places(): else: flash("Impossible de réserver ces places (Règle non respectée).") - club = booking_service.club_manager.find_by_name(club_name) - return render_template("welcome.html", club=club, competitions=booking_service.competition_manager.competitions) + # Recharger les données du club et de la compétition (après mise à jour) + updated_club = booking_service.club_manager.find_by_name(club_name) + competitions = booking_service.competition_manager.competitions + + return render_template("welcome.html", club=updated_club, competitions=competitions) @app.route("/clubsPoints") def clubs_points(): - return render_template("clubs_points.html", clubs=booking_service.club_manager.clubs) + """ + Affiche la liste des clubs et leurs points disponibles. + Page publique, sans besoin de login. + """ + clubs = booking_service.club_manager.clubs + return render_template("clubs_points.html", clubs=clubs) @app.route("/logout") def logout(): + """ + Déconnecte le club (retour à l'accueil). + """ return redirect(url_for("index")) diff --git a/data/clubs.json b/data/clubs.json index 300c3d907..a623b09c2 100644 --- a/data/clubs.json +++ b/data/clubs.json @@ -10,7 +10,7 @@ "id": "2", "name": "Iron Temple", "email": "admin@irontemple.com", - "points": "2" + "points": "0" }, { "id": "3", diff --git a/data/competitions.json b/data/competitions.json index 78cec8e06..258118aaa 100644 --- a/data/competitions.json +++ b/data/competitions.json @@ -3,7 +3,7 @@ { "name": "Spring Festival", "date": "2020-03-27 10:00:00", - "numberOfPlaces": "22" + "numberOfPlaces": "14" }, { "name": "Fall Classic", From 31c3da04e7057f5f86ba75bcb2820f7178a2a957 Mon Sep 17 00:00:00 2001 From: VincentDesmouceaux Date: Wed, 19 Feb 2025 13:39:32 +0100 Subject: [PATCH 10/10] =?UTF-8?q?mise=20en=20place=20du=20coverage=20pour?= =?UTF-8?q?=20la=20verification=20de=20la=20port=C3=A9e=20des=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 6148 bytes .coverage | Bin 0 -> 53248 bytes data/clubs.json | 2 +- data/competitions.json | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .coverage diff --git a/.DS_Store b/.DS_Store index ed2a65c8bc3d2bd8bb480af7d9ea7b7cbbf7c371..92e51b34986a28205eb6a042626ccd371dd32328 100644 GIT binary patch delta 92 zcmZoMXfc=|#>B`mu~2NHo}w@d0|Nsi1A_nqLvc>JVQ_MOZo$ODm5eN#=dfloZC2o5 mXPMa0xtX1Vp9838vmnQJ=E?jbmO$lTZA?&_Wpjkc8fF03!4o3@ literal 6148 zcmeHKJxT*X6n3_5Nt*~K?*CIHLR6|V9<8sCXxtQHxMoC)*Dy|9!4w0TX+Dm z@dO&bH#4}iY$Di*=zB2pZQjp*Gat;p3=yf$`JheIBBB6;vEINm$2iWTVoT1$#mc;+ zPe;_J5gk)p$rgtwAPW381^DbXaav=V&;`zJaem|Ru-)xv>BKI%akn@#&7vq7X9+Tb z(bMJY&Efs_SJ{JaWKTx^DtNh6ql8ZAw7kA}qlozHd4IXRem*ekadl}{@K>WHKBe(e z@QuNj_d=`%L^Um7Hv5x!;JShX60@u%CN()O**O2qRgUz zC{R|QW`DN0|L-im|Cfv8mna|#{3!)gJ?cgsY{~W3%I3J&YK$HR8|UQ~O$jE~j?DwN d;#CY|h-rQRh7NO!@WAAcfRRBaQQ$`v_yDRMibenc diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..3d50707efa8f2dd9a018079037391b59de676963 GIT binary patch literal 53248 zcmeI4du$s?9mjX=^<&p-j}s>`xwK^e>7~&nF}dm}$8og1Ba~iy^l|CqK-^|+uamvn zUe~)$98gv5y{hAQNPt8T2Zet?LOlKgAwc2}5Fms&?l1ZW(zXAe#EeKrC?S|nyu{Nk{r)-`xS-bI0@cyc>6~L zJc54$F5lwsxqqt$pZb^Y-a#A^^ACX6S{l%rqXn9CQ+j!|&}k;#Af*@GOI!fs%BGLCnUH1;iJ8si zS}6<2pRScEv~nPYo3U+nO4%u`ZF+^h>$=&XE}P|Tt4<#|tx-2st!$jD>5a;tP?{+x zSTV}xghC4eEnrtHoPIFHbI>aFx^=Dy_gkt~vrTrcLglN|@F73f7mcJxNBN8ASVN(B z)U07!4UgE?Xd1yCnhw3*x!urS(%wjDFDe!lv%tCxO|7_<1FgBW&t!ZUT9tM*#q=VA zrEh6#qsD>_O15Zb+uIszF_sP;1jYhCTA^0X(XL|AGUUufy{gUYQ{Gi~Sa_=&5;UD@ zF`S+m?bLJ{O+cM)cqL~z97&Ii@Xv^>8F{tkIgz#|E6$q-$2K>JKD$8YKM` z%~8_tBiLtxL+dB=kHIg+Ko9*Q&47hN&)7nlbyA~Yu)XjkoZf*@` zT?&3I?_Nh5rwe?Z+~nYe2NFO6NB{{S0VIF~kN^@u0!RP}AOR$>{Rjm403V_2{{Xqd zk!$dQ2NFO6NB{{S0VIF~kN^@u0!RP}AOR%swj>}2f;)--507^x`QXT~_aA`0Q}3Rd zzMuZdKR{mR$m`@^Z_5Ir(MSLZAOR$R1dsp{Kmter2_OL^fCP|0gMb{|$@^ae1f<}I z=zR-7fB#QMRgQd@%#vi{w~3D@M&rMXKOdixUy+}acf@`aJ1YHE`n>e8cwPLmcrbb+ z`fZ4g2NFO6NB{{S0VIF~IuSUW7C2`lbJBvXoS8)f9*$79^SV_sYdKx3)iZg0F>`F$ zUNFn*tZvaKHcU=uv`Qso>!nH&wq05owoQI&!7dda8v?Q6uEesMr8BL^2SIDJE3GBH z2>V%Gxf25*H@-YK3SS1*(@9X>)1_*zSj(!g*BDS%RZCl>Ks5rw z2fGx8J9ZHqLakVYd=AARpY+!AQB?!_kg9^z$0U&2 zw@E2F-uTt(F^8vIG|lwFK>nl%@|hmU)1B^W(S&X4v>)w{f^29DvL!RG7iqsf6#<3m zEhwxjcT2wk-6zAKySE3rem-8e0qGMVklyT&xlvWMRR4Zqiy#|A+9N?w5Vug^7AwvK zK;wZ;YPgdD+m~O3A=@mwb3#i1FgwPB@~*CxTWqDj|EHsK9O+AZBB8{8CI2p-iNBC| zCHkG{T=Y%yJ#tchUfvn|aqO7%hV%vL{o>!mm&Fftn(i?i2_OL^fCP{L5f% z0l^7vMYgg2kDeBs>8&XE&AIWtNBjCe@)5z=+Y4PkpXT~Myp`c?qpE7D{?JxIHik6e zQNa$01Dn;jtMz~2nBeT%?6(u^m0N7Z=l|hd8WKPPNB{{S0VIF~kN^@u z0!RP}Ac1X7Ajk)~IQ{*fC$~6w;eiB@01`j~NB{{S0VIF~kN^@u0!RP}Y(oOUU|eMT z{}X@W$eZNPIX@M7q(${X^yg3pd0{@PoYq#NuMdkP)JsqT%Zr}bX zFAl)zz`)Atg+Md~-u~3e>QSj5d_(=;IJ2@`5c_C^zLnqqMU<1^y(d}!_ocI6xs@6u za5h2eAH4k9>ebaN5B@zce?52oGtmSD_a{I_jMJ3ymDTfss7wRpmDNk(82Az~UY5YU zN2>o~_2^3%B2kfE7vGb6C=vx00lH4-(`n`m4KZhMkU0Yb%t@t~)8EgWzCPw8lguH6If(>w;&JB4 zGIL@v=1A|G56&ZncW#K>|ns2_OL^fCP{L5)RALjp(u2_OL^fCP{L5