Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
32569ef
Install dependencies and setup requirements and venv
devogs Sep 8, 2025
6cfd6d4
Merge pull request #1 from devogs/chore/setup-environment
devogs Sep 8, 2025
d4e2057
Create test to check missing email resiliency and flash message is pr…
devogs Sep 8, 2025
72c2c6e
Add an __init__.py file to treat the parent directory as a package
devogs Sep 8, 2025
946b053
Fix: email missing resiliency and flash message in index
devogs Sep 8, 2025
1dabf6b
Merge pull request #2 from devogs/bug/fix-unknown-email-crash
devogs Sep 8, 2025
1bd89f7
FEAT: Add TDD test for purchase flow resiliency
devogs Sep 8, 2025
eed9606
FIX: Implement purchase logic to prevent over-booking and improve UX
devogs Sep 8, 2025
9d88825
Merge pull request #3 from devogs/bug/prevent-overspending-points
devogs Sep 8, 2025
8e52a47
FEAT: Add TDD test for 12-place booking limit
devogs Sep 8, 2025
17f337b
FIX: Implement 12-place booking limit
devogs Sep 8, 2025
3892926
Merge pull request #4 from devogs/bug/fix-booking-limit-12
devogs Sep 8, 2025
5d913a8
FIX: Add test for booking places in past competitions
devogs Sep 8, 2025
eeed8e2
FEAT: Added datetime logic to prevent past bookings
devogs Sep 8, 2025
62e4df3
Merge pull request #5 from devogs/bug/fix-past-competition-booking
devogs Sep 8, 2025
d5e1768
FIX: Add test for point updates are not reflected
devogs Sep 9, 2025
8d6a8cf
FEAT: Add data persistence and comprehensive purchase validation
devogs Sep 9, 2025
ae1783a
Merge pull request #6 from devogs/bug/fix-point-not_updating
devogs Sep 9, 2025
2b99fd3
FEAT: Add tests for points display board
devogs Sep 9, 2025
c791581
FEAT: Implement club points display board
devogs Sep 9, 2025
bf7a67c
Merge pull request #7 from devogs/feat/points-display-board
devogs Sep 9, 2025
34a1d83
FEAT: Add test for clubs should not be able to book more than the com…
devogs Sep 9, 2025
d4fbcf4
FIX: Add a fix so clubs are not able to book more than the competitio…
devogs Sep 9, 2025
2919e36
Merge pull request #8 from devogs/bug/fix-competition-places-limit
devogs Sep 9, 2025
a0b0538
Add integration test
devogs Sep 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ bin
include
lib
.Python
tests/
.envrc
__pycache__
__pycache__
venv/
.coverage
5 changes: 5 additions & 0 deletions competitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"name": "Fall Classic",
"date": "2020-10-22 13:30:00",
"numberOfPlaces": "13"
},
{
"name": "Winter Classic",
"date": "2025-12-25 13:30:00",
"numberOfPlaces": "18"
}
]
}
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
filterwarnings =
ignore::DeprecationWarning
47 changes: 41 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
Werkzeug==1.0.1
bidict==0.23.1
blinker==1.9.0
Brotli==1.1.0
certifi==2025.8.3
charset-normalizer==3.4.3
click==8.2.1
ConfigArgParse==1.7.1
coverage==7.10.6
Flask==3.1.2
flask-cors==6.0.1
Flask-Login==0.6.3
gevent==25.5.1
geventhttpclient==2.3.4
greenlet==3.2.4
h11==0.16.0
idna==3.10
iniconfig==2.1.0
itsdangerous==2.2.0
Jinja2==3.1.6
locust==2.40.0
locust-cloud==1.26.3
MarkupSafe==3.0.2
msgpack==1.1.1
packaging==25.0
platformdirs==4.4.0
pluggy==1.6.0
psutil==7.0.0
Pygments==2.19.2
pytest==8.4.1
python-engineio==4.12.2
python-socketio==5.13.0
pyzmq==27.0.2
requests==2.32.5
setuptools==80.9.0
simple-websocket==1.1.0
urllib3==2.5.0
websocket-client==1.8.0
Werkzeug==3.1.3
wsproto==1.2.0
zope.event==5.1.1
zope.interface==7.2
77 changes: 69 additions & 8 deletions server.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import json
from flask import Flask,render_template,request,redirect,flash,url_for
from datetime import datetime
from flask import Flask,render_template,request,redirect,flash,url_for,session


def loadClubs():
with open('clubs.json') as c:
listOfClubs = json.load(c)['clubs']
return listOfClubs


def loadCompetitions():
with open('competitions.json') as comps:
listOfCompetitions = json.load(comps)['competitions']
return listOfCompetitions

def saveClubs(clubs_data):
with open('clubs.json', 'w') as c:
json.dump({"clubs": clubs_data}, c, indent=4)

def saveCompetitions(competitions_data):
with open('competitions.json', 'w') as comps:
json.dump({"competitions": competitions_data}, comps, indent=4)


app = Flask(__name__)
app.secret_key = 'something_special'
Expand All @@ -24,10 +32,32 @@ def loadCompetitions():
def index():
return render_template('index.html')

@app.route('/showSummary',methods=['POST'])

@app.route('/showSummary',methods=['GET', '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)
if request.method == 'POST':
found_clubs = [club for club in clubs if club['email'] == request.form['email']]
if found_clubs:
club = found_clubs[0]
session['club_email'] = club['email']
return render_template('welcome.html',club=club,competitions=competitions)
else:
flash("Sorry, that email was not found.")
session.pop('club_email', None)
return redirect(url_for('index'))

elif 'club_email' in session:
club_email = session['club_email']
found_clubs = [c for c in clubs if c['email'] == club_email]
if found_clubs:
club = found_clubs[0]
return render_template('welcome.html', club=club, competitions=competitions)
else:
session.pop('club_email', None)
flash("Your club's data was not found. Please log in again.")
return redirect(url_for('index'))

return redirect(url_for('index'))


@app.route('/book/<competition>/<club>')
Expand All @@ -46,12 +76,43 @@ def purchasePlaces():
competition = [c for c in competitions if c['name'] == request.form['competition']][0]
club = [c for c in clubs if c['name'] == request.form['club']][0]
placesRequired = int(request.form['places'])
competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired

# FIX for Bug 4: Block bookings for past competitions
competition_date = datetime.strptime(competition['date'], "%Y-%m-%d %H:%M:%S")
if competition_date < datetime.now():
flash("Booking for past competitions is not allowed.")
return redirect(url_for('book', competition=competition['name'], club=club['name']))

# FIX for Bug 3: Block bookings over 12 places
if placesRequired > 12:
flash("You cannot book more than 12 places per competition.")
return redirect(url_for('book', competition=competition['name'], club=club['name']))

# Check if competition has enough places
if int(competition['numberOfPlaces']) < placesRequired:
flash(f"Not enough places available in this competition. Only {competition['numberOfPlaces']} places left.")
return redirect(url_for('book', competition=competition['name'], club=club['name']))

# Check if club has enough points
if int(club['points']) < placesRequired:
flash(f"You do not have enough points to book {placesRequired} places. You currently have {club['points']} points.")
return redirect(url_for('book', competition=competition['name'], club=club['name']))

# All checks passed, proceed with purchase
competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired
club['points'] = str(int(club['points']) - placesRequired)

saveClubs(clubs)
saveCompetitions(competitions)

flash('Great-booking complete!')
return render_template('welcome.html', club=club, competitions=competitions)


# TODO: Add route for points display
@app.route('/pointsDisplay')
def pointsDisplay():
# Sort clubs by points in descending order
sorted_clubs = sorted(clubs, key=lambda c: int(c['points']), reverse=True)
return render_template('points.html', clubs=sorted_clubs)


@app.route('/logout')
Expand Down
17 changes: 16 additions & 1 deletion templates/booking.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Booking for {{competition['name']}} || GUDLFT</title>
<style>
.error-message {
color: red;
}
</style>
</head>
<body>
<h2>{{competition['name']}}</h2>
Places available: {{competition['numberOfPlaces']}}
<form action="/purchasePlaces" method="post">
<input type="hidden" name="club" value="{{club['name']}}">
<input type="hidden" name="competition" value="{{competition['name']}}">
<label for="places">How many places?</label><input type="number" name="places" id=""/>
<label for="places">How many places?</label><input type="number" name="places" id="places"/>
<button type="submit">Book</button>
</form>

{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class="flashes">
{% for message in messages %}
<li class="error-message">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</body>
</html>
13 changes: 13 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GUDLFT Registration</title>
<style>
.error-message {
color: red;
}
</style>
</head>
<body>
<h1>Welcome to the GUDLFT Registration Portal!</h1>
Expand All @@ -12,5 +17,13 @@ <h1>Welcome to the GUDLFT Registration Portal!</h1>
<input type="email" name="email" id=""/>
<button type="submit">Enter</button>
</form>

{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<p class="error-message">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</body>
</html>
28 changes: 28 additions & 0 deletions templates/points.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Points Display</title>
</head>
<body>
<h1>Club Points Board</h1>
<table>
<thead>
<tr>
<th>Club</th>
<th>Points</th>
</tr>
</thead>
<tbody>
{% for club in clubs %}
<tr>
<td>{{ club.name }}</td>
<td>{{ club.points }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<br>
<a href="{{ url_for('showSummary') }}">Back to Welcome</a>
</body>
</html>
1 change: 1 addition & 0 deletions templates/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</head>
<body>
<h2>Welcome, {{club['email']}} </h2><a href="{{url_for('logout')}}">Logout</a>
<a href="{{ url_for('pointsDisplay') }}">View Club Points</a>

{% with messages = get_flashed_messages()%}
{% if messages %}
Expand Down
Empty file added tests/__init__.py
Empty file.
43 changes: 43 additions & 0 deletions tests/test_board.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest
from server import app
from unittest.mock import patch


@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client


# Mock data to simulate the clubs with different point values
MOCK_CLUBS_DATA = [
{'name': 'Club Alpha', 'email': '[email protected]', 'points': '5'},
{'name': 'Club Beta', 'email': '[email protected]', 'points': '20'},
{'name': 'Club Gamma', 'email': '[email protected]', 'points': '10'}
]


@patch('server.clubs', MOCK_CLUBS_DATA)
def test_points_display_board(client):
"""
Test that the points display board loads and correctly sorts clubs by points.
"""
response = client.get('/pointsDisplay')

# Assert that the page loads successfully
assert response.status_code == 200
assert b"Club Points Board" in response.data

# Assert that the clubs are displayed in the correct sorted order (descending)
data = response.data.decode('utf-8')
assert "Club Beta" in data
assert "Club Gamma" in data
assert "Club Alpha" in data

# A more robust check for order: find the index of each club name in the response.
beta_index = data.find("Club Beta")
gamma_index = data.find("Club Gamma")
alpha_index = data.find("Club Alpha")

assert beta_index < gamma_index < alpha_index
Loading