From 6f5e6422cd7c3d797c661f39473891fbb8083949 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 13:41:21 +0100 Subject: [PATCH 01/11] bug/deduction-points-club: deduct points from club after booking --- server.py | 6 ++++++ tests/test_integration.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/test_integration.py diff --git a/server.py b/server.py index 4084baeac..b9790eca0 100644 --- a/server.py +++ b/server.py @@ -46,11 +46,17 @@ 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']) + + # Déduire les places disponibles competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired + # Déduire les points du club + club['points'] = int(club['points']) - placesRequired + flash('Great-booking complete!') return render_template('welcome.html', club=club, competitions=competitions) + # TODO: Add route for points display diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 000000000..79b4a34df --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,39 @@ +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # permet d'importer server.py qui est à la racine du projet + +import pytest +from server import app + +@pytest.fixture +def client(): + # Création d'un client test Flask pour simuler les requêtes HTTP + with app.test_client() as client: + yield client + + +def test_index(client): + # Test basique pour vérifier que la page d'accueil répond bien + response = client.get('/') + assert response.status_code == 200 + assert b'Welcome' in response.data or b'Bienvenue' in response.data or b'' in response.data + +def test_purchase_places_deducts_club_points(client): + """Teste que les points du club sont déduits après réservation""" + + # Étape 1: Connexion avec email valide (Simply Lift a 13 points) + summary_response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) + assert summary_response.status_code == 200 + + # Étape 2: Réservation de 2 places pour Spring Festival + data = { + 'competition': 'Spring Festival', + 'club': 'Simply Lift', + 'places': '2' + } + response = client.post('/purchasePlaces', data=data, follow_redirects=True) + assert response.status_code == 200 + assert b'Great-booking complete!' in response.data + + # Étape 3: Vérifier que les points ont été déduits (13 - 2 = 11) + assert b'Points available: 11' in response.data # ← CE TEST VA ÉCHOUER From 0584312a72980d4766b67572d4c35e93d57d953e Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 13:56:18 +0100 Subject: [PATCH 02/11] bug/past-competitions-validation: block booking for past events --- competitions.json | 36 +++++++++++++++++++++++------------- server.py | 9 ++++++++- tests/test_integration.py | 23 ++++++++++++++++++++++- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/competitions.json b/competitions.json index 039fc61bd..cb6c3b8c2 100644 --- a/competitions.json +++ b/competitions.json @@ -1,14 +1,24 @@ { - "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" - } - ] -} \ No newline at end of file + "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": "Winter Gala", + "date": "2025-12-15 09:00:00", + "numberOfPlaces": "30" + }, + { + "name": "New Year's Championship", + "date": "2026-01-02 11:00:00", + "numberOfPlaces": "20" + } + ] +} diff --git a/server.py b/server.py index b9790eca0..f2c759059 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,6 @@ import json from flask import Flask,render_template,request,redirect,flash,url_for - +from datetime import datetime def loadClubs(): with open('clubs.json') as c: @@ -46,6 +46,13 @@ 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']) + + # Vérifier la date de la compétition + competition_date = datetime.strptime(competition['date'], '%Y-%m-%d %H:%M:%S') + now = datetime.now() + if competition_date < now: + flash("Cannot book places for past competitions") + return render_template('welcome.html', club=club, competitions=competitions) # Déduire les places disponibles competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired diff --git a/tests/test_integration.py b/tests/test_integration.py index 79b4a34df..0f074978c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -36,4 +36,25 @@ def test_purchase_places_deducts_club_points(client): assert b'Great-booking complete!' in response.data # Étape 3: Vérifier que les points ont été déduits (13 - 2 = 11) - assert b'Points available: 11' in response.data # ← CE TEST VA ÉCHOUER + assert b'Points available: 11' in response.data + +def test_cannot_book_past_competitions(client): + """Teste qu'on ne peut pas réserver pour des compétitions passées""" + + # Connexion Simply Lift + summary_response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) + assert summary_response.status_code == 200 + + # Tentative de réservation pour Spring Festival (2020 - PASSÉ) + data_past = { + 'competition': 'Spring Festival', + 'club': 'Simply Lift', + 'places': '2' + } + response_past = client.post('/purchasePlaces', data=data_past, follow_redirects=True) + assert response_past.status_code == 200 + assert b"Cannot book places for past competitions" in response_past.data + assert b'Great-booking complete!' not in response_past.data + + # Vérifier que les points n'ont PAS été déduits (toujours 13) + assert b'Points available: 13' in response_past.data From b3ae940e5afa1f5b1712ced3c843bf1871901c53 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 14:01:09 +0100 Subject: [PATCH 03/11] bug/max-12-places: block bookings over 12 places limit --- server.py | 6 ++++++ tests/test_integration.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/server.py b/server.py index f2c759059..024fb2e59 100644 --- a/server.py +++ b/server.py @@ -54,6 +54,12 @@ def purchasePlaces(): flash("Cannot book places for past competitions") return render_template('welcome.html', club=club, competitions=competitions) + # Vérifier la limite de 12 places max + if placesRequired > 12: + flash("Cannot book more than 12 places per competition per club") + return render_template('welcome.html', club=club, competitions=competitions) + + # Déduire les places disponibles competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired # Déduire les points du club diff --git a/tests/test_integration.py b/tests/test_integration.py index 0f074978c..f5860772b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -58,3 +58,24 @@ def test_cannot_book_past_competitions(client): # Vérifier que les points n'ont PAS été déduits (toujours 13) assert b'Points available: 13' in response_past.data + +def test_cannot_book_more_than_12_places(client): + """Teste qu'on ne peut pas réserver plus de 12 places par réservation""" + + # Connexion Simply Lift (13 points) + summary_response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) + assert summary_response.status_code == 200 + + # Tentative de réservation de 13 places (AU-DESSUS DE 12) + data_excess = { + 'competition': 'Winter Gala', # Compétition future + 'club': 'Simply Lift', + 'places': '13' + } + response_excess = client.post('/purchasePlaces', data=data_excess, follow_redirects=True) + assert response_excess.status_code == 200 + assert b"Cannot book more than 12 places per competition per club" in response_excess.data # ← ÉCHEC + assert b'Great-booking complete!' not in response_excess.data + + # Vérifier que les points n'ont PAS été déduits (toujours 13) + assert b'Points available: 13' in response_excess.data From 82aa8c6f93e084aa8b76de74521e1f229adf4f85 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 14:10:55 +0100 Subject: [PATCH 04/11] bug/insufficient-points: block booking when insufficient club points --- server.py | 6 +++++- tests/test_integration.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 024fb2e59..6bbf3fca7 100644 --- a/server.py +++ b/server.py @@ -54,11 +54,15 @@ def purchasePlaces(): flash("Cannot book places for past competitions") return render_template('welcome.html', club=club, competitions=competitions) + # Vérification du nombre de points restants avant réservation + if placesRequired > int(club['points']): + flash("Not enough points") + return render_template('welcome.html', club=club, competitions=competitions) + # Vérifier la limite de 12 places max if placesRequired > 12: flash("Cannot book more than 12 places per competition per club") return render_template('welcome.html', club=club, competitions=competitions) - # Déduire les places disponibles competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired diff --git a/tests/test_integration.py b/tests/test_integration.py index f5860772b..91ac06c58 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -79,3 +79,25 @@ def test_cannot_book_more_than_12_places(client): # Vérifier que les points n'ont PAS été déduits (toujours 13) assert b'Points available: 13' in response_excess.data + +def test_cannot_spend_more_points_than_available(client): + """Teste qu'un club ne peut pas réserver plus de places que ses points disponibles""" + + # Connexion club Simply Lift (13 points) + summary_response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) + assert summary_response.status_code == 200 + + # Tentative de réservation de 20 places (plus que 13 points) + data_too_many = { + 'competition': 'Winter Gala', # compétition future + 'club': 'Simply Lift', + 'places': '20' + } + + response_too_many = client.post('/purchasePlaces', data=data_too_many, follow_redirects=True) + assert response_too_many.status_code == 200 + assert b"Not enough points" in response_too_many.data # ← Message attendu d'erreur + assert b'Great-booking complete!' not in response_too_many.data + + # Vérifier que les points restent inchangés (13) + assert b'Points available: 13' in response_too_many.data From 8d34dca0d2bd674b6e6666037649801b6042fb38 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 15:09:07 +0100 Subject: [PATCH 05/11] Add points leaderboard feature and fix competition places limit bug --- clubs.json | 25 ++++++++++++++----------- server.py | 30 ++++++++++++++++++++++++++---- templates/index.html | 1 + templates/points.html | 19 +++++++++++++++++++ tests/test_integration.py | 39 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 templates/points.html diff --git a/clubs.json b/clubs.json index 1d7ad1ffe..c4047451d 100644 --- a/clubs.json +++ b/clubs.json @@ -1,16 +1,19 @@ -{"clubs":[ +{ + "clubs": [ { - "name":"Simply Lift", - "email":"john@simplylift.co", - "points":"13" + "name": "Simply Lift", + "email": "john@simplylift.co", + "points": "13" }, { - "name":"Iron Temple", - "email": "admin@irontemple.com", - "points":"4" + "name": "Iron Temple", + "email": "admin@irontemple.com", + "points": "20" }, - { "name":"She Lifts", - "email": "kate@shelifts.co.uk", - "points":"12" + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "points": "22" } -]} \ No newline at end of file + ] +} \ No newline at end of file diff --git a/server.py b/server.py index 6bbf3fca7..695960000 100644 --- a/server.py +++ b/server.py @@ -13,6 +13,16 @@ def loadCompetitions(): listOfCompetitions = json.load(comps)['competitions'] return listOfCompetitions +def saveClubs(clubs_list): + """Sauvegarde la liste des clubs mise à jour dans clubs.json""" + with open('clubs.json', 'w') as f: + json.dump({'clubs': clubs_list}, f, indent=2) + +def saveCompetitions(competitions_list): + """Sauvegarde la liste des compétitions mise à jour dans competitions.json""" + with open('competitions.json', 'w') as f: + json.dump({'competitions': competitions_list}, f, indent=2) + app = Flask(__name__) app.secret_key = 'something_special' @@ -59,6 +69,11 @@ def purchasePlaces(): flash("Not enough points") return render_template('welcome.html', club=club, competitions=competitions) + # Vérifier que la compétition a assez de places disponibles + if placesRequired > int(competition['numberOfPlaces']): + flash("Not enough places available in this competition") + return render_template('welcome.html', club=club, competitions=competitions) + # Vérifier la limite de 12 places max if placesRequired > 12: flash("Cannot book more than 12 places per competition per club") @@ -68,14 +83,21 @@ def purchasePlaces(): competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired # Déduire les points du club club['points'] = int(club['points']) - placesRequired + + # Sauvegarder les changements dans les fichiers JSON + saveCompetitions(competitions) + saveClubs(clubs) flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) - - + # Toujours recharger clubs et competitions avant d'envoyer au template + return render_template('welcome.html', club=club, competitions=loadCompetitions()) -# TODO: Add route for points display +@app.route('/points') +def show_points(): + """Affiche le tableau des points de tous les clubs (lecture seule)""" + clubs = loadClubs() # Recharge pour état actuel + return render_template('points.html', clubs=clubs) @app.route('/logout') def logout(): diff --git a/templates/index.html b/templates/index.html index 926526b7d..cd0eac2de 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,5 +12,6 @@

Welcome to the GUDLFT Registration Portal!

+

Points leaderboard

\ No newline at end of file diff --git a/templates/points.html b/templates/points.html new file mode 100644 index 000000000..d64489a87 --- /dev/null +++ b/templates/points.html @@ -0,0 +1,19 @@ + + + + + Clubs Points + + +

Clubs and their Points

+ + + {% for club in clubs %} + + + + + {% endfor %} +
Club NamePoints
{{club['name']}}{{club['points']}}
+ + \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py index 91ac06c58..fe19be520 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -27,7 +27,7 @@ def test_purchase_places_deducts_club_points(client): # Étape 2: Réservation de 2 places pour Spring Festival data = { - 'competition': 'Spring Festival', + 'competition': 'Winter Gala', 'club': 'Simply Lift', 'places': '2' } @@ -96,8 +96,43 @@ def test_cannot_spend_more_points_than_available(client): response_too_many = client.post('/purchasePlaces', data=data_too_many, follow_redirects=True) assert response_too_many.status_code == 200 - assert b"Not enough points" in response_too_many.data # ← Message attendu d'erreur + assert b"Not enough points" in response_too_many.data assert b'Great-booking complete!' not in response_too_many.data # Vérifier que les points restent inchangés (13) assert b'Points available: 13' in response_too_many.data + +def test_cannot_book_more_places_than_available(client): + """Teste qu'on ne peut pas réserver plus de places que disponibles dans la compétition""" + + # Connexion She Lifts (22 points) + summary_response = client.post('/showSummary', data={'email': 'kate@shelifts.co.uk'}) + assert summary_response.status_code == 200 + + # Tentative réservation 21 places sur New Year's Championship (20 places dispo) + data_excess = { + 'competition': "New Year's Championship", # 20 places disponibles + 'club': 'She Lifts', # 22 points disponibles + 'places': '21' # 21 > 20 → doit échouer + } + + response_excess = client.post('/purchasePlaces', data=data_excess, follow_redirects=True) + assert response_excess.status_code == 200 + + # Doit afficher une erreur places insuffisantes + assert b"Not enough places available in this competition" in response_excess.data + assert b'Great-booking complete!' not in response_excess.data + + # Points n'ont pas été déduits + assert b'Points available: 22' in response_excess.data + + + +def test_points_page_displays_all_clubs(client): + """Teste que la page points affiche tous les clubs et leurs points""" + response = client.get('/points') + assert response.status_code == 200 + assert b'Simply Lift' in response.data + assert b'Iron Temple' in response.data + assert b'She Lifts' in response.data + assert b'13' in response.data # Simply Lift points From e7653ee31c12c23aeb03c8da22a9352bde0e39f0 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 16:52:16 +0100 Subject: [PATCH 06/11] Fix tests: reload data, update test values, add clubs.json & conftest --- clubs.json | 2 +- competitions.json | 4 ++-- server.py | 14 ++++++++---- tests/conftest.py | 48 +++++++++++++++++++++++++++++++++++++++ tests/test_integration.py | 27 ++++++++++------------ 5 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 tests/conftest.py diff --git a/clubs.json b/clubs.json index c4047451d..5184686ee 100644 --- a/clubs.json +++ b/clubs.json @@ -8,7 +8,7 @@ { "name": "Iron Temple", "email": "admin@irontemple.com", - "points": "20" + "points": "11" }, { "name": "She Lifts", diff --git a/competitions.json b/competitions.json index cb6c3b8c2..7a7b37fdf 100644 --- a/competitions.json +++ b/competitions.json @@ -18,7 +18,7 @@ { "name": "New Year's Championship", "date": "2026-01-02 11:00:00", - "numberOfPlaces": "20" + "numberOfPlaces": "10" } ] -} +} \ No newline at end of file diff --git a/server.py b/server.py index 695960000..d0b2079de 100644 --- a/server.py +++ b/server.py @@ -53,6 +53,10 @@ def book(competition,club): @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): + # Chargement des données + competitions = loadCompetitions() + clubs = loadClubs() + 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']) @@ -64,6 +68,11 @@ def purchasePlaces(): flash("Cannot book places for past competitions") return render_template('welcome.html', club=club, competitions=competitions) + # Vérifier la limite de 12 places max + if placesRequired > 12: + flash("Cannot book more than 12 places per competition per club") + return render_template('welcome.html', club=club, competitions=competitions) + # Vérification du nombre de points restants avant réservation if placesRequired > int(club['points']): flash("Not enough points") @@ -74,11 +83,6 @@ def purchasePlaces(): flash("Not enough places available in this competition") return render_template('welcome.html', club=club, competitions=competitions) - # Vérifier la limite de 12 places max - if placesRequired > 12: - flash("Cannot book more than 12 places per competition per club") - return render_template('welcome.html', club=club, competitions=competitions) - # Déduire les places disponibles competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired # Déduire les points du club diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..7ba64600b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +import json +import pytest +import sys +import os + +# Ajoute le répertoire parent pour server +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +@pytest.fixture(autouse=True, scope='function') +def reset_json_files(): + """Reset clubs.json et competitions.json avant chaque test""" + + original_clubs = { + "clubs": [ + {"name": "Simply Lift", "email": "john@simplylift.co", "points": "13"}, + {"name": "Iron Temple", "email": "admin@irontemple.com", "points": "11"}, + {"name": "She Lifts", "email": "kate@shelifts.co.uk", "points": "22"} + ] + } + + original_competitions = { + "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": "Winter Gala", "date": "2025-12-15 09:00:00", "numberOfPlaces": "30"}, + {"name": "New Year's Championship", "date": "2026-01-02 11:00:00", "numberOfPlaces": "10"} + ] + } + + # Supprime les fichiers existants d'abord + import os + if os.path.exists('clubs.json'): + os.remove('clubs.json') + if os.path.exists('competitions.json'): + os.remove('competitions.json') + + # Écriture des originaux + with open('clubs.json', 'w') as f: + json.dump(original_clubs, f, indent=2) + with open('competitions.json', 'w') as f: + json.dump(original_competitions, f, indent=2) + +@pytest.fixture +def client(): + from server import app + app.config['TESTING'] = True + with app.test_client() as client: + yield client diff --git a/tests/test_integration.py b/tests/test_integration.py index fe19be520..21f961cf8 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -11,7 +11,6 @@ def client(): with app.test_client() as client: yield client - def test_index(client): # Test basique pour vérifier que la page d'accueil répond bien response = client.get('/') @@ -74,7 +73,7 @@ def test_cannot_book_more_than_12_places(client): } response_excess = client.post('/purchasePlaces', data=data_excess, follow_redirects=True) assert response_excess.status_code == 200 - assert b"Cannot book more than 12 places per competition per club" in response_excess.data # ← ÉCHEC + assert b"Cannot book more than 12 places per competition per club" in response_excess.data assert b'Great-booking complete!' not in response_excess.data # Vérifier que les points n'ont PAS été déduits (toujours 13) @@ -87,11 +86,11 @@ def test_cannot_spend_more_points_than_available(client): summary_response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) assert summary_response.status_code == 200 - # Tentative de réservation de 20 places (plus que 13 points) + # Tentative de réservation de 12 places (12 <= 12 max, MAIS 12 > 11 points) data_too_many = { - 'competition': 'Winter Gala', # compétition future - 'club': 'Simply Lift', - 'places': '20' + 'competition': 'Winter Gala', # compétition future (30+ places) + 'club': 'Iron Temple', + 'places': '12' # 12 <= 12 max → passe → 12 > 11 points → "Not enough points" } response_too_many = client.post('/purchasePlaces', data=data_too_many, follow_redirects=True) @@ -99,8 +98,8 @@ def test_cannot_spend_more_points_than_available(client): assert b"Not enough points" in response_too_many.data assert b'Great-booking complete!' not in response_too_many.data - # Vérifier que les points restent inchangés (13) - assert b'Points available: 13' in response_too_many.data + # Vérifier que les points restent inchangés + assert b'Points available: 11' in response_too_many.data def test_cannot_book_more_places_than_available(client): """Teste qu'on ne peut pas réserver plus de places que disponibles dans la compétition""" @@ -109,12 +108,12 @@ def test_cannot_book_more_places_than_available(client): summary_response = client.post('/showSummary', data={'email': 'kate@shelifts.co.uk'}) assert summary_response.status_code == 200 - # Tentative réservation 21 places sur New Year's Championship (20 places dispo) + # Test de depassement data_excess = { - 'competition': "New Year's Championship", # 20 places disponibles - 'club': 'She Lifts', # 22 points disponibles - 'places': '21' # 21 > 20 → doit échouer - } + 'competition': "New Year's Championship", # 10 places + 'club': 'She Lifts', + 'places': '11' +} response_excess = client.post('/purchasePlaces', data=data_excess, follow_redirects=True) assert response_excess.status_code == 200 @@ -126,8 +125,6 @@ def test_cannot_book_more_places_than_available(client): # Points n'ont pas été déduits assert b'Points available: 22' in response_excess.data - - def test_points_page_displays_all_clubs(client): """Teste que la page points affiche tous les clubs et leurs points""" response = client.get('/points') From c045ada7fef2f57bc199ba345649558fff9a4515 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 17:12:27 +0100 Subject: [PATCH 07/11] Fix points not updating after booking by reloading data before rendering --- server.py | 55 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/server.py b/server.py index d0b2079de..891854c7f 100644 --- a/server.py +++ b/server.py @@ -2,53 +2,72 @@ from flask import Flask,render_template,request,redirect,flash,url_for from datetime import datetime + + 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 + + def saveClubs(clubs_list): """Sauvegarde la liste des clubs mise à jour dans clubs.json""" with open('clubs.json', 'w') as f: json.dump({'clubs': clubs_list}, f, indent=2) + + def saveCompetitions(competitions_list): """Sauvegarde la liste des compétitions mise à jour dans competitions.json""" with open('competitions.json', 'w') as f: json.dump({'competitions': competitions_list}, f, indent=2) + + 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(): + clubs = loadClubs() # Recharge pour état actuel club = [club for club in clubs if club['email'] == request.form['email']][0] + competitions = loadCompetitions() # Recharge pour état actuel return render_template('welcome.html',club=club,competitions=competitions) + + @app.route('/book//') def book(competition,club): + clubs = loadClubs() # Recharge pour état actuel + competitions = loadCompetitions() # Recharge pour état actuel 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) + return render_template('welcome.html', club=foundClub, competitions=competitions) + + @app.route('/purchasePlaces',methods=['POST']) @@ -66,27 +85,35 @@ def purchasePlaces(): now = datetime.now() if competition_date < now: flash("Cannot book places for past competitions") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template('welcome.html', + club=[c for c in loadClubs() if c['name'] == request.form['club']][0], + competitions=loadCompetitions()) # Vérifier la limite de 12 places max if placesRequired > 12: flash("Cannot book more than 12 places per competition per club") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template('welcome.html', + club=[c for c in loadClubs() if c['name'] == request.form['club']][0], + competitions=loadCompetitions()) # Vérification du nombre de points restants avant réservation if placesRequired > int(club['points']): flash("Not enough points") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template('welcome.html', + club=[c for c in loadClubs() if c['name'] == request.form['club']][0], + competitions=loadCompetitions()) # Vérifier que la compétition a assez de places disponibles if placesRequired > int(competition['numberOfPlaces']): flash("Not enough places available in this competition") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template('welcome.html', + club=[c for c in loadClubs() if c['name'] == request.form['club']][0], + competitions=loadCompetitions()) # Déduire les places disponibles - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired + competition['numberOfPlaces'] = str(int(competition['numberOfPlaces']) - placesRequired) # Déduire les points du club - club['points'] = int(club['points']) - placesRequired + club['points'] = str(int(club['points']) - placesRequired) # Sauvegarder les changements dans les fichiers JSON saveCompetitions(competitions) @@ -94,7 +121,11 @@ def purchasePlaces(): flash('Great-booking complete!') # Toujours recharger clubs et competitions avant d'envoyer au template - return render_template('welcome.html', club=club, competitions=loadCompetitions()) + return render_template('welcome.html', + club=[c for c in loadClubs() if c['name'] == request.form['club']][0], + competitions=loadCompetitions()) + + @app.route('/points') @@ -103,6 +134,8 @@ def show_points(): clubs = loadClubs() # Recharge pour état actuel return render_template('points.html', clubs=clubs) + + @app.route('/logout') def logout(): - return redirect(url_for('index')) \ No newline at end of file + return redirect(url_for('index')) From 89298008953e7dd9efd6e4a3e321faf0335748fc Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Thu, 27 Nov 2025 17:25:22 +0100 Subject: [PATCH 08/11] Unit tests --- tests/test_unit.py | 106 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/test_unit.py diff --git a/tests/test_unit.py b/tests/test_unit.py new file mode 100644 index 000000000..8b36acae4 --- /dev/null +++ b/tests/test_unit.py @@ -0,0 +1,106 @@ +# Tests unitaires pour la fonction purchasePlaces() + +import pytest +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # Permet d'importer server + +from unittest.mock import patch, MagicMock +from datetime import datetime +from server import loadClubs, loadCompetitions, app, purchasePlaces # Import app et purchasePlaces + +@pytest.fixture +def mock_data(): + """Données de test de base""" + clubs = [{"name": "Simply Lift", "email": "john@simplylift.co", "points": "13"}] + competitions = [{"name": "Winter Gala", "date": "2025-12-15 09:00:00", "numberOfPlaces": "30"}] + return clubs, competitions + +def test_purchase_places_reussite(mock_data): + """Test réservation 5 places """ + clubs, competitions = mock_data + + with app.test_request_context(method='POST', data={ + 'competition': 'Winter Gala', + 'club': 'Simply Lift', + 'places': '5' + }): + with patch('server.loadClubs', return_value=clubs), \ + patch('server.loadCompetitions', return_value=competitions): + + purchasePlaces() + + # Vérifications : déductions effectuées + assert int(competitions[0]['numberOfPlaces']) == 25 # 30-5 + assert int(clubs[0]['points']) == 8 # 13-5 + +def test_purchase_places_competition_passee(mock_data): + """Test : bloque si compétition passée""" + clubs, competitions = mock_data + competitions[0]['date'] = "2020-01-01 10:00:00" # Date passée (AVANT 2025) + + with app.test_request_context(method='POST', data={ + 'competition': 'Winter Gala', + 'club': 'Simply Lift', + 'places': '5' + }): + with patch('server.loadClubs', return_value=clubs), \ + patch('server.loadCompetitions', return_value=competitions): + + purchasePlaces() + + # Pas de modification des données + assert competitions[0]['numberOfPlaces'] == "30" + +def test_purchase_places_points_insuffisants(mock_data): + """Test : bloque si pas assez de points""" + clubs, competitions = mock_data + clubs[0]['points'] = "3" # Seulement 3 points + + with app.test_request_context(method='POST', data={ + 'competition': 'Winter Gala', + 'club': 'Simply Lift', + 'places': '5' + }): + with patch('server.loadClubs', return_value=clubs), \ + patch('server.loadCompetitions', return_value=competitions): + + purchasePlaces() + + # Points inchangés + assert clubs[0]['points'] == "3" + +def test_purchase_places_places_insuffisants(mock_data): + """Test : bloque si pas assez de places en compétition""" + clubs, competitions = mock_data + competitions[0]['numberOfPlaces'] = "3" # Seulement 3 places + + with app.test_request_context(method='POST', data={ + 'competition': 'Winter Gala', + 'club': 'Simply Lift', + 'places': '5' + }): + with patch('server.loadClubs', return_value=clubs), \ + patch('server.loadCompetitions', return_value=competitions): + + purchasePlaces() + + # Places inchangées + assert competitions[0]['numberOfPlaces'] == "3" + +def test_purchase_places_limite_12(mock_data): + """Test : bloque au-delà de 12 places par réservation""" + clubs, competitions = mock_data + + with app.test_request_context(method='POST', data={ + 'competition': 'Winter Gala', + 'club': 'Simply Lift', + 'places': '13' + }): + with patch('server.loadClubs', return_value=clubs), \ + patch('server.loadCompetitions', return_value=competitions): + + purchasePlaces() + + # Pas de déduction + assert competitions[0]['numberOfPlaces'] == "30" From 1ae46f20f6e8bddc3e051983b9c174e9dbcc87c9 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Fri, 28 Nov 2025 14:09:23 +0100 Subject: [PATCH 09/11] Fix last bugs: secure 'book' route and add logout redirect test --- .gitignore | 5 +++-- server.py | 19 ++++++++----------- tests/test_integration.py | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 2cba99d87..6026fc595 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.coverage +.DS_Store \ No newline at end of file diff --git a/server.py b/server.py index 891854c7f..d4c57b6c0 100644 --- a/server.py +++ b/server.py @@ -53,22 +53,23 @@ def showSummary(): return render_template('welcome.html',club=club,competitions=competitions) - - @app.route('/book//') def book(competition,club): + """ Route pour afficher le formulaire de réservation d'une compétition pour un club spécifique.""" + clubs = loadClubs() # Recharge pour état actuel competitions = loadCompetitions() # Recharge pour état actuel - foundClub = [c for c in clubs if c['name'] == club][0] - foundCompetition = [c for c in competitions if c['name'] == competition][0] + + foundClub = next((c for c in clubs if c['name'] == club), None) + foundCompetition = next((c for c in competitions if c['name'] == competition), None) + 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=foundClub, competitions=competitions) - - + return render_template('welcome.html', club=None, competitions=competitions) + @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): @@ -126,16 +127,12 @@ def purchasePlaces(): competitions=loadCompetitions()) - - @app.route('/points') def show_points(): """Affiche le tableau des points de tous les clubs (lecture seule)""" clubs = loadClubs() # Recharge pour état actuel return render_template('points.html', clubs=clubs) - - @app.route('/logout') def logout(): return redirect(url_for('index')) diff --git a/tests/test_integration.py b/tests/test_integration.py index 21f961cf8..48dc2fc08 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -133,3 +133,25 @@ def test_points_page_displays_all_clubs(client): assert b'Iron Temple' in response.data assert b'She Lifts' in response.data assert b'13' in response.data # Simply Lift points + +def test_book_route_success(): + """Test route /book/ valide → booking.html""" + client = app.test_client() + response = client.get('/book/Winter Gala/Simply Lift') + assert response.status_code == 200 + assert b"Winter Gala" in response.data + assert b"Places available" in response.data + +def test_book_route_not_found(): + """Test route /book/ invalide → welcome.html """ + client = app.test_client() + response = client.get('/book/Winter Gala/ClubInexistant') + assert response.status_code == 200 + assert b"Something went wrong" in response.data + +def test_logout_redirect(): + """Test que la route logout redirige vers l'index""" + client = app.test_client() + response = client.get('/logout', follow_redirects=False) + assert response.status_code == 302 # redirection + assert response.headers['Location'] == '/' # Redirige vers index From 1b35ce1d04159f219e23c7a72fcc09b618a12f57 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Fri, 28 Nov 2025 14:24:16 +0100 Subject: [PATCH 10/11] Add Locust performance tests (6 users, 100% success) - 85 requests, 0 failures - Avg GET: 5-6ms (<5s OK) - POST purchase: 14.7ms (<2s OK) - Max: 85ms (95th percentile) - Report: Locust_Test_Report.html --- Locust_Test_Report.html | 156 ++++++++++++++++++++++++++++++++++++++++ locustfile.py | 24 +++++++ 2 files changed, 180 insertions(+) create mode 100644 Locust_Test_Report.html create mode 100644 locustfile.py diff --git a/Locust_Test_Report.html b/Locust_Test_Report.html new file mode 100644 index 000000000..43c1411d3 --- /dev/null +++ b/Locust_Test_Report.html @@ -0,0 +1,156 @@ + + + + + + + + + + + Locust + + + + +
+ + + + + \ No newline at end of file diff --git a/locustfile.py b/locustfile.py new file mode 100644 index 000000000..f15287525 --- /dev/null +++ b/locustfile.py @@ -0,0 +1,24 @@ +from locust import HttpUser, task, between + +class ClubSecretary(HttpUser): + wait_time = between(1, 3) # Pause 1-3s entre actions + + @task + def visit_welcome(self): + self.client.get("/") # Page d'accueil + + @task + def book_page(self): + self.client.get("/book/Winter Gala/Simply Lift") # Page réservation + + @task + def points_page(self): + self.client.get("/points") # Tableau points + + @task + def purchase_places(self): + self.client.post("/purchasePlaces", { + "competition": "Winter Gala", + "club": "Simply Lift", + "places": "1" + }) \ No newline at end of file From 0ffee6d021a139c7f14be895ce09d599c16b2af3 Mon Sep 17 00:00:00 2001 From: CHAMPION Date: Fri, 28 Nov 2025 14:57:58 +0100 Subject: [PATCH 11/11] Update README.md --- .DS_Store | Bin 6148 -> 8196 bytes README.md | 123 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 96 insertions(+), 27 deletions(-) diff --git a/.DS_Store b/.DS_Store index 04a508e6f6048834346c5f1769770f2ff01806e6..ad8edd258b7da59ff5a018a0ac27e6a1f1a777ae 100644 GIT binary patch literal 8196 zcmeHMO>fgc5S{G@iK!|D1R@tCOI)ka0BS*8(vTjI;L=p(0La9NXnDIB0B5R62y$g z>)emEIWuz}S^!VfrWX1h1>_yGwiA{C%YbFTGGH074EzlY;F-;>c*1jEuUcstunhc{ z4Dk1Zi_W@c^h9g*)d8VO0O$owtA;V^0EKalt{FYiT0!AcR}aEKg()$FhGX4jbLg7U z6RkCzgocwak%g&Hgo+NFtISE%w6@YRU>P{h0N3tIsMbB|(aWj&-MbsfwjW6!7W|?5 zfd2NYY8Vlng>^&&@+qMT?L$h6bC?a-RWg`!z)0#MRzN9*ssr@{4;(e?;S$}0hc@CS zKzpG$yqt_?xqnbY?~+bjd8U|?Qj%3S|%p8Wp*6rL@_GGH0_ zR~Qfrjb@{cY!81xID#xK!ISzgFhatu;Oc_%%dZIOE(Cmu< MMS~TVf#1r&HyS)dVgLXD literal 6148 zcmeHLyGjE=6uo02t_eaKAz-uO517KrW(_MA7J@PZnPF^o|@7E?}H@}hH>zFF|IYk+&(>@&}^;MVR8B@>u%kBB|j?|;;!fwo; zo=xxx7mLyiv%#By`qZRWl9&8+hRJ`mar)?NOa7sCOdaF15_(t-pML;FbyJ6!KU;}s zO#X0l^^;%MnY`&)lTU29%r{F%c%p~6LIPYLe0*qf<4f<4&N6je-xM$B)O9gpJyX-< zOj94xGV7aGn$QpFT6jp#ntB%7gTb4+E{u9p$LNeCe~tP7YGBXiaPkUmZym4>SO>lw z;OB#j#^`B`6v{^jI{69!%wkvuj^!Q=ba?>iX^a$N1R+!@P?ZY(iXl`v#$BD~X^a%A zauSjm>u6@7-%x~P$G9ugNq7ovZym4>Bpt}8j|JZU*T=vACyVSS>wtCOUpXMMe#I~2 zl=R-3Iyv5JU9=h+2j@i!WeGaH9m@mWiur#98JKhV0O)Cq6k-Hne+Wn$Y-b(#Q3pN% DK3B}k diff --git a/README.md b/README.md index 61307d2cd..c9075ea33 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,120 @@ -# gudlift-registration +# GUDLFT - Réservation de Compétitions -1. Why +## 📖 Description +Application Flask pour les secrétaires de clubs permettant de réserver des places de compétitions en utilisant des points. - This is a proof of concept (POC) project to show a light-weight version of our competition booking platform. The aim is the keep things as light as possible, and use feedback from the users to iterate. +**Fonctionnalités principales :** +- Connexion sécurisée par email +- Réservation de places (max 12 par club) +- Déduction automatique des points +- Tableau public des points clubs -2. Getting Started +## Fonctionnalités Implémentées - This project uses the following technologies: +### Phase 1 - Authentification & Réservations +- [x] Connexion secrétaires via email/mot de passe +- [x] Liste compétitions à venir +- [x] Formulaire réservation `/book/mpétition>/` +- [x] Achat places `/purchasePlaces` +- [x] Déconnexion `/logout` - * Python v3.x+ +### Phase 2 - Transparence & Performance +- [x] Tableau points public `/points` (lecture seule) +- [x] Tests Locust : 6 utilisateurs simultanés + - GET pages : 5-6ms (< 5s ✅) + - POST achat : 14ms (< 2s ✅) +- [x] 100% Couverture pour le code server.py - * [Flask](https://flask.palletsprojects.com/en/1.1.x/) +### Contraintes Métier +- [x] Max 12 places par club/compétition +- [x] Nombre de points requis pour la réservation +- [x] Pas de réservation pour les compétitions passées - Whereas Django does a lot of things for us out of the box, Flask allows us to add only what we need. - +## 🛠️ Installation et démarrage - * [Virtual environment](https://virtualenv.pypa.io/en/stable/installation.html) +### Prérequis +- Python 3.8 ou plus récent +- `pip` installé - This ensures you'll be able to install the correct packages without interfering with Python on your machine. +### Installation des dépendances - Before you begin, please ensure you have this installed globally. +Dans votre terminal, positionnez-vous dans le dossier du projet puis exécutez : +pip install -r requirements.txt -3. Installation +Cette commande installe les bibliothèques nécessaires. - - After cloning, change into the directory and type virtualenv .. This will then set up a a virtual python environment within that directory. +### Lancement de l’application - - Next, type source bin/activate. You should see that your command prompt has changed to the name of the folder. This means that you can install packages in here without affecting affecting files outside. To deactivate, type deactivate +Pour démarrer l’application Flask localement, tapez : - - Rather than hunting around for the packages you need, you can install in one step. Type pip install -r requirements.txt. This will install all the packages listed in the respective file. If you install a package, make sure others know by updating the requirements.txt file. An easy way to do this is pip freeze > requirements.txt +flask --app server.py run -p 5000 - - Flask requires that you set an environmental variable to the python file. However you do that, you'll want to set the file to be server.py. Check [here](https://flask.palletsprojects.com/en/1.1.x/quickstart/#a-minimal-application) for more details - - You should now be ready to test the application. In the directory, type either flask run or python -m flask run. The app should respond with an address you should be able to go to using your browser. +L’application sera accessible ensuite à l’adresse : +`http://127.0.0.1:5000` -4. Current Setup +--- - The app is powered by [JSON files](https://www.tutorialspoint.com/json/json_quick_guide.htm). This is to get around having a DB until we actually need one. The main ones are: - - * competitions.json - list of competitions - * clubs.json - list of clubs with relevant information. You can look here to see what email addresses the app will accept for login. +## 🧪 Tests automatisés -5. Testing +### Lancement des tests unitaires et d’intégration - You are free to use whatever testing framework you like-the main thing is that you can show what tests you are using. +Les tests sont organisés dans le dossier `tests/`. Pour exécuter tous les tests, utilisez : - We also like to show how well we're testing, so there's a module called - [coverage](https://coverage.readthedocs.io/en/coverage-5.1/) you should add to your project. +coverage run -m pytest + +Cela lance tous les tests tout en mesurant la couverture du code. + +### Visualiser le rapport de couverture + +Pour obtenir un rapport détaillé de la couverture de code : + +coverage report -m + + +L’objectif est d’avoir un taux minimum de 60 % de couverture, mais ici la couverture est à 100 % sur `server.py`. + +--- + +## 🚀 Tests de performance avec Locust + +### Description + +Locust simule des utilisateurs réels pour tester la performance sous charge. Ici, 6 utilisateurs effectuent les actions de consultation et réservation. + +### Lancement des tests Locust + +Dans un nouveau terminal, lancez Locust avec : + +locust -f locustfile.py --host=http://localhost:5000 --users 6 --spawn-rate 1 --run-time 30s + + +Ensuite, ouvrez un navigateur à l’adresse : +`http://localhost:8089` + +Cliquez sur start avec 6 utilisateurs pour commencer les tests. + +### Résultat attendu + +Les temps de réponse doivent être : +- Inférieurs à 5 secondes pour le chargement des pages +- Inférieurs à 2 secondes pour les achats de places + +Notre rapport `Locust_Test_Report.html` contient les résultats détaillés. + +--- + +## Structure du projet + +python_testing/ +├── server.py # Application Flask principale +├── tests/ +│ ├── test_unit.py # Tests unitaires +│ ├── test_integration.py # Tests d’intégration +│ └── conftest.py # Configuration pytest +├── locustfile.py # Scénarios de tests de performance Locust +├── Locust_Test_Report.html # Rapport de test de performance généré par Locust +└── README.md # Ce fichier