From 466fb7fb8cf648b0c623f27f887bf91bf2ac4ac8 Mon Sep 17 00:00:00 2001 From: NM Date: Mon, 9 Mar 2026 18:08:01 +0100 Subject: [PATCH 01/18] Fix issue #1 : unknown email crashes app, add message in index template --- .gitignore | 3 ++- server.py | 8 +++++++- templates/index.html | 11 +++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2cba99d87..6c234efec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ lib .Python tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.idea/ \ No newline at end of file diff --git a/server.py b/server.py index 4084baeac..83785a34f 100644 --- a/server.py +++ b/server.py @@ -26,7 +26,13 @@ def index(): @app.route('/showSummary',methods=['POST']) def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] + + club = next((club for club in clubs if club['email'] == request.form['email']), None) + + if club is None: + flash("Sorry, that email wasn't found.") + return render_template("index.html", error="Email not found"), 404 + return render_template('welcome.html',club=club,competitions=competitions) diff --git a/templates/index.html b/templates/index.html index 926526b7d..56b19b23c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,17 @@

Welcome to the GUDLFT Registration Portal!

+ + {% with messages = get_flashed_messages()%} + {% if messages %} + + {% endif%} + {%endwith%} + Please enter your secretary email to continue:
From 9e1c454e4673244335d6b6f160c88c835bb9dcc0 Mon Sep 17 00:00:00 2001 From: NM Date: Wed, 11 Mar 2026 11:21:08 +0100 Subject: [PATCH 02/18] File change : add unit tests --- .gitignore | 1 - clubs.json | 2 +- server.py | 2 +- unit_tests/__init__.py | 0 unit_tests/conftest.py | 56 ++++++++++++++ unit_tests/test_app.py | 169 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 unit_tests/__init__.py create mode 100644 unit_tests/conftest.py create mode 100644 unit_tests/test_app.py diff --git a/.gitignore b/.gitignore index 6c234efec..e7e28eb67 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ bin include lib .Python -tests/ .envrc __pycache__ .idea/ \ No newline at end of file diff --git a/clubs.json b/clubs.json index 1d7ad1ffe..1e7f3a13f 100644 --- a/clubs.json +++ b/clubs.json @@ -11,6 +11,6 @@ }, { "name":"She Lifts", "email": "kate@shelifts.co.uk", - "points":"12" + "points":"15" } ]} \ No newline at end of file diff --git a/server.py b/server.py index 83785a34f..0e088771e 100644 --- a/server.py +++ b/server.py @@ -30,7 +30,7 @@ def showSummary(): club = next((club for club in clubs if club['email'] == request.form['email']), None) if club is None: - flash("Sorry, that email wasn't found.") + flash("Sorry, that email was not found.") return render_template("index.html", error="Email not found"), 404 return render_template('welcome.html',club=club,competitions=competitions) diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py new file mode 100644 index 000000000..01f7a0abe --- /dev/null +++ b/unit_tests/conftest.py @@ -0,0 +1,56 @@ +import pytest + +from random import randint + +from server import app, clubs + +@pytest.fixture +def client(): + my_app = app + with my_app.test_client() as client: + yield client + +def get_existing_mail(): + data = {"email": "kate@shelifts.co.uk"} + return data + +def get_existing_mail_2(): + data = {"email": "admin@irontemple.com"} + return data + +def get_unexisting_mail(): + data = {"email": "nicolas.marie@unexisting.com"} + return data + +def get_existing_competition_and_club(): + data = {"competition": "Spring Festival", "club": "She Lifts"} + return data + +def get_existing_competition_and_club_2(): + data = {"competition": "Spring Festival", "club": "Iron Temple"} + return data + +def get_consistent_purchasing_data(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = randint(1, 12) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +def get_inconsistent_purchasing_data(): + competition = "Spring Festival" + club_name = "Iron Temple" + club_points = 4 + places_to_book = randint(club_points+1, 12) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +def get_inconsistent_purchasing_data_over_12_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 13 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py new file mode 100644 index 000000000..bd72dfd29 --- /dev/null +++ b/unit_tests/test_app.py @@ -0,0 +1,169 @@ +from bs4 import BeautifulSoup +from flask import url_for + +from unit_tests.conftest import (client, get_existing_mail, get_existing_mail_2, + get_unexisting_mail, get_existing_competition_and_club, + get_existing_competition_and_club_2, + get_consistent_purchasing_data, get_inconsistent_purchasing_data, + get_inconsistent_purchasing_data_over_12_places) + +from server import clubs, competitions + +def test_index_status_code_ok(client): + response = client.get('/') + assert response.status_code == 200 + +def test_index_return_welcome(client): + response = client.get('/') + data = response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert "Please enter your secretary email to continue:" in data + assert "Email:" in data + +def test_index_mail_authentication_ok(client): + mail_data = get_existing_mail() + response = client.post('/showSummary', data=mail_data) + assert response.status_code == 200 + +def test_index_mail_authentication_return_summary(client): + mail_data = get_existing_mail() + response = client.post('/showSummary', data=mail_data) + data = response.data.decode('utf-8') + + assert "Welcome, kate@shelifts.co.uk" in data + assert "Spring Festival" in data + assert "Fall Classic" in data + assert "Points available: 15" in data + +def test_index_mail_authentication_fail(client): + mail_data = get_unexisting_mail() + response = client.post('/showSummary', data=mail_data) + assert response.status_code == 404 + assert "Sorry, that email was not found." in response.data.decode('utf-8') + +def test_summary_logout_redirect_status_code_ok(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 + +def test_summary_logout_redirect_return_welcome(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + + logout_response = client.get('/logout') + soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") + url = soup.find_all('a')[0].get('href') + redirect_response = client.get(url, follow_redirects=True) + + assert redirect_response.status_code == 200 + data = redirect_response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert "Please enter your secretary email to continue:" in data + assert "Email:" in data + +def test_booking_status_code_ok(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + + competition_and_club_data = get_existing_competition_and_club() + response = client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + assert response.status_code == 200 + +def test_booking_return_festival_page_booking(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + + competition_and_club_data = get_existing_competition_and_club() + response = client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + data = response.data.decode('utf-8') + assert "Spring Festival" in data + assert "Places available: " in data + assert "How many places?" in data + +def test_good_purchasing_places_status_code_ok(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_consistent_purchasing_data() + + response = client.post('/purchasePlaces', data=purchasing_data) + + assert response.status_code == 200 + +def test_good_purchasing_places_return_summary_page(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + + purchasing_data = get_consistent_purchasing_data() + the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] + the_competition =[competition for competition in competitions + if competition["name"] == purchasing_data['competition']][0] + + client.get(url_for(endpoint='book', + competition=the_competition['name'], + club=the_club['name'])) + + club_points = the_club['points'] + competition_places = the_competition['numberOfPlaces'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + new_points = int(club_points) - int(purchasing_data['places']) + new_competition_places = int(competition_places) - int(purchasing_data['places']) + + soup = BeautifulSoup(data, features="html.parser") + all_li_str = [str(li) for li in soup.find_all('li')] + the_club_name_utf8 = "%20".join(the_club['name'].split()) + the_competition_name_utf8 = "%20".join(the_competition['name'].split()) + li = (f'
  • \n' + f' {the_competition["name"]}
    \n' + f' Date: 2020-03-27 10:00:00\n' + f' Number of Places: {new_competition_places}\n \n' + f' Book Places\n' + f'
  • ') + + assert "Great-booking complete!" in data + assert f"Welcome, {the_club["email"]} " in data + assert li in all_li_str + assert f"Points available: {new_points}" in data + +def test_purchasing_places_fail_with_not_enough_points(client): + mail_data = get_existing_mail_2() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club_2() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_inconsistent_purchasing_data() + + response = client.post('/purchasePlaces', data=purchasing_data) + + assert response.status_code == 403 + +def test_purchasing_places_fails_with_over_12_places(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_inconsistent_purchasing_data_over_12_places() + + response = client.post('/purchasePlaces', data=purchasing_data) + + assert response.status_code == 403 From e37cc7ebea17f2632667afbc06338682500d3b9f Mon Sep 17 00:00:00 2001 From: NM Date: Wed, 11 Mar 2026 14:26:25 +0100 Subject: [PATCH 03/18] Modification : fix issue #2 - club purchase places with not enough points, update unit tests --- server.py | 5 +++++ unit_tests/test_app.py | 33 ++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index 0e088771e..2b9b03431 100644 --- a/server.py +++ b/server.py @@ -52,6 +52,11 @@ 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']) + + if placesRequired > int(club['points']): + flash("Sorry, you do not have enough points to purchase.") + return render_template('welcome.html', club=club, competitions=competitions, error="Points not enough"), 403 + competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired flash('Great-booking complete!') return render_template('welcome.html', club=club, competitions=competitions) diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index bd72dfd29..3f806a399 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -26,7 +26,7 @@ def test_index_mail_authentication_ok(client): response = client.post('/showSummary', data=mail_data) assert response.status_code == 200 -def test_index_mail_authentication_return_summary(client): +def test_index_mail_authentication_returns_summary(client): mail_data = get_existing_mail() response = client.post('/showSummary', data=mail_data) data = response.data.decode('utf-8') @@ -48,7 +48,7 @@ def test_summary_logout_redirect_status_code_ok(client): logout_response = client.get('/logout') assert logout_response.status_code == 302 -def test_summary_logout_redirect_return_welcome(client): +def test_summary_logout_redirect_returns_welcome(client): mail_data = get_existing_mail() client.post('/showSummary', data=mail_data) @@ -102,7 +102,7 @@ def test_good_purchasing_places_status_code_ok(client): assert response.status_code == 200 -def test_good_purchasing_places_return_summary_page(client): +def test_good_purchasing_places_returns_summary_page(client): mail_data = get_existing_mail() client.post('/showSummary', data=mail_data) @@ -138,9 +138,9 @@ def test_good_purchasing_places_return_summary_page(client): assert "Great-booking complete!" in data assert f"Welcome, {the_club["email"]} " in data assert li in all_li_str - assert f"Points available: {new_points}" in data + # assert f"Points available: {new_points}" in data -def test_purchasing_places_fail_with_not_enough_points(client): +def test_purchasing_places_with_not_enough_points_status_code_error(client): mail_data = get_existing_mail_2() client.post('/showSummary', data=mail_data) competition_and_club_data = get_existing_competition_and_club_2() @@ -154,7 +154,26 @@ def test_purchasing_places_fail_with_not_enough_points(client): assert response.status_code == 403 -def test_purchasing_places_fails_with_over_12_places(client): +def test_purchasing_places_with_not_enough_points_returns_sorry(client): + mail_data = get_existing_mail_2() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club_2() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_inconsistent_purchasing_data() + + the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + assert "Sorry, you do not have enough points to purchase." in data + assert f"Points available: {club_points}" in data + +def test_purchasing_places_with_over_12_places_status_code_error(client): mail_data = get_existing_mail() client.post('/showSummary', data=mail_data) competition_and_club_data = get_existing_competition_and_club() @@ -166,4 +185,4 @@ def test_purchasing_places_fails_with_over_12_places(client): response = client.post('/purchasePlaces', data=purchasing_data) - assert response.status_code == 403 + assert response.status_code == 200 From e829ef6cc194975c27f88638e797d5d060fb712c Mon Sep 17 00:00:00 2001 From: NM Date: Wed, 11 Mar 2026 15:51:42 +0100 Subject: [PATCH 04/18] Modification : fix issue #4 - clubs should not be able to book more than 12 places per competition, update unit tests --- server.py | 6 +++++- unit_tests/test_app.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 2b9b03431..90223ae41 100644 --- a/server.py +++ b/server.py @@ -53,7 +53,11 @@ def purchasePlaces(): club = [c for c in clubs if c['name'] == request.form['club']][0] placesRequired = int(request.form['places']) - if placesRequired > int(club['points']): + if placesRequired > 12: + flash("Sorry, you are not allow to purchase more than 12 places.") + return render_template('welcome.html', club=club, competitions=competitions, error="Places max reached"), 403 + + elif placesRequired > int(club['points']): flash("Sorry, you do not have enough points to purchase.") return render_template('welcome.html', club=club, competitions=competitions, error="Points not enough"), 403 diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index 3f806a399..3ad33e6a3 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -185,4 +185,22 @@ def test_purchasing_places_with_over_12_places_status_code_error(client): response = client.post('/purchasePlaces', data=purchasing_data) - assert response.status_code == 200 + assert response.status_code == 403 + +def test_purchasing_places_with_over_12_places_returns_sorry(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_inconsistent_purchasing_data_over_12_places() + the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + assert "Sorry, you are not allow to purchase more than 12 places." in data + assert f"Points available: {club_points}" in data From 8c119e9aa372f9282a0bf7fc8377951ef43b98fb Mon Sep 17 00:00:00 2001 From: NM Date: Wed, 11 Mar 2026 16:18:44 +0100 Subject: [PATCH 05/18] Modification : add improvement by checking negative number of places is typed while booking --- server.py | 6 +++++- unit_tests/conftest.py | 8 ++++++++ unit_tests/test_app.py | 38 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/server.py b/server.py index 90223ae41..1c4f1f296 100644 --- a/server.py +++ b/server.py @@ -53,7 +53,11 @@ def purchasePlaces(): club = [c for c in clubs if c['name'] == request.form['club']][0] placesRequired = int(request.form['places']) - if placesRequired > 12: + if placesRequired < 0: + flash("Sorry, you should type a positive number.") + return render_template('welcome.html', club=club, competitions=competitions, error="Negative number"), 403 + + elif placesRequired > 12: flash("Sorry, you are not allow to purchase more than 12 places.") return render_template('welcome.html', club=club, competitions=competitions, error="Places max reached"), 403 diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index 01f7a0abe..ac1015e6d 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -54,3 +54,11 @@ def get_inconsistent_purchasing_data_over_12_places(): data = {"competition": competition, "club": club_name, "places": str(places_to_book)} return data + +def get_inconsistent_purchasing_data_with_negative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = -2 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index 3ad33e6a3..b627d8fcd 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -5,7 +5,8 @@ get_unexisting_mail, get_existing_competition_and_club, get_existing_competition_and_club_2, get_consistent_purchasing_data, get_inconsistent_purchasing_data, - get_inconsistent_purchasing_data_over_12_places) + get_inconsistent_purchasing_data_over_12_places, + get_inconsistent_purchasing_data_with_negative_places) from server import clubs, competitions @@ -132,7 +133,8 @@ def test_good_purchasing_places_returns_summary_page(client): f' {the_competition["name"]}
    \n' f' Date: 2020-03-27 10:00:00\n' f' Number of Places: {new_competition_places}\n \n' - f' Book Places\n' + f' Book Places\n' f'') assert "Great-booking complete!" in data @@ -204,3 +206,35 @@ def test_purchasing_places_with_over_12_places_returns_sorry(client): assert "Sorry, you are not allow to purchase more than 12 places." in data assert f"Points available: {club_points}" in data + +def test_purchasing_places_with_negative_number_status_code_error(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_inconsistent_purchasing_data_with_negative_places() + + response = client.post('/purchasePlaces', data=purchasing_data) + + assert response.status_code == 403 + +def test_purchasing_places_with_negative_number_returns_sorry(client): + mail_data = get_existing_mail() + client.post('/showSummary', data=mail_data) + competition_and_club_data = get_existing_competition_and_club() + client.get(url_for(endpoint='book', + competition=competition_and_club_data['competition'], + club=competition_and_club_data['club'])) + + purchasing_data = get_inconsistent_purchasing_data_with_negative_places() + the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + assert "Sorry, you should type a positive number." in data + assert f"Points available: {club_points}" in data From 31f18631222969e0d7c69de8f7b268ca9bd0304d Mon Sep 17 00:00:00 2001 From: NM Date: Thu, 12 Mar 2026 10:04:42 +0100 Subject: [PATCH 06/18] Modification : fix issue #6 - points update not reflected, update unit tests with mocks --- clubs.json | 35 +++--- competitions.json | 10 +- server.py | 70 +++++++++-- unit_tests/conftest.py | 49 +++++++- unit_tests/test_app.py | 255 ++++++++++++++++++++++++----------------- 5 files changed, 280 insertions(+), 139 deletions(-) diff --git a/clubs.json b/clubs.json index 1e7f3a13f..05d4126c7 100644 --- a/clubs.json +++ b/clubs.json @@ -1,16 +1,19 @@ -{"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":"15" - } -]} \ No newline at end of file +{ + "clubs": [ + { + "name": "Simply Lift", + "email": "john@simplylift.co", + "points": "13" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "points": "12" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "points": "4" + } + ] +} \ No newline at end of file diff --git a/competitions.json b/competitions.json index 039fc61bd..7c11ce528 100644 --- a/competitions.json +++ b/competitions.json @@ -1,14 +1,14 @@ { "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" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 1c4f1f296..9a36e20e9 100644 --- a/server.py +++ b/server.py @@ -13,13 +13,38 @@ def loadCompetitions(): listOfCompetitions = json.load(comps)['competitions'] return listOfCompetitions - app = Flask(__name__) app.secret_key = 'something_special' competitions = loadCompetitions() clubs = loadClubs() +def update_club_booked_places(club, places, competition_name): + clubs.remove(club) + + club["points"] = str(int(club["points"]) - places) + + clubs.append(club) + save_clubs() + +def save_clubs(): + with open('clubs.json', 'w') as c: + listOfClubs = {"clubs": clubs} + json.dump(listOfClubs, c, indent=4) + +def update_competition_available_places(competition, places): + competitions.remove(competition) + + competition['numberOfPlaces'] = str(int(competition['numberOfPlaces']) - places) + + competitions.append(competition) + save_competitions() + +def save_competitions(): + with open('competitions.json', 'w') as comps: + listOfCompetitions = {"competitions": competitions} + json.dump(listOfCompetitions, comps, indent=4) + @app.route('/') def index(): return render_template('index.html') @@ -31,9 +56,11 @@ def showSummary(): if club is None: flash("Sorry, that email was not found.") - return render_template("index.html", error="Email not found"), 404 + return render_template(template_name_or_list="index.html", error="Email not found"), 404 - return render_template('welcome.html',club=club,competitions=competitions) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) @app.route('/book//') @@ -41,10 +68,14 @@ 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(template_name_or_list='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(template_name_or_list='welcome.html', + club=club, + competitions=competitions) @app.route('/purchasePlaces',methods=['POST']) @@ -52,22 +83,39 @@ 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']) + print(placesRequired) if placesRequired < 0: flash("Sorry, you should type a positive number.") - return render_template('welcome.html', club=club, competitions=competitions, error="Negative number"), 403 + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error="Negative number"), 403 elif placesRequired > 12: - flash("Sorry, you are not allow to purchase more than 12 places.") - return render_template('welcome.html', club=club, competitions=competitions, error="Places max reached"), 403 + flash("Sorry, you are not allow to purchase more than 12 places for this competition.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error="Places max reached"), 403 elif placesRequired > int(club['points']): flash("Sorry, you do not have enough points to purchase.") - return render_template('welcome.html', club=club, competitions=competitions, error="Points not enough"), 403 + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error="Points not enough"), 403 + + update_club_booked_places(club=club, + places=placesRequired, + competition_name=competition["name"]) + + update_competition_available_places(competition=competition, places=placesRequired) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) # TODO: Add route for points display diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index ac1015e6d..7e30da2a2 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -10,26 +10,68 @@ def client(): with my_app.test_client() as client: yield client +@pytest.fixture +def get_clubs(): + the_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" + } + ] + return the_clubs + +@pytest.fixture +def get_competitions(): + the_competitions = [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "numberOfPlaces": "13" + }, + { + "name": "Spring Festival", + "date": "2020-03-27 10:00:00", + "numberOfPlaces": "25" + } + ] + return the_competitions + +@pytest.fixture def get_existing_mail(): data = {"email": "kate@shelifts.co.uk"} return data +@pytest.fixture def get_existing_mail_2(): data = {"email": "admin@irontemple.com"} return data +@pytest.fixture def get_unexisting_mail(): data = {"email": "nicolas.marie@unexisting.com"} return data +@pytest.fixture def get_existing_competition_and_club(): data = {"competition": "Spring Festival", "club": "She Lifts"} return data +@pytest.fixture def get_existing_competition_and_club_2(): data = {"competition": "Spring Festival", "club": "Iron Temple"} return data +@pytest.fixture def get_consistent_purchasing_data(): competition = "Spring Festival" club_name = "She Lifts" @@ -38,6 +80,7 @@ def get_consistent_purchasing_data(): return data +@pytest.fixture def get_inconsistent_purchasing_data(): competition = "Spring Festival" club_name = "Iron Temple" @@ -47,7 +90,8 @@ def get_inconsistent_purchasing_data(): return data -def get_inconsistent_purchasing_data_over_12_places(): +@pytest.fixture +def purchasing_data_over_12_places(): competition = "Spring Festival" club_name = "She Lifts" places_to_book = 13 @@ -55,7 +99,8 @@ def get_inconsistent_purchasing_data_over_12_places(): return data -def get_inconsistent_purchasing_data_with_negative_places(): +@pytest.fixture +def purchasing_data_with_negative_places(): competition = "Spring Festival" club_name = "She Lifts" places_to_book = -2 diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index b627d8fcd..16bdf86ef 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -1,14 +1,7 @@ from bs4 import BeautifulSoup from flask import url_for -from unit_tests.conftest import (client, get_existing_mail, get_existing_mail_2, - get_unexisting_mail, get_existing_competition_and_club, - get_existing_competition_and_club_2, - get_consistent_purchasing_data, get_inconsistent_purchasing_data, - get_inconsistent_purchasing_data_over_12_places, - get_inconsistent_purchasing_data_with_negative_places) - -from server import clubs, competitions +import server def test_index_status_code_ok(client): response = client.get('/') @@ -22,36 +15,36 @@ def test_index_return_welcome(client): assert "Please enter your secretary email to continue:" in data assert "Email:" in data -def test_index_mail_authentication_ok(client): - mail_data = get_existing_mail() - response = client.post('/showSummary', data=mail_data) +def test_index_mail_authentication_ok(get_existing_mail, get_clubs, mocker, client): + mocker.patch('server.clubs', get_clubs) + response = client.post('/showSummary', data=get_existing_mail) assert response.status_code == 200 -def test_index_mail_authentication_returns_summary(client): - mail_data = get_existing_mail() - response = client.post('/showSummary', data=mail_data) +def test_index_mail_authentication_returns_summary(mocker, client, get_existing_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + response = client.post('/showSummary', data=get_existing_mail) data = response.data.decode('utf-8') assert "Welcome, kate@shelifts.co.uk" in data assert "Spring Festival" in data assert "Fall Classic" in data - assert "Points available: 15" in data + assert "Points available: 12" in data -def test_index_mail_authentication_fail(client): - mail_data = get_unexisting_mail() - response = client.post('/showSummary', data=mail_data) +def test_index_mail_authentication_fail(mocker, client, get_unexisting_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + response = client.post('/showSummary', data=get_unexisting_mail) assert response.status_code == 404 assert "Sorry, that email was not found." in response.data.decode('utf-8') -def test_summary_logout_redirect_status_code_ok(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) +def test_summary_logout_redirect_status_code_ok(mocker, client, get_existing_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) logout_response = client.get('/logout') assert logout_response.status_code == 302 -def test_summary_logout_redirect_returns_welcome(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) +def test_summary_logout_redirect_returns_welcome(mocker, client, get_existing_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) logout_response = client.get('/logout') soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") @@ -65,51 +58,71 @@ def test_summary_logout_redirect_returns_welcome(client): assert "Please enter your secretary email to continue:" in data assert "Email:" in data -def test_booking_status_code_ok(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) +def test_booking_status_code_ok(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) - competition_and_club_data = get_existing_competition_and_club() response = client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) assert response.status_code == 200 -def test_booking_return_festival_page_booking(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) +def test_booking_return_festival_page_booking(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) - competition_and_club_data = get_existing_competition_and_club() response = client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) data = response.data.decode('utf-8') assert "Spring Festival" in data assert "Places available: " in data assert "How many places?" in data -def test_good_purchasing_places_status_code_ok(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club() +def test_good_purchasing_places_status_code_ok(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_consistent_purchasing_data, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - purchasing_data = get_consistent_purchasing_data() + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') - response = client.post('/purchasePlaces', data=purchasing_data) + response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) assert response.status_code == 200 -def test_good_purchasing_places_returns_summary_page(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) +def test_good_purchasing_places_returns_summary_page(mocker, client, get_existing_mail, get_clubs, + get_consistent_purchasing_data, + get_competitions): + + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) - purchasing_data = get_consistent_purchasing_data() - the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] - the_competition =[competition for competition in competitions + client.post('/showSummary', data=get_existing_mail) + + purchasing_data = get_consistent_purchasing_data + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + the_competition =[competition for competition in server.competitions if competition["name"] == purchasing_data['competition']][0] client.get(url_for(endpoint='book', @@ -119,6 +132,9 @@ def test_good_purchasing_places_returns_summary_page(client): club_points = the_club['points'] competition_places = the_competition['numberOfPlaces'] + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + response = client.post('/purchasePlaces', data=purchasing_data) data = response.data.decode('utf-8') @@ -140,97 +156,126 @@ def test_good_purchasing_places_returns_summary_page(client): assert "Great-booking complete!" in data assert f"Welcome, {the_club["email"]} " in data assert li in all_li_str - # assert f"Points available: {new_points}" in data + assert f"Points available: {new_points}" in data -def test_purchasing_places_with_not_enough_points_status_code_error(client): - mail_data = get_existing_mail_2() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club_2() - client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) +def test_purchasing_places_not_enough_points_status_code_error(mocker, + client, + get_existing_mail_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data, + get_clubs): - purchasing_data = get_inconsistent_purchasing_data() + mocker.patch('server.clubs', get_clubs) - response = client.post('/purchasePlaces', data=purchasing_data) + client.post('/showSummary', data=get_existing_mail_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) + + response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) assert response.status_code == 403 -def test_purchasing_places_with_not_enough_points_returns_sorry(client): - mail_data = get_existing_mail_2() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club_2() - client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) +def test_purchasing_places_not_enough_points_returns_sorry(mocker, + client, + get_existing_mail_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data, + get_clubs): + mocker.patch('server.clubs', get_clubs) - purchasing_data = get_inconsistent_purchasing_data() + client.post('/showSummary', data=get_existing_mail_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) - the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] - club_points = the_club['points'] + the_club = [club for club in server.clubs + if club["name"] == get_existing_competition_and_club_2['club']][0] - response = client.post('/purchasePlaces', data=purchasing_data) + response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) data = response.data.decode('utf-8') assert "Sorry, you do not have enough points to purchase." in data - assert f"Points available: {club_points}" in data + assert f"Points available: {the_club['points']}" in data -def test_purchasing_places_with_over_12_places_status_code_error(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club() - client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) +def test_purchasing_places_over_12_places_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_data_over_12_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) - purchasing_data = get_inconsistent_purchasing_data_over_12_places() + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - response = client.post('/purchasePlaces', data=purchasing_data) + response = client.post('/purchasePlaces', data=purchasing_data_over_12_places) assert response.status_code == 403 -def test_purchasing_places_with_over_12_places_returns_sorry(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club() +def test_purchasing_places_over_12_places_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_data_over_12_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - purchasing_data = get_inconsistent_purchasing_data_over_12_places() - the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] + purchasing_data = purchasing_data_over_12_places + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] response = client.post('/purchasePlaces', data=purchasing_data) data = response.data.decode('utf-8') - assert "Sorry, you are not allow to purchase more than 12 places." in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data -def test_purchasing_places_with_negative_number_status_code_error(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club() +def test_purchasing_places_negative_number_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_clubs, + purchasing_data_with_negative_places): + + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - purchasing_data = get_inconsistent_purchasing_data_with_negative_places() + purchasing_data = purchasing_data_with_negative_places response = client.post('/purchasePlaces', data=purchasing_data) assert response.status_code == 403 -def test_purchasing_places_with_negative_number_returns_sorry(client): - mail_data = get_existing_mail() - client.post('/showSummary', data=mail_data) - competition_and_club_data = get_existing_competition_and_club() +def test_purchasing_places_negative_number_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_data_with_negative_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', - competition=competition_and_club_data['competition'], - club=competition_and_club_data['club'])) + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - purchasing_data = get_inconsistent_purchasing_data_with_negative_places() - the_club = [club for club in clubs if club["name"] == purchasing_data['club']][0] + purchasing_data = purchasing_data_with_negative_places + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] response = client.post('/purchasePlaces', data=purchasing_data) From 50a648c6047850d5305237ea8fbdfed4afe7e744 Mon Sep 17 00:00:00 2001 From: NM Date: Thu, 12 Mar 2026 10:49:48 +0100 Subject: [PATCH 07/18] Modification : refactor code according to snake case --- competitions.json | 4 +-- server.py | 60 +++++++++++++++++++++--------------------- templates/booking.html | 4 +-- templates/index.html | 2 +- templates/welcome.html | 4 +-- unit_tests/conftest.py | 6 ++--- unit_tests/test_app.py | 2 +- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/competitions.json b/competitions.json index 7c11ce528..d8ccf9715 100644 --- a/competitions.json +++ b/competitions.json @@ -3,12 +3,12 @@ { "name": "Fall Classic", "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13" + "number_of_places": "13" }, { "name": "Spring Festival", "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25" + "number_of_places": "25" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 9a36e20e9..7df9cb32f 100644 --- a/server.py +++ b/server.py @@ -1,23 +1,23 @@ import json -from flask import Flask,render_template,request,redirect,flash,url_for +from flask import Flask, render_template, request, redirect, flash, url_for -def loadClubs(): +def load_clubs(): with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs + list_of_clubs = json.load(c)['clubs'] + return list_of_clubs -def loadCompetitions(): +def load_competitions(): with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions + list_of_competitions = json.load(comps)['competitions'] + return list_of_competitions app = Flask(__name__) app.secret_key = 'something_special' -competitions = loadCompetitions() -clubs = loadClubs() +competitions = load_competitions() +clubs = load_clubs() def update_club_booked_places(club, places, competition_name): clubs.remove(club) @@ -29,28 +29,28 @@ def update_club_booked_places(club, places, competition_name): def save_clubs(): with open('clubs.json', 'w') as c: - listOfClubs = {"clubs": clubs} - json.dump(listOfClubs, c, indent=4) + list_of_clubs = {"clubs": clubs} + json.dump(list_of_clubs, c, indent=4) def update_competition_available_places(competition, places): competitions.remove(competition) - competition['numberOfPlaces'] = str(int(competition['numberOfPlaces']) - places) + competition['number_of_places'] = str(int(competition['number_of_places']) - places) competitions.append(competition) save_competitions() def save_competitions(): with open('competitions.json', 'w') as comps: - listOfCompetitions = {"competitions": competitions} - json.dump(listOfCompetitions, comps, indent=4) + list_of_competitions = {"competitions": competitions} + json.dump(list_of_competitions, comps, indent=4) @app.route('/') def index(): return render_template('index.html') -@app.route('/showSummary',methods=['POST']) -def showSummary(): +@app.route('/showSummary', methods=['POST']) +def show_summary(): club = next((club for club in clubs if club['email'] == request.form['email']), None) @@ -64,13 +64,14 @@ def showSummary(): @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: +def book(competition, club): + found_club = [c for c in clubs if c['name'] == club][0] + print(f"COMPETITIONS : {competitions}") + found_competition = [c for c in competitions if c['name'] == competition][0] + if found_club and found_competition: return render_template(template_name_or_list='booking.html', - club=foundClub, - competition=foundCompetition) + club=found_club, + competition=found_competition) else: flash("Something went wrong-please try again") return render_template(template_name_or_list='welcome.html', @@ -79,27 +80,26 @@ def book(competition,club): @app.route('/purchasePlaces',methods=['POST']) -def purchasePlaces(): +def purchase_places(): 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']) - print(placesRequired) + places_required = int(request.form['places']) - if placesRequired < 0: + if places_required < 0: flash("Sorry, you should type a positive number.") return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions, error="Negative number"), 403 - elif placesRequired > 12: + elif places_required > 12: flash("Sorry, you are not allow to purchase more than 12 places for this competition.") return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions, error="Places max reached"), 403 - elif placesRequired > int(club['points']): + elif places_required > int(club['points']): flash("Sorry, you do not have enough points to purchase.") return render_template(template_name_or_list='welcome.html', club=club, @@ -107,10 +107,10 @@ def purchasePlaces(): error="Points not enough"), 403 update_club_booked_places(club=club, - places=placesRequired, + places=places_required, competition_name=competition["name"]) - update_competition_available_places(competition=competition, places=placesRequired) + update_competition_available_places(competition=competition, places=places_required) flash('Great-booking complete!') return render_template(template_name_or_list='welcome.html', diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..7bd33142c 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -6,8 +6,8 @@

    {{competition['name']}}

    - Places available: {{competition['numberOfPlaces']}} - + Places available: {{competition['number_of_places']}} + diff --git a/templates/index.html b/templates/index.html index 56b19b23c..8dc504edb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,7 +18,7 @@

    Welcome to the GUDLFT Registration Portal!

    {%endwith%} Please enter your secretary email to continue: - + diff --git a/templates/welcome.html b/templates/welcome.html index ff6b261a2..e790e2284 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -22,8 +22,8 @@

    Competitions:

  • {{comp['name']}}
    Date: {{comp['date']}}
    - Number of Places: {{comp['numberOfPlaces']}} - {%if comp['numberOfPlaces']|int >0%} + Number of Places: {{comp['number_of_places']}} + {%if comp['number_of_places']|int >0%} Book Places {%endif%}
  • diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index 7e30da2a2..970165106 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -2,7 +2,7 @@ from random import randint -from server import app, clubs +from server import app @pytest.fixture def client(): @@ -36,12 +36,12 @@ def get_competitions(): { "name": "Fall Classic", "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13" + "number_of_places": "13" }, { "name": "Spring Festival", "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25" + "number_of_places": "25" } ] return the_competitions diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index 16bdf86ef..c825df547 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -130,7 +130,7 @@ def test_good_purchasing_places_returns_summary_page(mocker, client, get_existin club=the_club['name'])) club_points = the_club['points'] - competition_places = the_competition['numberOfPlaces'] + competition_places = the_competition['number_of_places'] mocker.patch('server.save_clubs') mocker.patch('server.save_competitions') From 8ab253f78611a00ec786f664515e478a31971b98 Mon Sep 17 00:00:00 2001 From: NM Date: Fri, 13 Mar 2026 14:18:29 +0100 Subject: [PATCH 08/18] Modification : fix issue #4 - club should not be able to book more than 12 places per competition, update tests --- clubs.json | 10 +++++----- server.py | 10 ++++++++-- templates/booking.html | 2 +- templates/index.html | 2 +- unit_tests/conftest.py | 20 ++++++++++++++++---- unit_tests/test_app.py | 40 ++++++++++++++++++++++++++++++++-------- 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/clubs.json b/clubs.json index 05d4126c7..716c50141 100644 --- a/clubs.json +++ b/clubs.json @@ -1,10 +1,5 @@ { "clubs": [ - { - "name": "Simply Lift", - "email": "john@simplylift.co", - "points": "13" - }, { "name": "She Lifts", "email": "kate@shelifts.co.uk", @@ -14,6 +9,11 @@ "name": "Iron Temple", "email": "admin@irontemple.com", "points": "4" + }, + { + "name": "Simply Lift", + "email": "john@simplylift.co", + "points": "13" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 7df9cb32f..3de0121cc 100644 --- a/server.py +++ b/server.py @@ -22,6 +22,10 @@ def load_competitions(): def update_club_booked_places(club, places, competition_name): clubs.remove(club) + club.setdefault("booked_places", {}) + current = int(club["booked_places"].get(competition_name, 0)) + club["booked_places"][competition_name] = str(current + places) + club["points"] = str(int(club["points"]) - places) clubs.append(club) @@ -66,7 +70,6 @@ def show_summary(): @app.route('/book//') def book(competition, club): found_club = [c for c in clubs if c['name'] == club][0] - print(f"COMPETITIONS : {competitions}") found_competition = [c for c in competitions if c['name'] == competition][0] if found_club and found_competition: return render_template(template_name_or_list='booking.html', @@ -85,6 +88,9 @@ def purchase_places(): club = [c for c in clubs if c['name'] == request.form['club']][0] places_required = int(request.form['places']) + cumulative_places = places_required + int(club["booked_places"][competition["name"]]) \ + if "booked_places" in club else places_required + if places_required < 0: flash("Sorry, you should type a positive number.") return render_template(template_name_or_list='welcome.html', @@ -92,7 +98,7 @@ def purchase_places(): competitions=competitions, error="Negative number"), 403 - elif places_required > 12: + elif cumulative_places > 12: flash("Sorry, you are not allow to purchase more than 12 places for this competition.") return render_template(template_name_or_list='welcome.html', club=club, diff --git a/templates/booking.html b/templates/booking.html index 7bd33142c..a9b12c600 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -7,7 +7,7 @@

    {{competition['name']}}

    Places available: {{competition['number_of_places']}} - + diff --git a/templates/index.html b/templates/index.html index 8dc504edb..56b19b23c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,7 +18,7 @@

    Welcome to the GUDLFT Registration Portal!

    {%endwith%} Please enter your secretary email to continue: - + diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index 970165106..41ef39e52 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -25,7 +25,10 @@ def get_clubs(): }, { "name":"She Lifts", "email": "kate@shelifts.co.uk", - "points":"12" + "points":"12", + "booked_places": { + "Spring Festival": "7" + } } ] return the_clubs @@ -75,7 +78,7 @@ def get_existing_competition_and_club_2(): def get_consistent_purchasing_data(): competition = "Spring Festival" club_name = "She Lifts" - places_to_book = randint(1, 12) + places_to_book = randint(1, 5) data = {"competition": competition, "club": club_name, "places": str(places_to_book)} return data @@ -91,7 +94,7 @@ def get_inconsistent_purchasing_data(): return data @pytest.fixture -def purchasing_data_over_12_places(): +def purchasing_over_12_places(): competition = "Spring Festival" club_name = "She Lifts" places_to_book = 13 @@ -100,7 +103,16 @@ def purchasing_data_over_12_places(): return data @pytest.fixture -def purchasing_data_with_negative_places(): +def purchasing_13_cumulative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 6 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_with_negative_places(): competition = "Spring Festival" club_name = "She Lifts" places_to_book = -2 diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index c825df547..fe522af8d 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -202,7 +202,7 @@ def test_purchasing_places_over_12_places_status_code_error(mocker, client, get_existing_mail, get_existing_competition_and_club, - purchasing_data_over_12_places, + purchasing_over_12_places, get_clubs): mocker.patch('server.clubs', get_clubs) @@ -211,7 +211,7 @@ def test_purchasing_places_over_12_places_status_code_error(mocker, competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - response = client.post('/purchasePlaces', data=purchasing_data_over_12_places) + response = client.post('/purchasePlaces', data=purchasing_over_12_places) assert response.status_code == 403 @@ -219,7 +219,7 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, client, get_existing_mail, get_existing_competition_and_club, - purchasing_data_over_12_places, + purchasing_over_12_places, get_clubs): mocker.patch('server.clubs', get_clubs) @@ -229,7 +229,31 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - purchasing_data = purchasing_data_over_12_places + purchasing_data = purchasing_over_12_places + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + +def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_13_cumulative_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + purchasing_data = purchasing_13_cumulative_places the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] @@ -244,7 +268,7 @@ def test_purchasing_places_negative_number_status_code_error(mocker, get_existing_mail, get_existing_competition_and_club, get_clubs, - purchasing_data_with_negative_places): + purchasing_with_negative_places): mocker.patch('server.clubs', get_clubs) @@ -254,7 +278,7 @@ def test_purchasing_places_negative_number_status_code_error(mocker, competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - purchasing_data = purchasing_data_with_negative_places + purchasing_data = purchasing_with_negative_places response = client.post('/purchasePlaces', data=purchasing_data) @@ -264,7 +288,7 @@ def test_purchasing_places_negative_number_returns_sorry(mocker, client, get_existing_mail, get_existing_competition_and_club, - purchasing_data_with_negative_places, + purchasing_with_negative_places, get_clubs): mocker.patch('server.clubs', get_clubs) @@ -274,7 +298,7 @@ def test_purchasing_places_negative_number_returns_sorry(mocker, competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - purchasing_data = purchasing_data_with_negative_places + purchasing_data = purchasing_with_negative_places the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] From a77fa8179e30d98bce325a6b58ffb21203da52e0 Mon Sep 17 00:00:00 2001 From: NM Date: Fri, 13 Mar 2026 16:08:14 +0100 Subject: [PATCH 09/18] Modification : fix issue #5 - booking places in past competitions should not be possible, add two more unit tests for that bug fix --- clubs.json | 10 +-- competitions.json | 5 ++ server.py | 18 ++++- unit_tests/conftest.py | 7 +- unit_tests/test_app.py | 149 +++++++++++++++++++++++++++++------------ 5 files changed, 137 insertions(+), 52 deletions(-) diff --git a/clubs.json b/clubs.json index 716c50141..661547fc7 100644 --- a/clubs.json +++ b/clubs.json @@ -5,15 +5,15 @@ "email": "kate@shelifts.co.uk", "points": "12" }, - { - "name": "Iron Temple", - "email": "admin@irontemple.com", - "points": "4" - }, { "name": "Simply Lift", "email": "john@simplylift.co", "points": "13" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "points": "4" } ] } \ No newline at end of file diff --git a/competitions.json b/competitions.json index d8ccf9715..26cea4132 100644 --- a/competitions.json +++ b/competitions.json @@ -9,6 +9,11 @@ "name": "Spring Festival", "date": "2020-03-27 10:00:00", "number_of_places": "25" + }, + { + "name": "Winter Power", + "date": "2026-06-14 10:12:26", + "number_of_places": "15" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 3de0121cc..7624a541c 100644 --- a/server.py +++ b/server.py @@ -1,3 +1,4 @@ +from datetime import datetime import json from flask import Flask, render_template, request, redirect, flash, url_for @@ -71,7 +72,20 @@ def show_summary(): def book(competition, club): found_club = [c for c in clubs if c['name'] == club][0] found_competition = [c for c in competitions if c['name'] == competition][0] - if found_club and found_competition: + + now = datetime.now() + + competition_date = datetime.strptime(found_competition['date'], '%Y-%m-%d %H:%M:%S') + + if now > competition_date: + flash("Sorry, this competition is outdated. Booking not possible.") + the_club = next((a_club for a_club in clubs if a_club['name'] == club), None) + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions, + error = "Competition outdated"), 403 + + elif found_club and found_competition: return render_template(template_name_or_list='booking.html', club=found_club, competition=found_competition) @@ -118,7 +132,7 @@ def purchase_places(): update_competition_available_places(competition=competition, places=places_required) - flash('Great-booking complete!') + flash(f'Great-booking complete!') return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions) diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index 41ef39e52..73fc678b8 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -43,7 +43,7 @@ def get_competitions(): }, { "name": "Spring Festival", - "date": "2020-03-27 10:00:00", + "date": "2026-07-27 10:00:00", "number_of_places": "25" } ] @@ -74,6 +74,11 @@ def get_existing_competition_and_club_2(): data = {"competition": "Spring Festival", "club": "Iron Temple"} return data +@pytest.fixture +def get_existing_competition_and_club_3(): + data = {"competition": "Fall Classic", "club": "Iron Temple"} + return data + @pytest.fixture def get_consistent_purchasing_data(): competition = "Spring Festival" diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index fe522af8d..fc4c4f11b 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -1,15 +1,16 @@ +from Scripts.bottle import response from bs4 import BeautifulSoup from flask import url_for import server def test_index_status_code_ok(client): - response = client.get('/') - assert response.status_code == 200 + client_response = client.get('/') + assert client_response.status_code == 200 def test_index_return_welcome(client): - response = client.get('/') - data = response.data.decode('utf-8') + client_response = client.get('/') + data = client_response.data.decode('utf-8') assert "Welcome to the GUDLFT Registration Portal!" in data assert "Please enter your secretary email to continue:" in data @@ -17,13 +18,13 @@ def test_index_return_welcome(client): def test_index_mail_authentication_ok(get_existing_mail, get_clubs, mocker, client): mocker.patch('server.clubs', get_clubs) - response = client.post('/showSummary', data=get_existing_mail) - assert response.status_code == 200 + client_response = client.post('/showSummary', data=get_existing_mail) + assert client_response.status_code == 200 def test_index_mail_authentication_returns_summary(mocker, client, get_existing_mail, get_clubs): mocker.patch('server.clubs', get_clubs) - response = client.post('/showSummary', data=get_existing_mail) - data = response.data.decode('utf-8') + client_response = client.post('/showSummary', data=get_existing_mail) + data = client_response.data.decode('utf-8') assert "Welcome, kate@shelifts.co.uk" in data assert "Spring Festival" in data @@ -32,9 +33,10 @@ def test_index_mail_authentication_returns_summary(mocker, client, get_existing_ def test_index_mail_authentication_fail(mocker, client, get_unexisting_mail, get_clubs): mocker.patch('server.clubs', get_clubs) - response = client.post('/showSummary', data=get_unexisting_mail) - assert response.status_code == 404 - assert "Sorry, that email was not found." in response.data.decode('utf-8') + client_response = client.post('/showSummary', data=get_unexisting_mail) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 404 + assert "Sorry, that email was not found." in data def test_summary_logout_redirect_status_code_ok(mocker, client, get_existing_mail, get_clubs): mocker.patch('server.clubs', get_clubs) @@ -62,30 +64,36 @@ def test_booking_status_code_ok(mocker, client, get_existing_mail, get_existing_competition_and_club, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + client.post('/showSummary', data=get_existing_mail) - response = client.get(url_for(endpoint='book', + client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - assert response.status_code == 200 + assert client_response.status_code == 200 def test_booking_return_festival_page_booking(mocker, client, get_existing_mail, get_existing_competition_and_club, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + client.post('/showSummary', data=get_existing_mail) - response = client.get(url_for(endpoint='book', + client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - data = response.data.decode('utf-8') + data = client_response.data.decode('utf-8') assert "Spring Festival" in data assert "Places available: " in data assert "How many places?" in data @@ -95,9 +103,11 @@ def test_good_purchasing_places_status_code_ok(mocker, get_existing_mail, get_existing_competition_and_club, get_consistent_purchasing_data, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail) client.get(url_for(endpoint='book', @@ -107,9 +117,9 @@ def test_good_purchasing_places_status_code_ok(mocker, mocker.patch('server.save_clubs') mocker.patch('server.save_competitions') - response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) + client_response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) - assert response.status_code == 200 + assert client_response.status_code == 200 def test_good_purchasing_places_returns_summary_page(mocker, client, get_existing_mail, get_clubs, get_consistent_purchasing_data, @@ -135,8 +145,8 @@ def test_good_purchasing_places_returns_summary_page(mocker, client, get_existin mocker.patch('server.save_clubs') mocker.patch('server.save_competitions') - response = client.post('/purchasePlaces', data=purchasing_data) - data = response.data.decode('utf-8') + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') new_points = int(club_points) - int(purchasing_data['places']) new_competition_places = int(competition_places) - int(purchasing_data['places']) @@ -147,7 +157,7 @@ def test_good_purchasing_places_returns_summary_page(mocker, client, get_existin the_competition_name_utf8 = "%20".join(the_competition['name'].split()) li = (f'
  • \n' f' {the_competition["name"]}
    \n' - f' Date: 2020-03-27 10:00:00\n' + f' Date: 2026-07-27 10:00:00\n' f' Number of Places: {new_competition_places}\n \n' f' Book Places\n' @@ -163,26 +173,30 @@ def test_purchasing_places_not_enough_points_status_code_error(mocker, get_existing_mail_2, get_existing_competition_and_club_2, get_inconsistent_purchasing_data, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail_2) client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_2['competition'], club=get_existing_competition_and_club_2['club'])) - response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) - assert response.status_code == 403 + assert client_response.status_code == 403 def test_purchasing_places_not_enough_points_returns_sorry(mocker, client, get_existing_mail_2, get_existing_competition_and_club_2, get_inconsistent_purchasing_data, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail_2) client.get(url_for(endpoint='book', @@ -192,8 +206,8 @@ def test_purchasing_places_not_enough_points_returns_sorry(mocker, the_club = [club for club in server.clubs if club["name"] == get_existing_competition_and_club_2['club']][0] - response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) - data = response.data.decode('utf-8') + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + data = client_response.data.decode('utf-8') assert "Sorry, you do not have enough points to purchase." in data assert f"Points available: {the_club['points']}" in data @@ -203,25 +217,29 @@ def test_purchasing_places_over_12_places_status_code_error(mocker, get_existing_mail, get_existing_competition_and_club, purchasing_over_12_places, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail) client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) - response = client.post('/purchasePlaces', data=purchasing_over_12_places) + client_response = client.post('/purchasePlaces', data=purchasing_over_12_places) - assert response.status_code == 403 + assert client_response.status_code == 403 def test_purchasing_places_over_12_places_returns_sorry(mocker, client, get_existing_mail, get_existing_competition_and_club, purchasing_over_12_places, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -233,8 +251,8 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] - response = client.post('/purchasePlaces', data=purchasing_data) - data = response.data.decode('utf-8') + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data @@ -244,8 +262,10 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, get_existing_mail, get_existing_competition_and_club, purchasing_13_cumulative_places, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -257,8 +277,8 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] - response = client.post('/purchasePlaces', data=purchasing_data) - data = response.data.decode('utf-8') + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data @@ -268,9 +288,11 @@ def test_purchasing_places_negative_number_status_code_error(mocker, get_existing_mail, get_existing_competition_and_club, get_clubs, - purchasing_with_negative_places): + purchasing_with_negative_places, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -280,17 +302,19 @@ def test_purchasing_places_negative_number_status_code_error(mocker, purchasing_data = purchasing_with_negative_places - response = client.post('/purchasePlaces', data=purchasing_data) + client_response = client.post('/purchasePlaces', data=purchasing_data) - assert response.status_code == 403 + assert client_response.status_code == 403 def test_purchasing_places_negative_number_returns_sorry(mocker, client, get_existing_mail, get_existing_competition_and_club, purchasing_with_negative_places, - get_clubs): + get_clubs, + get_competitions): mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -302,8 +326,45 @@ def test_purchasing_places_negative_number_returns_sorry(mocker, the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] - response = client.post('/purchasePlaces', data=purchasing_data) - data = response.data.decode('utf-8') + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') assert "Sorry, you should type a positive number." in data assert f"Points available: {club_points}" in data + +def test_purchasing_places_past_competitions_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club_3, + get_clubs, + get_competitions): + + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + client.post('/showSummary', data=get_existing_mail) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) + + assert client_response.status_code == 403 + +def test_purchasing_places_past_competitions_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club_3, + get_clubs, + get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + client.post('/showSummary', data=get_existing_mail) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) + + data = client_response.data.decode('utf-8') + + assert "Sorry, this competition is outdated. Booking not possible." in data From d0bbbca718b3371fa839dc8e5010dfbef198b3ca Mon Sep 17 00:00:00 2001 From: NM Date: Fri, 13 Mar 2026 16:37:43 +0100 Subject: [PATCH 10/18] Modification : refactor unit tests --- unit_tests/test_app.py | 50 +++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index fc4c4f11b..c1fd78fac 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -1,9 +1,12 @@ -from Scripts.bottle import response from bs4 import BeautifulSoup from flask import url_for import server +def patch_clubs_and_competitions(mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + def test_index_status_code_ok(client): client_response = client.get('/') assert client_response.status_code == 200 @@ -67,8 +70,7 @@ def test_booking_status_code_ok(mocker, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -85,8 +87,7 @@ def test_booking_return_festival_page_booking(mocker, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -106,8 +107,7 @@ def test_good_purchasing_places_status_code_ok(mocker, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) client.get(url_for(endpoint='book', @@ -125,8 +125,7 @@ def test_good_purchasing_places_returns_summary_page(mocker, client, get_existin get_consistent_purchasing_data, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -176,8 +175,7 @@ def test_purchasing_places_not_enough_points_status_code_error(mocker, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail_2) client.get(url_for(endpoint='book', @@ -195,8 +193,8 @@ def test_purchasing_places_not_enough_points_returns_sorry(mocker, get_inconsistent_purchasing_data, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail_2) client.get(url_for(endpoint='book', @@ -219,8 +217,8 @@ def test_purchasing_places_over_12_places_status_code_error(mocker, purchasing_over_12_places, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) client.get(url_for(endpoint='book', @@ -238,8 +236,8 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, purchasing_over_12_places, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -264,8 +262,8 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, purchasing_13_cumulative_places, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -291,8 +289,7 @@ def test_purchasing_places_negative_number_status_code_error(mocker, purchasing_with_negative_places, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -313,8 +310,8 @@ def test_purchasing_places_negative_number_returns_sorry(mocker, purchasing_with_negative_places, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -339,8 +336,7 @@ def test_purchasing_places_past_competitions_status_code_error(mocker, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) @@ -356,8 +352,8 @@ def test_purchasing_places_past_competitions_returns_sorry(mocker, get_existing_competition_and_club_3, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) From c9a89fcfe45298563da10028ab306465bbcceae6 Mon Sep 17 00:00:00 2001 From: NM Date: Sat, 14 Mar 2026 10:07:24 +0100 Subject: [PATCH 11/18] Modification : fix issue #282 - clubs should not be able to book more than places available, update unit tests --- clubs.json | 2 +- competitions.json | 2 +- server.py | 35 +++++++++++++++----------- unit_tests/conftest.py | 24 ++++++++++++++++++ unit_tests/test_app.py | 56 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 102 insertions(+), 17 deletions(-) diff --git a/clubs.json b/clubs.json index 661547fc7..3a58634fc 100644 --- a/clubs.json +++ b/clubs.json @@ -13,7 +13,7 @@ { "name": "Iron Temple", "email": "admin@irontemple.com", - "points": "4" + "points": "5" } ] } \ No newline at end of file diff --git a/competitions.json b/competitions.json index 26cea4132..ffad148bd 100644 --- a/competitions.json +++ b/competitions.json @@ -13,7 +13,7 @@ { "name": "Winter Power", "date": "2026-06-14 10:12:26", - "number_of_places": "15" + "number_of_places": "4" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 7624a541c..c6eb2865f 100644 --- a/server.py +++ b/server.py @@ -83,7 +83,7 @@ def book(competition, club): return render_template(template_name_or_list='welcome.html', club=the_club, competitions=competitions, - error = "Competition outdated"), 403 + error = "Outdated"), 403 elif found_club and found_competition: return render_template(template_name_or_list='booking.html', @@ -105,26 +105,31 @@ def purchase_places(): cumulative_places = places_required + int(club["booked_places"][competition["name"]]) \ if "booked_places" in club else places_required + error_message = "" + error_tag = "" + if places_required < 0: - flash("Sorry, you should type a positive number.") - return render_template(template_name_or_list='welcome.html', - club=club, - competitions=competitions, - error="Negative number"), 403 + error_message = "Sorry, you should type a positive number." + error_tag = "Negative number" elif cumulative_places > 12: - flash("Sorry, you are not allow to purchase more than 12 places for this competition.") - return render_template(template_name_or_list='welcome.html', - club=club, - competitions=competitions, - error="Places max reached"), 403 + error_message = "Sorry, you are not allow to purchase more than 12 places for this competition." + error_tag = "Over 12 places" + + elif places_required > int(competition['number_of_places']): + error_message = "Sorry, there are not enough places available for this competition." + error_tag = "Not enough places" elif places_required > int(club['points']): - flash("Sorry, you do not have enough points to purchase.") + error_message = "Sorry, you do not have enough points to purchase." + error_tag = "Not enough points" + + if error_message and error_tag: + flash(error_message) return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions, - error="Points not enough"), 403 + error=error_tag), 403 update_club_booked_places(club=club, places=places_required, @@ -132,7 +137,9 @@ def purchase_places(): update_competition_available_places(competition=competition, places=places_required) - flash(f'Great-booking complete!') + flash(f"Great! Booking of {places_required} places for " + f"{competition['name']} competition complete!") + return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions) diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index 73fc678b8..de800055b 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -29,6 +29,11 @@ def get_clubs(): "booked_places": { "Spring Festival": "7" } + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "points": "5" } ] return the_clubs @@ -45,6 +50,11 @@ def get_competitions(): "name": "Spring Festival", "date": "2026-07-27 10:00:00", "number_of_places": "25" + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4" } ] return the_competitions @@ -79,6 +89,11 @@ def get_existing_competition_and_club_3(): data = {"competition": "Fall Classic", "club": "Iron Temple"} return data +@pytest.fixture +def get_existing_competition_and_club_4(): + data = {"competition": "Winter Power", "club": "Power Lift"} + return data + @pytest.fixture def get_consistent_purchasing_data(): competition = "Spring Festival" @@ -124,3 +139,12 @@ def purchasing_with_negative_places(): data = {"competition": competition, "club": club_name, "places": str(places_to_book)} return data + +@pytest.fixture +def purchasing_places_more_than_available(): + competition = "Winter Power" + club_name = "Power Lift" + places_to_book = 5 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index c1fd78fac..0662ed215 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -162,7 +162,8 @@ def test_good_purchasing_places_returns_summary_page(mocker, client, get_existin f'{the_club_name_utf8}">Book Places\n' f'
  • ') - assert "Great-booking complete!" in data + assert (f"Great! Booking of {purchasing_data['places']} places for " + f"{purchasing_data['competition']} competition complete!") in data assert f"Welcome, {the_club["email"]} " in data assert li in all_li_str assert f"Points available: {new_points}" in data @@ -207,6 +208,7 @@ def test_purchasing_places_not_enough_points_returns_sorry(mocker, client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) data = client_response.data.decode('utf-8') + assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you do not have enough points to purchase." in data assert f"Points available: {the_club['points']}" in data @@ -252,6 +254,7 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, client_response = client.post('/purchasePlaces', data=purchasing_data) data = client_response.data.decode('utf-8') + assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data @@ -278,6 +281,7 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, client_response = client.post('/purchasePlaces', data=purchasing_data) data = client_response.data.decode('utf-8') + assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data @@ -326,6 +330,7 @@ def test_purchasing_places_negative_number_returns_sorry(mocker, client_response = client.post('/purchasePlaces', data=purchasing_data) data = client_response.data.decode('utf-8') + assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you should type a positive number." in data assert f"Points available: {club_points}" in data @@ -364,3 +369,52 @@ def test_purchasing_places_past_competitions_returns_sorry(mocker, data = client_response.data.decode('utf-8') assert "Sorry, this competition is outdated. Booking not possible." in data + +def test_purchasing_places_over_available_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club_4, + get_clubs, + get_competitions, + purchasing_places_more_than_available): + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + + client.post('/showSummary', data=get_existing_mail) + + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) + + purchasing_data = purchasing_places_more_than_available + + client_response = client.post('/purchasePlaces', data=purchasing_data) + + assert client_response.status_code == 403 + +def test_purchasing_places_over_available_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club_4, + purchasing_places_more_than_available, + get_clubs, + get_competitions): + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + + client.post('/showSummary', data=get_existing_mail) + + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) + + purchasing_data = purchasing_places_more_than_available + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, there are not enough places available for this competition." in data + assert f"Points available: {club_points}" in data From aea108918740d13f31adc2cd1683b42f71b7db71 Mon Sep 17 00:00:00 2001 From: NM Date: Sat, 14 Mar 2026 11:30:08 +0100 Subject: [PATCH 12/18] Modification : add improvement - message when competition is sold out, add one more unit test for that improvement, update unit tests --- clubs.json | 2 +- server.py | 25 ++++++++++++--- unit_tests/conftest.py | 7 ++++- unit_tests/test_app.py | 71 ++++++++++++++++++++++++++---------------- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/clubs.json b/clubs.json index 3a58634fc..be358d0c8 100644 --- a/clubs.json +++ b/clubs.json @@ -13,7 +13,7 @@ { "name": "Iron Temple", "email": "admin@irontemple.com", - "points": "5" + "points": "3" } ] } \ No newline at end of file diff --git a/server.py b/server.py index c6eb2865f..c5ccc61da 100644 --- a/server.py +++ b/server.py @@ -77,20 +77,37 @@ def book(competition, club): competition_date = datetime.strptime(found_competition['date'], '%Y-%m-%d %H:%M:%S') + error_message = "" + error_tag = "" + + the_competition = next((a_competition for a_competition in competitions + if a_competition['name'] == competition), None) + competition_places = int(the_competition['number_of_places']) + if now > competition_date: - flash("Sorry, this competition is outdated. Booking not possible.") + error_message = "Sorry, this competition is outdated. Booking not possible." + error_tag = "Outdated" + + elif competition_places == 0: + error_message = "Sorry, this competition is sold out. Booking not possible." + error_tag = "Sold out" + + if error_message and error_tag: + flash(error_message) + the_club = next((a_club for a_club in clubs if a_club['name'] == club), None) + return render_template(template_name_or_list='welcome.html', club=the_club, competitions=competitions, - error = "Outdated"), 403 + error=error_tag), 403 - elif found_club and found_competition: + if found_club and found_competition: return render_template(template_name_or_list='booking.html', club=found_club, competition=found_competition) else: - flash("Something went wrong-please try again") + flash("Sorry, something went wrong. Please try again.") return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions) diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index de800055b..bf00d00f4 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -55,6 +55,11 @@ def get_competitions(): "name": "Winter Power", "date": "2026-06-26 12:16:00", "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" } ] return the_competitions @@ -91,7 +96,7 @@ def get_existing_competition_and_club_3(): @pytest.fixture def get_existing_competition_and_club_4(): - data = {"competition": "Winter Power", "club": "Power Lift"} + data = {"competition": "Summer Stronger", "club": "Power Lift"} return data @pytest.fixture diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index 0662ed215..2cdfabf66 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -223,6 +223,7 @@ def test_purchasing_places_over_12_places_status_code_error(mocker, patch_clubs_and_competitions(mocker, get_clubs, get_competitions) client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) @@ -234,7 +235,6 @@ def test_purchasing_places_over_12_places_status_code_error(mocker, def test_purchasing_places_over_12_places_returns_sorry(mocker, client, get_existing_mail, - get_existing_competition_and_club, purchasing_over_12_places, get_clubs, get_competitions): @@ -243,11 +243,12 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, client.post('/showSummary', data=get_existing_mail) + purchasing_data = purchasing_over_12_places + client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - purchasing_data = purchasing_over_12_places the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] @@ -261,7 +262,6 @@ def test_purchasing_places_over_12_places_returns_sorry(mocker, def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, client, get_existing_mail, - get_existing_competition_and_club, purchasing_13_cumulative_places, get_clubs, get_competitions): @@ -270,11 +270,12 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, client.post('/showSummary', data=get_existing_mail) + purchasing_data = purchasing_13_cumulative_places + client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - purchasing_data = purchasing_13_cumulative_places the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] @@ -288,7 +289,6 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, def test_purchasing_places_negative_number_status_code_error(mocker, client, get_existing_mail, - get_existing_competition_and_club, get_clubs, purchasing_with_negative_places, get_competitions): @@ -297,12 +297,12 @@ def test_purchasing_places_negative_number_status_code_error(mocker, client.post('/showSummary', data=get_existing_mail) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - purchasing_data = purchasing_with_negative_places + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + client_response = client.post('/purchasePlaces', data=purchasing_data) assert client_response.status_code == 403 @@ -310,7 +310,6 @@ def test_purchasing_places_negative_number_status_code_error(mocker, def test_purchasing_places_negative_number_returns_sorry(mocker, client, get_existing_mail, - get_existing_competition_and_club, purchasing_with_negative_places, get_clubs, get_competitions): @@ -319,11 +318,12 @@ def test_purchasing_places_negative_number_returns_sorry(mocker, client.post('/showSummary', data=get_existing_mail) + purchasing_data = purchasing_with_negative_places + client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - purchasing_data = purchasing_with_negative_places the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] @@ -373,7 +373,6 @@ def test_purchasing_places_past_competitions_returns_sorry(mocker, def test_purchasing_places_over_available_status_code_error(mocker, client, get_existing_mail, - get_existing_competition_and_club_4, get_clubs, get_competitions, purchasing_places_more_than_available): @@ -382,12 +381,12 @@ def test_purchasing_places_over_available_status_code_error(mocker, client.post('/showSummary', data=get_existing_mail) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_4['competition'], - club=get_existing_competition_and_club_4['club'])) - purchasing_data = purchasing_places_more_than_available + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + client_response = client.post('/purchasePlaces', data=purchasing_data) assert client_response.status_code == 403 @@ -395,7 +394,6 @@ def test_purchasing_places_over_available_status_code_error(mocker, def test_purchasing_places_over_available_returns_sorry(mocker, client, get_existing_mail, - get_existing_competition_and_club_4, purchasing_places_more_than_available, get_clubs, get_competitions): @@ -404,11 +402,12 @@ def test_purchasing_places_over_available_returns_sorry(mocker, client.post('/showSummary', data=get_existing_mail) + purchasing_data = purchasing_places_more_than_available + client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_4['competition'], - club=get_existing_competition_and_club_4['club'])) + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - purchasing_data = purchasing_places_more_than_available the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] club_points = the_club['points'] @@ -418,3 +417,23 @@ def test_purchasing_places_over_available_returns_sorry(mocker, assert f"Welcome, {the_club["email"]} " in data assert "Sorry, there are not enough places available for this competition." in data assert f"Points available: {club_points}" in data + +def test_purchasing_places_sold_out_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club_4, + get_clubs, + get_competitions): + + patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + + client.post('/showSummary', data=get_existing_mail) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert "Sorry, this competition is sold out. Booking not possible." in data From 24552f14d07bb0f10bff5aaff4cb5811bd9ce8cb Mon Sep 17 00:00:00 2001 From: NM Date: Sun, 15 Mar 2026 09:21:22 +0100 Subject: [PATCH 13/18] Modification : refactor unit tests with class --- .gitignore | 3 +- clubs.json | 17 +- competitions.json | 17 +- unit_tests/test_app.py | 643 +++++++++++++++++++---------------------- 4 files changed, 317 insertions(+), 363 deletions(-) diff --git a/.gitignore b/.gitignore index e7e28eb67..23535d79f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ lib .Python .envrc __pycache__ -.idea/ \ No newline at end of file +.idea/ +.coverage \ No newline at end of file diff --git a/clubs.json b/clubs.json index be358d0c8..275f7712b 100644 --- a/clubs.json +++ b/clubs.json @@ -1,10 +1,5 @@ { "clubs": [ - { - "name": "She Lifts", - "email": "kate@shelifts.co.uk", - "points": "12" - }, { "name": "Simply Lift", "email": "john@simplylift.co", @@ -13,7 +8,17 @@ { "name": "Iron Temple", "email": "admin@irontemple.com", - "points": "3" + "points": "4" + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "points": "5" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "points": "12" } ] } \ No newline at end of file diff --git a/competitions.json b/competitions.json index ffad148bd..8ef8e0d06 100644 --- a/competitions.json +++ b/competitions.json @@ -5,15 +5,20 @@ "date": "2020-10-22 13:30:00", "number_of_places": "13" }, - { - "name": "Spring Festival", - "date": "2020-03-27 10:00:00", - "number_of_places": "25" - }, { "name": "Winter Power", - "date": "2026-06-14 10:12:26", + "date": "2026-06-26 12:16:00", "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "25" } ] } \ No newline at end of file diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index 2cdfabf66..a76b7d0d1 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -1,439 +1,382 @@ +import pytest +import server + from bs4 import BeautifulSoup from flask import url_for -import server -def patch_clubs_and_competitions(mocker, get_clubs, get_competitions): - mocker.patch('server.clubs', get_clubs) - mocker.patch('server.competitions', get_competitions) - -def test_index_status_code_ok(client): - client_response = client.get('/') - assert client_response.status_code == 200 - -def test_index_return_welcome(client): - client_response = client.get('/') - data = client_response.data.decode('utf-8') - - assert "Welcome to the GUDLFT Registration Portal!" in data - assert "Please enter your secretary email to continue:" in data - assert "Email:" in data - -def test_index_mail_authentication_ok(get_existing_mail, get_clubs, mocker, client): - mocker.patch('server.clubs', get_clubs) - client_response = client.post('/showSummary', data=get_existing_mail) - assert client_response.status_code == 200 - -def test_index_mail_authentication_returns_summary(mocker, client, get_existing_mail, get_clubs): - mocker.patch('server.clubs', get_clubs) - client_response = client.post('/showSummary', data=get_existing_mail) - data = client_response.data.decode('utf-8') +class TestApp: + + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + + @staticmethod + def test_index_status_code_ok(client): + client_response = client.get('/') + assert client_response.status_code == 200 - assert "Welcome, kate@shelifts.co.uk" in data - assert "Spring Festival" in data - assert "Fall Classic" in data - assert "Points available: 12" in data + @staticmethod + def test_index_return_welcome(client): + client_response = client.get('/') + data = client_response.data.decode('utf-8') -def test_index_mail_authentication_fail(mocker, client, get_unexisting_mail, get_clubs): - mocker.patch('server.clubs', get_clubs) - client_response = client.post('/showSummary', data=get_unexisting_mail) - data = client_response.data.decode('utf-8') - assert client_response.status_code == 404 - assert "Sorry, that email was not found." in data + assert "Welcome to the GUDLFT Registration Portal!" in data + assert "Please enter your secretary email to continue:" in data + assert "Email:" in data -def test_summary_logout_redirect_status_code_ok(mocker, client, get_existing_mail, get_clubs): - mocker.patch('server.clubs', get_clubs) - client.post('/showSummary', data=get_existing_mail) - logout_response = client.get('/logout') - assert logout_response.status_code == 302 + @staticmethod + def test_index_mail_authentication_ok(get_existing_mail, mocker, client): + client_response = client.post('/showSummary', data=get_existing_mail) + assert client_response.status_code == 200 -def test_summary_logout_redirect_returns_welcome(mocker, client, get_existing_mail, get_clubs): - mocker.patch('server.clubs', get_clubs) - client.post('/showSummary', data=get_existing_mail) + @staticmethod + def test_index_mail_authentication_returns_summary(client, get_existing_mail): + client_response = client.post('/showSummary', data=get_existing_mail) + data = client_response.data.decode('utf-8') - logout_response = client.get('/logout') - soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") - url = soup.find_all('a')[0].get('href') - redirect_response = client.get(url, follow_redirects=True) + assert "Welcome, kate@shelifts.co.uk" in data + assert "Spring Festival" in data + assert "Fall Classic" in data + assert "Points available: 12" in data - assert redirect_response.status_code == 200 - data = redirect_response.data.decode('utf-8') - - assert "Welcome to the GUDLFT Registration Portal!" in data - assert "Please enter your secretary email to continue:" in data - assert "Email:" in data - -def test_booking_status_code_ok(mocker, - client, - get_existing_mail, - get_existing_competition_and_club, - get_clubs, - get_competitions): - - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) - - client.post('/showSummary', data=get_existing_mail) - - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - - assert client_response.status_code == 200 - -def test_booking_return_festival_page_booking(mocker, - client, - get_existing_mail, - get_existing_competition_and_club, - get_clubs, - get_competitions): - - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) - - client.post('/showSummary', data=get_existing_mail) - - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - data = client_response.data.decode('utf-8') - assert "Spring Festival" in data - assert "Places available: " in data - assert "How many places?" in data - -def test_good_purchasing_places_status_code_ok(mocker, - client, - get_existing_mail, - get_existing_competition_and_club, - get_consistent_purchasing_data, - get_clubs, - get_competitions): - - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) - - client.post('/showSummary', data=get_existing_mail) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - - mocker.patch('server.save_clubs') - mocker.patch('server.save_competitions') - - client_response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) - - assert client_response.status_code == 200 - -def test_good_purchasing_places_returns_summary_page(mocker, client, get_existing_mail, get_clubs, - get_consistent_purchasing_data, - get_competitions): - - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) - - client.post('/showSummary', data=get_existing_mail) - - purchasing_data = get_consistent_purchasing_data - the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] - the_competition =[competition for competition in server.competitions - if competition["name"] == purchasing_data['competition']][0] - - client.get(url_for(endpoint='book', - competition=the_competition['name'], - club=the_club['name'])) - - club_points = the_club['points'] - competition_places = the_competition['number_of_places'] + @staticmethod + def test_index_mail_authentication_fail(client, get_unexisting_mail): + client_response = client.post('/showSummary', data=get_unexisting_mail) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 404 + assert "Sorry, that email was not found." in data - mocker.patch('server.save_clubs') - mocker.patch('server.save_competitions') + @staticmethod + def test_summary_logout_redirect_status_code_ok(client, get_existing_mail): + client.post('/showSummary', data=get_existing_mail) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 - client_response = client.post('/purchasePlaces', data=purchasing_data) - data = client_response.data.decode('utf-8') + @staticmethod + def test_summary_logout_redirect_returns_welcome(client, get_existing_mail): + client.post('/showSummary', data=get_existing_mail) - new_points = int(club_points) - int(purchasing_data['places']) - new_competition_places = int(competition_places) - int(purchasing_data['places']) + logout_response = client.get('/logout') + soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") + url = soup.find_all('a')[0].get('href') + redirect_response = client.get(url, follow_redirects=True) - soup = BeautifulSoup(data, features="html.parser") - all_li_str = [str(li) for li in soup.find_all('li')] - the_club_name_utf8 = "%20".join(the_club['name'].split()) - the_competition_name_utf8 = "%20".join(the_competition['name'].split()) - li = (f'
  • \n' - f' {the_competition["name"]}
    \n' - f' Date: 2026-07-27 10:00:00\n' - f' Number of Places: {new_competition_places}\n \n' - f' Book Places\n' - f'
  • ') + assert redirect_response.status_code == 200 + data = redirect_response.data.decode('utf-8') - assert (f"Great! Booking of {purchasing_data['places']} places for " - f"{purchasing_data['competition']} competition complete!") in data - assert f"Welcome, {the_club["email"]} " in data - assert li in all_li_str - assert f"Points available: {new_points}" in data + assert "Welcome to the GUDLFT Registration Portal!" in data + assert "Please enter your secretary email to continue:" in data + assert "Email:" in data -def test_purchasing_places_not_enough_points_status_code_error(mocker, - client, - get_existing_mail_2, - get_existing_competition_and_club_2, - get_inconsistent_purchasing_data, - get_clubs, - get_competitions): + @staticmethod + def test_booking_status_code_ok(client, + get_existing_mail, + get_existing_competition_and_club): - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + client.post('/showSummary', data=get_existing_mail) - client.post('/showSummary', data=get_existing_mail_2) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_2['competition'], - club=get_existing_competition_and_club_2['club'])) + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + assert client_response.status_code == 200 - assert client_response.status_code == 403 + @staticmethod + def test_booking_return_festival_page_booking(client, + get_existing_mail, + get_existing_competition_and_club): -def test_purchasing_places_not_enough_points_returns_sorry(mocker, - client, - get_existing_mail_2, - get_existing_competition_and_club_2, - get_inconsistent_purchasing_data, - get_clubs, - get_competitions): + client.post('/showSummary', data=get_existing_mail) - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + data = client_response.data.decode('utf-8') + assert "Spring Festival" in data + assert "Places available: " in data + assert "How many places?" in data - client.post('/showSummary', data=get_existing_mail_2) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_2['competition'], - club=get_existing_competition_and_club_2['club'])) + @staticmethod + def test_good_purchasing_places_status_code_ok(client, + get_existing_mail, + get_existing_competition_and_club, + get_consistent_purchasing_data): + + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + client_response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) + + assert client_response.status_code == 200 + + @staticmethod + def test_good_purchasing_places_returns_summary_page(client, + get_existing_mail, + get_consistent_purchasing_data): + + client.post('/showSummary', data=get_existing_mail) + + purchasing_data = get_consistent_purchasing_data + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + the_competition =[competition for competition in server.competitions + if competition["name"] == purchasing_data['competition']][0] + + client.get(url_for(endpoint='book', + competition=the_competition['name'], + club=the_club['name'])) + + club_points = the_club['points'] + competition_places = the_competition['number_of_places'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + new_points = int(club_points) - int(purchasing_data['places']) + new_competition_places = int(competition_places) - int(purchasing_data['places']) + + soup = BeautifulSoup(data, features="html.parser") + all_li_str = [str(li) for li in soup.find_all('li')] + the_club_name_utf8 = "%20".join(the_club['name'].split()) + the_competition_name_utf8 = "%20".join(the_competition['name'].split()) + li = (f'
  • \n' + f' {the_competition["name"]}
    \n' + f' Date: 2026-07-27 10:00:00\n' + f' Number of Places: {new_competition_places}\n \n' + f' Book Places\n' + f'
  • ') + + assert (f"Great! Booking of {purchasing_data['places']} places for " + f"{purchasing_data['competition']} competition complete!") in data + assert f"Welcome, {the_club["email"]} " in data + assert li in all_li_str + assert f"Points available: {new_points}" in data + + @staticmethod + def test_purchasing_places_not_enough_points_status_code_error(client, + get_existing_mail_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data): + + client.post('/showSummary', data=get_existing_mail_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) + + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + + assert client_response.status_code == 403 + + @staticmethod + def test_purchasing_places_not_enough_points_returns_sorry(client, + get_existing_mail_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data): - the_club = [club for club in server.clubs - if club["name"] == get_existing_competition_and_club_2['club']][0] + client.post('/showSummary', data=get_existing_mail_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) - client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) - data = client_response.data.decode('utf-8') + the_club = [club for club in server.clubs + if club["name"] == get_existing_competition_and_club_2['club']][0] - assert f"Welcome, {the_club["email"]} " in data - assert "Sorry, you do not have enough points to purchase." in data - assert f"Points available: {the_club['points']}" in data + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + data = client_response.data.decode('utf-8') -def test_purchasing_places_over_12_places_status_code_error(mocker, - client, - get_existing_mail, - get_existing_competition_and_club, - purchasing_over_12_places, - get_clubs, - get_competitions): + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you do not have enough points to purchase." in data + assert f"Points available: {the_club['points']}" in data - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + @staticmethod + def test_purchasing_places_over_12_places_status_code_error(client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_over_12_places): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_existing_mail) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) - client_response = client.post('/purchasePlaces', data=purchasing_over_12_places) + client_response = client.post('/purchasePlaces', data=purchasing_over_12_places) - assert client_response.status_code == 403 + assert client_response.status_code == 403 -def test_purchasing_places_over_12_places_returns_sorry(mocker, - client, - get_existing_mail, - purchasing_over_12_places, - get_clubs, - get_competitions): + @staticmethod + def test_purchasing_places_over_12_places_returns_sorry(client, + get_existing_mail, + purchasing_over_12_places): - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + client.post('/showSummary', data=get_existing_mail) - client.post('/showSummary', data=get_existing_mail) + purchasing_data = purchasing_over_12_places - purchasing_data = purchasing_over_12_places + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] - the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] - club_points = the_club['points'] + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') - client_response = client.post('/purchasePlaces', data=purchasing_data) - data = client_response.data.decode('utf-8') + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data - assert f"Welcome, {the_club["email"]} " in data - assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data - assert f"Points available: {club_points}" in data + @staticmethod + def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, + get_existing_mail, + purchasing_13_cumulative_places): -def test_purchasing_places_over_12_cumulative_places_returns_sorry(mocker, - client, - get_existing_mail, - purchasing_13_cumulative_places, - get_clubs, - get_competitions): + client.post('/showSummary', data=get_existing_mail) - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + purchasing_data = purchasing_13_cumulative_places - client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - purchasing_data = purchasing_13_cumulative_places + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') - the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] - club_points = the_club['points'] + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data - client_response = client.post('/purchasePlaces', data=purchasing_data) - data = client_response.data.decode('utf-8') + @staticmethod + def test_purchasing_places_negative_number_status_code_error(client, + get_existing_mail, + purchasing_with_negative_places): - assert f"Welcome, {the_club["email"]} " in data - assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data - assert f"Points available: {club_points}" in data + client.post('/showSummary', data=get_existing_mail) -def test_purchasing_places_negative_number_status_code_error(mocker, - client, - get_existing_mail, - get_clubs, - purchasing_with_negative_places, - get_competitions): + purchasing_data = purchasing_with_negative_places - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - client.post('/showSummary', data=get_existing_mail) + client_response = client.post('/purchasePlaces', data=purchasing_data) - purchasing_data = purchasing_with_negative_places + assert client_response.status_code == 403 - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) + @staticmethod + def test_purchasing_places_negative_number_returns_sorry(client, + get_existing_mail, + purchasing_with_negative_places): - client_response = client.post('/purchasePlaces', data=purchasing_data) + client.post('/showSummary', data=get_existing_mail) - assert client_response.status_code == 403 + purchasing_data = purchasing_with_negative_places -def test_purchasing_places_negative_number_returns_sorry(mocker, - client, - get_existing_mail, - purchasing_with_negative_places, - get_clubs, - get_competitions): + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] - client.post('/showSummary', data=get_existing_mail) + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') - purchasing_data = purchasing_with_negative_places + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you should type a positive number." in data + assert f"Points available: {club_points}" in data - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) + @staticmethod + def test_purchasing_places_past_competitions_status_code_error(client, + get_existing_mail, + get_existing_competition_and_club_3): - the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] - club_points = the_club['points'] + client.post('/showSummary', data=get_existing_mail) - client_response = client.post('/purchasePlaces', data=purchasing_data) - data = client_response.data.decode('utf-8') + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) - assert f"Welcome, {the_club["email"]} " in data - assert "Sorry, you should type a positive number." in data - assert f"Points available: {club_points}" in data + assert client_response.status_code == 403 -def test_purchasing_places_past_competitions_status_code_error(mocker, - client, + @staticmethod + def test_purchasing_places_past_competitions_returns_sorry(client, get_existing_mail, - get_existing_competition_and_club_3, - get_clubs, - get_competitions): + get_existing_competition_and_club_3): - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + client.post('/showSummary', data=get_existing_mail) - client.post('/showSummary', data=get_existing_mail) + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_3['competition'], - club=get_existing_competition_and_club_3['club'])) + data = client_response.data.decode('utf-8') - assert client_response.status_code == 403 + assert "Sorry, this competition is outdated. Booking not possible." in data -def test_purchasing_places_past_competitions_returns_sorry(mocker, - client, - get_existing_mail, - get_existing_competition_and_club_3, - get_clubs, - get_competitions): + @staticmethod + def test_purchasing_places_over_available_status_code_error(client, + get_existing_mail, + purchasing_places_more_than_available): - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + client.post('/showSummary', data=get_existing_mail) - client.post('/showSummary', data=get_existing_mail) + purchasing_data = purchasing_places_more_than_available - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_3['competition'], - club=get_existing_competition_and_club_3['club'])) + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - data = client_response.data.decode('utf-8') + client_response = client.post('/purchasePlaces', data=purchasing_data) - assert "Sorry, this competition is outdated. Booking not possible." in data + assert client_response.status_code == 403 -def test_purchasing_places_over_available_status_code_error(mocker, - client, + @staticmethod + def test_purchasing_places_over_available_returns_sorry(client, get_existing_mail, - get_clubs, - get_competitions, purchasing_places_more_than_available): - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) - - client.post('/showSummary', data=get_existing_mail) - - purchasing_data = purchasing_places_more_than_available - - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) - - client_response = client.post('/purchasePlaces', data=purchasing_data) - - assert client_response.status_code == 403 - -def test_purchasing_places_over_available_returns_sorry(mocker, - client, - get_existing_mail, - purchasing_places_more_than_available, - get_clubs, - get_competitions): - - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) - - client.post('/showSummary', data=get_existing_mail) - - purchasing_data = purchasing_places_more_than_available + client.post('/showSummary', data=get_existing_mail) - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) + purchasing_data = purchasing_places_more_than_available - the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] - club_points = the_club['points'] + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) - client_response = client.post('/purchasePlaces', data=purchasing_data) - data = client_response.data.decode('utf-8') + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] - assert f"Welcome, {the_club["email"]} " in data - assert "Sorry, there are not enough places available for this competition." in data - assert f"Points available: {club_points}" in data + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') -def test_purchasing_places_sold_out_status_code_error(mocker, - client, - get_existing_mail, - get_existing_competition_and_club_4, - get_clubs, - get_competitions): + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, there are not enough places available for this competition." in data + assert f"Points available: {club_points}" in data - patch_clubs_and_competitions(mocker, get_clubs, get_competitions) + @staticmethod + def test_purchasing_places_sold_out_status_code_error(client, + get_existing_mail, + get_existing_competition_and_club_4): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_existing_mail) - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_4['competition'], - club=get_existing_competition_and_club_4['club'])) + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) - data = client_response.data.decode('utf-8') + data = client_response.data.decode('utf-8') - assert client_response.status_code == 403 - assert "Sorry, this competition is sold out. Booking not possible." in data + assert client_response.status_code == 403 + assert "Sorry, this competition is sold out. Booking not possible." in data From f93042dd5cb821f1a8a23f8c65d39051b6c95d45 Mon Sep 17 00:00:00 2001 From: NM Date: Mon, 16 Mar 2026 09:39:27 +0100 Subject: [PATCH 14/18] Modification : add auth feature with password, add new template to signup, change password and see profile --- clubs.json | 14 ++-- server.py | 114 +++++++++++++++++++++++++++++++-- templates/change_password.html | 35 ++++++++++ templates/index.html | 8 ++- templates/profile.html | 32 +++++++++ templates/sign_up.html | 37 +++++++++++ templates/welcome.html | 5 +- unit_tests/conftest.py | 18 ++++-- unit_tests/test_app.py | 92 +++++++++++++------------- 9 files changed, 290 insertions(+), 65 deletions(-) create mode 100644 templates/change_password.html create mode 100644 templates/profile.html create mode 100644 templates/sign_up.html diff --git a/clubs.json b/clubs.json index 275f7712b..094e90d73 100644 --- a/clubs.json +++ b/clubs.json @@ -3,22 +3,26 @@ { "name": "Simply Lift", "email": "john@simplylift.co", + "password": "pbkdf2:sha256:150000$eHVGmFVY$00afed3d5a39621cc5aad05d069b8d8643c9e3e47435471f40861e608e825711", "points": "13" }, - { - "name": "Iron Temple", - "email": "admin@irontemple.com", - "points": "4" - }, { "name": "Power Lift", "email": "admin@powerlift.com", + "password": "pbkdf2:sha256:150000$YEgAXbf8$09b68fdd4ab198aaf541a4b3a3b680449fff602e43bbfb1e602e970e9acf7b4c", "points": "5" }, { "name": "She Lifts", "email": "kate@shelifts.co.uk", + "password": "pbkdf2:sha256:150000$g7r5qSMn$fecca97e2fcca7b093a4ca60789c4ebc0b6f4c0331aaaeee1ffa9f03a3404ad5", "points": "12" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": "pbkdf2:sha256:150000$G0hKG0dK$434a98095c828d2735c63724fe9ae142a88d4aaba9489733c7fd614db655237a", + "points": "4" } ] } \ No newline at end of file diff --git a/server.py b/server.py index c5ccc61da..04c61669c 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,8 @@ -from datetime import datetime import json + +from datetime import datetime from flask import Flask, render_template, request, redirect, flash, url_for +from werkzeug.security import generate_password_hash, check_password_hash def load_clubs(): @@ -20,6 +22,8 @@ def load_competitions(): competitions = load_competitions() clubs = load_clubs() +CLUB_POINTS = 15 + def update_club_booked_places(club, places, competition_name): clubs.remove(club) @@ -50,24 +54,120 @@ def save_competitions(): list_of_competitions = {"competitions": competitions} json.dump(list_of_competitions, comps, indent=4) +def add_club(name, email, password, points): + clubs.append({"name": name, "email": email, "password": password, "points": points}) + save_clubs() + +def update_club_password(club, password): + club["password"] = password + save_clubs() + return club + @app.route('/') def index(): return render_template('index.html') -@app.route('/showSummary', methods=['POST']) -def show_summary(): +@app.route('/signUp') +def sign_up(): + return render_template('sign_up.html') + +@app.route('/profile/', methods=['GET']) +def profile(club): + the_club = next((c for c in clubs if c['name'] == club), None) + + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Club not found"), 404 + + return render_template(template_name_or_list='profile.html', club=the_club) + +@app.route('/profile', methods=['POST']) +def profile_post(): + club_name = request.form['name'] + club_email = request.form['email'] + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + club_exists = next((c for c in clubs if c['email'] == club_email or c['name'] == club_name), None) + if club_exists is None: + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('sign_up')) + + hashed_password = generate_password_hash(club_password) + add_club(club_name, club_email, hashed_password, str(CLUB_POINTS)) + + the_club = next((c for c in clubs if c['email'] == club_email), None) + + if the_club is None: + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='sign_up.html') + + flash("Great! You have successfully signed up.") + return render_template(template_name_or_list='profile.html', club=the_club) + + else: + flash("Sorry, the club already exists.") + return render_template(template_name_or_list='sign_up.html') - club = next((club for club in clubs if club['email'] == request.form['email']), None) +@app.route('/changePassword/', methods=['GET', 'POST']) +def change_password(club): + if request.method == 'GET': + the_club = next((c for c in clubs if c['name'] == club), None) - if club is None: + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + return render_template(template_name_or_list='change_password.html', club=the_club) + else: + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('change_password')) + + the_club = next((c for c in clubs if c['name'] == club), None) + hashed_password = generate_password_hash(club_password) + + if check_password_hash(the_club['password'], club_password): + flash('Sorry, you have to type a new different password.') + return render_template(template_name_or_list='change_password.html', club=the_club) + + the_club = update_club_password(the_club, hashed_password) + + if the_club: + flash("Great! You have successfully changed your password.") + return render_template(template_name_or_list='profile.html', club=the_club) + + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='index.html') + + +@app.route('/showSummary/', methods=['GET']) +def show_summary(club): + the_club = next((c for c in clubs if c['name'] == club), None) + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions) + +@app.route('/showSummary', methods=['POST']) +def show_summary_post(): + the_club = next((c for c in clubs if c['email'] == request.form['email']), None) + + if the_club is None: flash("Sorry, that email was not found.") return render_template(template_name_or_list="index.html", error="Email not found"), 404 + if not check_password_hash(the_club['password'], request.form['password']): + flash("Sorry, the password is incorrect.") + return render_template(template_name_or_list="index.html",) + return render_template(template_name_or_list='welcome.html', - club=club, + club=the_club, competitions=competitions) - @app.route('/book//') def book(competition, club): found_club = [c for c in clubs if c['name'] == club][0] diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 000000000..7fbd77c33 --- /dev/null +++ b/templates/change_password.html @@ -0,0 +1,35 @@ + + + + + GUDLFT Registration + + +

    Welcome to the GUDLFT - Change password page !

    + + {% with messages = get_flashed_messages()%} + {% if messages %} +
      + {% for message in messages %} +
    • {{message}}
    • + {% endfor %} +
    + {% endif%} + {%endwith%} + + Please enter your new password:

    + + Name : {{club['name']}}
    + Email : {{club['email']}} +

    + + + + + + + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 56b19b23c..5b2a76631 100644 --- a/templates/index.html +++ b/templates/index.html @@ -17,10 +17,16 @@

    Welcome to the GUDLFT Registration Portal!

    {% endif%} {%endwith%} - Please enter your secretary email to continue: + Please enter your secretary email and your password to continue or sign up +
    +
    + + + +
    diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 000000000..9b05a3332 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,32 @@ + + + + + Summary | GUDLFT Profile + + +

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

    Logout + + {% with messages = get_flashed_messages()%} + {% if messages %} +
      + {% for message in messages %} +
    • {{message}}
    • + {% endfor %} +
    + {% endif%} +

    Profile:

    +
      +
    • Name : {{club['name']}}
    • +
    • Email : {{club['email']}}
    • +
    • password : {{club['password']}} - Change password
    • +
      +
    • Points available: {{club['points']}}
    • +
    +
    + Go to home page + + {%endwith%} + + + \ No newline at end of file diff --git a/templates/sign_up.html b/templates/sign_up.html new file mode 100644 index 000000000..49c6be945 --- /dev/null +++ b/templates/sign_up.html @@ -0,0 +1,37 @@ + + + + + GUDLFT Sign up + + +

    Welcome to the GUDLFT sign up page!

    + + {% with messages = get_flashed_messages()%} + {% if messages %} +
      + {% for message in messages %} +
    • {{message}}
    • + {% endfor %} +
    + {% endif%} + {%endwith%} + + Please enter your secretary email and your password: +
    + + + + + + + + + + + + + +
    + + \ No newline at end of file diff --git a/templates/welcome.html b/templates/welcome.html index e790e2284..355e23b3d 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -5,7 +5,7 @@ Summary | GUDLFT Registration -

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

    Logout +

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

    Logout

    {% with messages = get_flashed_messages()%} {% if messages %} @@ -15,6 +15,9 @@

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

    Logout {% endfor %} {% endif%} + + Go to profile +
    Points available: {{club['points']}}

    Competitions:

      diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py index bf00d00f4..62724e05a 100644 --- a/unit_tests/conftest.py +++ b/unit_tests/conftest.py @@ -1,7 +1,7 @@ import pytest from random import randint - +from werkzeug.security import generate_password_hash from server import app @pytest.fixture @@ -16,15 +16,18 @@ def get_clubs(): { "name":"Simply Lift", "email":"john@simplylift.co", + "password": generate_password_hash("tp1_Tmn28"), "points":"13" }, { "name":"Iron Temple", "email": "admin@irontemple.com", + "password": generate_password_hash("tp2_Tmn29"), "points":"4" }, { "name":"She Lifts", "email": "kate@shelifts.co.uk", + "password": generate_password_hash("tp3_Tmn30"), "points":"12", "booked_places": { "Spring Festival": "7" @@ -33,6 +36,7 @@ def get_clubs(): { "name": "Power Lift", "email": "admin@powerlift.com", + "password": generate_password_hash("tp4_Tmn40"), "points": "5" } ] @@ -65,18 +69,18 @@ def get_competitions(): return the_competitions @pytest.fixture -def get_existing_mail(): - data = {"email": "kate@shelifts.co.uk"} +def get_credentials(): + data = {"email": "kate@shelifts.co.uk", "password": "tp3_Tmn30"} return data @pytest.fixture -def get_existing_mail_2(): - data = {"email": "admin@irontemple.com"} +def get_credentials_2(): + data = {"email": "admin@irontemple.com", "password": "tp2_Tmn29"} return data @pytest.fixture -def get_unexisting_mail(): - data = {"email": "nicolas.marie@unexisting.com"} +def get_unexisting_credentials(): + data = {"email": "nicolas.marie@unexisting.com", "password": "er45_shet"} return data @pytest.fixture diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py index a76b7d0d1..dcdd98489 100644 --- a/unit_tests/test_app.py +++ b/unit_tests/test_app.py @@ -26,17 +26,19 @@ def test_index_return_welcome(client): data = client_response.data.decode('utf-8') assert "Welcome to the GUDLFT Registration Portal!" in data - assert "Please enter your secretary email to continue:" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data assert "Email:" in data + assert "Password:" in data @staticmethod - def test_index_mail_authentication_ok(get_existing_mail, mocker, client): - client_response = client.post('/showSummary', data=get_existing_mail) + def test_index_mail_authentication_ok(get_credentials, client): + client_response = client.post('/showSummary', data=get_credentials) assert client_response.status_code == 200 @staticmethod - def test_index_mail_authentication_returns_summary(client, get_existing_mail): - client_response = client.post('/showSummary', data=get_existing_mail) + def test_index_mail_authentication_returns_summary(client, get_credentials): + client_response = client.post('/showSummary', data=get_credentials) data = client_response.data.decode('utf-8') assert "Welcome, kate@shelifts.co.uk" in data @@ -45,21 +47,21 @@ def test_index_mail_authentication_returns_summary(client, get_existing_mail): assert "Points available: 12" in data @staticmethod - def test_index_mail_authentication_fail(client, get_unexisting_mail): - client_response = client.post('/showSummary', data=get_unexisting_mail) + def test_index_mail_authentication_fail(client, get_unexisting_credentials): + client_response = client.post('/showSummary', data=get_unexisting_credentials) data = client_response.data.decode('utf-8') assert client_response.status_code == 404 assert "Sorry, that email was not found." in data @staticmethod - def test_summary_logout_redirect_status_code_ok(client, get_existing_mail): - client.post('/showSummary', data=get_existing_mail) + def test_summary_logout_redirect_status_code_ok(client, get_credentials): + client.post('/showSummary', data=get_credentials) logout_response = client.get('/logout') assert logout_response.status_code == 302 @staticmethod - def test_summary_logout_redirect_returns_welcome(client, get_existing_mail): - client.post('/showSummary', data=get_existing_mail) + def test_summary_logout_redirect_returns_welcome(client, get_credentials): + client.post('/showSummary', data=get_credentials) logout_response = client.get('/logout') soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") @@ -70,15 +72,17 @@ def test_summary_logout_redirect_returns_welcome(client, get_existing_mail): data = redirect_response.data.decode('utf-8') assert "Welcome to the GUDLFT Registration Portal!" in data - assert "Please enter your secretary email to continue:" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data assert "Email:" in data + assert "Password:" in data @staticmethod def test_booking_status_code_ok(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], @@ -88,10 +92,10 @@ def test_booking_status_code_ok(client, @staticmethod def test_booking_return_festival_page_booking(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], @@ -103,11 +107,11 @@ def test_booking_return_festival_page_booking(client, @staticmethod def test_good_purchasing_places_status_code_ok(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club, get_consistent_purchasing_data): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], club=get_existing_competition_and_club['club'])) @@ -118,10 +122,10 @@ def test_good_purchasing_places_status_code_ok(client, @staticmethod def test_good_purchasing_places_returns_summary_page(client, - get_existing_mail, + get_credentials, get_consistent_purchasing_data): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = get_consistent_purchasing_data the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] @@ -161,11 +165,11 @@ def test_good_purchasing_places_returns_summary_page(client, @staticmethod def test_purchasing_places_not_enough_points_status_code_error(client, - get_existing_mail_2, + get_credentials_2, get_existing_competition_and_club_2, get_inconsistent_purchasing_data): - client.post('/showSummary', data=get_existing_mail_2) + client.post('/showSummary', data=get_credentials_2) client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_2['competition'], club=get_existing_competition_and_club_2['club'])) @@ -176,11 +180,11 @@ def test_purchasing_places_not_enough_points_status_code_error(client, @staticmethod def test_purchasing_places_not_enough_points_returns_sorry(client, - get_existing_mail_2, + get_credentials_2, get_existing_competition_and_club_2, get_inconsistent_purchasing_data): - client.post('/showSummary', data=get_existing_mail_2) + client.post('/showSummary', data=get_credentials_2) client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_2['competition'], club=get_existing_competition_and_club_2['club'])) @@ -197,11 +201,11 @@ def test_purchasing_places_not_enough_points_returns_sorry(client, @staticmethod def test_purchasing_places_over_12_places_status_code_error(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club, purchasing_over_12_places): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client.get(url_for(endpoint='book', competition=get_existing_competition_and_club['competition'], @@ -213,10 +217,10 @@ def test_purchasing_places_over_12_places_status_code_error(client, @staticmethod def test_purchasing_places_over_12_places_returns_sorry(client, - get_existing_mail, + get_credentials, purchasing_over_12_places): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = purchasing_over_12_places @@ -236,10 +240,10 @@ def test_purchasing_places_over_12_places_returns_sorry(client, @staticmethod def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, - get_existing_mail, + get_credentials, purchasing_13_cumulative_places): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = purchasing_13_cumulative_places @@ -259,10 +263,10 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, @staticmethod def test_purchasing_places_negative_number_status_code_error(client, - get_existing_mail, + get_credentials, purchasing_with_negative_places): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = purchasing_with_negative_places @@ -276,10 +280,10 @@ def test_purchasing_places_negative_number_status_code_error(client, @staticmethod def test_purchasing_places_negative_number_returns_sorry(client, - get_existing_mail, + get_credentials, purchasing_with_negative_places): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = purchasing_with_negative_places @@ -299,10 +303,10 @@ def test_purchasing_places_negative_number_returns_sorry(client, @staticmethod def test_purchasing_places_past_competitions_status_code_error(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club_3): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_3['competition'], @@ -312,10 +316,10 @@ def test_purchasing_places_past_competitions_status_code_error(client, @staticmethod def test_purchasing_places_past_competitions_returns_sorry(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club_3): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_3['competition'], @@ -327,10 +331,10 @@ def test_purchasing_places_past_competitions_returns_sorry(client, @staticmethod def test_purchasing_places_over_available_status_code_error(client, - get_existing_mail, + get_credentials, purchasing_places_more_than_available): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = purchasing_places_more_than_available @@ -344,10 +348,10 @@ def test_purchasing_places_over_available_status_code_error(client, @staticmethod def test_purchasing_places_over_available_returns_sorry(client, - get_existing_mail, + get_credentials, purchasing_places_more_than_available): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) purchasing_data = purchasing_places_more_than_available @@ -367,10 +371,10 @@ def test_purchasing_places_over_available_returns_sorry(client, @staticmethod def test_purchasing_places_sold_out_status_code_error(client, - get_existing_mail, + get_credentials, get_existing_competition_and_club_4): - client.post('/showSummary', data=get_existing_mail) + client.post('/showSummary', data=get_credentials) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_4['competition'], From 9bf3e248f0910e25ddbf6047d6715de21f77331b Mon Sep 17 00:00:00 2001 From: NM Date: Mon, 16 Mar 2026 14:26:46 +0100 Subject: [PATCH 15/18] Modification and file change : separate tests by unit and integration tests --- clubs.json | 25 +-- competitions.json | 2 +- server.py | 7 +- {unit_tests => tests}/__init__.py | 0 {unit_tests => tests}/conftest.py | 10 + tests/integration_tests/__init__.py | 0 .../integration_tests}/test_app.py | 159 +--------------- tests/unit_tests/__init__.py | 0 tests/unit_tests/test_app.py | 173 ++++++++++++++++++ 9 files changed, 208 insertions(+), 168 deletions(-) rename {unit_tests => tests}/__init__.py (100%) rename {unit_tests => tests}/conftest.py (95%) create mode 100644 tests/integration_tests/__init__.py rename {unit_tests => tests/integration_tests}/test_app.py (63%) create mode 100644 tests/unit_tests/__init__.py create mode 100644 tests/unit_tests/test_app.py diff --git a/clubs.json b/clubs.json index 094e90d73..32d4ca476 100644 --- a/clubs.json +++ b/clubs.json @@ -3,26 +3,29 @@ { "name": "Simply Lift", "email": "john@simplylift.co", - "password": "pbkdf2:sha256:150000$eHVGmFVY$00afed3d5a39621cc5aad05d069b8d8643c9e3e47435471f40861e608e825711", + "password": "pbkdf2:sha256:150000$8BTKb0Yl$c040ad52566882eac8a429b09be47e180f71a026b55f41c4cde6c1b15cd44adc", "points": "13" }, { - "name": "Power Lift", - "email": "admin@powerlift.com", - "password": "pbkdf2:sha256:150000$YEgAXbf8$09b68fdd4ab198aaf541a4b3a3b680449fff602e43bbfb1e602e970e9acf7b4c", - "points": "5" + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": "pbkdf2:sha256:150000$0OZMEHCy$0b1641c4f5eb8fa277738155f1b2dd2cd54708eecdb2a0d907b904c7f40bc10d", + "points": "4" }, { "name": "She Lifts", "email": "kate@shelifts.co.uk", - "password": "pbkdf2:sha256:150000$g7r5qSMn$fecca97e2fcca7b093a4ca60789c4ebc0b6f4c0331aaaeee1ffa9f03a3404ad5", - "points": "12" + "password": "tp5_Tmn50", + "points": "12", + "booked_places": { + "Spring Festival": "7" + } }, { - "name": "Iron Temple", - "email": "admin@irontemple.com", - "password": "pbkdf2:sha256:150000$G0hKG0dK$434a98095c828d2735c63724fe9ae142a88d4aaba9489733c7fd614db655237a", - "points": "4" + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": "pbkdf2:sha256:150000$2d5MVukB$2d6fa64ec0e446a7f7e6f447ebc631c98aeec6c216df67750f0000a247b0d05f", + "points": "5" } ] } \ No newline at end of file diff --git a/competitions.json b/competitions.json index 8ef8e0d06..377789cb3 100644 --- a/competitions.json +++ b/competitions.json @@ -18,7 +18,7 @@ { "name": "Spring Festival", "date": "2026-07-27 10:00:00", - "number_of_places": "25" + "number_of_places": "20" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 04c61669c..9c74dd19f 100644 --- a/server.py +++ b/server.py @@ -10,7 +10,6 @@ def load_clubs(): list_of_clubs = json.load(c)['clubs'] return list_of_clubs - def load_competitions(): with open('competitions.json') as comps: list_of_competitions = json.load(comps)['competitions'] @@ -59,7 +58,8 @@ def add_club(name, email, password, points): save_clubs() def update_club_password(club, password): - club["password"] = password + hashed_password = generate_password_hash(password) + club["password"] = hashed_password save_clubs() return club @@ -129,13 +129,12 @@ def change_password(club): return redirect(url_for('change_password')) the_club = next((c for c in clubs if c['name'] == club), None) - hashed_password = generate_password_hash(club_password) if check_password_hash(the_club['password'], club_password): flash('Sorry, you have to type a new different password.') return render_template(template_name_or_list='change_password.html', club=the_club) - the_club = update_club_password(the_club, hashed_password) + the_club = update_club_password(the_club, club_password) if the_club: flash("Great! You have successfully changed your password.") diff --git a/unit_tests/__init__.py b/tests/__init__.py similarity index 100% rename from unit_tests/__init__.py rename to tests/__init__.py diff --git a/unit_tests/conftest.py b/tests/conftest.py similarity index 95% rename from unit_tests/conftest.py rename to tests/conftest.py index 62724e05a..ae97f997e 100644 --- a/unit_tests/conftest.py +++ b/tests/conftest.py @@ -112,6 +112,16 @@ def get_consistent_purchasing_data(): return data +@pytest.fixture +def get_new_club(): + club = { + "name": "New Club", + "email": "new@newclub.com", + "password": generate_password_hash("tp6_Tmn60"), + "points": "12" + } + return club + @pytest.fixture def get_inconsistent_purchasing_data(): competition = "Spring Festival" diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/unit_tests/test_app.py b/tests/integration_tests/test_app.py similarity index 63% rename from unit_tests/test_app.py rename to tests/integration_tests/test_app.py index dcdd98489..84c74f89f 100644 --- a/unit_tests/test_app.py +++ b/tests/integration_tests/test_app.py @@ -5,8 +5,7 @@ from flask import url_for -class TestApp: - +class TestIntegrationApp: @pytest.fixture(autouse=True) def setup(self, mocker, get_clubs, get_competitions): mocker.patch('server.clubs', get_clubs) @@ -15,50 +14,6 @@ def setup(self, mocker, get_clubs, get_competitions): mocker.patch('server.save_clubs') mocker.patch('server.save_competitions') - @staticmethod - def test_index_status_code_ok(client): - client_response = client.get('/') - assert client_response.status_code == 200 - - @staticmethod - def test_index_return_welcome(client): - client_response = client.get('/') - data = client_response.data.decode('utf-8') - - assert "Welcome to the GUDLFT Registration Portal!" in data - assert ('Please enter your secretary email and your password to continue ' - 'or sign up') in data - assert "Email:" in data - assert "Password:" in data - - @staticmethod - def test_index_mail_authentication_ok(get_credentials, client): - client_response = client.post('/showSummary', data=get_credentials) - assert client_response.status_code == 200 - - @staticmethod - def test_index_mail_authentication_returns_summary(client, get_credentials): - client_response = client.post('/showSummary', data=get_credentials) - data = client_response.data.decode('utf-8') - - assert "Welcome, kate@shelifts.co.uk" in data - assert "Spring Festival" in data - assert "Fall Classic" in data - assert "Points available: 12" in data - - @staticmethod - def test_index_mail_authentication_fail(client, get_unexisting_credentials): - client_response = client.post('/showSummary', data=get_unexisting_credentials) - data = client_response.data.decode('utf-8') - assert client_response.status_code == 404 - assert "Sorry, that email was not found." in data - - @staticmethod - def test_summary_logout_redirect_status_code_ok(client, get_credentials): - client.post('/showSummary', data=get_credentials) - logout_response = client.get('/logout') - assert logout_response.status_code == 302 - @staticmethod def test_summary_logout_redirect_returns_welcome(client, get_credentials): client.post('/showSummary', data=get_credentials) @@ -77,19 +32,6 @@ def test_summary_logout_redirect_returns_welcome(client, get_credentials): assert "Email:" in data assert "Password:" in data - @staticmethod - def test_booking_status_code_ok(client, - get_credentials, - get_existing_competition_and_club): - - client.post('/showSummary', data=get_credentials) - - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - - assert client_response.status_code == 200 - @staticmethod def test_booking_return_festival_page_booking(client, get_credentials, @@ -105,21 +47,6 @@ def test_booking_return_festival_page_booking(client, assert "Places available: " in data assert "How many places?" in data - @staticmethod - def test_good_purchasing_places_status_code_ok(client, - get_credentials, - get_existing_competition_and_club, - get_consistent_purchasing_data): - - client.post('/showSummary', data=get_credentials) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - - client_response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) - - assert client_response.status_code == 200 - @staticmethod def test_good_purchasing_places_returns_summary_page(client, get_credentials, @@ -157,27 +84,13 @@ def test_good_purchasing_places_returns_summary_page(client, f'{the_club_name_utf8}">Book Places\n' f'') + assert client_response.status_code == 200 assert (f"Great! Booking of {purchasing_data['places']} places for " f"{purchasing_data['competition']} competition complete!") in data assert f"Welcome, {the_club["email"]} " in data assert li in all_li_str assert f"Points available: {new_points}" in data - @staticmethod - def test_purchasing_places_not_enough_points_status_code_error(client, - get_credentials_2, - get_existing_competition_and_club_2, - get_inconsistent_purchasing_data): - - client.post('/showSummary', data=get_credentials_2) - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_2['competition'], - club=get_existing_competition_and_club_2['club'])) - - client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) - - assert client_response.status_code == 403 - @staticmethod def test_purchasing_places_not_enough_points_returns_sorry(client, get_credentials_2, @@ -195,26 +108,11 @@ def test_purchasing_places_not_enough_points_returns_sorry(client, client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) data = client_response.data.decode('utf-8') + assert client_response.status_code == 403 assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you do not have enough points to purchase." in data assert f"Points available: {the_club['points']}" in data - @staticmethod - def test_purchasing_places_over_12_places_status_code_error(client, - get_credentials, - get_existing_competition_and_club, - purchasing_over_12_places): - - client.post('/showSummary', data=get_credentials) - - client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club['competition'], - club=get_existing_competition_and_club['club'])) - - client_response = client.post('/purchasePlaces', data=purchasing_over_12_places) - - assert client_response.status_code == 403 - @staticmethod def test_purchasing_places_over_12_places_returns_sorry(client, get_credentials, @@ -234,6 +132,7 @@ def test_purchasing_places_over_12_places_returns_sorry(client, client_response = client.post('/purchasePlaces', data=purchasing_data) data = client_response.data.decode('utf-8') + assert client_response.status_code == 403 assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data @@ -261,23 +160,6 @@ def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data assert f"Points available: {club_points}" in data - @staticmethod - def test_purchasing_places_negative_number_status_code_error(client, - get_credentials, - purchasing_with_negative_places): - - client.post('/showSummary', data=get_credentials) - - purchasing_data = purchasing_with_negative_places - - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) - - client_response = client.post('/purchasePlaces', data=purchasing_data) - - assert client_response.status_code == 403 - @staticmethod def test_purchasing_places_negative_number_returns_sorry(client, get_credentials, @@ -297,23 +179,11 @@ def test_purchasing_places_negative_number_returns_sorry(client, client_response = client.post('/purchasePlaces', data=purchasing_data) data = client_response.data.decode('utf-8') + assert client_response.status_code == 403 assert f"Welcome, {the_club["email"]} " in data assert "Sorry, you should type a positive number." in data assert f"Points available: {club_points}" in data - @staticmethod - def test_purchasing_places_past_competitions_status_code_error(client, - get_credentials, - get_existing_competition_and_club_3): - - client.post('/showSummary', data=get_credentials) - - client_response = client.get(url_for(endpoint='book', - competition=get_existing_competition_and_club_3['competition'], - club=get_existing_competition_and_club_3['club'])) - - assert client_response.status_code == 403 - @staticmethod def test_purchasing_places_past_competitions_returns_sorry(client, get_credentials, @@ -327,24 +197,8 @@ def test_purchasing_places_past_competitions_returns_sorry(client, data = client_response.data.decode('utf-8') - assert "Sorry, this competition is outdated. Booking not possible." in data - - @staticmethod - def test_purchasing_places_over_available_status_code_error(client, - get_credentials, - purchasing_places_more_than_available): - - client.post('/showSummary', data=get_credentials) - - purchasing_data = purchasing_places_more_than_available - - client.get(url_for(endpoint='book', - competition=purchasing_data['competition'], - club=purchasing_data['club'])) - - client_response = client.post('/purchasePlaces', data=purchasing_data) - assert client_response.status_code == 403 + assert "Sorry, this competition is outdated. Booking not possible." in data @staticmethod def test_purchasing_places_over_available_returns_sorry(client, @@ -365,6 +219,7 @@ def test_purchasing_places_over_available_returns_sorry(client, client_response = client.post('/purchasePlaces', data=purchasing_data) data = client_response.data.decode('utf-8') + assert client_response.status_code == 403 assert f"Welcome, {the_club["email"]} " in data assert "Sorry, there are not enough places available for this competition." in data assert f"Points available: {club_points}" in data diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py new file mode 100644 index 000000000..5a4f121de --- /dev/null +++ b/tests/unit_tests/test_app.py @@ -0,0 +1,173 @@ +import pytest +import server + +from werkzeug.security import check_password_hash +from flask import url_for + + +class TestUnitApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + mocker.patch('server.save_clubs') + + @staticmethod + def test_index_status_code_ok(client): + client_response = client.get('/') + assert client_response.status_code == 200 + + @staticmethod + def test_index_return_welcome(client): + client_response = client.get('/') + data = client_response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_index_mail_authentication_ok(get_credentials, client): + client_response = client.post('/showSummary', data=get_credentials) + assert client_response.status_code == 200 + + @staticmethod + def test_index_mail_authentication_returns_summary(client, get_credentials): + client_response = client.post('/showSummary', data=get_credentials) + data = client_response.data.decode('utf-8') + + assert "Welcome, kate@shelifts.co.uk" in data + assert "Spring Festival" in data + assert "Fall Classic" in data + assert "Points available: 12" in data + + @staticmethod + def test_index_mail_authentication_fail(client, get_unexisting_credentials): + client_response = client.post('/showSummary', data=get_unexisting_credentials) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 404 + assert "Sorry, that email was not found." in data + + @staticmethod + def test_summary_logout_redirect_status_code_ok(client, get_credentials): + client.post('/showSummary', data=get_credentials) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 + + @staticmethod + def test_booking_status_code_ok(client, + get_credentials, + get_existing_competition_and_club): + + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + assert client_response.status_code == 200 + + @staticmethod + def test_signup_status_code_ok(client): + client_response = client.get('/signUp') + assert client_response.status_code == 200 + + @staticmethod + def test_signup_returns_welcome(client): + client_response = client.get('/signUp') + data = client_response.data.decode('utf-8') + assert "Welcome to the GUDLFT sign up page!" in data + assert ("Club name:" in data) + assert ("Email:" in data) + assert ("Password:" in data) + assert ("Confirm Password:" in data) + + @staticmethod + def test_change_password_status_code_ok(client): + client_response = client.get('/changePassword/Simply Lift') + assert client_response.status_code == 200 + + @staticmethod + def test_update_password_ok(get_credentials): + the_club = next((c for c in server.clubs if c['email'] == get_credentials["email"]), None) + the_club = server.update_club_password(the_club, "tp5_Tmn50") + + assert check_password_hash(the_club['password'], "tp5_Tmn50") + + @staticmethod + def test_update_password_fails(get_credentials): + the_club = next((c for c in server.clubs if c['email'] == get_credentials["email"]), None) + the_club = server.update_club_password(the_club, "tp5_Tmn50") + + assert not check_password_hash(the_club['password'], "tp6_Tmn60") + + @staticmethod + def test_add_club_ok(get_new_club): + server.add_club(name=get_new_club["name"], + email=get_new_club["email"], + password=get_new_club["password"], + points=get_new_club["points"]) + + assert get_new_club in server.clubs + + @staticmethod + def test_profile_ok(client): + client_response = client.get('/profile/Simply Lift') + assert client_response.status_code == 200 + + @staticmethod + def test_unexisting_profile_fails(client): + client_response = client.get('/profile/Unexisting Club') + assert client_response.status_code == 404 + + @staticmethod + def test_profile_returns_welcome(client): + client_response = client.get('/profile/Simply Lift') + data = client_response.data.decode('utf-8') + assert "Welcome, john@simplylift.co" in data + assert "Profile:" in data + assert "Name : Simply Lift" in data + assert "Email : john@simplylift.co" in data + assert "Points available: 13" in data + + @staticmethod + def test_update_booked_places_ok(get_existing_competition_and_club_2): + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in server.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + server.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert club['booked_places'][competition_name] == str(5) + + @staticmethod + def test_update_booked_places_fails(get_existing_competition_and_club_2): + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in server.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + server.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert not club['booked_places'][competition_name] == str(6) + + @staticmethod + def test_update_competition_available_places(get_existing_competition_and_club): + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in server.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + server.update_competition_available_places(competition=competition, places=5) + + assert competition['number_of_places'] == str(places_available - 5) + + @staticmethod + def test_update_competition_available_places_fails(get_existing_competition_and_club): + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in server.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + server.update_competition_available_places(competition=competition, places=5) + + assert not competition['number_of_places'] == str(places_available - 4) From 2441f12b4791161085affaa6883d660a1caf503f Mon Sep 17 00:00:00 2001 From: NM Date: Mon, 16 Mar 2026 15:01:19 +0100 Subject: [PATCH 16/18] Modification : protect routes with flask session --- clubs.json | 23 +++++++--------- server.py | 78 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/clubs.json b/clubs.json index 32d4ca476..dcffab73a 100644 --- a/clubs.json +++ b/clubs.json @@ -2,30 +2,27 @@ "clubs": [ { "name": "Simply Lift", - "email": "john@simplylift.co", - "password": "pbkdf2:sha256:150000$8BTKb0Yl$c040ad52566882eac8a429b09be47e180f71a026b55f41c4cde6c1b15cd44adc", + "email": "john@simplylift.co.uk", + "password": "pbkdf2:sha256:150000$hRdMG3j8$75089df7d666a0a804b2ab30832ee5ead0af0f9da1a16792aebb227f26736140", "points": "13" }, { "name": "Iron Temple", "email": "admin@irontemple.com", - "password": "pbkdf2:sha256:150000$0OZMEHCy$0b1641c4f5eb8fa277738155f1b2dd2cd54708eecdb2a0d907b904c7f40bc10d", + "password": "pbkdf2:sha256:150000$vRos0Pry$191eef653bbe787936e8329844c04c082ea74f91ed2da129ed0258a9cf4f0b62", "points": "4" }, - { - "name": "She Lifts", - "email": "kate@shelifts.co.uk", - "password": "tp5_Tmn50", - "points": "12", - "booked_places": { - "Spring Festival": "7" - } - }, { "name": "Power Lift", "email": "admin@powerlift.com", - "password": "pbkdf2:sha256:150000$2d5MVukB$2d6fa64ec0e446a7f7e6f447ebc631c98aeec6c216df67750f0000a247b0d05f", + "password": "pbkdf2:sha256:150000$NSg2p4vF$e817bb5a870bf4fea7e0687a9afe4b620a6ba2402d6190450d13407b00e39848", "points": "5" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "password": "pbkdf2:sha256:150000$8LvMXlh1$84c53ce7ad7b0adb694806647bc1fd7f2c748b5e3a41d5ad76f515c148e86252", + "points": "12" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 9c74dd19f..921cb4638 100644 --- a/server.py +++ b/server.py @@ -1,7 +1,7 @@ import json from datetime import datetime -from flask import Flask, render_template, request, redirect, flash, url_for +from flask import Flask, render_template, request, redirect, flash, url_for, session from werkzeug.security import generate_password_hash, check_password_hash @@ -73,13 +73,17 @@ def sign_up(): @app.route('/profile/', methods=['GET']) def profile(club): - the_club = next((c for c in clubs if c['name'] == club), None) + if "club" in session and session['club'] == club: + the_club = next((c for c in clubs if c['name'] == club), None) - if the_club is None: - flash("Sorry, that club was not found.") - return render_template(template_name_or_list="index.html", error="Club not found"), 404 + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Club not found"), 404 - return render_template(template_name_or_list='profile.html', club=the_club) + return render_template(template_name_or_list='profile.html', club=the_club) + + flash("Sorry, you are not allow to see that profile.") + return render_template(template_name_or_list='index.html') @app.route('/profile', methods=['POST']) def profile_post(): @@ -112,44 +116,52 @@ def profile_post(): @app.route('/changePassword/', methods=['GET', 'POST']) def change_password(club): - if request.method == 'GET': - the_club = next((c for c in clubs if c['name'] == club), None) + if "club" in session and session['club'] == club: + if request.method == 'GET': + the_club = next((c for c in clubs if c['name'] == club), None) - if the_club is None: - flash("Sorry, that club was not found.") - return render_template(template_name_or_list="index.html", error="Email not found"), 404 + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 - return render_template(template_name_or_list='change_password.html', club=the_club) - else: - club_password = request.form['password'] - club_password_confirmation = request.form['confirm_password'] + return render_template(template_name_or_list='change_password.html', club=the_club) + else: + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] - if club_password != club_password_confirmation: - flash('Sorry, passwords do not match') - return redirect(url_for('change_password')) + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('change_password')) - the_club = next((c for c in clubs if c['name'] == club), None) + the_club = next((c for c in clubs if c['name'] == club), None) - if check_password_hash(the_club['password'], club_password): - flash('Sorry, you have to type a new different password.') - return render_template(template_name_or_list='change_password.html', club=the_club) + if check_password_hash(the_club['password'], club_password): + flash('Sorry, you have to type a new different password.') + return render_template(template_name_or_list='change_password.html', club=the_club) - the_club = update_club_password(the_club, club_password) + the_club = update_club_password(the_club, club_password) - if the_club: - flash("Great! You have successfully changed your password.") - return render_template(template_name_or_list='profile.html', club=the_club) + if the_club: + flash("Great! You have successfully changed your password.") + return render_template(template_name_or_list='profile.html', club=the_club) - flash("Sorry, something went wrong. Please try again.") - return render_template(template_name_or_list='index.html') + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='index.html') + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html') @app.route('/showSummary/', methods=['GET']) def show_summary(club): - the_club = next((c for c in clubs if c['name'] == club), None) - return render_template(template_name_or_list='welcome.html', - club=the_club, - competitions=competitions) + if "club" in session and session['club'] == club: + the_club = next((c for c in clubs if c['name'] == club), None) + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions) + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html') @app.route('/showSummary', methods=['POST']) def show_summary_post(): @@ -163,6 +175,8 @@ def show_summary_post(): flash("Sorry, the password is incorrect.") return render_template(template_name_or_list="index.html",) + session["club"] = the_club["name"] + return render_template(template_name_or_list='welcome.html', club=the_club, competitions=competitions) From da2361e63ce8096fe13626c0bd2353512855f7aa Mon Sep 17 00:00:00 2001 From: NM Date: Mon, 16 Mar 2026 15:44:55 +0100 Subject: [PATCH 17/18] Modification : update tests --- server.py | 71 +++++++++++++++-------------- tests/conftest.py | 5 ++ tests/integration_tests/test_app.py | 10 ++-- tests/unit_tests/test_app.py | 31 ++++++++----- 4 files changed, 68 insertions(+), 49 deletions(-) diff --git a/server.py b/server.py index 921cb4638..8ca2b8786 100644 --- a/server.py +++ b/server.py @@ -83,7 +83,7 @@ def profile(club): return render_template(template_name_or_list='profile.html', club=the_club) flash("Sorry, you are not allow to see that profile.") - return render_template(template_name_or_list='index.html') + return render_template(template_name_or_list='index.html', error="Not allow"), 403 @app.route('/profile', methods=['POST']) def profile_post(): @@ -149,7 +149,7 @@ def change_password(club): return render_template(template_name_or_list='index.html') flash("Sorry, you are not allow to do this action.") - return render_template(template_name_or_list='index.html') + return render_template(template_name_or_list='index.html', error="Not allow"), 403 @app.route('/showSummary/', methods=['GET']) @@ -161,7 +161,7 @@ def show_summary(club): competitions=competitions) flash("Sorry, you are not allow to do this action.") - return render_template(template_name_or_list='index.html') + return render_template(template_name_or_list='index.html', error="Not allow"), 403 @app.route('/showSummary', methods=['POST']) def show_summary_post(): @@ -183,48 +183,51 @@ def show_summary_post(): @app.route('/book//') def book(competition, club): - found_club = [c for c in clubs if c['name'] == club][0] - found_competition = [c for c in competitions if c['name'] == competition][0] + if "club" in session and session['club'] == club: + found_club = [c for c in clubs if c['name'] == club][0] + found_competition = [c for c in competitions if c['name'] == competition][0] - now = datetime.now() + now = datetime.now() - competition_date = datetime.strptime(found_competition['date'], '%Y-%m-%d %H:%M:%S') + competition_date = datetime.strptime(found_competition['date'], '%Y-%m-%d %H:%M:%S') - error_message = "" - error_tag = "" + error_message = "" + error_tag = "" - the_competition = next((a_competition for a_competition in competitions - if a_competition['name'] == competition), None) - competition_places = int(the_competition['number_of_places']) + the_competition = next((a_competition for a_competition in competitions + if a_competition['name'] == competition), None) + competition_places = int(the_competition['number_of_places']) - if now > competition_date: - error_message = "Sorry, this competition is outdated. Booking not possible." - error_tag = "Outdated" + if now > competition_date: + error_message = "Sorry, this competition is outdated. Booking not possible." + error_tag = "Outdated" - elif competition_places == 0: - error_message = "Sorry, this competition is sold out. Booking not possible." - error_tag = "Sold out" + elif competition_places == 0: + error_message = "Sorry, this competition is sold out. Booking not possible." + error_tag = "Sold out" - if error_message and error_tag: - flash(error_message) + if error_message and error_tag: + flash(error_message) - the_club = next((a_club for a_club in clubs if a_club['name'] == club), None) + the_club = next((a_club for a_club in clubs if a_club['name'] == club), None) - return render_template(template_name_or_list='welcome.html', - club=the_club, - competitions=competitions, - error=error_tag), 403 + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions, + error=error_tag), 403 - if found_club and found_competition: - return render_template(template_name_or_list='booking.html', - club=found_club, - competition=found_competition) - else: - flash("Sorry, something went wrong. Please try again.") - return render_template(template_name_or_list='welcome.html', - club=club, - competitions=competitions) + if found_club and found_competition: + return render_template(template_name_or_list='booking.html', + club=found_club, + competition=found_competition) + else: + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 @app.route('/purchasePlaces',methods=['POST']) def purchase_places(): diff --git a/tests/conftest.py b/tests/conftest.py index ae97f997e..4792d2456 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -78,6 +78,11 @@ def get_credentials_2(): data = {"email": "admin@irontemple.com", "password": "tp2_Tmn29"} return data +@pytest.fixture +def get_credentials_3(): + data = {"email": "admin@powerlift.com", "password": "tp4_Tmn40"} + return data + @pytest.fixture def get_unexisting_credentials(): data = {"email": "nicolas.marie@unexisting.com", "password": "er45_shet"} diff --git a/tests/integration_tests/test_app.py b/tests/integration_tests/test_app.py index 84c74f89f..f9cd1ead1 100644 --- a/tests/integration_tests/test_app.py +++ b/tests/integration_tests/test_app.py @@ -186,10 +186,10 @@ def test_purchasing_places_negative_number_returns_sorry(client, @staticmethod def test_purchasing_places_past_competitions_returns_sorry(client, - get_credentials, + get_credentials_2, get_existing_competition_and_club_3): - client.post('/showSummary', data=get_credentials) + client.post('/showSummary', data=get_credentials_2) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_3['competition'], @@ -226,10 +226,12 @@ def test_purchasing_places_over_available_returns_sorry(client, @staticmethod def test_purchasing_places_sold_out_status_code_error(client, - get_credentials, + get_credentials_3, get_existing_competition_and_club_4): - client.post('/showSummary', data=get_credentials) + client.post('/showSummary', data=get_credentials_3) + print(get_credentials_3) + print(get_existing_competition_and_club_4) client_response = client.get(url_for(endpoint='book', competition=get_existing_competition_and_club_4['competition'], diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index 5a4f121de..4de37c381 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -2,7 +2,7 @@ import server from werkzeug.security import check_password_hash -from flask import url_for +from flask import url_for, session class TestUnitApp: @@ -86,7 +86,10 @@ def test_signup_returns_welcome(client): @staticmethod def test_change_password_status_code_ok(client): - client_response = client.get('/changePassword/Simply Lift') + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/changePassword/She Lifts') assert client_response.status_code == 200 @staticmethod @@ -114,23 +117,29 @@ def test_add_club_ok(get_new_club): @staticmethod def test_profile_ok(client): - client_response = client.get('/profile/Simply Lift') + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') assert client_response.status_code == 200 @staticmethod - def test_unexisting_profile_fails(client): - client_response = client.get('/profile/Unexisting Club') - assert client_response.status_code == 404 + def test_profile_without_authentication_fails(client): + client_response = client.get('/profile/Simply Lift') + assert client_response.status_code == 403 @staticmethod def test_profile_returns_welcome(client): - client_response = client.get('/profile/Simply Lift') + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') data = client_response.data.decode('utf-8') - assert "Welcome, john@simplylift.co" in data + assert "Welcome, kate@shelifts.co.uk" in data assert "Profile:" in data - assert "Name : Simply Lift" in data - assert "Email : john@simplylift.co" in data - assert "Points available: 13" in data + assert "Name : She Lifts" in data + assert "Email : kate@shelifts.co.uk" in data + assert "Points available: 12" in data @staticmethod def test_update_booked_places_ok(get_existing_competition_and_club_2): From 05a72dc6b60e3112b536cd221f9d6c03a0018d7d Mon Sep 17 00:00:00 2001 From: NM Date: Mon, 16 Mar 2026 17:47:16 +0100 Subject: [PATCH 18/18] File change and modification : implement points board feature in issue #7, add new template, new route and css --- server.py | 15 +++++++++++---- static/styles.css | 37 +++++++++++++++++++++++++++++++++++++ templates/index.html | 3 +++ templates/points_board.html | 31 +++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 static/styles.css create mode 100644 templates/points_board.html diff --git a/server.py b/server.py index 8ca2b8786..877cb69d8 100644 --- a/server.py +++ b/server.py @@ -276,10 +276,17 @@ def purchase_places(): return render_template(template_name_or_list='welcome.html', club=club, competitions=competitions) - - -# TODO: Add route for points display - +@app.route('/pointsBoard') +def points_board(): + clubs_for_board=[] + for club in clubs: + if clubs.index(club) %2 == 0: + club["color"] = "#cccccc" + else: + club["color"] = "#aaaaaa" + clubs_for_board.append(club) + print(clubs_for_board) + return render_template(template_name_or_list='points_board.html', clubs=clubs_for_board) @app.route('/logout') def logout(): diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 000000000..cb24cfe32 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,37 @@ +.board{ + margin-left: 50px; + display: block; + border:1px black solid; + border-radius: 10px; + width: fit-content; +} + +.board table{ + width:400px; +} + +.board table tr{ + width: 100%; +} + +.board table thead div{ + background-color: #000000; + color: white; + font-weight: bold; +} + +.board table td{ + color: black; +} + +.board table td p{ + text-align: center; +} + +.board .cell{ + border-radius: 10px; + align-items: center; + display: flex; + justify-content: center; + padding: 5px 10px; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 5b2a76631..b00fd6206 100644 --- a/templates/index.html +++ b/templates/index.html @@ -29,5 +29,8 @@

      Welcome to the GUDLFT Registration Portal!

      +
      +
      + To see the clubs points board : click here \ No newline at end of file diff --git a/templates/points_board.html b/templates/points_board.html new file mode 100644 index 000000000..e3f4df107 --- /dev/null +++ b/templates/points_board.html @@ -0,0 +1,31 @@ + + + + + GUDLFT Registration + + + +

      Welcome to the GUDLFT clubs points board!

      +
      +

      ⯈ Here is the board for all the clubs and their points.

      +
      + + + + + + {% for club in clubs %} + + + + + {% endfor %} +
      Club
      Points
      {{ club.name }}
      {{ club.points }}
      +
      +
      +
      + To continue into the application, please log in or sign up +
      + + \ No newline at end of file