Skip to content

Commit 80571be

Browse files
authored
Merge pull request #4 from mumz0/bugfix/limit-booking-places
Bugfix/limit booking places
2 parents 9a82069 + 588682e commit 80571be

File tree

6 files changed

+310
-26
lines changed

6 files changed

+310
-26
lines changed

Diff for: clubs.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
},
1212
{ "name":"She Lifts",
1313
"email": "kate@shelifts.co.uk",
14-
"points":"12"
14+
"points":"50"
1515
}
16-
]}
16+
]}

Diff for: server.py

+51-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import json
99

1010
from flask import Flask, flash, redirect, render_template, request, session, url_for
11-
from werkzeug.exceptions import BadRequest
11+
from werkzeug.exceptions import BadRequest, Unauthorized
1212

1313

1414
def load_clubs():
@@ -38,7 +38,7 @@ def index():
3838
return render_template("index.html")
3939

4040

41-
@app.route("/show_summary", methods=["POST"])
41+
@app.route("/show_summary", methods=["POST", "GET"])
4242
def show_summary():
4343
"""
4444
Handle POST request to render the summary page for a specific club.
@@ -55,6 +55,12 @@ def show_summary():
5555
5656
"""
5757
try:
58+
if request.method == "GET":
59+
email = session.get("email")
60+
if email:
61+
club = [club for club in clubs if club["email"] == email][0]
62+
return render_template("welcome.html", club=club, competitions=competitions)
63+
5864
email = request.form.get("email")
5965
if not email:
6066
flash("Email is required.")
@@ -76,7 +82,17 @@ def show_summary():
7682

7783
@app.route("/book/<competition>/<club>")
7884
def book(competition, club):
79-
"""Render the booking page for a specific competition and club."""
85+
"""
86+
Render the booking page for a specific competition and club.
87+
88+
:param competition: The name of the competition.
89+
:type competition: str
90+
:param club: The name of the club.
91+
:type club: str
92+
:return: The rendered booking page if the club and competition are found,
93+
otherwise the welcome page with an error message.
94+
:rtype: werkzeug.wrappers.Response
95+
"""
8096
found_club = [c for c in clubs if c["name"] == club][0]
8197
found_competition = [c for c in competitions if c["name"] == competition][0]
8298
if found_club and found_competition:
@@ -87,16 +103,44 @@ def book(competition, club):
87103

88104
@app.route("/purchase_places", methods=["POST"])
89105
def purchase_places():
90-
"""Handle the reservation of places for a specific competition and club."""
106+
"""
107+
Handle the reservation of places for a specific competition and club.
108+
109+
This function processes the form submission for booking places in a competition.
110+
It checks if the user is authenticated, validates the requested number of places,
111+
and updates the competition and club data accordingly.
112+
113+
:raises Unauthorized: If the user is not authenticated.
114+
:raises BadRequest: If the requested number of places is invalid.
115+
:return: The rendered welcome page with a success message if the booking is successful,
116+
otherwise the welcome page with an error message.
117+
:rtype: werkzeug.wrappers.Response
118+
"""
119+
if not session.get("email"):
120+
raise Unauthorized("You must be connected.")
121+
91122
competition = [c for c in competitions if c["name"] == request.form["competition"]][0]
92123
club = [c for c in clubs if c["name"] == request.form["club"]][0]
93124
places_required = int(request.form["places"])
94125

95-
if places_required > int(club["points"]) or places_required > int(competition["numberOfPlaces"]):
126+
# Initialize 'clubBookings' for the competition if not present
127+
if "clubBookings" not in competition:
128+
competition["clubBookings"] = {}
129+
130+
# Initialize the club's booking count for this competition if not present
131+
if club["name"] not in competition["clubBookings"]:
132+
competition["clubBookings"][club["name"]] = 0
133+
134+
if places_required > int(club["points"]) or places_required > int(competition["numberOfPlaces"]) or places_required > 12:
135+
raise BadRequest("Invalid data provided")
136+
137+
if competition["clubBookings"][club["name"]] + places_required <= 12:
138+
competition["clubBookings"][club["name"]] += places_required
139+
club["points"] = int(club["points"]) - places_required
140+
competition["numberOfPlaces"] = int(competition["numberOfPlaces"]) - places_required
141+
else:
96142
raise BadRequest("Invalid data provided")
97143

98-
club["points"] = int(club["points"]) - places_required
99-
competition["numberOfPlaces"] = int(competition["numberOfPlaces"]) - places_required
100144
flash("Great - booking complete!")
101145
return render_template("welcome.html", club=club, competitions=competitions)
102146

Diff for: templates/booking.html

+18
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,26 @@
66
<title>Booking for {{ competition['name'] }} || GUDLFT</title>
77
</head>
88
<body>
9+
<a href="/show_summary" style="text-decoration: none; color: inherit;">
10+
Retour
11+
</a>
912
<h2>{{ competition['name'] }}</h2>
1013
Places available: {{ competition['numberOfPlaces'] }}
14+
<p>
15+
Places already booked:
16+
{% if 'clubBookings' in competition and club['name'] in competition['clubBookings'] %}
17+
{{ competition['clubBookings'][club['name']]}}/12
18+
{% else %}
19+
0/12
20+
{% endif %}
21+
</p>
1122
<form id="bookingForm" action="/purchase_places" method="post" onsubmit="validateBooking(event)">
1223
<input type="hidden" name="club" value="{{ club['name'] }}">
1324
<input type="hidden" name="competition" value="{{ competition['name'] }}">
1425
<input type="hidden" id="availablePlaces" value="{{ competition['numberOfPlaces'] }}">
1526
<input type="hidden" id="clubPlaces" value="{{ club['points'] }}">
27+
<input type="hidden" id="competitionBookings" value="{% if 'clubBookings' in competition and club['name'] in competition['clubBookings'] %}{{ competition['clubBookings'][club['name']] }}{% else %}0{% endif %}">
28+
<input type="hidden" id="clubName" value="{{ club['name'] }}">
1629
<label for="places">How many places?</label>
1730
<input type="number" name="places" id="places">
1831
<button type="submit">Book</button>
@@ -33,6 +46,7 @@ <h2>{{ competition['name'] }}</h2>
3346
const availablePlaces = parseInt(document.getElementById('availablePlaces').value, 10);
3447
const clubPlaces = parseInt(document.getElementById('clubPlaces').value, 10);
3548
const errorDiv = document.getElementById('error-message');
49+
const placesAlreadyBooked = parseInt(document.getElementById('competitionBookings').value, 10);
3650

3751
// Clear previous error message
3852
errorDiv.textContent = '';
@@ -46,6 +60,10 @@ <h2>{{ competition['name'] }}</h2>
4660
errorDiv.textContent = "You cannot book more places than are available in the competition.";
4761
return;
4862
}
63+
if (numberOfPlaces > maxPlaces || placesAlreadyBooked + numberOfPlaces > maxPlaces) {
64+
errorDiv.textContent = "You cannot book more than 12 places.";
65+
return;
66+
}
4967
if (numberOfPlaces > clubPlaces) {
5068
errorDiv.textContent = "You cannot book more places than you have.";
5169
return;

Diff for: templates/welcome.html

+21-17
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,36 @@
55
<title>Summary | GUDLFT Registration</title>
66
</head>
77
<body>
8-
<h2>Welcome, {{club['email']}} </h2><a href="{{url_for('logout')}}">Logout</a>
8+
<h2>Welcome, {{ club['email'] }} </h2><a href="{{ url_for('logout') }}">Logout</a>
99

10-
{% with messages = get_flashed_messages()%}
10+
{% with messages = get_flashed_messages() %}
1111
{% if messages %}
1212
<ul>
13-
{% for message in messages %}
14-
<li style="color: red;">{{ message }}</li>
15-
{% endfor %}
16-
</ul>
17-
{% endif%}
18-
Points available: {{club['points']}}
13+
{% for message in messages %}
14+
<li style="color: red;">{{ message }}</li>
15+
{% endfor %}
16+
</ul>
17+
{% endif %}
18+
{% endwith %}
19+
20+
Points available: {{ club['points'] }}
1921
<h3>Competitions:</h3>
2022
<ul>
21-
{% for comp in competitions%}
23+
{% for comp in competitions %}
2224
<li>
23-
{{comp['name']}}<br />
24-
Date: {{comp['date']}}</br>
25-
Number of Places: {{comp['numberOfPlaces']}}
26-
{%if comp['numberOfPlaces']|int >0%}
27-
<a href="{{ url_for('book',competition=comp['name'],club=club['name']) }}">Book Places</a>
28-
{%endif%}
25+
{{ comp['name'] }}<br />
26+
Date: {{ comp['date'] }}<br />
27+
Number of Places: {{ comp['numberOfPlaces'] }}
28+
{% if comp['numberOfPlaces']|int > 0 %}
29+
{% if (comp['clubBookings'][club['name']] if 'clubBookings' in comp and club['name'] in comp['clubBookings'] else 0) < 12 %}
30+
<a href="{{ url_for('book', competition=comp['name'], club=club['name']) }}">Book Places</a>
31+
{% else %}
32+
<span style="color: green;">Max places booked</span>
33+
{% endif %}
34+
{% endif %}
2935
</li>
3036
<hr />
3137
{% endfor %}
3238
</ul>
33-
{%endwith%}
34-
3539
</body>
3640
</html>

Diff for: tests/integration_tests/test_integration.py

+130
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,64 @@ def test_login_no_email(client):
123123
assert "Email is required" in response_text
124124

125125

126+
def test_get_welcome_page_with_email_in_session(client, mocker):
127+
"""
128+
Test accessing the welcome page with a valid email in the session.
129+
130+
This test verifies that the welcome page is accessible and displays the correct
131+
information when a valid email is present in the session.
132+
133+
:param client: The Flask test client.
134+
:type client: flask.testing.FlaskClient
135+
:param mocker: The mock object.
136+
:type mocker: pytest_mock.MockerFixture
137+
:return: None
138+
"""
139+
clubs = mocker.patch("server.clubs", sample_clubs()["clubs"])
140+
with client.session_transaction() as sess:
141+
sess["email"] = clubs[0]["email"]
142+
143+
response = client.get("/show_summary")
144+
assert response.status_code == 200
145+
assert b"Welcome" in response.data
146+
assert b"Points available" in response.data
147+
148+
149+
def test_get_welcome_page_without_email_in_session(client):
150+
"""
151+
Test accessing the welcome page without an email in the session.
152+
153+
This test verifies that the user is redirected to the home page when no email
154+
is present in the session.
155+
156+
:param client: The Flask test client.
157+
:type client: flask.testing.FlaskClient
158+
:return: None
159+
"""
160+
response = client.get("/show_summary")
161+
assert response.status_code == 302
162+
assert response.headers["Location"] == "http://localhost/"
163+
164+
165+
def test_get_welcome_page_with_invalid_email_in_session(client):
166+
"""
167+
Test accessing the welcome page with an invalid email in the session.
168+
169+
This test verifies that the user is redirected to the home page when an invalid
170+
email is present in the session.
171+
172+
:param client: The Flask test client.
173+
:type client: flask.testing.FlaskClient
174+
:return: None
175+
"""
176+
with client.session_transaction() as sess:
177+
sess["email"] = "invalid@example.com"
178+
179+
response = client.get("/show_summary")
180+
assert response.status_code == 302
181+
assert response.headers["Location"] == "http://localhost/"
182+
183+
126184
def test_purchase_success(client, mocker):
127185
"""
128186
Test the purchase process when it is successful.
@@ -213,6 +271,25 @@ def test_purchase_exceeding_club_points(client, mocker):
213271
assert "Invalid data provided" in response_text
214272

215273

274+
def test_purchase_places_exceeding_maximum_places(client, mocker):
275+
"""
276+
Test that attempting to purchase more than the maximum allowed places (12) results in a BadRequest.
277+
278+
:param client: The Flask test client.
279+
:type client: flask.testing.FlaskClient
280+
:param mocker: The mock object.
281+
:type mocker: pytest_mock.MockerFixture
282+
:return: None
283+
"""
284+
mocker.patch("server.clubs", sample_clubs()["clubs"])
285+
mocker.patch("server.competitions", sample_competitions()["competitions"])
286+
with client.session_transaction() as session:
287+
session["email"] = "club1@test.com"
288+
response = client.post("/purchase_places", data={"competition": "Comp1", "club": "Club1", "places": 13})
289+
assert response.status_code == 400
290+
assert b"Invalid data provided" in response.data
291+
292+
216293
def test_deduct_club_points_and_competition_places_after_purchase_process(client, mocker):
217294
"""
218295
Test the deduction of club points and competition places after a purchase.
@@ -246,3 +323,56 @@ def test_deduct_club_points_and_competition_places_after_purchase_process(client
246323
assert "Great - booking complete!" in response_text
247324
assert club["points"] == int(inital_club_points) - places_required
248325
assert competition["numberOfPlaces"] == int(initial_competition_places) - places_required
326+
327+
328+
def test_access_without_email_in_session(client, mocker):
329+
"""
330+
Test that accessing a route without an email in the session raises an Unauthorized exception.
331+
332+
:param client: The Flask test client.
333+
:type client: flask.testing.FlaskClient
334+
:param mocker: The mock object.
335+
:type mocker: pytest_mock.MockerFixture
336+
:return: None
337+
"""
338+
clubs = mocker.patch("server.clubs", sample_clubs()["clubs"])
339+
competitions = mocker.patch("server.competitions", sample_competitions()["competitions"])
340+
places_required = 2
341+
342+
# No email set in session
343+
with client.session_transaction() as session:
344+
session["email"] = None
345+
346+
response = client.post(
347+
"/purchase_places", data={"competition": competitions[0]["name"], "club": clubs[0]["name"], "places": places_required}, follow_redirects=True
348+
)
349+
350+
# Check for Unauthorized exception
351+
assert response.status_code == 401
352+
assert b"You must be connected." in response.data
353+
354+
355+
def test_update_booking_insufficient_points(client, mocker):
356+
"""
357+
Test the update of a booking when the club has insufficient points.
358+
359+
This test verifies that the booking is not updated if the club does not
360+
have enough points to book the required places.
361+
362+
:return: None
363+
"""
364+
clubs = mocker.patch("server.clubs", sample_clubs()["clubs"])
365+
competitions = mocker.patch("server.competitions", sample_competitions()["competitions"])
366+
places_required = 14
367+
368+
with client.session_transaction() as session:
369+
session["email"] = clubs[0]["email"]
370+
371+
response = client.post(
372+
"/purchase_places", data={"competition": competitions[0]["name"], "club": clubs[0]["name"], "places": places_required}, follow_redirects=True
373+
)
374+
375+
assert response.status_code == 400
376+
assert int(competitions[0]["clubBookings"][clubs[0]["name"]]) == 0
377+
assert int(clubs[0]["points"]) == 13
378+
assert int(competitions[0]["numberOfPlaces"]) == 25

0 commit comments

Comments
 (0)