Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .flaskenv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLASK_APP=server
FLASK_ENV=development
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ bin
include
lib
.Python
tests/
.envrc
__pycache__
__pycache__
.venv
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
100 changes: 85 additions & 15 deletions server.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import json
from flask import Flask,render_template,request,redirect,flash,url_for

from datetime import datetime
from flask import session
import logging


def load_json_data(filename: str, key: str):
try:
with open(filename) as f:
data = json.load(f)
if key not in data:
logging.warning(f"Key '{key}' not found in {filename}")
return []
return data[key]
except (FileNotFoundError, json.JSONDecodeError) as e:
logging.warning(f"Unable to load {filename}: {e}")
return []

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

return load_json_data("clubs.json", "clubs")

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


app = Flask(__name__)
Expand All @@ -22,32 +32,91 @@ 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', '').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')
#
session['club_name'] = club['name']
return render_template('welcome.html', club=club, competitions=competitions)


@app.route('/book/<competition>/<club>')
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)


@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_name = session.get('club_name')
if not club_name:
flash("You must be logged in to book places.")
return render_template('index.html', clubs=clubs)

club = next((c for c in clubs if c['name'] == club_name), None)
if not club:
flash("Club not found in session.")
return render_template('index.html', clubs=clubs)


places = request.form.get('places')
if not places:
flash("Please enter a number of places.")
return render_template('welcome.html', club=club, competitions=competitions)

# même si on a vérifié dans le formulaire sur le HTML avec un input de type "number",
# on doit quand même gérer le cas où un utilisateur malveillant envoie une valeur non numérique
# en modifiant via les outils de développement du navigateur.
try:
placesRequired = int(places)
except ValueError:
flash("Invalid number of places.")
return render_template('welcome.html', club=club, competitions=competitions)

club_points = int(club['points'])

if placesRequired <= 0:
flash("Invalid number of places.")
return render_template('welcome.html', club=club, competitions=competitions)

if placesRequired > club_points:
flash("Cannot book more places than club points.")
return render_template('welcome.html', club=club, competitions=competitions)

if placesRequired > int(competition['numberOfPlaces']):
flash("Cannot book more places than available.")
return render_template('welcome.html', club=club, competitions=competitions)

already_booked = int(competition.get('booked_by', {}).get(club['name'], 0))

if placesRequired > MAX_BOOKING or already_booked + placesRequired > MAX_BOOKING:
flash(f"Cannot book more than {MAX_BOOKING} places per club for this competition.")
return render_template('welcome.html', club=club, competitions=competitions)

competition.setdefault('booked_by', {})[club['name']] = already_booked + placesRequired
competition['numberOfPlaces'] = int(competition['numberOfPlaces']) - placesRequired
club['points'] = club_points - placesRequired
flash('Great - booking complete!')
return render_template('welcome.html', club=club, competitions=competitions)


Expand All @@ -56,4 +125,5 @@ def purchasePlaces():

@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('index'))
1 change: 0 additions & 1 deletion templates/booking.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
<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=""/>
<button type="submit">Book</button>
Expand Down
26 changes: 26 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,36 @@
<body>
<h1>Welcome to the GUDLFT Registration Portal!</h1>
Please enter your secretary email to continue:
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class="flashes" style="color: red;">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<form action="showSummary" method="post">
<label for="email">Email:</label>
<input type="email" name="email" id=""/>
<button type="submit">Enter</button>
</form>
<h2>Clubs Points Summary</h2>
<table style="border-collapse: collapse; width: 30%; background-color: #f5f5f5; color: #333;">
<thead>
<tr style="background-color: #e0e0e0;">
<th style="border: 1px solid #ccc; padding: 6px;">Club</th>
<th style="border: 1px solid #ccc; padding: 6px;">Points</th>
</tr>
</thead>
<tbody>
{% for club in clubs %}
<tr style="text-align: center;">
<td style="border: 1px solid #ccc; padding: 6px;">{{ club['name'] }}</td>
<td style="border: 1px solid #ccc; padding: 6px;">{{ club['points'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
22 changes: 22 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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():
app.config['TESTING'] = True
with app.test_client() as client:
with client.session_transaction() as session:
session['club_name'] = "Club A"
yield client
21 changes: 21 additions & 0 deletions tests/unit/test_board_with_clubs_and_their_points.py
Original file line number Diff line number Diff line change
@@ -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
81 changes: 81 additions & 0 deletions tests/unit/test_book_more_than_12_places.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import server

"""
Unit test file to check that clubs cannot book more than 12 places in total.

Test 1: Club A books exactly 12 places -> success
Test 2: Club A tries to book 13 places -> rejected
Test 3: Club A has already booked 6 places, tries to book 7 more -> rejected (cumulative > 12)
Test 4: Club A has already booked 6 places, tries to book 5 more -> accepted (cumulative = 11)
"""


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


def test_cannot_exceed_12_places_cumulatively(client):
server.clubs = [{"name": "Club A", "points": "13"}]
server.competitions = [{
"name": "Comp 1",
"numberOfPlaces": "25",
"booked_by": {"Club A": 6}
}]

response = client.post('/purchasePlaces', data={
'competition': 'Comp 1',
'club': 'Club A',
'places': '7'
})

assert response.status_code == 200
assert b"Cannot book more than" in response.data
assert int(server.competitions[0]['numberOfPlaces']) == 25
assert int(server.clubs[0]['points']) == 13


def test_booking_allowed_when_cumulative_under_limit(client):
server.clubs = [{"name": "Club A", "points": "13"}]
server.competitions = [{
"name": "Comp 1",
"numberOfPlaces": "25",
"booked_by": {"Club A": 6}
}]

response = client.post('/purchasePlaces', data={
'competition': 'Comp 1',
'club': 'Club A',
'places': '5'
})

assert response.status_code == 200
assert b"Great - booking complete!" in response.data
assert int(server.competitions[0]['numberOfPlaces']) == 20
assert int(server.clubs[0]['points']) == 8
assert server.competitions[0]['booked_by']['Club A'] == 11
20 changes: 20 additions & 0 deletions tests/unit/test_book_more_than_places_available.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import server

"""
Unit test to verify that users cannot book more places than available.
"""

def test_booking_more_places_than_available(client):
server.clubs = [{"name": "Club A", "points": "20"}]
server.competitions = [{"name": "Comp 1", "numberOfPlaces": "5"}]

response = client.post('/purchasePlaces', data={
'competition': 'Comp 1',
'club': 'Club A',
'places': '10'
})

assert response.status_code == 200
assert b"Cannot book more places than available." in response.data
assert int(server.competitions[0]['numberOfPlaces']) == 5
assert int(server.clubs[0]['points']) == 20
34 changes: 34 additions & 0 deletions tests/unit/test_book_past_competition.py
Original file line number Diff line number Diff line change
@@ -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
Loading