diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 000000000..10950ca46 --- /dev/null +++ b/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=server +FLASK_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2cba99d87..f577272a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.venv \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 139affa05..7a3b0bf04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,13 @@ click==7.1.2 +colorama==0.4.6 Flask==1.1.2 +iniconfig==2.3.0 itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 +packaging==25.0 +pluggy==1.6.0 +Pygments==2.19.2 +pytest==8.4.2 +python-dotenv==1.1.1 Werkzeug==1.0.1 diff --git a/server.py b/server.py index 4084baeac..e53d984e2 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,6 @@ import json from flask import Flask,render_template,request,redirect,flash,url_for +from datetime import datetime def loadClubs(): @@ -22,20 +23,32 @@ def loadCompetitions(): @app.route('/') def index(): - return render_template('index.html') + return render_template('index.html', clubs=clubs) @app.route('/showSummary',methods=['POST']) def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] - return render_template('welcome.html',club=club,competitions=competitions) + email = request.form.get('email', '') + email = email.strip() + club = next((c for c in clubs if c.get('email', '').strip() == email), None) + if not email or not club: + flash("Sorry, that email was not found, please try again.") + return render_template('index.html') + return render_template('welcome.html', club=club, competitions=competitions) @app.route('/book//') def book(competition,club): foundClub = [c for c in clubs if c['name'] == club][0] foundCompetition = [c for c in competitions if c['name'] == competition][0] + + competition_date = datetime.strptime(foundCompetition['date'], "%Y-%m-%d %H:%M:%S") + if competition_date < datetime.now(): + flash("This competition has already taken place, booking is not allowed.") + return render_template('welcome.html', club=foundClub, competitions=competitions) + if foundClub and foundCompetition: return render_template('booking.html',club=foundClub,competition=foundCompetition) + else: flash("Something went wrong-please try again") return render_template('welcome.html', club=club, competitions=competitions) @@ -43,11 +56,24 @@ def book(competition,club): @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): + MAX_BOOKING = 12 competition = [c for c in competitions if c['name'] == request.form['competition']][0] club = [c for c in clubs if c['name'] == request.form['club']][0] placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired - flash('Great-booking complete!') + club_points = int(club['points']) + + if placesRequired > club_points: + flash("Cannot book more places than club points.") + return render_template('welcome.html', club=club, competitions=competitions) + + + if placesRequired > MAX_BOOKING: + flash(f"Cannot book more than {MAX_BOOKING} places for this competition.") + return render_template('welcome.html', club=club, competitions=competitions) + + competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired + club['points'] = club_points - placesRequired + flash('Great - booking complete!') return render_template('welcome.html', club=club, competitions=competitions) diff --git a/templates/index.html b/templates/index.html index 926526b7d..3be9546c9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,10 +7,36 @@

Welcome to the GUDLFT Registration Portal!

Please enter your secretary email to continue: + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %}
+

Clubs Points Summary

+ + + + + + + + + {% for club in clubs %} + + + + + {% endfor %} + +
ClubPoints
{{ club['name'] }}{{ club['points'] }}
\ No newline at end of file diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 000000000..7153001ce --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,23 @@ +import os +import sys +import pytest + + +"""Pytest configuration file to set up the testing environment. +This file adds the project root directory to sys.path to ensure that +""" + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +if ROOT_DIR not in sys.path: + sys.path.insert(0, ROOT_DIR) + +from server import app + +@pytest.fixture +def client(): + """Flask test client fixture. + Provides a test client for the Flask application defined in server.py. + """ + app.config['TESTING'] = True + with app.test_client() as client: + yield client \ No newline at end of file diff --git a/tests/unit/test_board_with_clubs_and_their_points.py b/tests/unit/test_board_with_clubs_and_their_points.py new file mode 100644 index 000000000..eb832fae4 --- /dev/null +++ b/tests/unit/test_board_with_clubs_and_their_points.py @@ -0,0 +1,21 @@ +import server + +""" +Unit test file to check if the index page displays the clubs and their points correctly +Test 1: The page loads successfully (status code 200) +Test 2: Each club name and its points appear in the HTML +""" + +def test_index_page_loads(client): + response = client.get('/') + assert response.status_code == 200 + assert b"Clubs Points Summary" in response.data + + +def test_index_page_displays_clubs_points(client): + response = client.get('/') + data = response.data.decode() + + for club in server.clubs: + assert club["name"] in data + assert club["points"] in data diff --git a/tests/unit/test_book_more_than_12_places.py b/tests/unit/test_book_more_than_12_places.py new file mode 100644 index 000000000..e238b9569 --- /dev/null +++ b/tests/unit/test_book_more_than_12_places.py @@ -0,0 +1,47 @@ +import server + +""" +Unit test file to check that clubs cannot book more than 12 places. + +Test 1: Club A has 13 points and competition 1 has 25 places. + - Book 12 places + - status code 200 + - message: "Great - booking complete!" + - competition places decreased + +Test 2: Club A has 13 points and competition 1 has 25 places. + - Book 13 places + - status code 200 + - message contains "Cannot book more than" + - competition places unchanged +""" + + +def test_book_12_places_allowed(client): + server.clubs = [{"name": "Club A", "points": "13"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "25"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '12' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 13 + + +def test_cannot_book_more_than_12_places(client): + server.clubs = [{"name": "Club A", "points": "13"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "25"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '13' + }) + + assert response.status_code == 200 + assert b"Cannot book more than" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 25 \ No newline at end of file diff --git a/tests/unit/test_book_past_competition.py b/tests/unit/test_book_past_competition.py new file mode 100644 index 000000000..7436ca6ff --- /dev/null +++ b/tests/unit/test_book_past_competition.py @@ -0,0 +1,34 @@ +import server +from datetime import datetime, timedelta + +""" +Unit test file to check that booking is not allowed for past competitions + +Test 1: A competition in the future -> user can access booking page + - status code 200 + - page contains "How many places?" +Test 2: A competition in the past -> booking is refused. + - status code 200 + - message contains "This competition has already taken place" +""" + +def test_can_book_future_competition(client): + future_date = (datetime.now() + timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S") + server.competitions = [{"name": "Future Comp", "date": future_date, "numberOfPlaces": "10"}] + server.clubs = [{"name": "Club A", "email": "a@a.com", "points": "10"}] + + response = client.get('/book/Future Comp/Club A') + + assert response.status_code == 200 + assert b"How many places?" in response.data + + +def test_cannot_book_past_competition(client): + past_date = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S") + server.competitions = [{"name": "Old Comp", "date": past_date, "numberOfPlaces": "10"}] + server.clubs = [{"name": "Club A", "email": "a@a.com", "points": "10"}] + + response = client.get('/book/Old Comp/Club A') + + assert response.status_code == 200 + assert b"This competition has already taken place" in response.data diff --git a/tests/unit/test_book_place_with_enough_point.py b/tests/unit/test_book_place_with_enough_point.py new file mode 100644 index 000000000..efe3df3dd --- /dev/null +++ b/tests/unit/test_book_place_with_enough_point.py @@ -0,0 +1,44 @@ +import server + +""" +Unit test file for the place booking feature. +The purpose is to verify that booking places is only possible with sufficient points. + +Test 1: the club has 4 points and wants to book 4 places — booking accepted : + - status code 200 + - confirmation message + - remaining places updated correctly. +Test 2: the club has 4 points and wants to book 5 places — booking refused : + - status code 200 + - error message + - remaining places unchanged +""" + + +def test_book_places_with_enough_points(client): + server.clubs = [{"name": "Club A", "points": "4"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "5"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '4' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 1 + +def test_book_places_without_enough_points(client): + server.clubs = [{"name": "Club B", "points": "4"}] + server.competitions = [{"name": "Comp 2", "numberOfPlaces": "5"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 2', + 'club': 'Club B', + 'places': '5' + }) + + assert response.status_code == 200 + assert b"Cannot book more places than club points." in response.data + assert int(server.competitions[0]['numberOfPlaces']) == 5 \ No newline at end of file diff --git a/tests/unit/test_booking_decrease_club_points.py b/tests/unit/test_booking_decrease_club_points.py new file mode 100644 index 000000000..d1c54da16 --- /dev/null +++ b/tests/unit/test_booking_decrease_club_points.py @@ -0,0 +1,27 @@ +import server + +""" +Unit test file to verify that club points are correctly updated after booking. + +Test 1: Club has 10 points, books 3 places, points should decrease by 3. + - status code 200 + - success message "Great - booking complete!" + - club points decrease by 3 + +""" + + +def test_club_points_decrease_after_booking(client): + server.clubs = [{"name": "Club A", "points": "10"}] + server.competitions = [{"name": "Comp 1", "numberOfPlaces": "20"}] + + response = client.post('/purchasePlaces', data={ + 'competition': 'Comp 1', + 'club': 'Club A', + 'places': '3' + }) + + assert response.status_code == 200 + assert b"Great - booking complete!" in response.data + assert int(server.clubs[0]['points']) == 7 + diff --git a/tests/unit/test_check_email_show_summary.py b/tests/unit/test_check_email_show_summary.py new file mode 100644 index 000000000..4e6428c9b --- /dev/null +++ b/tests/unit/test_check_email_show_summary.py @@ -0,0 +1,27 @@ +import pytest + + +"""Unit tests for the /showSummary route in server.py. +test1 : Valid email should return 200 and welcome message +test2 : Unknown email should return 200 and error message +test3 : Invalid email (empty or whitespace) should return 200 and error message + +""" + + +def test_show_summary_with_valid_email(client): + response = client.post('/showSummary', data={'email': 'john@simplylift.co'}) + assert response.status_code == 200 + assert b'Welcome' in response.data + + +def test_show_summary_with_unknown_email(client): + response = client.post('/showSummary', data={'email': 'unknown@example.com'}) + assert response.status_code == 200 + assert b"Sorry, that email was not found" in response.data + + +def test_show_summary_with_invalid_email(client): + response = client.post('/showSummary', data={'email': ' '}) + assert response.status_code == 200 + assert b"Sorry, that email was not found" in response.data \ No newline at end of file