Skip to content

Bugfix/deduct reserved places from competition #284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e443b17
Refactor server.py for improved readability and environment variable …
wilodorico May 16, 2025
45eb5bb
Add test configuration and fixtures; update .gitignore to include .fl…
wilodorico May 16, 2025
740c0b8
Add test for handling without email in login
wilodorico May 16, 2025
d60ef20
Add condition if email empty redirect index and flash message
wilodorico May 16, 2025
cc6c04b
display message flash in template index.html
wilodorico May 16, 2025
4d29e2d
Add test with non existent email in base and rename test for clarity
wilodorico May 16, 2025
e1c17a4
add try except to catch the IndexError exception and add flash message
wilodorico May 16, 2025
4b0de5f
Add test for loading valid JSON file return correct json
wilodorico May 16, 2025
032f3a9
Add JSONServices class with static method to load JSON files
wilodorico May 16, 2025
4203947
add test for non existent file return exception FileNotFoundError wit…
wilodorico May 16, 2025
2723254
Add error handling for FileNotFoundError in JSONServices.load method
wilodorico May 16, 2025
6e55e5f
Refacto code to use JsonService for load data clubs and competitions
wilodorico May 16, 2025
1528d11
Added test for club reservations exceeding their points limit
wilodorico May 23, 2025
c8f4b6c
Add a condition to check that the number of places requested does not…
wilodorico May 23, 2025
9637d7a
Display flash message in booking.html
wilodorico May 23, 2025
2e37cb2
Add LIMITED_BOOKING_PLACE constant and add test for club cannot book …
wilodorico May 23, 2025
5822bfc
Add validation for booking limit in purchasePlaces function
wilodorico May 23, 2025
69264b2
Refactor test setup by creating helper functions for mock clubs and c…
wilodorico May 23, 2025
c2e2d09
Add save method and corresponding test for JSONServices class
wilodorico Jun 6, 2025
f138791
Deduct competition places when a club books and save updated competit…
wilodorico Jun 6, 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__
.env
.flake8
1 change: 1 addition & 0 deletions globals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LIMITED_BOOKING_PLACE = 12
16 changes: 16 additions & 0 deletions json_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import json


class JSONServices:
@staticmethod
def load(filename: str):
try:
with open(filename) as file:
return json.load(file)
except FileNotFoundError:
raise FileNotFoundError(f"File {filename} not found.")

@staticmethod
def save(file_name, data):
with open(file_name, "w") as file:
json.dump(data, file, indent=4)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
Werkzeug==1.0.1
pytest==8.3.5
python-dotenv==1.1.0
106 changes: 69 additions & 37 deletions server.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,91 @@
import json
from flask import Flask,render_template,request,redirect,flash,url_for
import os

from dotenv import load_dotenv
from flask import Flask, flash, redirect, render_template, request, url_for

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


def loadCompetitions():
with open('competitions.json') as comps:
listOfCompetitions = json.load(comps)['competitions']
return listOfCompetitions
from globals import LIMITED_BOOKING_PLACE
from json_services import JSONServices

load_dotenv() # Load environment variables from .env file

app = Flask(__name__)
app.secret_key = 'something_special'
app.secret_key = os.getenv("SECRET_KEY")

competitions = loadCompetitions()
clubs = loadClubs()

@app.route('/')
@app.route("/")
def index():
return render_template('index.html')

@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)
return render_template("index.html")


@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]
@app.route("/showSummary", methods=["POST"])
def showSummary():
email = request.form["email"]
if email == "":
flash("Please enter an email", "error")
return render_template("index.html")

try:
clubs = JSONServices.load("clubs.json")["clubs"]
competitions = JSONServices.load("competitions.json")["competitions"]
club = [club for club in clubs if club["email"] == email][0]
except FileNotFoundError as ex:
print(ex)
flash("Error loading data. Please contact support.", "error")
return render_template("index.html")
except IndexError:
flash("Email not found", "error")
return render_template("index.html")

return render_template("welcome.html", club=club, competitions=competitions)


@app.route("/book/<competition>/<club>")
def book(competition, club):
clubs = JSONServices.load("clubs.json")["clubs"]
competitions = JSONServices.load("competitions.json")["competitions"]
foundClub = [c for c in clubs if c["name"] == club][0]
foundCompetition = [c for c in competitions if c["name"] == competition][0]
if foundClub and foundCompetition:
return render_template('booking.html',club=foundClub,competition=foundCompetition)
return render_template("booking.html", club=foundClub, competition=foundCompetition)
else:
flash("Something went wrong-please try again")
return render_template('welcome.html', club=club, competitions=competitions)
return render_template("welcome.html", club=club, competitions=competitions)


@app.route('/purchasePlaces',methods=['POST'])
@app.route("/purchasePlaces", methods=["POST"])
def purchasePlaces():
competition = [c for c in competitions if c['name'] == request.form['competition']][0]
club = [c for c in clubs if c['name'] == request.form['club']][0]
placesRequired = int(request.form['places'])
competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired
flash('Great-booking complete!')
return render_template('welcome.html', club=club, competitions=competitions)
clubs = JSONServices.load("clubs.json")["clubs"]
competitions = JSONServices.load("competitions.json")["competitions"]
competition = [c for c in competitions if c["name"] == request.form["competition"]][0]
club = [c for c in clubs if c["name"] == request.form["club"]][0]
placesRequired = int(request.form["places"])

club_points = int(club["points"])

if placesRequired > club_points:
flash("Not enough points", "error")
return render_template("booking.html", club=club, competition=competition)

if placesRequired > LIMITED_BOOKING_PLACE:
flash(f"Maximum booking limit is {LIMITED_BOOKING_PLACE} places", "error")
return render_template("booking.html", club=club, competition=competition)

number_of_places = int(competition["numberOfPlaces"]) - placesRequired

competition["numberOfPlaces"] = str(number_of_places)
JSONServices.save("competitions.json", {"competitions": competitions})

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


# TODO: Add route for points display


@app.route('/logout')
@app.route("/logout")
def logout():
return redirect(url_for('index'))
return redirect(url_for("index"))


if __name__ == "__main__":
app.run(debug=os.getenv("FLASK_DEBUG") == "1")
24 changes: 17 additions & 7 deletions templates/booking.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@
</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=""/>
<button type="submit">Book</button>
</form>
{% with messages = get_flashed_messages()%}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{message}}</li>
{% endfor %}
</ul>
{% endif%}
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>
</form>
{%endwith%}
</body>
</html>
9 changes: 9 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
<title>GUDLFT Registration</title>
</head>
<body>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class=flashes>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<h1>Welcome to the GUDLFT Registration Portal!</h1>
Please enter your secretary email to continue:
<form action="showSummary" method="post">
Expand Down
Empty file added tests/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pytest

from server import app as flask_app


@pytest.fixture
def app():
flask_app.config.update({"TESTING": True})
yield flask_app


@pytest.fixture
def client(app):
return app.test_client()
Empty file added tests/json_services/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions tests/json_services/test_json_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import json

import pytest

from json_services import JSONServices


def test_load_valid_json_file(mocker):
mock_data = {"clubs": [{"email": "[email protected]", "name": "Club Test"}]}

mocker.patch("builtins.open", mocker.mock_open(read_data=json.dumps(mock_data)))
result = JSONServices.load("clubs.json")
assert result == mock_data


def test_load_non_existent_json_file(mocker):
mocker.patch("builtins.open", side_effect=FileNotFoundError)
with pytest.raises(FileNotFoundError) as exc_info:
JSONServices.load("non_existent_file.json")
assert str(exc_info.value) == "File non_existent_file.json not found."


def test_save_json_file(mocker):
mock_data = {"clubs": [{"email": "[email protected]", "name": "Club Test", "points": "10"}]}

mock_open = mocker.patch("builtins.open", mocker.mock_open())
mock_json_dump = mocker.patch("json.dump")

JSONServices.save("clubs.json", mock_data)

mock_open.assert_called_once_with("clubs.json", "w")
mock_json_dump.assert_called_once_with(mock_data, mock_open(), indent=4)
63 changes: 63 additions & 0 deletions tests/test_booking_place.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from globals import LIMITED_BOOKING_PLACE


def get_mock_clubs(points: str):
return {"clubs": [{"email": "[email protected]", "name": "Club Test", "points": points}]}


def get_mock_competitions(places: str):
return {"competitions": [{"name": "Competition Test", "date": "2020-03-27 10:00:00", "numberOfPlaces": places}]}


def test_club_cannot_book_more_than_points(client, mocker):
mock_clubs = get_mock_clubs("5")
mock_competitions = get_mock_competitions("10")

mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions])

response = client.post(
"/purchasePlaces", data={"competition": "Competition Test", "club": "Club Test", "places": "6"}
)

assert response.status_code == 200
assert b"Not enough points" in response.data


def test_club_cannot_book_more_than_limit(client, mocker):
places_requested = str(LIMITED_BOOKING_PLACE + 1)
club_points = str(LIMITED_BOOKING_PLACE + 5)
number_of_competition_places = str(LIMITED_BOOKING_PLACE + 5)
mock_clubs = get_mock_clubs(club_points)
mock_competitions = get_mock_competitions(number_of_competition_places)

mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions])

response = client.post(
"/purchasePlaces", data={"competition": "Competition Test", "club": "Club Test", "places": places_requested}
)

assert response.status_code == 200
assert f"Maximum booking limit is {LIMITED_BOOKING_PLACE} places".encode() in response.data


def test_competition_places_are_deducted_when_club_books(client, mocker):
mock_clubs = get_mock_clubs(points="15")
mock_competitions = get_mock_competitions(places="20")
places_requested = "5"

mocker.patch("server.JSONServices.load", side_effect=[mock_clubs, mock_competitions])
mocker.patch("json_services.open", mocker.mock_open())

mock_json_dump = mocker.patch("json.dump")

response = client.post(
"/purchasePlaces", data={"competition": "Competition Test", "club": "Club Test", "places": places_requested}
)

assert response.status_code == 200

called_data = mock_json_dump.call_args[0][0]
competitions = called_data["competitions"]

updated = next(c for c in competitions if c["name"] == "Competition Test")
assert updated["numberOfPlaces"] == "15"
10 changes: 10 additions & 0 deletions tests/test_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def test_login_without_email(client):
response = client.post("/showSummary", data={"email": ""})
assert response.status_code == 200
assert b"Please enter an email" in response.data


def test_login_with_non_existent_email(client):
response = client.post("/showSummary", data={"email": "[email protected]"})
assert response.status_code == 200
assert b"Email not found" in response.data