From e443b17e57a9ea238117eed7c7cfe4d3491e5a70 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 09:59:44 +0200 Subject: [PATCH 01/20] Refactor server.py for improved readability and environment variable management; install pytest and python-dotenv; update .gitignore and requirements.txt --- .gitignore | 3 ++- requirements.txt | 2 ++ server.py | 67 ++++++++++++++++++++++++++++-------------------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 2cba99d87..2bca99245 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ lib .Python tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.env diff --git a/requirements.txt b/requirements.txt index 139affa05..48948c695 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 Werkzeug==1.0.1 +pytest==8.3.5 +python-dotenv==1.1.0 \ No newline at end of file diff --git a/server.py b/server.py index 4084baeac..e5e3b8ca3 100644 --- a/server.py +++ b/server.py @@ -1,59 +1,70 @@ import json -from flask import Flask,render_template,request,redirect,flash,url_for +import os + +from dotenv import load_dotenv +from flask import Flask, flash, redirect, render_template, request, url_for + +load_dotenv() # Load environment variables from .env file def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs + 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 + with open("competitions.json") as comps: + listOfCompetitions = json.load(comps)["competitions"] + return listOfCompetitions app = Flask(__name__) -app.secret_key = 'something_special' +app.secret_key = os.getenv("SECRET_KEY") competitions = loadCompetitions() clubs = loadClubs() -@app.route('/') + +@app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -@app.route('/showSummary',methods=['POST']) + +@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) + 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] +@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) + 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=club, competitions=competitions) -@app.route('/purchasePlaces',methods=['POST']) +@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) + 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') +@app.route("/logout") def logout(): - return redirect(url_for('index')) \ No newline at end of file + return redirect(url_for("index")) + + +if __name__ == "__main__": + app.run(debug=os.getenv("FLASK_DEBUG") == "1") From 45eb5bbb8319bd29f24cbc607b61f06cc591c3bc Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 13:00:25 +0200 Subject: [PATCH 02/20] Add test configuration and fixtures; update .gitignore to include .flake8 and retrieve tests/ --- .gitignore | 2 +- tests/__init__.py | 0 tests/conftest.py | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py diff --git a/.gitignore b/.gitignore index 2bca99245..f017d8e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ bin include lib .Python -tests/ .envrc __pycache__ .env +.flake8 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..714fa5abc --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from server import app as flask_app + + +@pytest.fixture +def app(): + flask_app.config.update({"TESTING": True}) + yield flask_app + + +@pytest.fixture +def client(app): + return app.test_client() From 740c0b8042bb98987794e4d9e3a5d07fed1e3a5f Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 13:27:09 +0200 Subject: [PATCH 03/20] Add test for handling without email in login --- tests/test_login.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/test_login.py diff --git a/tests/test_login.py b/tests/test_login.py new file mode 100644 index 000000000..19fdfeedb --- /dev/null +++ b/tests/test_login.py @@ -0,0 +1,4 @@ +def test_login_with_non_existing_email(client): + response = client.post("/showSummary", data={"email": ""}) + assert response.status_code == 200 + assert b"Please enter an email" in response.data From d60ef20f641a997e52ebbe814396992a007c4359 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 13:28:47 +0200 Subject: [PATCH 04/20] Add condition if email empty redirect index and flash message --- server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server.py b/server.py index e5e3b8ca3..202de991b 100644 --- a/server.py +++ b/server.py @@ -33,6 +33,11 @@ def index(): @app.route("/showSummary", methods=["POST"]) def showSummary(): + email = request.form["email"] + if email == "": + flash("Please enter an email") + return render_template("index.html") + club = [club for club in clubs if club["email"] == request.form["email"]][0] return render_template("welcome.html", club=club, competitions=competitions) From cc6c04bca0a0b19bbf856487ad1c550183d6bec7 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 13:29:47 +0200 Subject: [PATCH 05/20] display message flash in template index.html --- server.py | 2 +- templates/index.html | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 202de991b..9ade6be92 100644 --- a/server.py +++ b/server.py @@ -35,7 +35,7 @@ def index(): def showSummary(): email = request.form["email"] if email == "": - flash("Please enter an email") + flash("Please enter an email", "error") return render_template("index.html") club = [club for club in clubs if club["email"] == request.form["email"]][0] diff --git a/templates/index.html b/templates/index.html index 926526b7d..37a2460f8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,6 +5,15 @@ GUDLFT Registration + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %}

Welcome to the GUDLFT Registration Portal!

Please enter your secretary email to continue:
From 4d29e2df099ddbf09f71b6de96b506e49d06696c Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 16:09:54 +0200 Subject: [PATCH 06/20] Add test with non existent email in base and rename test for clarity --- tests/test_login.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_login.py b/tests/test_login.py index 19fdfeedb..74764ca8c 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,4 +1,10 @@ -def test_login_with_non_existing_email(client): +def test_login_without_email(client): response = client.post("/showSummary", data={"email": ""}) assert response.status_code == 200 assert b"Please enter an email" in response.data + + +def test_login_with_non_existent_email(client): + response = client.post("/showSummary", data={"email": "nonexistent@test.com"}) + assert response.status_code == 200 + assert b"Email not found" in response.data From e1c17a4f2ae517776eb15831a14c5dd2ef4bcd14 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 16:11:46 +0200 Subject: [PATCH 07/20] add try except to catch the IndexError exception and add flash message --- server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 9ade6be92..baa883abd 100644 --- a/server.py +++ b/server.py @@ -37,8 +37,11 @@ def showSummary(): if email == "": flash("Please enter an email", "error") return render_template("index.html") - - club = [club for club in clubs if club["email"] == request.form["email"]][0] + try: + club = [club for club in clubs if club["email"] == email][0] + except IndexError: + flash("Email not found", "error") + return render_template("index.html") return render_template("welcome.html", club=club, competitions=competitions) From 4b0de5fa7d92c00f61c45bbf623cbd94935f825b Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 18:50:49 +0200 Subject: [PATCH 08/20] Add test for loading valid JSON file return correct json --- tests/json_services/__init__.py | 0 tests/json_services/test_json_services.py | 11 +++++++++++ 2 files changed, 11 insertions(+) create mode 100644 tests/json_services/__init__.py create mode 100644 tests/json_services/test_json_services.py diff --git a/tests/json_services/__init__.py b/tests/json_services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/json_services/test_json_services.py b/tests/json_services/test_json_services.py new file mode 100644 index 000000000..13fc6fabb --- /dev/null +++ b/tests/json_services/test_json_services.py @@ -0,0 +1,11 @@ +import json + +from json_services import JSONServices + + +def test_load_valid_json_file(mocker): + mock_data = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test"}]} + + mocker.patch("builtins.open", mocker.mock_open(read_data=json.dumps(mock_data))) + result = JSONServices.load("clubs.json") + assert result == mock_data From 032f3a92e48e163f5f89b38094e7354d89f257ba Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 18:51:48 +0200 Subject: [PATCH 09/20] Add JSONServices class with static method to load JSON files --- json_services.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 json_services.py diff --git a/json_services.py b/json_services.py new file mode 100644 index 000000000..8871643e6 --- /dev/null +++ b/json_services.py @@ -0,0 +1,8 @@ +import json + + +class JSONServices: + @staticmethod + def load(filename: str): + with open(filename) as file: + return json.load(file) From 42039479ef8443b0981b9cc313f54b8fdb5de675 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 19:05:16 +0200 Subject: [PATCH 10/20] add test for non existent file return exception FileNotFoundError with message --- tests/json_services/test_json_services.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/json_services/test_json_services.py b/tests/json_services/test_json_services.py index 13fc6fabb..fb3dc7f80 100644 --- a/tests/json_services/test_json_services.py +++ b/tests/json_services/test_json_services.py @@ -1,5 +1,7 @@ import json +import pytest + from json_services import JSONServices @@ -9,3 +11,10 @@ def test_load_valid_json_file(mocker): mocker.patch("builtins.open", mocker.mock_open(read_data=json.dumps(mock_data))) result = JSONServices.load("clubs.json") assert result == mock_data + + +def test_load_non_existent_json_file(mocker): + mocker.patch("builtins.open", side_effect=FileNotFoundError) + with pytest.raises(FileNotFoundError) as exc_info: + JSONServices.load("non_existent_file.json") + assert str(exc_info.value) == "File non_existent_file.json not found." From 272325467f80a9ccd900461b5964987f1e74959a Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 19:05:37 +0200 Subject: [PATCH 11/20] Add error handling for FileNotFoundError in JSONServices.load method --- json_services.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/json_services.py b/json_services.py index 8871643e6..fff68be2d 100644 --- a/json_services.py +++ b/json_services.py @@ -4,5 +4,8 @@ class JSONServices: @staticmethod def load(filename: str): - with open(filename) as file: - return json.load(file) + try: + with open(filename) as file: + return json.load(file) + except FileNotFoundError: + raise FileNotFoundError(f"File {filename} not found.") From 6e55e5f542ea29a499a4c2b8fc6b0dd26b11b992 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 16 May 2025 20:25:14 +0200 Subject: [PATCH 12/20] Refacto code to use JsonService for load data clubs and competitions --- server.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/server.py b/server.py index baa883abd..2f362effe 100644 --- a/server.py +++ b/server.py @@ -1,30 +1,15 @@ -import json import os from dotenv import load_dotenv from flask import Flask, flash, redirect, render_template, request, url_for -load_dotenv() # Load environment variables from .env file - - -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 +from json_services import JSONServices +load_dotenv() # Load environment variables from .env file app = Flask(__name__) app.secret_key = os.getenv("SECRET_KEY") -competitions = loadCompetitions() -clubs = loadClubs() - @app.route("/") def index(): @@ -37,16 +22,26 @@ def showSummary(): if email == "": flash("Please enter an email", "error") return render_template("index.html") + try: + clubs = JSONServices.load("clubs.json")["clubs"] + competitions = JSONServices.load("competitions.json")["competitions"] club = [club for club in clubs if club["email"] == email][0] + except FileNotFoundError as ex: + print(ex) + flash("Error loading data. Please contact support.", "error") + return render_template("index.html") except IndexError: flash("Email not found", "error") return render_template("index.html") + return render_template("welcome.html", club=club, competitions=competitions) @app.route("/book//") def book(competition, club): + clubs = JSONServices.load("clubs.json")["clubs"] + competitions = JSONServices.load("competitions.json")["competitions"] 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: @@ -58,6 +53,8 @@ def book(competition, club): @app.route("/purchasePlaces", methods=["POST"]) def purchasePlaces(): + clubs = JSONServices.load("clubs.json")["clubs"] + competitions = JSONServices.load("competitions.json")["competitions"] 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"]) From 1528d110ca1ed70889178e14db1b4982acb8ae85 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 23 May 2025 10:33:57 +0200 Subject: [PATCH 13/20] Added test for club reservations exceeding their points limit --- tests/test_booking_place.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/test_booking_place.py diff --git a/tests/test_booking_place.py b/tests/test_booking_place.py new file mode 100644 index 000000000..65260c0df --- /dev/null +++ b/tests/test_booking_place.py @@ -0,0 +1,15 @@ +def test_club_cannot_book_more_than_points(client, mocker): + mock_clubs = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": "5"}]} + + mock_competitions = { + "competitions": [{"name": "Competition Test", "date": "2020-03-27 10:00:00", "numberOfPlaces": "10"}] + } + + mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions]) + + response = client.post( + "/purchasePlaces", data={"competition": "Competition Test", "club": "Club Test", "places": "6"} + ) + + assert response.status_code == 200 + assert b"Not enough points" in response.data From c8f4b6ceffb5b61e062613a1f56f3ad23d87d4a5 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 23 May 2025 10:36:58 +0200 Subject: [PATCH 14/20] Add a condition to check that the number of places requested does not exceed the club's number of points --- server.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server.py b/server.py index 2f362effe..206b505f1 100644 --- a/server.py +++ b/server.py @@ -58,6 +58,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"]) + + club_points = int(club["points"]) + + if placesRequired > club_points: + flash("Not enough points", "error") + return render_template("booking.html", club=club, competition=competition) + competition["numberOfPlaces"] = int(competition["numberOfPlaces"]) - placesRequired flash("Great-booking complete!") return render_template("welcome.html", club=club, competitions=competitions) From 9637d7ac278ea7c1a796da0852d88db7f372468e Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 23 May 2025 10:37:56 +0200 Subject: [PATCH 15/20] Display flash message in booking.html --- templates/booking.html | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..569ac22c4 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -6,12 +6,22 @@

{{competition['name']}}

- Places available: {{competition['numberOfPlaces']}} - - - - - - + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + Places available: {{competition['numberOfPlaces']}} +
+ + + + + +
+ {%endwith%} \ No newline at end of file From 2e37cb29a23d991e3974533d10182c19bff02504 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 23 May 2025 13:41:42 +0200 Subject: [PATCH 16/20] Add LIMITED_BOOKING_PLACE constant and add test for club cannot book more place than limit --- globals.py | 1 + tests/test_booking_place.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 globals.py diff --git a/globals.py b/globals.py new file mode 100644 index 000000000..38424ffb6 --- /dev/null +++ b/globals.py @@ -0,0 +1 @@ +LIMITED_BOOKING_PLACE = 12 diff --git a/tests/test_booking_place.py b/tests/test_booking_place.py index 65260c0df..378792c42 100644 --- a/tests/test_booking_place.py +++ b/tests/test_booking_place.py @@ -1,3 +1,6 @@ +from globals import LIMITED_BOOKING_PLACE + + def test_club_cannot_book_more_than_points(client, mocker): mock_clubs = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": "5"}]} @@ -13,3 +16,25 @@ def test_club_cannot_book_more_than_points(client, mocker): assert response.status_code == 200 assert b"Not enough points" in response.data + + +def test_club_cannot_book_more_than_limit(client, mocker): + places_requested = str(LIMITED_BOOKING_PLACE + 1) + club_points = str(LIMITED_BOOKING_PLACE + 5) + number_of_competition_places = str(LIMITED_BOOKING_PLACE + 5) + mock_clubs = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": club_points}]} + + mock_competitions = { + "competitions": [ + {"name": "Competition Test", "date": "2020-03-27 10:00:00", "numberOfPlaces": number_of_competition_places} + ] + } + + mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions]) + + response = client.post( + "/purchasePlaces", data={"competition": "Competition Test", "club": "Club Test", "places": places_requested} + ) + + assert response.status_code == 200 + assert f"Maximum booking limit is {LIMITED_BOOKING_PLACE} places".encode() in response.data From 5822bfcd047ca4335110f5f4d0c71dd9fc9794cd Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 23 May 2025 13:42:27 +0200 Subject: [PATCH 17/20] Add validation for booking limit in purchasePlaces function --- server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server.py b/server.py index 206b505f1..c708d275f 100644 --- a/server.py +++ b/server.py @@ -3,6 +3,7 @@ from dotenv import load_dotenv from flask import Flask, flash, redirect, render_template, request, url_for +from globals import LIMITED_BOOKING_PLACE from json_services import JSONServices load_dotenv() # Load environment variables from .env file @@ -65,6 +66,10 @@ def purchasePlaces(): flash("Not enough points", "error") return render_template("booking.html", club=club, competition=competition) + if placesRequired > LIMITED_BOOKING_PLACE: + flash(f"Maximum booking limit is {LIMITED_BOOKING_PLACE} places", "error") + return render_template("booking.html", club=club, competition=competition) + competition["numberOfPlaces"] = int(competition["numberOfPlaces"]) - placesRequired flash("Great-booking complete!") return render_template("welcome.html", club=club, competitions=competitions) From 69264b238c1b4c9fd75fe261934adbd3006b10c6 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 23 May 2025 13:47:28 +0200 Subject: [PATCH 18/20] Refactor test setup by creating helper functions for mock clubs and competitions --- tests/test_booking_place.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_booking_place.py b/tests/test_booking_place.py index 378792c42..730ce51cb 100644 --- a/tests/test_booking_place.py +++ b/tests/test_booking_place.py @@ -1,12 +1,17 @@ from globals import LIMITED_BOOKING_PLACE -def test_club_cannot_book_more_than_points(client, mocker): - mock_clubs = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": "5"}]} +def get_mock_clubs(points: str): + return {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": points}]} + + +def get_mock_competitions(places: str): + return {"competitions": [{"name": "Competition Test", "date": "2020-03-27 10:00:00", "numberOfPlaces": places}]} - mock_competitions = { - "competitions": [{"name": "Competition Test", "date": "2020-03-27 10:00:00", "numberOfPlaces": "10"}] - } + +def test_club_cannot_book_more_than_points(client, mocker): + mock_clubs = get_mock_clubs("5") + mock_competitions = get_mock_competitions("10") mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions]) @@ -22,13 +27,8 @@ def test_club_cannot_book_more_than_limit(client, mocker): places_requested = str(LIMITED_BOOKING_PLACE + 1) club_points = str(LIMITED_BOOKING_PLACE + 5) number_of_competition_places = str(LIMITED_BOOKING_PLACE + 5) - mock_clubs = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": club_points}]} - - mock_competitions = { - "competitions": [ - {"name": "Competition Test", "date": "2020-03-27 10:00:00", "numberOfPlaces": number_of_competition_places} - ] - } + mock_clubs = get_mock_clubs(club_points) + mock_competitions = get_mock_competitions(number_of_competition_places) mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions]) From c2e2d098e703f4f69de68e7190a7fd3ae23e4457 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 6 Jun 2025 12:28:48 +0200 Subject: [PATCH 19/20] Add save method and corresponding test for JSONServices class --- json_services.py | 5 +++++ tests/json_services/test_json_services.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/json_services.py b/json_services.py index fff68be2d..7f0e98460 100644 --- a/json_services.py +++ b/json_services.py @@ -9,3 +9,8 @@ def load(filename: str): return json.load(file) except FileNotFoundError: raise FileNotFoundError(f"File {filename} not found.") + + @staticmethod + def save(file_name, data): + with open(file_name, "w") as file: + json.dump(data, file, indent=4) diff --git a/tests/json_services/test_json_services.py b/tests/json_services/test_json_services.py index fb3dc7f80..09198beab 100644 --- a/tests/json_services/test_json_services.py +++ b/tests/json_services/test_json_services.py @@ -18,3 +18,15 @@ def test_load_non_existent_json_file(mocker): with pytest.raises(FileNotFoundError) as exc_info: JSONServices.load("non_existent_file.json") assert str(exc_info.value) == "File non_existent_file.json not found." + + +def test_save_json_file(mocker): + mock_data = {"clubs": [{"email": "clubtest@test.com", "name": "Club Test", "points": "10"}]} + + mock_open = mocker.patch("builtins.open", mocker.mock_open()) + mock_json_dump = mocker.patch("json.dump") + + JSONServices.save("clubs.json", mock_data) + + mock_open.assert_called_once_with("clubs.json", "w") + mock_json_dump.assert_called_once_with(mock_data, mock_open(), indent=4) From f1387910579f8c9f1c944e635ef895b601099e09 Mon Sep 17 00:00:00 2001 From: wilfried Date: Fri, 6 Jun 2025 12:29:24 +0200 Subject: [PATCH 20/20] Deduct competition places when a club books and save updated competition data --- server.py | 6 +++++- tests/test_booking_place.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index c708d275f..637c3aaf2 100644 --- a/server.py +++ b/server.py @@ -70,7 +70,11 @@ def purchasePlaces(): flash(f"Maximum booking limit is {LIMITED_BOOKING_PLACE} places", "error") return render_template("booking.html", club=club, competition=competition) - competition["numberOfPlaces"] = int(competition["numberOfPlaces"]) - placesRequired + number_of_places = int(competition["numberOfPlaces"]) - placesRequired + + competition["numberOfPlaces"] = str(number_of_places) + JSONServices.save("competitions.json", {"competitions": competitions}) + flash("Great-booking complete!") return render_template("welcome.html", club=club, competitions=competitions) diff --git a/tests/test_booking_place.py b/tests/test_booking_place.py index 730ce51cb..cf99ca99e 100644 --- a/tests/test_booking_place.py +++ b/tests/test_booking_place.py @@ -38,3 +38,26 @@ def test_club_cannot_book_more_than_limit(client, mocker): assert response.status_code == 200 assert f"Maximum booking limit is {LIMITED_BOOKING_PLACE} places".encode() in response.data + + +def test_competition_places_are_deducted_when_club_books(client, mocker): + mock_clubs = get_mock_clubs(points="15") + mock_competitions = get_mock_competitions(places="20") + places_requested = "5" + + mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions]) + mocker.patch("json_services.open", mocker.mock_open()) + + mock_json_dump = mocker.patch("json.dump") + + response = client.post( + "/purchasePlaces", data={"competition": "Competition Test", "club": "Club Test", "places": places_requested} + ) + + assert response.status_code == 200 + + called_data = mock_json_dump.call_args[0][0] + competitions = called_data["competitions"] + + updated = next(c for c in competitions if c["name"] == "Competition Test") + assert updated["numberOfPlaces"] == "15"