From 2a5131a89d34dae7ad2ec372d32f63276da6d64a Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Mon, 1 Jun 2026 19:08:47 +0200 Subject: [PATCH 1/8] Testing folder and files initiated. utils.py created for unit testing. Testing environment created. --- CACHEDIR.TAG | 4 ++++ pyvenv.cfg | 11 +++++++++++ utils.py | 0 3 files changed, 15 insertions(+) create mode 100644 CACHEDIR.TAG create mode 100644 pyvenv.cfg create mode 100644 utils.py diff --git a/CACHEDIR.TAG b/CACHEDIR.TAG new file mode 100644 index 000000000..837feeff9 --- /dev/null +++ b/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by Python virtualenv. +# For information about cache directory tags, see: +# https://bford.info/cachedir/ \ No newline at end of file diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 000000000..5d632cc2b --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,11 @@ +home = /Library/Frameworks/Python.framework/Versions/3.13/bin +implementation = CPython +version_info = 3.13.3.final.0 +version = 3.13.3 +executable = /Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13 +command = /Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13 -m virtualenv /Users/quentintellier/Documents/2 - Python Projects/P11_2 - GÜDLFT/Python_Testing +virtualenv = 21.2.4 +include-system-site-packages = false +base-prefix = /Library/Frameworks/Python.framework/Versions/3.13 +base-exec-prefix = /Library/Frameworks/Python.framework/Versions/3.13 +base-executable = /Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13 diff --git a/utils.py b/utils.py new file mode 100644 index 000000000..e69de29bb From 232f1336f044ce037fecd9210436a757056e4bfc Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Wed, 17 Jun 2026 15:46:43 +0200 Subject: [PATCH 2/8] =?UTF-8?q?setup=20des=20fichiers=20pour=20test=20de?= =?UTF-8?q?=20coverage.=20Migration=20des=20fonctions=20utils=20dans=20le?= =?UTF-8?q?=20module=20utils.=20Impl=C3=A9mentation=20des=20tests=20unitai?= =?UTF-8?q?res=20pour=20les=20fonctions=20utils.=20Affichage=20des=20messa?= =?UTF-8?q?ges=20flash=20dans=20le=20template=20index.=20Cr=C3=A9ation=20d?= =?UTF-8?q?es=20fonctions=20utils=20pour=20le=20login.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 8196 bytes .coverage | Bin 0 -> 53248 bytes .coveragerc | 2 ++ server.py | 26 +++++++++++--------------- templates/index.html | 9 +++++++++ utils.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 .coverage create mode 100644 .coveragerc diff --git a/.DS_Store b/.DS_Store index 04a508e6f6048834346c5f1769770f2ff01806e6..8518e2f64fa4a87a6036764cd2f82ae577fd7317 100644 GIT binary patch delta 364 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD77(QH}hr%jz7$c**Q2SHn1>C zZRTN7WvtI&C}GHD$YDrk$Y&@^DlaZb%E?ax>fD}Gkds+lVqkEMk%^gwm5rU9lbxGA zHaH`{Jh&vWq_o&6u_zkE3(3#VNrJHxlfp7n%i{$^ob&Ta5;OBsi@+K(Q&NFSV!|`? zQu524@=Nnliotq=GwZ=JoSYn-@dD!2)h0$pItnHRwK@vbmPQ6Zrm0RskqFbn1HQIp%0uX=z1m;Fybgfh+B5&Eh4`$9PDx9P!4tA$e)#SR^G@@f_>(;IifAoc&D0E zlXHuzTtm;jIh5nZuZlEhH4ek`tAc}|9`MFuzWm-v)1Y1{_rsfgdh675hLIn?Zc^hn z8}wAV`|3iWytZcEyBT*B=4rOvC>*a?Q=MkKc}Caa{j>WGkCN$5!lS6)7k)P$GMrne z#-ZLkIcDk_!wGnx2|r0lHGQGIjVnua5Usx7n(k|Hj|n9W5Sj~gvhL7p^H2#nhPHo8 z2F|XmC41Gh@S-~sb)ANlFK?~Q>N?{t&}@uXi=&l%p}e|k-m&6tl(bgzt1#7tbW0vF zjp|(k>4d{TPb5#9Bz2B-megY;*+b!M6!PWG)mh;r>6(pXWnomv70RbhnIknKcwXYi ziK!rXOF9G&od}-2`g=~)6HnHiH&eyePvpzzPtB@0SE1S9=ltE}OkCyr*;s!-lRiJP zQ&r|IX_Y^h2OGDXKFub3jvLUBr8!ug0R$)PIF#6F`NJsgzx*=po_y{}>#GV#Uu%0x zRf&_+@V@GYp-ZP#K5d2X>#btF=Y%{9h3lf_`+ezn>fAs%E{AkXHxzLlo;)jkn3fS9 za2dO|*5f+qlM7Gt+cfC(_@b4;!0|fC`HD}&s#Kc$d2sS^)FeU~#X1ejkcQ}?GIgQJ zUY@aZ`L#VV;Zg@qM-6vwl!+2=_v91Bsk6~VCz6pnkb;xMm5DpnZ6_4%K+?|STpCx5 z#CnHorHc8C_UJiOMN?VNKCSO?6E_?D%vZ?^{8nzXW){k4&zhrQGJ@6Q)C}pJz`)V5 zCNDEnJ?qfUw3B^7voyU$n(SN*K4Hi&)e(3`RL$N}bUIb?2B}!#3w+alY|sxD2tWV= z5P$##AOHafKmY;|fB*#MPatb%%mQEkXY4-=`!9OJ0s#m>00Izz00bZa0SG_<0uX?} zODRywWKY}r&qB^Eo7vTs0;TL}Q-2JQDP~u#45&PK*@@KmY;|fB*y_009U<00Izz z00gwaP9|rJw(8eIy0<}{ZXx5*U5X;v@4GUnU-a9<0c{EEZwk6y!$H)eTlriK{2Q`O z+pabmP2PU#`tSEIUioypt`ao2Wyp6rst+T#AJQEV9FvZ4>3JN1>P#O1&>__K{}&AV zSNnnv48I`&0SG_<0uX=z1Rwwb2tWV=5J&}_+-S>u9#`l3`aeTQxMV$#BaE;AasR)x zJ+TG>2tWV=5P$##AOHafKmY;|m_vcAnKhR9`+w8^&!8VH5P$##AOHafKmY;|fB*y_ z009Whp+GjfWU2rEe`whM*bnD0M1%wZ2tWV=5P$##AOHafKmY;|IA#L*OwP1k$$U^U zbC&h`p9jCK/') diff --git a/templates/index.html b/templates/index.html index 926526b7d..c40ee1fcd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,15 @@

Welcome to the GUDLFT Registration Portal!

+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} Please enter your secretary email to continue:
diff --git a/utils.py b/utils.py index e69de29bb..acab6669a 100644 --- a/utils.py +++ b/utils.py @@ -0,0 +1,28 @@ +import json + +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 getClubByEmail(email, clubs): + email = lowercaseEmail(stripWhitespace(email)) + for club in clubs: + if lowercaseEmail(stripWhitespace(club['email'])) == email: + return club + return None + +def lowercaseEmail(email): + """Convertit l'email en minuscules pour une comparaison insensible à la casse.""" + return email.lower() + +def stripWhitespace(email): + """Supprime les espaces avant et après l'email pour une comparaison précise.""" + return email.strip() \ No newline at end of file From 7568d8fa8a0135faeead3feb28c6555a0b3da442 Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Wed, 17 Jun 2026 16:40:41 +0200 Subject: [PATCH 3/8] refactor: switch to Flask application factory and inject test data - move route registration into create_app - inject clubs and competitions into the app factory for tests - use current_app.config inside route handlers - add pytest warning filters for Flask/Werkzeug deprecations --- pytest.ini | 6 ++++ server.py | 94 +++++++++++++++++++++++++++++++----------------------- 2 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..5a9e34b4e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +filterwarnings = + ignore:ast\.Str is deprecated and will be removed in Python 3\.14; use ast\.Constant instead:DeprecationWarning + ignore:Constant\.__init__ got an unexpected keyword argument 's'\. Support for arbitrary keyword arguments is deprecated and will be removed in Python 3\.15\.:DeprecationWarning + ignore:Attribute s is deprecated and will be removed in Python 3\.14; use value instead:DeprecationWarning + ignore:Constant\.__init__ missing 1 required positional argument.*:DeprecationWarning \ No newline at end of file diff --git a/server.py b/server.py index 00e4ab14b..4b187e3fd 100644 --- a/server.py +++ b/server.py @@ -1,55 +1,69 @@ -import json -from flask import Flask,render_template,request,redirect,flash,url_for +from flask import Flask,render_template,request,redirect,flash,url_for,current_app from utils import loadClubs, loadCompetitions, getClubByEmail -app = Flask(__name__) -app.secret_key = 'something_special' +def create_app(config=None, clubs=None, competitions=None): + app = Flask(__name__) + app.secret_key = 'something_special' + if config: + app.config.update(config) -competitions = loadCompetitions() -clubs = loadClubs() + app.config['COMPETITIONS'] = competitions if competitions is not None else loadCompetitions() + app.config['CLUBS'] = clubs if clubs is not None else loadClubs() -@app.route('/') -def index(): - return render_template('index.html') + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/showSummary',methods=['POST']) + def showSummary(): + available_clubs = current_app.config['CLUBS'] + available_competitions = current_app.config['COMPETITIONS'] + + if available_clubs is None or available_competitions is None: + flash("Error loading clubs or competitions data.") + return redirect(url_for('index')) + + club = getClubByEmail(request.form['email'], available_clubs) + if club: + return render_template('welcome.html',club=club,competitions=available_competitions) -@app.route('/showSummary',methods=['POST']) -def showSummary(): - if clubs is None or competitions is None: - flash("Error loading clubs or competitions data.") - return redirect(url_for('index')) - - club = getClubByEmail(request.form['email'], clubs) - if club: - return render_template('welcome.html',club=club,competitions=competitions) - else: flash("Unfortunately, the email you entered was not found.") return redirect(url_for('index')) -@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] - 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('/book//') + def book(competition,club): + available_clubs = current_app.config['CLUBS'] + available_competitions = current_app.config['COMPETITIONS'] + foundClub = [c for c in available_clubs if c['name'] == club][0] + foundCompetition = [c for c in available_competitions if c['name'] == competition][0] + 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=available_competitions) -@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) + @app.route('/purchasePlaces',methods=['POST']) + def purchasePlaces(): + available_clubs = current_app.config['CLUBS'] + available_competitions = current_app.config['COMPETITIONS'] + competition = [c for c in available_competitions if c['name'] == request.form['competition']][0] + club = [c for c in available_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=available_competitions) -# TODO: Add route for points display + # TODO: Add route for points display + + + @app.route('/logout') + def logout(): + return redirect(url_for('index')) + + return app -@app.route('/logout') -def logout(): - return redirect(url_for('index')) \ No newline at end of file +app = create_app() \ No newline at end of file From dde1e43de82bcc83bda0d5a0cb6d0d844e5aa85c Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Thu, 18 Jun 2026 11:39:48 +0200 Subject: [PATCH 4/8] Warning filter added for pytest. Integration tests on login feature implemented --- pytest.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 5a9e34b4e..d7e6f75c9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,4 +3,5 @@ filterwarnings = ignore:ast\.Str is deprecated and will be removed in Python 3\.14; use ast\.Constant instead:DeprecationWarning ignore:Constant\.__init__ got an unexpected keyword argument 's'\. Support for arbitrary keyword arguments is deprecated and will be removed in Python 3\.15\.:DeprecationWarning ignore:Attribute s is deprecated and will be removed in Python 3\.14; use value instead:DeprecationWarning - ignore:Constant\.__init__ missing 1 required positional argument.*:DeprecationWarning \ No newline at end of file + ignore:Constant\.__init__ missing 1 required positional argument.*:DeprecationWarning + ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning \ No newline at end of file From 041e262f5d84dc4db417ae429f760a847138e4ef Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Fri, 19 Jun 2026 12:54:42 +0200 Subject: [PATCH 5/8] refactor(book route, utils): improve robustness with safe lookups - Replace fragile [0] indexing with safe getClubByName/getCompetitionByName lookups in /book route - Validate config data presence and handle missing club/competition separately - Make JSON loaders return None on errors (OSError, JSONDecodeError, KeyError) - Add helper functions for safe dictionary lookups - Update corresponding unit tests to match new None-return behavior --- .DS_Store | Bin 8196 -> 8196 bytes .coverage | Bin 53248 -> 53248 bytes server.py | 25 +++++++++++++++++-------- utils.py | 34 +++++++++++++++++++++++++++------- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/.DS_Store b/.DS_Store index 8518e2f64fa4a87a6036764cd2f82ae577fd7317..537739ef6f798307758963abfe7994bd7674eb7b 100644 GIT binary patch delta 39 vcmZp1XmOa}&nUeyU^hRb^kyCbb|%i0;^ds9{QMlo&5S~JOq-)bR&fIW?7j{|B4+~!`1OHrpYrfNbv6}@2eEHaNSXdYuV<$WMhe(yA<`(26mZTQz zXXd4(R_JAvE% zr7@Nfr>QLYERBg^X>shPGH|+_$%JcX%2Y52$c4Ffg$2KV#tk4K(R3 d{}-T1*Z4))fXW$}SilrBGniu8{A)g&0RY`)T_pej delta 112 zcmZozz}&Eac>{|B2NS&=1!`uvlv{Z9f#Y#8|G@!J4J4EQ(O#Gg@s7 diff --git a/server.py b/server.py index 4b187e3fd..1842d3281 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,5 @@ from flask import Flask,render_template,request,redirect,flash,url_for,current_app -from utils import loadClubs, loadCompetitions, getClubByEmail +from utils import loadClubs, loadCompetitions, getClubByEmail, getCompetitionByName, getClubByName def create_app(config=None, clubs=None, competitions=None): app = Flask(__name__) @@ -35,14 +35,23 @@ def showSummary(): def book(competition,club): available_clubs = current_app.config['CLUBS'] available_competitions = current_app.config['COMPETITIONS'] - foundClub = [c for c in available_clubs if c['name'] == club][0] - foundCompetition = [c for c in available_competitions if c['name'] == competition][0] - 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=available_competitions) + if available_clubs is None or available_competitions is None: + flash("Error loading clubs or competitions data.") + return redirect(url_for('index')) + + found_competition = getCompetitionByName(competition, available_competitions) + found_club = getClubByName(club, available_clubs) + + if found_club is None: + flash("Invalid booking URL. Please check the club name.") + return redirect(url_for('index')) + + if found_competition is None: + flash("Invalid booking URL. Please check the competition name.") + return render_template('welcome.html', club=found_club, competitions=available_competitions) + + return render_template('booking.html', club=found_club, competition=found_competition) @app.route('/purchasePlaces',methods=['POST']) def purchasePlaces(): diff --git a/utils.py b/utils.py index acab6669a..0247f9efd 100644 --- a/utils.py +++ b/utils.py @@ -1,15 +1,21 @@ import json def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs + try: + with open('clubs.json') as c: + listOfClubs = json.load(c)['clubs'] + return listOfClubs + except (OSError, json.JSONDecodeError, KeyError): + return None def loadCompetitions(): - with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions + try: + with open('competitions.json') as comps: + listOfCompetitions = json.load(comps)['competitions'] + return listOfCompetitions + except (OSError, json.JSONDecodeError, KeyError): + return None def getClubByEmail(email, clubs): @@ -25,4 +31,18 @@ def lowercaseEmail(email): def stripWhitespace(email): """Supprime les espaces avant et après l'email pour une comparaison précise.""" - return email.strip() \ No newline at end of file + return email.strip() + +def getCompetitionByName(name, competitions): + """Récupère une compétition par son nom dans la liste des compétitions.""" + for competition in competitions: + if competition['name'] == name: + return competition + return None + +def getClubByName(name, clubs): + """Récupère un club par son nom dans la liste des clubs.""" + for club in clubs: + if club['name'] == name: + return club + return None \ No newline at end of file From ddcbb9ed8ddb0be9b80878373ca374918a2c248e Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Mon, 22 Jun 2026 14:48:18 +0200 Subject: [PATCH 6/8] feat(purchase): add robust booking validation and flash feedback harden the purchase route when clubs/competitions data is missing or invalid replace fragile list indexing with safe lookup helpers add getClubPoints/getCompetitionPlaces with invalid-input handling add validateBooking to enforce available club points checks display flash messages on the booking page when validation fails update available places only after successful validation --- server.py | 39 +++++++++++++++++++++++++++++++++++---- templates/booking.html | 10 ++++++++++ utils.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/server.py b/server.py index 1842d3281..cc5e4ad9a 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,14 @@ from flask import Flask,render_template,request,redirect,flash,url_for,current_app -from utils import loadClubs, loadCompetitions, getClubByEmail, getCompetitionByName, getClubByName +from utils import ( + loadClubs, + loadCompetitions, + getClubByEmail, + getCompetitionByName, + getClubByName, + getClubPoints, + getCompetitionPlaces, + validateBooking, +) def create_app(config=None, clubs=None, competitions=None): app = Flask(__name__) @@ -57,12 +66,34 @@ def book(competition,club): def purchasePlaces(): available_clubs = current_app.config['CLUBS'] available_competitions = current_app.config['COMPETITIONS'] - competition = [c for c in available_competitions if c['name'] == request.form['competition']][0] - club = [c for c in available_clubs if c['name'] == request.form['club']][0] + + if available_clubs is None or available_competitions is None: + flash("Error loading clubs or competitions data.") + return redirect(url_for('index')) + + competition = getCompetitionByName(request.form['competition'], available_competitions) + club = getClubByName(request.form['club'], available_clubs) placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired + + if competition is None or club is None: + flash("Invalid booking request. Please check the club and competition names.") + return redirect(url_for('index')) + + validation_errors = validateBooking( + getClubPoints(club), + getCompetitionPlaces(competition), + placesRequired, + ) + + if validation_errors: + for error in validation_errors: + flash(error) + return render_template('booking.html', club=club, competition=competition) + + competition['numberOfPlaces'] = str(getCompetitionPlaces(competition) - placesRequired) flash('Great-booking complete!') return render_template('welcome.html', club=club, competitions=available_competitions) + # TODO: Add route for points display diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..2d805514c 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -5,6 +5,16 @@ Booking for {{competition['name']}} || GUDLFT +

GUDLFT Booking

+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %}

{{competition['name']}}

Places available: {{competition['numberOfPlaces']}} diff --git a/utils.py b/utils.py index 0247f9efd..1d71d075b 100644 --- a/utils.py +++ b/utils.py @@ -45,4 +45,31 @@ def getClubByName(name, clubs): for club in clubs: if club['name'] == name: return club - return None \ No newline at end of file + return None + +def getClubPoints(club): + """Récupère le nombre de points d'un club.""" + if not isinstance(club, dict): + return None + try: + return int(club.get('points', None)) + except (TypeError, ValueError): + return None + +def getCompetitionPlaces(competition): + """Récupère le nombre de places disponibles pour une compétition.""" + if not isinstance(competition, dict): + return None + try: + return int(competition.get('numberOfPlaces', None)) + except (TypeError, ValueError): + return None + +def validateBooking(club_points, competition_places, placesRequested): + """Valide si un club peut réserver des places pour une compétition.""" + errors = [] + + if placesRequested > club_points: + errors.append("Not enough points available in your club to book the requested number of places.") + + return errors \ No newline at end of file From a161a4de4fb8dfbfebc9a48de21e99d2eeda7874 Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Mon, 22 Jun 2026 16:21:31 +0200 Subject: [PATCH 7/8] refactor(server): improve booking feedback with purchased places count - Update flash message to display the number of places purchased - Remove unnecessary blank lines --- server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server.py b/server.py index cc5e4ad9a..be11a4a83 100644 --- a/server.py +++ b/server.py @@ -91,11 +91,9 @@ def purchasePlaces(): return render_template('booking.html', club=club, competition=competition) competition['numberOfPlaces'] = str(getCompetitionPlaces(competition) - placesRequired) - flash('Great-booking complete!') + flash(f'Booking complete: {placesRequired} places purchased.') return render_template('welcome.html', club=club, competitions=available_competitions) - - # TODO: Add route for points display From 2e1b07ca44b87013b08569d44b1fde16308c624a Mon Sep 17 00:00:00 2001 From: Quentin Tellier Date: Mon, 22 Jun 2026 17:20:48 +0200 Subject: [PATCH 8/8] feat(booking): add client-side place limit validation with dynamic constraints - Implement dynamic max value for booking input based on club points, available places, and 12-place cap - Add HTML5 validation (min, required) and disable submit when no booking possible - Add server-side validation to reject requests exceeding 12-place limit --- templates/booking.html | 5 +++-- utils.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/templates/booking.html b/templates/booking.html index 2d805514c..0ccd0b1b7 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -17,11 +17,12 @@

GUDLFT Booking

{% endwith %}

{{competition['name']}}

Places available: {{competition['numberOfPlaces']}} + {% set max_places = [12, club['points']|int, competition['numberOfPlaces']|int] | min %} - - + + \ No newline at end of file diff --git a/utils.py b/utils.py index 1d71d075b..7afa78a40 100644 --- a/utils.py +++ b/utils.py @@ -72,4 +72,7 @@ def validateBooking(club_points, competition_places, placesRequested): if placesRequested > club_points: errors.append("Not enough points available in your club to book the requested number of places.") + if placesRequested > 12: + errors.append("You cannot book more than 12 places per competition.") + return errors \ No newline at end of file