sHNZ;D#*a|hA70GwIBbKh@kPwRw=oXAOU#Qc2{Wh{U?8r;0Nwvx6m*>`
z(I3yFlIv&m;9sbaM?YiifXb1<_)0b2fT;fWOU+T%Ei(~lj*4V1D&*zZ3J+jB{w$f_
z37=r@Yj@NKQ&1f@p&tAU6YvgdAuTzlSj@suEJ7vk3#eQ0ItJr=sQxEV{eDIz=RH)i
zhEF2uO(-N%P&N)k?QE3wISiv-j#|JrRLIYw2EJ(xnrtSHLrw57YQj8QpNM+yCDZ~p
z+x8=qiN6LqPlFb43&YWGiWwjVwbC@yLi(Ww8fEJP|IB
z*%sA42(`dDQ;EMCR?*;#$5BUc3Y8?)rkMrRK_BWZ&>Is`1Ekva-dKnF08}oGL><8_
z)Q-zh?~y$ig{M#(z2Q>mK%v&N=BHH;)QZPocbt#vcmgBwd*ov9lgA8Hgj(1l)Pl>g
zHg2%>ov4LXT0cca`ah@*yEiCwq;MCN{prt{5N4qUdJMZ@0k+4DsO0=NDmiOCZx+zj
znt>XxJJ!P)s3Tg5(fAM4JZF&&y3QpE-DqgVQ7pxAs8HRcQ%>Oz8Yk
z$=C=L`czcXb+VuL#}?EJP!lggEqEDfp|7Ey-+>{_?;N%bU#fxn71YY_qs~6`1rxeB
zRJ}L$!F&wDov81I1E`%J##sCuHSrz$d9~Rl*=wRUl!p5EXQvwlO*{#8X7h0fuE!F*
zjatyyIp)jAMTKx7Dn~Y8iYJ=lZ8<~L-xNIKr_oc9t
zhOW2|XVUJ=y^;ggjZ=#azKBlAp0+aPwsOM*)2X~<+{tR{I7qK_qM%|Jg
z?m~06Gf~%T9X7;ssDb}Lt+dS|V=vU_Pok36#X?+d+pE1~e)mUX6WX&-8ytgLNGbZ^
z9Mm}OD-?8=JFOp~KlKZ!iGM|{+;_2gFdB7at*|X-VLVQ?^*2#Tc@8zW>W$V1_1x1KgY!`l
z+KwJPinZ}4RPx?LUE@1gi}{`4SIr9~5*t%b#1_~eo8eS!hig%{g*T5w5n(L^23#Q4d9T5`}0AI`d_ymA`?ZSc!_n7pSASj0&Caax+0N
z>Zt0WCTNazFvZrhY<(bx&|ZLwz+_a;J-?jz>nvB=4jXNUeW(Espw9ji)KPqnTIhAG
zi}z3yhO96PZi3obI%uJ=BgeR+=|wCMqJ2qc$)G({VE9;co1M!GALm8R1e;_CA9O@p9~d6{zI8io-Er
zmAQsbqjF;=*2Asn!9&;=FQ68lwA#D}9zuOS9+g{*aU||WJ@1CTW>y@J!8BxG7-nO0
z9Em!D1*kK888yKh=)rx~GpL>4#su_TV{#-7HE}1@b5CFjPQWDH|E(0lY4{p-eQscT
z413*v2cSauB4*$}kq4bWZF|C6^K*MNwxfLwrr{}D_g`ls+8s6S^Va>?S@-`21-+A#
z)|-`0$2QdWU|YO^$r%2IY43$fs%fYY&cQ~w276#7DkrLMFz<_6s2pmBdafNRiMvVW
zcluKB;4sv?d^##8E}$Op*=Uk81nX0eK^;X$)a}W^2poorn!impzG3lv$?++sGVhDP3(`FaHw@0DiSkL6RkomcpoYPr%~7W
ziv9dg)XwW~F(#rGkhO*QE9(c*pb1Bzc2bHyxD9nydr=`jh#vgH)^DQv)!b@cy@9BR
zBx4sGjmnM9sOOKOFP=js<3*Q(28h~b{++KGdZ;f!CCwhxgH;%dr!gMyVGPE-WhU;6
z%7IeUgbPtSehW3uAynwUu%DknMa;cLp%#Vk?PekmHlW@Xl^cE0gX2;C7Gn)uiR!lo
zYvX>@M2BtrDb!K?ii&W}9p(rluqpL;B&S@bKZVXTj6!`4Z$jPo)7S|EcbequgTtuj
zV}Go~JPg`p{)Mvy6{&Bq6<$L{ByzX;n^X$=Q=fv$sX6%I_x~aa(`eX=T4~cg=D~It
zNIeIGa0F&z5hmgu)X{y18t`ZI!)w?a|3oirve(!GHC`O*xenNp{BtrX=*-5U&U6tf
zM7yk~P@%tV^{O!0AAp*u1%_Z>Yd!{3pMV;7F6yk`L=PT89oY}~;P3y}D8$nc`gb!>
zH`Ks`P_Nu)QP*rAdhitLY=1*#v;W)XeutqJ{s^|i5{$#msBuoBZdcGd<~`H%9nQZa
z4bx~Ci2E=J{q~t(IGr$y`ZCmu692&zYW%}|FAqb7YzrpiN2qIa*IMm8^Un9d2-+K2(=nWSe^lt7!Z4g>+n1n1
zzS+7L^?apkJA8?CY52v~tG#b_7J=&67nNM2Q3IEuuHk&tgo{zxy$;oX2Wmkdq6fc0
zZR{R4!01YQ|Jzg084f{Zc^)d1CD;>R#(MY_Dx|;K&%+Ox{;8-?W?>r6!E~&|X81d5
zV+{|QA1-aILyfL8gTm8v*n$}t`cJddeyFU@MeU>*wZOTkYqT6AaTBWFA=I@xh1%hH
zOu#=-?}O%t%qzD)_NBfMbM^gyj)Iadt;+nS8-v}bzllBYC)7Z#5AzcnbFnM_fDdEy
zBPL0UF_rq;s0duPHvPb~564clzlz%E=a|C$PQZs|WtrHK`gF|3cTpkrIck2ObV0Q*
zz+QL;HDJs~=J`BSeH9MX=NN@a$IPwB!S2+zVLo2P;kx(zJ~kgbhuZleREWKfo8$_?
zXzEQ-?cK3H4nT!=9LC`aY>0Xem@8C_IZ;K|M}${$@=T2>L2T)jp`
zi+0(e6{~w63andPROl%#^^7krE%b~xeJU~^dp*K8In|St)*-ch#ZS*v_Y3KHcx!2X
z(S$Nj{(}JzZ!N31IX%+5B63zv{fhF{6(P+A9^N{xaO8wB#YIJh(>xQ4JjD||CF3TR
UjVYX1TILyTp07yQc_ixp02;~qU;qFB
delta 10994
zcmYM(34D)NzQ^&WS;!`f1c_|_h$SQ;n=M3QUt^0sMUZF_NsY7(b^R?ZnwC;)nHIG-
ziZ&EQ6}@O@RI9CJwCbX@b!N&0`;!Qf1P6f5sRxa3HM`5yn&4|G|_R=
zu^qaw7~^rV(e1oPA&!P`&=((IG(N#-!5E7TFa;y97Y5>Z48v(y2Nz%vuE6@Z8TH;F
z)SkXVZREDC|55YYO=Gf|umy(Dp%;eYC~Jvz9%`Ufw!YKWKeqMrwtfe-5QRbG1Y;d+
zj2b`9*1M_B{LWws8n_U(!Sfs2yBD7Vq3a^>5J1n2L?4_eDjf1a(yN
zu@>$`PQ|H0E$9lmHSkXqYNL~4I{IKB)gbi581%tLsE{V2j;bqaN4cm8$D-aVL-n7J
zoTl?CDi;o5IDUt$-T5`eaXbDLnxyi7n2G~%II80|)Ic9&5PoXwmrxU3!(hB;^+p4{D{)rl(8a3c!)Pj=JO$4$~
z^#Q1UW3dfR!VKJiW%vzhV|{4!#tCi;+Q}r;^(#jm#Y)r;-b4+&)z&{io!wCk!!xJ}
zuc6-i2^EoQR8D!fHb)kT-KnQzA@HTs0IB8o8g}rfX&*O4P;<9=6AYK7{h~ws2%@^n&3CAkKXM~
zQbnN_)(jP?_Ne}yQMoe&lkqv!&NpKO?nC9&S?q{+Q1dm*bevYq?<}A&7(YNw;M>6-
z1!@8Ds3XWkg|aJZf;?123Q<`<3AMvARK(_5UqMA^B_`lHtcRbVTPyqr1%<2{yJ16?
zsR_qgpGG~Oh6?#=)Q&&2?cbsz=8J#z#~?v;3Q&=H1DV3viJJFT7V+;y!GnK9
zVQ19N#$XJVVi+#LJluq#_y8NAKMA6GJZiyhQ4z{PMPwjq;(XM?OHlonp%%O)+ifO%
zj|PSQI4X2sqq6)8w!)uK5s1ohoTk_XYeJ6dUxpfRwyjs7j&23&h$>MF+=Kdl97Mf$
z%S}NO-$$L17je_|@k4cpKt&`TwSWxtz)`3q9F6?fDdHbFxXspYp%&Dli#h8y=%U^a
z!*MDqvhD@8VI3+<_o7yQ7Ih>)qRz^*t7(r$^~*pFFaUKFg{bQ`3$@evsAPN^y+RF>Wey(rl4r+)p{USLu+OJj%TbY=k8Vx0h=NwQ(sp>ucG!XXqV2-k_$_JyH&FenQ44&8Nf_LV
zKRz%E`{T=~=NC~Szl%!tKQR$QdJ})0Wma!~^Km$;<8D+WPN0(JTU)<}d^;S!J|?Ss
zVN>d3P)Yd`>L@nY_K$EV^&e1Qybi2R8yNcFT
zUPtv;hNeRV>IidDJI+H*FcG!D=dm%qg?@Mfwb9Sr6!-;pu35u#9j7_<{+NX2)-9+3
z&Y=c;fK4!lI5)#?7>~uM_m-l@IfCkc6O%D;fcX*Xh`MF&85A^drS%JJMEw_A4Tpcd2}Lva|oa5C1#S5P~A3$?>aRK$+i_AhMxI}D@!C)Bu43?=?b4u1*?
zSsH4^?NKZ4g4)?&tbcwhQc6;(sP!=~w4V;HcvZ<(rEJE#QIr`&v)I#^77aqqDJcSzn5^94#
zqUQPcaN@5Hk7-b7gGZPUHb#Xg6P0w`FdX}!2AF`Fuo(4zIVxfms0FM=O|TvHTd^Ot
zfUi)waTQ(YZpQw211DqpD0|kZBdE0X)2Ji(29-m;`6kpM
zs3Q)?V$4P@WG8CF?t>H*$}`rBm`43N>Vs1MNi$$3>g>9p`u9dXAA%lOZhZk2nc1i#
zsK8i!4Rxfut=|~k&hHeI?Qx^c1liUesEFiZeH?>Y`Ln2_nTJ})tM>V7)NR;hJ%(Dy
z*BFUcu_gY2dOvZD#^wB5Q)op)Kh*u7WnG7w@IzD%oI>?GgWAy}Y=hBb&A>x2f%<$@
z`%c?_9h0cnEvWh8IoYUwui;2Hg##2avFJs9Jl4Z!
zP+7bHV{t8Nf)7y(zhM0r`cU_tXf_szT5#+{;;(CyNQ3V0SZs);r~xWaFK)y}_zo&!
zXHfU}H*4HereALir+qLsz$vJO&c|N(2FBqv)B?QRPn(?vTjNooYGdt+H9JSWFdh|=
z5>!@CM=j_zR0Iy$`ekfQz4j#Y?*&;HPklDFz_+nEy3bJ101r^tBYrYh3g8&6{n
z`jnXGy-_QkVe6YPm-<(z?2jwumk~Q)GQN%j@MBEG;AhN-ts{1zUW$Rb|CJQ#(C{JZ
zI-SMF5XX$Ts?SGt+=F`YGPXo-&PQjTfeAPe!*B*Fd6%GW!43?;lc@d|QT?h>$?5wX
z@mIFSQBZPZqORQ-tdG;Ii?AN`^{55xLxubXYT(D#hR>Uc+n^@MLrplv*5{z!TZ3BQ
z`_B{q1{BWHpn-0p7T{TK7Em8GKr(8fT~G_jM-4R1)?YzgtBtmP0z;`^!W8@`>UPA;
zFh|)5)joCx@z(@PZNnDyp?)591m9yI2Fx@Iip1K~)6g3;QSay2_MsR|eKabUN>E3z
z7`5Z|sPD*OjK=TX6zWrWgqaxjg87qb0BXf$*cDfx`d!3G{5Ntz9RFEnpc$xztwt?)
zJ^JA;TR(_e*lFt}^rh~;LqR*P#*X+1mHnCjWkT2!HBdfg<3wzaTT#h*6_uR+v&{lp
zTeDE(bw?fXJk$}b#TYz{%;R=0Q_v1?VQ18pcpXbnp?YGD`7vpX3S}ZT#4N0ZBTzdT
zi`wBN)COjvuJuCH5v@dR^c__HDvZ$mKSCjjh6||BR^uQHp35R|Jf`87s29EFnF#_>
z$r_1D%9f}j=!QzZe)f3*HlscrwSaY~g>S}S=65P7Xn@100ngd`54Qe0Y6pSyO%B9k
z4E0P~ABp{_KZ8Md1oazn1{HyGn1EMN6Z^hso(H2lg@$?*w6h+Vfdf$sc?orPYj8O3
z!DrCtC9|Lz=udqKM&MdhuIxn})lpPLzO?mAm`wdHcEN}R#9twQa)EhJjyl_|*a44W
z0Y1XEIA)>w_xV+*34X#__!y(ndy(03Ji4f-qHe=r^uckc_a>t@IBOB{S7=|OAsjcO
z4<5lBJc&tI`(^VZl!01UA&$chs0ese*qp$2)FV;ZKLqvuGIZfbsENNv9r=AXg+3H~
zUNJA^q0V+G>YD9FCFgC_z_ngA3(dBUKs}#^isTY3!gp+Y@M81#e=^3?J_NPFa@0cH
zb1C>!Sb-X71L`b~STA4z^?RrYJw~m(!4mUcGU~|MV>%AOBwS?c`%p=F8#Uo?sE7x=
zR`b5wiK7rpLo#||F4n<8n2KXD5SL-i0?~{55!8Z?qc(6EHNj16j1SQpW0#s7X@dFG
zlW;69!g0F)w=V2@AQ&CCvCTgNXsD*xQtwx1B>~-@cOhFx8KI*-h*a+94
zB6J8{cpm-m0oG!E$78v<*S_dW-G!mp0^_g~Hp2pJf{U;X?m{Kw52))Ix5EAwpo{uy
z)R`YZMdDjb#%rhu2e0J(70MV2nm8W$DR+`lXTBM=^7k+d|Bi~p|DcZIAu4hWR+$N0
zsH19%nxHiXV^>=rV(a6vF74A-5r5s^1vDt>7NgE`o9$4AdVUf$;2G3{FQbm)Dr%w5
zY7?hI1`l{OHmUZKo@?2{qY`Z;W_Kf&-74iO5Kevxs`&F>=aJG+o*wZ|7uoVgdx<+
zupTZz4e(dgQJg@X6?@fl=ZxAPnYy|@N5@Lf#BYgiw{
z-ZIxH5!+Mmk8N=gDwH2%2mB57UhDOy{V8lseFL_^^QiagY%ulin5p|ei-HE;Z@q_|
zs3&eTpXSM^h3&%>yo2c&`L_AHU;wIp9xA!2P!aqHWAQw8!(UOk(P5MMZgjz#-~TZb
z^kOM0n`c@VqKo+AYk$e|5&nKu2oJF@*
z)r*Sv{Br`84Xj&PHYZV$_7|Q9G%`+ISsxRCiG!{|~y*e}}0jq55TGJoZFI
zWC~{E22^fbbyLs)&O7E)8j4EBDAWMMa59cT7k-NR0Np{o_d6zFV5RxlZHtYl7osL!
zh{}OV)P$d)HhKd!j{6}6h2DRs=@5bnSt|PC0MtYyQD<0;%8iBS!mX%&r_c+(MD;t5
zes~Wx(I2+G&b#I)5|9YHooos^!$H^>i%>~bfx3q4Q9s95Q1?A>mwDb3^Qk|HgYWR_pDHs^BdklkxAjR3p2)m2o|+{Gje+G_^th}yvb
z)Tj12RHWWV7oJ63ub1X=qKy`n2~$g?<9o!)I;#E2xOS?Y4z?
zF`R}2w!@bgO8u6t|ADQkhaNWldZUtSJZj())b)E2HQ~z`fa_4>ZbvQX7`pHxDi_?p
zQD{gZ;)qF#4AdD8MrCOCsv?#d=3@TJN9|VN2Y%YDv~+a7Uy6)Jb+E`9%^F^
zj@JC);&u{E!O63}fK%wO2|HlW$7ZK}QCU44wUf!H1g8fm)6Z0QRq*@0(=s`!Bk8;Zj!MOJ5%3?-S9eUpoA0r#Nu$w!K>H{<32G-
zIvI6jdr%Sh(Hi^TrhNovF~75ff_D0M%s`)$<~JYW1us`EFK
z6S1iFP8fl?*aDxzM0_2i@FUcbT}G!#!o*2M#f8nMXS$jsOs$G+al5u}Sy@p@;kc3s
zMO9&G9$uc~CX`i$v>6<>yl-MiRc@aPfmMS>z8LP4+SZktk(t`A>bq$k{=NhMa$?8y
yBE55OR-|`8*V59z6qc5hoY+w`y~=;yz=-8VAJki(x-P70)VjTOtNQJ#jQ&5}sp-@J
diff --git a/app/translations/fr/LC_MESSAGES/messages.po b/app/translations/fr/LC_MESSAGES/messages.po
index 6bbdabb..1afe5ce 100644
--- a/app/translations/fr/LC_MESSAGES/messages.po
+++ b/app/translations/fr/LC_MESSAGES/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-08-17 14:18-0400\n"
+"POT-Creation-Date: 2026-08-25 18:15-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME \n"
"Language: fr\n"
@@ -19,12 +19,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
-#: app/app.py:562
+#: app/app.py:564
msgid "Please log in to access this page."
msgstr "Veuillez vous connecter pour accéder à cette page."
-#: app/forms.py:37 app/routes/auth.py:229 app/routes/auth.py:388
-#: app/routes/auth.py:402 app/routes/users/contracts.py:98
+#: app/forms.py:37 app/routes/auth.py:230 app/routes/auth.py:389
+#: app/routes/auth.py:403 app/routes/users/contracts.py:103
#, python-format
msgid "%(field)s: %(msg)s"
msgstr "%(field)s : %(msg)s"
@@ -108,11 +108,11 @@ msgstr "Vous devez choisir un joueur."
msgid "Notes must be 2000 characters or less."
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
-#: app/validators.py:513 app/validators.py:993
+#: app/validators.py:513 app/validators.py:994
msgid "Invalid coach selection."
msgstr "Sélection de coach invalide."
-#: app/validators.py:519 app/validators.py:999
+#: app/validators.py:519 app/validators.py:1000
msgid "Invalid manager selection."
msgstr "Sélection de gérant invalide."
@@ -248,32 +248,32 @@ msgstr "Une sélection doit accepter au moins un joueur."
msgid "The player limit must be a whole number."
msgstr "La limite de joueurs doit être un nombre entier."
-#: app/validators.py:959
+#: app/validators.py:960
msgid "End date cannot be before start date."
msgstr "La date de fin ne peut pas précéder la date de début."
-#: app/validators.py:986 app/validators.py:987
+#: app/validators.py:987 app/validators.py:988
msgid "Team name is required."
msgstr "Le nom de l’équipe est obligatoire."
-#: app/validators.py:1011
+#: app/validators.py:1012
msgid "Scores run from 1 to 10."
msgstr "Les notes vont de 1 à 10."
-#: app/validators.py:1012
+#: app/validators.py:1013
msgid "A score must be a whole number from 1 to 10."
msgstr "Une note doit être un nombre entier de 1 à 10."
-#: app/routes/auth.py:246
+#: app/routes/auth.py:247
msgid "This account has been deactivated."
msgstr "Ce compte a été désactivé."
-#: app/routes/auth.py:281
+#: app/routes/auth.py:282
#, python-format
msgid "Welcome back, %(username)s!"
msgstr "Bon retour, %(username)s !"
-#: app/routes/auth.py:311
+#: app/routes/auth.py:312
msgid ""
"Login unsuccessful. Please check your username and password, or ask a "
"president for help."
@@ -281,32 +281,32 @@ msgstr ""
"Échec de la connexion. Vérifiez le nom d’utilisateur et le mot de passe, "
"ou demandez de l’aide à un président."
-#: app/routes/auth.py:377
+#: app/routes/auth.py:378
msgid "Your registration could not be processed. Please try again."
msgstr "Votre inscription n'a pas pu être traitée. Veuillez réessayer."
-#: app/routes/auth.py:419 app/routes/users/accounts.py:343
+#: app/routes/auth.py:420 app/routes/users/accounts.py:343
msgid "Username already exists."
msgstr "Ce nom d’utilisateur est déjà pris."
-#: app/routes/auth.py:423 app/routes/users/accounts.py:347
+#: app/routes/auth.py:424 app/routes/users/accounts.py:347
msgid "Email already registered."
msgstr "Cette adresse courriel est déjà enregistrée."
-#: app/routes/auth.py:430 app/routes/auth.py:623
+#: app/routes/auth.py:431 app/routes/auth.py:624
#: app/routes/users/accounts.py:121
msgid "This Discord account is already linked to another account."
msgstr "Ce compte Discord est déjà lié à un autre compte."
-#: app/routes/auth.py:472
+#: app/routes/auth.py:473
msgid "Your account has been created! You can now log in."
msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter."
-#: app/routes/auth.py:497
+#: app/routes/auth.py:498
msgid "Discord OAuth2 is not configured."
msgstr "La connexion Discord n’est pas configurée."
-#: app/routes/auth.py:546
+#: app/routes/auth.py:547
msgid ""
"Discord authorization could not be verified. Please start the connection "
"again from this page."
@@ -314,72 +314,72 @@ msgstr ""
"L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion "
"depuis cette page."
-#: app/routes/auth.py:555
+#: app/routes/auth.py:556
msgid "Discord authorization failed. No code received."
msgstr "L’autorisation Discord a échoué : aucun code reçu."
-#: app/routes/auth.py:579
+#: app/routes/auth.py:580
msgid "Failed to connect to Discord. Please try again."
msgstr "Impossible de joindre Discord. Veuillez réessayer."
-#: app/routes/auth.py:583
+#: app/routes/auth.py:584
msgid "Failed to obtain Discord access token."
msgstr "Impossible d’obtenir le jeton d’accès Discord."
-#: app/routes/auth.py:598 app/routes/auth.py:608
+#: app/routes/auth.py:599 app/routes/auth.py:609
msgid "Failed to fetch Discord user profile."
msgstr "Impossible de récupérer le profil Discord."
-#: app/routes/auth.py:615
+#: app/routes/auth.py:616
msgid "Please log in to connect your Discord account."
msgstr "Veuillez vous connecter pour lier votre compte Discord."
-#: app/routes/auth.py:634
+#: app/routes/auth.py:635
msgid "Discord account connected!"
msgstr "Compte Discord connecté !"
-#: app/routes/auth.py:681
+#: app/routes/auth.py:682
msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
-#: app/routes/auth.py:709
+#: app/routes/auth.py:710
msgid "You have been logged out."
msgstr "Vous avez été déconnecté."
-#: app/routes/evaluations.py:37
+#: app/routes/evaluations.py:53
msgid "You do not have permission to view evaluations."
msgstr "Vous n’avez pas les droits pour consulter les évaluations."
-#: app/routes/evaluations.py:126
+#: app/routes/evaluations.py:143
msgid "You do not have permission to evaluate players."
msgstr "Vous n’avez pas les droits pour évaluer des joueurs."
-#: app/routes/evaluations.py:131 app/routes/evaluations.py:215
+#: app/routes/evaluations.py:148 app/routes/evaluations.py:234
msgid "You do not have permission to evaluate players in this tryout."
msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection."
-#: app/routes/evaluations.py:142
+#: app/routes/evaluations.py:159
msgid "Player is not registered for this tryout."
msgstr "Ce joueur n’est pas inscrit à cette sélection."
-#: app/routes/evaluations.py:147
+#: app/routes/evaluations.py:164
msgid "Can only evaluate players."
msgstr "Seuls des joueurs peuvent être évalués."
-#: app/routes/evaluations.py:191
+#: app/routes/evaluations.py:210
msgid "Evaluation submitted successfully!"
msgstr "Évaluation enregistrée."
-#: app/routes/evaluations.py:193
+#: app/routes/evaluations.py:212
msgid "Evaluation updated!"
msgstr "Évaluation mise à jour."
-#: app/routes/evaluations.py:210 app/routes/teams.py:341
-#: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455
-#: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:471
-#: app/routes/tryouts.py:491 app/routes/tryouts.py:515
-#: app/routes/tryouts.py:562 app/routes/tryouts.py:598
-#: app/routes/tryouts.py:621
+#: app/routes/evaluations.py:229 app/routes/teams.py:340
+#: app/routes/teams.py:383 app/routes/teams.py:426 app/routes/teams.py:454
+#: app/routes/teams.py:482 app/routes/teams.py:519 app/routes/tryouts.py:507
+#: app/routes/tryouts.py:531 app/routes/tryouts.py:559
+#: app/routes/tryouts.py:610 app/routes/tryouts.py:650
+#: app/routes/tryouts.py:677
msgid "Permission denied."
msgstr "Accès refusé."
@@ -401,15 +401,15 @@ msgstr ""
msgid "Match scheduled successfully!"
msgstr "Match planifié."
-#: app/routes/matches.py:459 app/routes/team_matches.py:220
+#: app/routes/matches.py:459 app/routes/team_matches.py:235
msgid "You do not have permission to edit this match."
msgstr "Vous n’avez pas les droits pour modifier ce match."
-#: app/routes/matches.py:552 app/routes/team_matches.py:250
+#: app/routes/matches.py:552 app/routes/team_matches.py:265
msgid "Match updated successfully!"
msgstr "Match mis à jour."
-#: app/routes/matches.py:588 app/routes/team_matches.py:265
+#: app/routes/matches.py:588 app/routes/team_matches.py:284
msgid "You do not have permission to delete this match."
msgstr "Vous n’avez pas les droits pour supprimer ce match."
@@ -417,249 +417,249 @@ msgstr "Vous n’avez pas les droits pour supprimer ce match."
msgid "This tryout has ended. Matches can no longer be deleted."
msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés."
-#: app/routes/matches.py:604 app/routes/team_matches.py:269
+#: app/routes/matches.py:604 app/routes/team_matches.py:289
msgid "Match deleted successfully."
msgstr "Match supprimé."
-#: app/routes/team_matches.py:113
+#: app/routes/team_matches.py:128
msgid "You do not have permission to schedule matches for this team."
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe."
-#: app/routes/team_matches.py:205
+#: app/routes/team_matches.py:220
#, python-format
msgid "Team match \"%(title)s\" scheduled successfully!"
msgstr "Match d’équipe « %(title)s » planifié."
-#: app/routes/teams.py:44
+#: app/routes/teams.py:43
msgid "Use My Team(s) to view your teams."
msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
-#: app/routes/teams.py:47
+#: app/routes/teams.py:46
msgid "You do not have permission to view teams."
msgstr "Vous n’avez pas les droits pour consulter les équipes."
-#: app/routes/teams.py:79
+#: app/routes/teams.py:78
msgid "This page is for players."
msgstr "Cette page est réservée aux joueurs."
-#: app/routes/teams.py:186
+#: app/routes/teams.py:185
msgid "You do not have permission to create teams."
msgstr "Vous n’avez pas les droits pour créer une équipe."
-#: app/routes/teams.py:197 app/routes/teams.py:239
+#: app/routes/teams.py:196 app/routes/teams.py:238
#, python-format
msgid "Team \"%(name)s\" already exists."
msgstr "L’équipe « %(name)s » existe déjà."
-#: app/routes/teams.py:218
+#: app/routes/teams.py:217
#, python-format
msgid "Team \"%(name)s\" created successfully!"
msgstr "Équipe « %(name)s » créée."
-#: app/routes/teams.py:228
+#: app/routes/teams.py:227
msgid "You do not have permission to edit this team."
msgstr "Vous n’avez pas les droits pour modifier cette équipe."
-#: app/routes/teams.py:272
+#: app/routes/teams.py:271
#, python-format
msgid "Team \"%(name)s\" updated successfully!"
msgstr "Équipe « %(name)s » mise à jour."
-#: app/routes/teams.py:300
+#: app/routes/teams.py:299
msgid "You do not have permission to delete teams."
msgstr "Vous n’avez pas les droits pour supprimer une équipe."
-#: app/routes/teams.py:331
+#: app/routes/teams.py:330
#, python-format
msgid "Team \"%(name)s\" deleted successfully."
msgstr "Équipe « %(name)s » supprimée."
-#: app/routes/teams.py:348
+#: app/routes/teams.py:347
msgid "Please select a coach."
msgstr "Veuillez choisir un coach."
-#: app/routes/teams.py:353
+#: app/routes/teams.py:352
msgid "Only coaches can be assigned as coach."
msgstr "Seuls les coachs peuvent être assignés comme coach."
-#: app/routes/teams.py:359
+#: app/routes/teams.py:358
#, python-format
msgid "%(username)s is already a coach of %(name)s."
msgstr "%(username)s est déjà coach de %(name)s."
-#: app/routes/teams.py:372
+#: app/routes/teams.py:371
#, python-format
msgid "%(username)s added as coach of %(name)s."
msgstr "%(username)s a été ajouté comme coach de %(name)s."
-#: app/routes/teams.py:391
+#: app/routes/teams.py:390
msgid "Please select a manager."
msgstr "Veuillez choisir un gérant."
-#: app/routes/teams.py:396
+#: app/routes/teams.py:395
msgid "Only managers can be assigned as manager."
msgstr "Seuls les gérants peuvent être assignés comme gérant."
-#: app/routes/teams.py:402
+#: app/routes/teams.py:401
#, python-format
msgid "%(username)s is already a manager of %(name)s."
msgstr "%(username)s est déjà gérant de %(name)s."
-#: app/routes/teams.py:415
+#: app/routes/teams.py:414
#, python-format
msgid "%(username)s added as manager of %(name)s."
msgstr "%(username)s a été ajouté comme gérant de %(name)s."
-#: app/routes/teams.py:445
+#: app/routes/teams.py:444
#, python-format
msgid "Coach removed from %(name)s."
msgstr "Coach retiré de %(name)s."
-#: app/routes/teams.py:473
+#: app/routes/teams.py:472
#, python-format
msgid "Manager removed from %(name)s."
msgstr "Gérant retiré de %(name)s."
-#: app/routes/teams.py:490 app/routes/tryouts.py:524
+#: app/routes/teams.py:489 app/routes/tryouts.py:568
msgid "Please select a player."
msgstr "Veuillez choisir un joueur."
-#: app/routes/teams.py:496
+#: app/routes/teams.py:495
msgid "Can only assign players to teams."
msgstr "Seuls des joueurs peuvent être assignés à une équipe."
-#: app/routes/teams.py:502
+#: app/routes/teams.py:501
#, python-format
msgid "%(username)s is already on %(name)s."
msgstr "%(username)s fait déjà partie de %(name)s."
-#: app/routes/teams.py:510
+#: app/routes/teams.py:509
#, python-format
msgid "%(username)s added to %(name)s!"
msgstr "%(username)s a été ajouté à %(name)s."
-#: app/routes/teams.py:527 app/routes/teams.py:605
+#: app/routes/teams.py:526 app/routes/teams.py:604
#, python-format
msgid "%(username)s is not on %(name)s."
msgstr "%(username)s ne fait pas partie de %(name)s."
-#: app/routes/teams.py:535
+#: app/routes/teams.py:534
#, python-format
msgid "%(username)s removed from %(name)s."
msgstr "%(username)s a été retiré de %(name)s."
-#: app/routes/teams.py:572 app/routes/teams.py:594
+#: app/routes/teams.py:571 app/routes/teams.py:593
msgid "You do not have permission to add notes to this team."
msgstr "Vous n’avez pas les droits pour ajouter des notes à cette équipe."
-#: app/routes/teams.py:584
+#: app/routes/teams.py:583
msgid "Team notes added successfully!"
msgstr "Notes d’équipe ajoutées."
-#: app/routes/teams.py:599 app/routes/users/notes.py:222
+#: app/routes/teams.py:598 app/routes/users/notes.py:222
#: app/routes/users/notes.py:262
msgid "Can only add notes for players."
msgstr "Il n’est possible d’ajouter des notes que pour des joueurs."
-#: app/routes/teams.py:619
+#: app/routes/teams.py:618
#, python-format
msgid "Note added for %(username)s!"
msgstr "Note ajoutée pour %(username)s."
-#: app/routes/tryouts.py:125
+#: app/routes/tryouts.py:147
msgid "You do not have permission to create tryouts."
msgstr "Vous n’avez pas les droits pour créer une sélection."
-#: app/routes/tryouts.py:172
+#: app/routes/tryouts.py:194
msgid "Tryout created successfully!"
msgstr "Sélection créée."
-#: app/routes/tryouts.py:185
+#: app/routes/tryouts.py:211
msgid "You do not have permission to edit this tryout."
msgstr "Vous n’avez pas les droits pour modifier cette sélection."
-#: app/routes/tryouts.py:189
+#: app/routes/tryouts.py:215
msgid "This tryout has ended and can no longer be modified."
msgstr "Cette sélection est terminée et ne peut plus être modifiée."
-#: app/routes/tryouts.py:229
+#: app/routes/tryouts.py:261
msgid "Tryout updated successfully!"
msgstr "Sélection mise à jour."
-#: app/routes/tryouts.py:269
+#: app/routes/tryouts.py:301
msgid "You do not have permission to view this tryout."
msgstr "Vous n’avez pas les droits pour consulter cette sélection."
-#: app/routes/tryouts.py:437
+#: app/routes/tryouts.py:469
msgid "Only players can register for tryouts."
msgstr "Seuls les joueurs peuvent s’inscrire à une sélection."
-#: app/routes/tryouts.py:442
+#: app/routes/tryouts.py:474
msgid "This tryout is not accepting registrations."
msgstr "Cette sélection n’accepte pas d’inscriptions."
-#: app/routes/tryouts.py:449
+#: app/routes/tryouts.py:481
msgid "You are already registered for this tryout."
msgstr "Vous êtes déjà inscrit à cette sélection."
-#: app/routes/tryouts.py:455 app/routes/tryouts.py:546
+#: app/routes/tryouts.py:487 app/routes/tryouts.py:590
msgid "This tryout is full."
msgstr "Cette sélection est complète."
-#: app/routes/tryouts.py:461
+#: app/routes/tryouts.py:493
msgid "Successfully registered for tryout!"
msgstr "Inscription à la sélection réussie."
-#: app/routes/tryouts.py:481
+#: app/routes/tryouts.py:517
#, python-format
msgid "Tryout status updated to %(new_status)s."
msgstr "Statut de la sélection mis à jour : %(new_status)s."
-#: app/routes/tryouts.py:505
+#: app/routes/tryouts.py:545
msgid "Registration status updated."
msgstr "Statut d’inscription mis à jour."
-#: app/routes/tryouts.py:532
+#: app/routes/tryouts.py:576
msgid "Can only register players."
msgstr "Seuls des joueurs peuvent être inscrits."
-#: app/routes/tryouts.py:538
+#: app/routes/tryouts.py:582
#, python-format
msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s est déjà inscrit à cette sélection."
-#: app/routes/tryouts.py:552
+#: app/routes/tryouts.py:596
#, python-format
msgid "%(username)s registered for tryout!"
msgstr "%(username)s est inscrit à la sélection."
-#: app/routes/tryouts.py:588
+#: app/routes/tryouts.py:636
#, python-format
msgid "%(username)s removed from tryout."
msgstr "%(username)s a été retiré de la sélection."
-#: app/routes/tryouts.py:610
+#: app/routes/tryouts.py:662
#, python-format
msgid "Team \"%(team_name)s\" created!"
msgstr "Équipe « %(team_name)s » créée."
-#: app/routes/tryouts.py:643 app/routes/users/notes.py:350
+#: app/routes/tryouts.py:699 app/routes/users/notes.py:350
msgid "That player is not registered for this tryout."
msgstr "Ce joueur n’est pas inscrit à cette sélection."
-#: app/routes/tryouts.py:648
+#: app/routes/tryouts.py:704
msgid "Player is already on this team."
msgstr "Ce joueur est déjà dans cette équipe."
-#: app/routes/tryouts.py:653
+#: app/routes/tryouts.py:709
msgid "Player added to team!"
msgstr "Joueur ajouté à l’équipe."
-#: app/routes/tryouts.py:663
+#: app/routes/tryouts.py:723
msgid "You do not have permission to delete this tryout."
msgstr "Vous n’avez pas les droits pour supprimer cette sélection."
-#: app/routes/tryouts.py:699
+#: app/routes/tryouts.py:759
msgid "Tryout deleted successfully."
msgstr "Sélection supprimée."
@@ -732,32 +732,37 @@ msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
msgid "Only coaches can manage availability."
msgstr "Seuls les coachs peuvent gérer leurs disponibilités."
-#: app/routes/users/contracts.py:86
+#: app/routes/users/contracts.py:91
msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat."
-#: app/routes/users/contracts.py:105
+#: app/routes/users/contracts.py:110
msgid "You do not have permission to upload a contract for this player."
msgstr "Vous n’avez pas les droits pour téléverser un contrat pour ce joueur."
-#: app/routes/users/contracts.py:145
+#: app/routes/users/contracts.py:137 app/routes/users/contracts.py:180
+#: app/routes/users/contracts.py:203 app/routes/users/contracts.py:222
+msgid "Contract storage is temporarily unavailable. Please try again later."
+msgstr ""
+
+#: app/routes/users/contracts.py:152
#, python-format
msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contrat téléversé pour %(username)s."
-#: app/routes/users/contracts.py:159
+#: app/routes/users/contracts.py:166
msgid "Only the player can upload their signed contract."
msgstr "Seul le joueur peut téléverser son contrat signé."
-#: app/routes/users/contracts.py:177
+#: app/routes/users/contracts.py:188
msgid "Signed contract uploaded successfully!"
msgstr "Contrat signé téléversé."
-#: app/routes/users/contracts.py:187 app/routes/users/contracts.py:202
+#: app/routes/users/contracts.py:198 app/routes/users/contracts.py:214
msgid "You do not have permission to download this contract."
msgstr "Vous n’avez pas les droits pour télécharger ce contrat."
-#: app/routes/users/contracts.py:205
+#: app/routes/users/contracts.py:217
msgid "No signed contract available."
msgstr "Aucun contrat signé disponible."
@@ -850,48 +855,48 @@ msgstr "Contexte de match invalide."
msgid "That player did not participate in this match."
msgstr "Ce joueur n’a pas participé à ce match."
-#: app/routes/users/one_on_one.py:23
+#: app/routes/users/one_on_one.py:22
msgid "Only players can request One on One sessions."
msgstr "Seuls les joueurs peuvent demander une rencontre individuelle."
-#: app/routes/users/one_on_one.py:36
+#: app/routes/users/one_on_one.py:35
msgid "You do not have a coach assigned to your team."
msgstr "Aucun coach n’est assigné à votre équipe."
-#: app/routes/users/one_on_one.py:67
+#: app/routes/users/one_on_one.py:66
msgid "Cannot request One on One - no coach assigned."
msgstr "Impossible de demander une rencontre : aucun coach assigné."
-#: app/routes/users/one_on_one.py:93
+#: app/routes/users/one_on_one.py:92
msgid "The requested time is not within the coach's availability."
msgstr "L’horaire demandé ne correspond à aucune disponibilité du coach."
-#: app/routes/users/one_on_one.py:121
+#: app/routes/users/one_on_one.py:120
msgid "Your One on One request has been submitted!"
msgstr "Votre demande de rencontre a été envoyée."
-#: app/routes/users/one_on_one.py:166
+#: app/routes/users/one_on_one.py:165
msgid "Only coaches can accept One on One requests."
msgstr "Seuls les coachs peuvent accepter une demande de rencontre."
-#: app/routes/users/one_on_one.py:172 app/routes/users/one_on_one.py:222
+#: app/routes/users/one_on_one.py:171 app/routes/users/one_on_one.py:221
msgid "This request is not for you."
msgstr "Cette demande ne vous est pas destinée."
-#: app/routes/users/one_on_one.py:176 app/routes/users/one_on_one.py:226
+#: app/routes/users/one_on_one.py:175 app/routes/users/one_on_one.py:225
msgid "This request has already been processed."
msgstr "Cette demande a déjà été traitée."
-#: app/routes/users/one_on_one.py:203
+#: app/routes/users/one_on_one.py:202
#, python-format
msgid "One on One request from %(player)s has been approved!"
msgstr "La demande de rencontre de %(player)s a été approuvée."
-#: app/routes/users/one_on_one.py:216
+#: app/routes/users/one_on_one.py:215
msgid "Only coaches can reject One on One requests."
msgstr "Seuls les coachs peuvent refuser une demande de rencontre."
-#: app/routes/users/one_on_one.py:263
+#: app/routes/users/one_on_one.py:262
#, python-format
msgid "One on One request from %(player)s has been rejected."
msgstr "La demande de rencontre de %(player)s a été refusée."
@@ -1051,7 +1056,7 @@ msgstr "%(total)s au total"
msgid "Next"
msgstr "Suivant"
-#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:148
+#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:154
#: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3
msgid "Dashboard"
msgstr "Tableau de bord"
@@ -1086,39 +1091,39 @@ msgstr "Gestion des équipes"
msgid "Manage Users"
msgstr "Gestion des utilisateurs"
-#: app/templates/layouts/base.html:98
+#: app/templates/layouts/base.html:104
#: app/templates/pages/player_personal_notes.html:2
#: app/templates/pages/player_personal_notes.html:3
msgid "My Notes"
msgstr "Mes notes"
-#: app/templates/layouts/base.html:106
+#: app/templates/layouts/base.html:112
msgid "Notes & One on One"
msgstr "Notes et rencontres individuelles"
-#: app/templates/layouts/base.html:113 app/templates/pages/contracts.html:2
+#: app/templates/layouts/base.html:119 app/templates/pages/contracts.html:2
#: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9
msgid "Contracts"
msgstr "Contrats"
-#: app/templates/layouts/base.html:120 app/templates/pages/profile.html:2
+#: app/templates/layouts/base.html:126 app/templates/pages/profile.html:2
#: app/templates/pages/profile.html:3
msgid "My Profile"
msgstr "Mon profil"
-#: app/templates/layouts/base.html:131
+#: app/templates/layouts/base.html:137
msgid "Logout"
msgstr "Déconnexion"
-#: app/templates/layouts/base.html:152
+#: app/templates/layouts/base.html:158
msgid "Toggle dark mode"
msgstr "Basculer le mode sombre"
-#: app/templates/layouts/base.html:164 app/templates/layouts/base.html:183
+#: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189
msgid "Dismiss"
msgstr "Fermer"
-#: app/templates/layouts/base.html:193
+#: app/templates/layouts/base.html:199
msgid "UdeS team manager"
msgstr "UdeS team manager"
@@ -1136,16 +1141,15 @@ msgstr "Ajouter une note personnelle"
#: app/templates/pages/dashboard.html:249
#: app/templates/pages/dashboard.html:363 app/templates/pages/my_teams.html:52
#: app/templates/pages/notes.html:122
-#: app/templates/pages/players_to_evaluate.html:16
#: app/templates/pages/team_match_form.html:114
-#: app/templates/pages/teams.html:145 app/templates/pages/view_tryout.html:148
-#: app/templates/pages/view_tryout.html:492
+#: app/templates/pages/teams.html:145 app/templates/pages/view_tryout.html:154
+#: app/templates/pages/view_tryout.html:498
msgid "Player"
msgstr "Joueur"
#: app/templates/pages/add_note.html:49 app/templates/pages/teams.html:216
#: app/templates/pages/upload_contract.html:18
-#: app/templates/pages/view_tryout.html:124
+#: app/templates/pages/view_tryout.html:130
msgid "-- Select a player --"
msgstr "-- Choisir un joueur --"
@@ -1190,11 +1194,11 @@ msgstr "Notes d’équipe (référence)"
#: app/templates/pages/evaluate_player.html:131
#: app/templates/pages/notes.html:100
#: app/templates/pages/personal_notes.html:36
-#: app/templates/pages/view_tryout.html:303
+#: app/templates/pages/view_tryout.html:309
msgid "Add Note"
msgstr "Ajouter une note"
-#: app/templates/pages/calendar.html:9 app/templates/pages/view_tryout.html:296
+#: app/templates/pages/calendar.html:9 app/templates/pages/view_tryout.html:302
msgid "Schedule"
msgstr "Planifier"
@@ -1300,7 +1304,7 @@ msgstr ""
"individuelles"
#: app/templates/pages/coach_availability.html:14
-#: app/templates/pages/profile.html:213
+#: app/templates/pages/profile.html:256
msgid "Loading availability grid..."
msgstr "Chargement de la grille de disponibilités..."
@@ -1309,7 +1313,7 @@ msgid "Save Availability"
msgstr "Enregistrer les disponibilités"
#: app/templates/pages/coach_availability.html:22
-#: app/templates/pages/profile.html:197 app/templates/pages/profile.html:217
+#: app/templates/pages/profile.html:240 app/templates/pages/profile.html:260
msgid "Clear All"
msgstr "Tout effacer"
@@ -1340,11 +1344,10 @@ msgstr "Contrat"
#: app/templates/pages/dashboard.html:321
#: app/templates/pages/match_form.html:60 app/templates/pages/my_teams.html:53
#: app/templates/pages/notes.html:126 app/templates/pages/one_on_one.html:73
-#: app/templates/pages/players_to_evaluate.html:19
#: app/templates/pages/team_match_form.html:61
#: app/templates/pages/team_matches.html:33 app/templates/pages/teams.html:146
-#: app/templates/pages/users.html:24 app/templates/pages/view_tryout.html:149
-#: app/templates/pages/view_tryout.html:321
+#: app/templates/pages/users.html:24 app/templates/pages/view_tryout.html:155
+#: app/templates/pages/view_tryout.html:327
msgid "Status"
msgstr "Statut"
@@ -1353,11 +1356,10 @@ msgid "Uploaded"
msgstr "Téléversé le"
#: app/templates/pages/contracts.html:30 app/templates/pages/dashboard.html:175
-#: app/templates/pages/notes.html:127
-#: app/templates/pages/players_to_evaluate.html:20
-#: app/templates/pages/team_matches.html:34 app/templates/pages/teams.html:151
-#: app/templates/pages/users.html:26 app/templates/pages/view_tryout.html:154
-#: app/templates/pages/view_tryout.html:323
+#: app/templates/pages/notes.html:127 app/templates/pages/team_matches.html:34
+#: app/templates/pages/teams.html:151 app/templates/pages/users.html:26
+#: app/templates/pages/view_tryout.html:160
+#: app/templates/pages/view_tryout.html:329
msgid "Actions"
msgstr "Actions"
@@ -1412,7 +1414,7 @@ msgstr "Formats acceptés : PDF, DOC, DOCX, JPG, PNG"
#: app/templates/pages/contracts.html:101
#: app/templates/pages/match_form.html:265 app/templates/pages/notes.html:189
#: app/templates/pages/teams.html:48 app/templates/pages/teams.html:295
-#: app/templates/pages/view_tryout.html:239
+#: app/templates/pages/view_tryout.html:245
msgid "Cancel"
msgstr "Annuler"
@@ -1526,7 +1528,7 @@ msgstr "Titre"
#: app/templates/pages/notes.html:123 app/templates/pages/one_on_one.html:70
#: app/templates/pages/team_match_form.html:34
#: app/templates/pages/team_matches.html:29
-#: app/templates/pages/view_tryout.html:317
+#: app/templates/pages/view_tryout.html:323
msgid "Date"
msgstr "Date"
@@ -1539,7 +1541,7 @@ msgstr "Matchs à venir"
#: app/templates/pages/dashboard.html:202
#: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:90
#: app/templates/pages/notes.html:69 app/templates/pages/team_matches.html:26
-#: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:315
+#: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:321
msgid "Match"
msgstr "Match"
@@ -1622,7 +1624,7 @@ msgid "Avg Score"
msgstr "Note moyenne"
#: app/templates/pages/dashboard.html:373
-#: app/templates/pages/view_tryout.html:526
+#: app/templates/pages/view_tryout.html:532
msgid "No evaluations yet."
msgstr "Aucune évaluation pour l’instant."
@@ -1818,7 +1820,7 @@ msgid "Recommended Position"
msgstr "Poste recommandé"
#: app/templates/pages/evaluate_player.html:109
-#: app/templates/pages/view_tryout.html:274
+#: app/templates/pages/view_tryout.html:280
msgid "-- Select Position --"
msgstr "-- Choisir un poste --"
@@ -1839,63 +1841,63 @@ msgid "Add note for this player"
msgstr "Ajouter une note pour ce joueur"
#: app/templates/pages/evaluate_player.html:147
-#: app/templates/pages/view_tryout.html:493
+#: app/templates/pages/view_tryout.html:499
msgid "Evaluator"
msgstr "Évaluateur"
#: app/templates/pages/evaluate_player.html:148
-#: app/templates/pages/view_tryout.html:494
+#: app/templates/pages/view_tryout.html:500
msgid "Mecanics"
msgstr "Mécaniques"
#: app/templates/pages/evaluate_player.html:149
-#: app/templates/pages/view_tryout.html:495
+#: app/templates/pages/view_tryout.html:501
msgid "Cohesion"
msgstr "Cohésion"
#: app/templates/pages/evaluate_player.html:150
-#: app/templates/pages/view_tryout.html:496
+#: app/templates/pages/view_tryout.html:502
msgid "Communication"
msgstr "Communication"
#: app/templates/pages/evaluate_player.html:151
-#: app/templates/pages/view_tryout.html:497
+#: app/templates/pages/view_tryout.html:503
msgid "Gamesense"
msgstr "Sens du jeu"
#: app/templates/pages/evaluate_player.html:152
-#: app/templates/pages/view_tryout.html:498
+#: app/templates/pages/view_tryout.html:504
msgid "Versatility"
msgstr "Polyvalence"
#: app/templates/pages/evaluate_player.html:153
-#: app/templates/pages/view_tryout.html:499
+#: app/templates/pages/view_tryout.html:505
msgid "Discipline"
msgstr "Discipline"
#: app/templates/pages/evaluate_player.html:154
-#: app/templates/pages/view_tryout.html:500
+#: app/templates/pages/view_tryout.html:506
msgid "Analysis"
msgstr "Analyse"
#: app/templates/pages/evaluate_player.html:155
-#: app/templates/pages/view_tryout.html:501
+#: app/templates/pages/view_tryout.html:507
msgid "Sport Ethics"
msgstr "Éthique sportive"
#: app/templates/pages/evaluate_player.html:156
-#: app/templates/pages/view_tryout.html:502
+#: app/templates/pages/view_tryout.html:508
msgid "Mental"
msgstr "Mental"
#: app/templates/pages/evaluate_player.html:157
-#: app/templates/pages/view_tryout.html:503
+#: app/templates/pages/view_tryout.html:509
msgid "Overall"
msgstr "Global"
#: app/templates/pages/evaluate_player.html:158
#: app/templates/pages/my_teams.html:54
-#: app/templates/pages/view_tryout.html:280
+#: app/templates/pages/view_tryout.html:286
msgid "Position"
msgstr "Poste"
@@ -2000,7 +2002,7 @@ msgstr "Réinitialiser la plage horaire"
msgid "Reset Time"
msgstr "Réinitialiser l’heure"
-#: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:193
+#: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:236
msgid "Loading..."
msgstr "Chargement..."
@@ -2017,7 +2019,7 @@ msgid "End Time"
msgstr "Heure de fin"
#: app/templates/pages/match_form.html:114
-#: app/templates/pages/tryout_form.html:103
+#: app/templates/pages/tryout_form.html:110
msgid "Description"
msgstr "Description"
@@ -2131,14 +2133,14 @@ msgstr "Aucun joueur dans cette équipe."
#: app/templates/pages/my_teams.html:93 app/templates/pages/notes.html:124
#: app/templates/pages/one_on_one.html:71
#: app/templates/pages/team_matches.html:30
-#: app/templates/pages/view_tryout.html:319
+#: app/templates/pages/view_tryout.html:325
msgid "Time"
msgstr "Heure"
#: app/templates/pages/my_teams.html:95
#: app/templates/pages/team_match_form.html:115
#: app/templates/pages/team_matches.html:32
-#: app/templates/pages/view_tryout.html:320
+#: app/templates/pages/view_tryout.html:326
msgid "Presence"
msgstr "Présence"
@@ -2389,18 +2391,6 @@ msgstr "Notes récentes"
msgid "Players to Evaluate"
msgstr "Joueurs à évaluer"
-#: app/templates/pages/players_to_evaluate.html:17
-msgid "Contact"
-msgstr "Coordonnées"
-
-#: app/templates/pages/players_to_evaluate.html:18
-msgid "Attendance"
-msgstr "Présence"
-
-#: app/templates/pages/players_to_evaluate.html:51
-msgid "No players registered for this tryout."
-msgstr "Aucun joueur inscrit à cette sélection."
-
#: app/templates/pages/profile.html:21
msgid "Account Information"
msgstr "Informations du compte"
@@ -2437,23 +2427,23 @@ msgstr "Évaluations données"
msgid "No statistics available for this role."
msgstr "Aucune statistique pour ce rôle."
-#: app/templates/pages/profile.html:152
+#: app/templates/pages/profile.html:195
msgid "My Contracts"
msgstr "Mes contrats"
-#: app/templates/pages/profile.html:174
+#: app/templates/pages/profile.html:217
msgid "View All Contracts"
msgstr "Voir tous les contrats"
-#: app/templates/pages/profile.html:178
+#: app/templates/pages/profile.html:221
msgid "No contracts have been uploaded for you yet."
msgstr "Aucun contrat n’a encore été téléversé pour vous."
-#: app/templates/pages/profile.html:188
+#: app/templates/pages/profile.html:231
msgid "My Disponibilities"
msgstr "Mes disponibilités"
-#: app/templates/pages/profile.html:191
+#: app/templates/pages/profile.html:234
msgid ""
"Select your available time blocks for matches (5pm to 12am). Green = "
"selected, Gray = available to select."
@@ -2461,11 +2451,11 @@ msgstr ""
"Choisissez vos plages disponibles pour les matchs (17 h à minuit). Vert ="
" sélectionné, gris = disponible."
-#: app/templates/pages/profile.html:208
+#: app/templates/pages/profile.html:251
msgid "My Coaching Availability"
msgstr "Mes disponibilités de coaching"
-#: app/templates/pages/profile.html:209
+#: app/templates/pages/profile.html:252
msgid ""
"Select time slots when you're available for One on One sessions (8am to "
"10pm)."
@@ -2473,7 +2463,7 @@ msgstr ""
"Choisissez les plages où vous êtes disponible pour des rencontres "
"individuelles (8 h à 22 h)."
-#: app/templates/pages/profile.html:457
+#: app/templates/pages/profile.html:500
msgid "Click or click-and-drag to select your available hours"
msgstr "Cliquez ou faites glisser pour choisir vos heures de disponibilité"
@@ -2612,7 +2602,7 @@ msgid "No players on this team. Add players in the Teams page first."
msgstr "Aucun joueur dans cette équipe. Ajoutez-en d’abord depuis la page Équipes."
#: app/templates/pages/team_match_form.html:107
-#: app/templates/pages/view_tryout.html:318
+#: app/templates/pages/view_tryout.html:324
msgid "Participants"
msgstr "Participants"
@@ -2658,11 +2648,11 @@ msgid "Note History"
msgstr "Historique des notes"
#: app/templates/pages/teams.html:2 app/templates/pages/teams.html:3
-#: app/templates/pages/view_tryout.html:225
+#: app/templates/pages/view_tryout.html:231
msgid "Teams"
msgstr "Équipes"
-#: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:228
+#: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:234
msgid "New Team"
msgstr "Nouvelle équipe"
@@ -2686,11 +2676,11 @@ msgstr "Coach assigné"
msgid "-- No coach assigned --"
msgstr "-- Aucun coach assigné --"
-#: app/templates/pages/teams.html:38 app/templates/pages/tryout_form.html:70
+#: app/templates/pages/teams.html:38
msgid "Assigned Manager"
msgstr "Gérant assigné"
-#: app/templates/pages/teams.html:40 app/templates/pages/tryout_form.html:72
+#: app/templates/pages/teams.html:40
msgid "-- No manager assigned --"
msgstr "-- Aucun gérant assigné --"
@@ -2700,7 +2690,7 @@ msgstr "Créer l’équipe"
#: app/templates/pages/teams.html:64 app/templates/pages/users.html:51
#: app/templates/pages/view_tryout.html:29
-#: app/templates/pages/view_tryout.html:453
+#: app/templates/pages/view_tryout.html:459
msgid "Edit"
msgstr "Modifier"
@@ -2762,7 +2752,7 @@ msgstr "Poste ou rôle"
msgid "Remove %(player)s from %(team)s?"
msgstr "Retirer %(player)s de %(team)s ?"
-#: app/templates/pages/teams.html:186 app/templates/pages/view_tryout.html:205
+#: app/templates/pages/teams.html:186 app/templates/pages/view_tryout.html:211
msgid "Remove"
msgstr "Retirer"
@@ -2859,15 +2849,23 @@ msgstr "Équipe visée"
msgid "-- No target team --"
msgstr "-- Aucune équipe visée --"
-#: app/templates/pages/tryout_form.html:83
+#: app/templates/pages/tryout_form.html:70
+msgid "Assigned Managers"
+msgstr "Gérants assignés"
+
+#: app/templates/pages/tryout_form.html:85
+msgid "Select one or more managers for this tryout."
+msgstr "Sélectionnez un ou plusieurs gérants pour cette sélection."
+
+#: app/templates/pages/tryout_form.html:90
msgid "Assigned Coaches"
msgstr "Coachs assignés"
-#: app/templates/pages/tryout_form.html:99
+#: app/templates/pages/tryout_form.html:106
msgid "Select one or more coaches for this tryout."
msgstr "Choisissez un ou plusieurs coachs pour cette sélection."
-#: app/templates/pages/tryout_form.html:104
+#: app/templates/pages/tryout_form.html:111
msgid "Enter any details about the tryout..."
msgstr "Précisions sur la sélection..."
@@ -2921,7 +2919,7 @@ msgid "Tryout Details"
msgstr "Détail de la sélection"
#: app/templates/pages/view_tryout.html:14
-#: app/templates/pages/view_tryout.html:300
+#: app/templates/pages/view_tryout.html:306
msgid "Schedule Match"
msgstr "Planifier un match"
@@ -2941,39 +2939,39 @@ msgstr ""
msgid "Delete Tryout"
msgstr "Supprimer la sélection"
-#: app/templates/pages/view_tryout.html:108
+#: app/templates/pages/view_tryout.html:114
msgid "Register for this Tryout"
msgstr "S’inscrire à cette sélection"
-#: app/templates/pages/view_tryout.html:118
+#: app/templates/pages/view_tryout.html:124
msgid "Add Player to Tryout"
msgstr "Ajouter un joueur à la sélection"
-#: app/templates/pages/view_tryout.html:131
+#: app/templates/pages/view_tryout.html:137
msgid "Register Player"
msgstr "Inscrire un joueur"
-#: app/templates/pages/view_tryout.html:151
+#: app/templates/pages/view_tryout.html:157
msgid "Evaluation"
msgstr "Évaluation"
-#: app/templates/pages/view_tryout.html:173
+#: app/templates/pages/view_tryout.html:179
msgid "Registered"
msgstr "Inscrit"
-#: app/templates/pages/view_tryout.html:174
+#: app/templates/pages/view_tryout.html:180
msgid "Attended"
msgstr "Présent"
-#: app/templates/pages/view_tryout.html:175
+#: app/templates/pages/view_tryout.html:181
msgid "No Show"
msgstr "Absent"
-#: app/templates/pages/view_tryout.html:194
+#: app/templates/pages/view_tryout.html:200
msgid "Evaluate player"
msgstr "Évaluer le joueur"
-#: app/templates/pages/view_tryout.html:202
+#: app/templates/pages/view_tryout.html:208
#, python-format
msgid ""
"Remove %(name)s from this tryout? They will also be removed from every "
@@ -2982,63 +2980,63 @@ msgstr ""
"Retirer %(name)s de cette sélection ? Il sera aussi retiré de toutes ses "
"équipes et de tous ses matchs."
-#: app/templates/pages/view_tryout.html:204
+#: app/templates/pages/view_tryout.html:210
msgid "Remove player from tryout"
msgstr "Retirer le joueur de la sélection"
-#: app/templates/pages/view_tryout.html:214
+#: app/templates/pages/view_tryout.html:220
msgid "No players registered yet."
msgstr "Aucun joueur inscrit pour l’instant."
-#: app/templates/pages/view_tryout.html:237
+#: app/templates/pages/view_tryout.html:243
msgid "Team name"
msgstr "Nom de l’équipe"
-#: app/templates/pages/view_tryout.html:238
+#: app/templates/pages/view_tryout.html:244
msgid "Create"
msgstr "Créer"
-#: app/templates/pages/view_tryout.html:257
+#: app/templates/pages/view_tryout.html:263
msgid "No players assigned yet."
msgstr "Aucun joueur assigné pour l’instant."
-#: app/templates/pages/view_tryout.html:265
+#: app/templates/pages/view_tryout.html:271
msgid "Select player..."
msgstr "Choisir un joueur..."
-#: app/templates/pages/view_tryout.html:282
+#: app/templates/pages/view_tryout.html:288
msgid "Add"
msgstr "Ajouter"
-#: app/templates/pages/view_tryout.html:287
+#: app/templates/pages/view_tryout.html:293
msgid "No teams created yet."
msgstr "Aucune équipe créée."
-#: app/templates/pages/view_tryout.html:316
+#: app/templates/pages/view_tryout.html:322
msgid "Type"
msgstr "Type"
-#: app/templates/pages/view_tryout.html:381
+#: app/templates/pages/view_tryout.html:387
msgid "Toggle your attendance"
msgstr "Basculer votre présence"
-#: app/templates/pages/view_tryout.html:456
+#: app/templates/pages/view_tryout.html:462
msgid "Add Note for this Match"
msgstr "Ajouter une note pour ce match"
-#: app/templates/pages/view_tryout.html:457
+#: app/templates/pages/view_tryout.html:463
msgid "Note"
msgstr "Note"
-#: app/templates/pages/view_tryout.html:475
+#: app/templates/pages/view_tryout.html:481
msgid "No matches scheduled yet. Check back later!"
msgstr "Aucun match planifié pour l’instant. Revenez plus tard."
-#: app/templates/pages/view_tryout.html:485
+#: app/templates/pages/view_tryout.html:491
msgid "Evaluation Summary"
msgstr "Synthèse de l’évaluation"
-#: app/templates/pages/view_tryout.html:504
+#: app/templates/pages/view_tryout.html:510
msgid "Recommendation"
msgstr "Recommandation"
@@ -3106,3 +3104,13 @@ msgstr "Voir le profil"
#~ msgid "Save Disponibilities"
#~ msgstr "Enregistrer mes disponibilités"
+
+#~ msgid "Contact"
+#~ msgstr "Coordonnées"
+
+#~ msgid "Attendance"
+#~ msgstr "Présence"
+
+#~ msgid "No players registered for this tryout."
+#~ msgstr "Aucun joueur inscrit à cette sélection."
+
diff --git a/docs/deployment.md b/docs/deployment.md
index a8e37f4..532cad2 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -67,6 +67,28 @@ documents every variable. Copying it verbatim gives a configuration that
refuses to start until `SECRET_KEY` and `DATABASE_URL` are filled in, rather
than one that starts and is wide open (OPS-003).
+### Google Drive contract storage
+
+New contracts are stored in the owner's personal Google Drive when
+`DOCUMENT_STORAGE_BACKEND=google_drive`. Enable the Google Drive API in a
+Google Cloud project, create an OAuth client for the owner, and authorize it
+once with the `drive.file` scope. Configure the resulting values outside the
+repository:
+
+```text
+DOCUMENT_STORAGE_BACKEND=google_drive
+GOOGLE_DRIVE_FOLDER_ID=
+GOOGLE_DRIVE_CLIENT_ID=
+GOOGLE_DRIVE_CLIENT_SECRET=
+GOOGLE_DRIVE_REFRESH_TOKEN=
+```
+
+The refresh token can create, download, and delete contracts created by this
+application. Keep all four values out of source control. The database stores
+only `gdrive://` references, so existing local-file rows continue to
+work after the change. Use `DOCUMENT_STORAGE_BACKEND=local` only for tests,
+local development, or while migrating legacy files.
+
### Binding and proxy trust — read this before going live (OPS-002)
Two variables decide whether the rate limiter, the account lockout and the
diff --git a/pyproject.toml b/pyproject.toml
index 91a98b1..b910ea5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,7 +31,7 @@ show_missing = true
[tool.ruff]
line-length = 100
-target-version = "py312"
+target-version = "py313"
exclude = [".venv", "venv", "migrations", "docs"]
[tool.ruff.lint]
diff --git a/requirements.txt b/requirements.txt
index 610279c..36b574c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,6 +24,8 @@ Flask-SQLAlchemy==3.1.1
Flask-WTF==1.3.0
frozenlist==1.8.0
greenlet==3.5.4
+google-api-python-client==2.198.0
+google-auth==2.56.3
idna==3.18
itsdangerous==2.2.0
Jinja2==3.1.6
diff --git a/tests/conftest.py b/tests/conftest.py
index ccefa72..e043943 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -80,6 +80,7 @@ def documents_in_a_throwaway_directory(tmp_path, monkeypatch):
cannot leave a PDF in someone's working tree.
"""
monkeypatch.setenv('DOCUMENTS_ROOT', str(tmp_path / 'documents'))
+ monkeypatch.setenv('DOCUMENT_STORAGE_BACKEND', 'local')
@pytest.fixture
diff --git a/tests/test_database_url.py b/tests/test_database_url.py
index 6ff9233..46b55c4 100644
--- a/tests/test_database_url.py
+++ b/tests/test_database_url.py
@@ -1,17 +1,17 @@
"""Which PostgreSQL driver the application actually asks for — QUA-001.
`postgresql://…` is not "whichever driver is installed". SQLAlchemy reads it
-as psycopg2 and imports that module when the engine is created.
-requirements.txt pins psycopg 3 and no psycopg2, so a clean install against
-the URL Render hands out — the same form docs/database-restore.md documents
-— fails before the first request:
+as psycopg2 unless the URL names a driver. requirements.txt declares psycopg
+3, so create_app() must explicitly select it.
- ModuleNotFoundError: No module named 'psycopg2'
-
-create_app() now names the driver. These tests pin that down, and the last
-one proves the failure is real rather than theoretical.
+The test suite must not assume that psycopg2 is absent: a developer's virtual
+environment can contain optional packages in addition to the declared
+dependencies. These tests instead pin the application's selected driver and
+the dependency contract that production installs use.
"""
+from pathlib import Path
+
import pytest
from sqlalchemy import create_engine
@@ -80,16 +80,13 @@ class TestTheFactory:
assert app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite:///')
-class TestTheFailureIsReal:
- """Not a hypothetical: this is what the deployed configuration did."""
+class TestDriverContract:
+ def test_production_dependencies_select_psycopg_3(self):
+ requirements = Path(__file__).resolve().parents[1] / 'requirements.txt'
+ declared = requirements.read_text(encoding='utf-8')
- def test_psycopg2_is_not_installed(self):
- with pytest.raises(ImportError):
- import psycopg2 # noqa: F401
-
- def test_a_driverless_url_cannot_build_an_engine(self):
- with pytest.raises(ModuleNotFoundError):
- create_engine('postgresql://u:p@host/db')
+ assert 'psycopg[binary]==' in declared
+ assert 'psycopg2' not in declared
def test_the_normalised_url_can(self):
engine = create_engine(normalise_database_url('postgresql://u:p@host/db'))
diff --git a/tests/test_storage.py b/tests/test_storage.py
index e96197d..3b43f44 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -19,7 +19,13 @@ import os
import pytest
from app.extensions import db
-from app.storage import CONTRACTS_DIR, document_path, documents_root
+from app.storage import (
+ CONTRACTS_DIR,
+ GOOGLE_DRIVE_PATH_PREFIX,
+ document_path,
+ documents_root,
+ store_uploaded_document,
+)
class TestDocumentsRoot:
@@ -87,6 +93,22 @@ class TestDocumentPath:
assert first_old == second_old, 'and must not move the ones already filed'
+class TestGoogleDriveStorage:
+ def test_new_uploads_store_an_opaque_drive_reference(self, monkeypatch):
+ from werkzeug.datastructures import FileStorage
+
+ monkeypatch.setenv('DOCUMENT_STORAGE_BACKEND', 'google_drive')
+ monkeypatch.setattr(
+ 'app.storage.upload_google_drive_file',
+ lambda **_kwargs: 'drive-file-123',
+ )
+ upload = FileStorage(stream=_pdf(), filename='contract.pdf', content_type='application/pdf')
+
+ stored_path = store_uploaded_document(upload, os.path.join(CONTRACTS_DIR, 'new.pdf'))
+
+ assert stored_path == f'{GOOGLE_DRIVE_PATH_PREFIX}drive-file-123'
+
+
class TestThroughTheUploadRoute:
@pytest.fixture
def uploaded(self, app, client, as_role, make_user):