fix(auth): retirer un CAPTCHA qui ne protegeait rien, filtrer autrement

SEC-AUTH-008. L addition a deux operandes entre 1 et 10 avait dix-neuf
reponses possibles et se resolvait en lisant la question comme une chaine.
Elle n arretait aucune inscription automatisee. Elle ajoutait en revanche
une etape a chaque personne, lecteur d ecran compris, contre une apparence
de protection — ce qui est pire que rien, puisque ca se compte comme une
protection.

L autre branche proposee par l audit etait un vrai service de CAPTCHA :
un tiers, une cle d API, une requete a chaque affichage, et un script
etranger remis dans script-src, defaisant le travail qui a ferme
SEC-WEB-001. Disproportionne pour le site d un club.

A la place, deux verifications invisibles pour un visiteur : un champ
piege, cache par la feuille de style et hors du parcours clavier, qu un
robot remplisseur complete et qu une personne ne voit jamais ; et un delai
minimal entre la remise du formulaire et son retour, l horodatage etant
dans la session signee et non dans un champ.

Le plafond est dit dans le code plutot que sous-entendu : ceci arrete le
pourriel de masse, pas quelqu un qui lit la page. Ce qui filtrerait
vraiment les inscriptions serait l activation des comptes par le staff —
is_active_account vaut True par defaut. C est une decision de produit.

L angle « session forgeable » du constat tombe : avec SECRET_KEY
compromise (SEC-001), on forge une session connectee sur n importe quel
compte et on n a aucune raison de s inscrire.

Au passage, ARCH-005 en partie : le bloc « regenerer, purger les mots de
passe, re-rendre » etait recopie quatre fois. Un seul helper, et la purge
des mots de passe ne peut plus etre oubliee dans la cinquieme copie.

Les refus sont journalises avec leur motif — c est le seul endroit ou un
abus du formulaire devient visible — mais restent indistinguables pour
l expediteur : nommer la regle indique comment la contourner.

429 tests.
This commit is contained in:
GGThed
2026-08-11 12:14:50 -04:00
parent d0a9e75fe6
commit d8541678a6
9 changed files with 823 additions and 556 deletions
+106 -85
View File
@@ -2,12 +2,12 @@
This module handles user authentication including login with account lockout This module handles user authentication including login with account lockout
protection, logout with session clearing, and new user registration with protection, logout with session clearing, and new user registration with
password policy enforcement and CAPTCHA verification. password policy enforcement and sign-up screening.
""" """
import os import os
import secrets import secrets
import uuid import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
@@ -38,6 +38,18 @@ MAX_LOCKOUT_MINUTES = 240
#: response time stops telling a caller which usernames exist (SEC-017). #: response time stops telling a caller which usernames exist (SEC-017).
_ABSENT_USER_HASH = None _ABSENT_USER_HASH = None
#: Session key recording when the registration form was handed out.
REGISTRATION_ISSUED_KEY = 'registration_form_issued_at'
#: Name of the honeypot input. Plausible enough that a form-filler wants it,
#: absent from the visible form. Hidden by .honeypot in style.css — not by an
#: inline style, so that the rule survives a tightening of style-src.
REGISTRATION_HONEYPOT_FIELD = 'website'
#: Floor on how long a genuine registration takes. Eleven fields and a
#: password typed twice; three seconds is generous.
MIN_REGISTRATION_SECONDS = 3
# Discord OAuth2 configuration # Discord OAuth2 configuration
DISCORD_CLIENT_ID = os.getenv('DISCORD_CLIENT_ID') DISCORD_CLIENT_ID = os.getenv('DISCORD_CLIENT_ID')
DISCORD_CLIENT_SECRET = os.getenv('DISCORD_CLIENT_SECRET') DISCORD_CLIENT_SECRET = os.getenv('DISCORD_CLIENT_SECRET')
@@ -116,41 +128,65 @@ def cooloff_minutes(failed_attempts):
return min(LOCKOUT_DURATION_MINUTES * (2**steps), MAX_LOCKOUT_MINUTES) return min(LOCKOUT_DURATION_MINUTES * (2**steps), MAX_LOCKOUT_MINUTES)
def generate_captcha(): def issue_registration_challenge():
"""Generate a simple math CAPTCHA challenge. """Mark that the registration form has been handed out, and when.
Creates a random addition problem and stores the answer in the session. Kept in the signed session rather than in a form field, so that the
timestamp is not something the submitter can choose. Left in place across
a failed submission: someone correcting a typo should not be told to slow
down, and a robot has already paid for the round trip by then.
"""
session.setdefault(REGISTRATION_ISSUED_KEY, time.time())
def check_registration_challenge(form):
"""Say why this registration should be refused, or None to accept.
What replaced the arithmetic CAPTCHA, and why (SEC-AUTH-008).
`a + b = ?` with both operands between 1 and 10 has nineteen possible
answers and is solvable by reading the string. It stopped no automated
registration whatsoever. What it did do was add a step for every human,
including anyone using a screen reader, in exchange for an appearance of
protection — which is worse than no protection, because it gets counted
as one.
The audit's alternative was a real CAPTCHA service. That means a third
party, an API key, a request on every page load, and putting a foreign
script back into script-src — undoing the CSP work that closed
SEC-WEB-001. Disproportionate for a club site.
So: two checks that cost the visitor nothing.
- a honeypot field, hidden in the stylesheet, that a form-filling
robot completes and a person never sees;
- a minimum dwell time between being handed the form and sending it
back. Eleven fields and a password typed twice do not get filled in
under three seconds, and a POST with no issued form at all never
fetched the page.
Be clear about the ceiling: this stops commodity spam, not somebody who
looks at the form for five minutes. The thing that would actually gate
registration is staff activation of new accounts, which does not exist —
`is_active_account` defaults to True. That is a product decision, not one
to slip in here.
The session-forgery angle in the constat is moot: with SECRET_KEY
compromised (SEC-001) an attacker forges a logged-in session for any
account and has no reason to register at all.
Returns: Returns:
dict: A dictionary with 'question' (e.g., '3 + 7') and 'id' keys. str | None: a short reason for the log, or None to let it through.
""" """
import random if form.get(REGISTRATION_HONEYPOT_FIELD, '').strip():
return 'honeypot'
a = random.randint(1, 10) issued_at = session.get(REGISTRATION_ISSUED_KEY)
b = random.randint(1, 10) if issued_at is None:
captcha_id = str(uuid.uuid4()) return 'no-form-issued'
session['captcha_id'] = captcha_id if time.time() - issued_at < MIN_REGISTRATION_SECONDS:
session['captcha_answer'] = a + b return 'too-fast'
return {'question': f'{a} + {b} = ?', 'id': captcha_id} return None
def verify_captcha(user_answer):
"""Verify the CAPTCHA answer from the session.
Args:
user_answer: The user's submitted answer (string or int).
Returns:
bool: True if the answer matches the stored CAPTCHA, False otherwise.
"""
try:
expected = session.pop('captcha_answer', None)
session.pop('captcha_id', None)
if expected is None:
return False
return int(user_answer) == expected
except (ValueError, TypeError):
return False
auth_bp = Blueprint('auth', __name__, url_prefix='/auth') auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@@ -276,14 +312,33 @@ def login():
return render_template('pages/login.html') return render_template('pages/login.html')
def _rerender_registration(form_data):
"""Re-render the registration form after a refusal.
Was copied out four times, near-identically (ARCH-005). Dropping the two
password fields is the part that must not be forgotten in the fifth copy:
echoing a password back into the HTML puts it in the browser's cache and
in any proxy log along the way.
"""
form_data = dict(form_data)
form_data.pop('password', None)
form_data.pop('confirm_password', None)
return render_template(
'pages/register.html',
esport_games=ESPORT_GAMES,
honeypot_field=REGISTRATION_HONEYPOT_FIELD,
form_data=form_data,
)
@auth_bp.route('/register', methods=['GET', 'POST']) @auth_bp.route('/register', methods=['GET', 'POST'])
@limiter.limit("20 per hour") @limiter.limit("20 per hour")
def register(): def register():
"""Handle new player registration with CAPTCHA and password policy. """Handle new player registration.
GET: Render the registration form with E-Sports games list and CAPTCHA. GET: Render the registration form with the E-Sports games list.
POST: Validate all inputs, verify CAPTCHA, enforce password policy, POST: Screen the submission (see check_registration_challenge), validate
and create a new player account. every input against RegisterSchema, and create a new player account.
Only players can register through this form. Validates username/email Only players can register through this form. Validates username/email
uniqueness and password confirmation. uniqueness and password confirmation.
@@ -299,20 +354,15 @@ def register():
form_data = dict(request.form) form_data = dict(request.form)
form_data['games'] = request.form.getlist('games') form_data['games'] = request.form.getlist('games')
# Validate CAPTCHA first refusal = check_registration_challenge(request.form)
captcha_answer = request.form.get('captcha_answer', '') if refusal is not None:
if not verify_captcha(captcha_answer): # Logged, because this is the only place abuse of the sign-up
flash(_('Incorrect CAPTCHA answer. Please try again.'), 'danger') # form becomes visible at all. Deliberately vague to the sender:
captcha = generate_captcha() # naming the honeypot tells whoever tripped it how to avoid it.
# Clear password fields only on CAPTCHA failure log_auth_event('account.registration_refused', reason=refusal)
form_data.pop('password', None) flash(_('Your registration could not be processed. Please try again.'), 'danger')
form_data.pop('confirm_password', None) issue_registration_challenge()
return render_template( return _rerender_registration(form_data)
'pages/register.html',
esport_games=ESPORT_GAMES,
captcha=captcha,
form_data=form_data,
)
# Validate input with marshmallow schema # Validate input with marshmallow schema
register_schema = RegisterSchema() register_schema = RegisterSchema()
@@ -322,16 +372,7 @@ def register():
for field, messages in err.messages.items(): for field, messages in err.messages.items():
for msg in messages: for msg in messages:
flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger') flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger')
captcha = generate_captcha() return _rerender_registration(form_data)
# Clear password fields on validation failure
form_data.pop('password', None)
form_data.pop('confirm_password', None)
return render_template(
'pages/register.html',
esport_games=ESPORT_GAMES,
captcha=captcha,
form_data=form_data,
)
username = validated['username'] username = validated['username']
email = validated['email'] email = validated['email']
@@ -345,27 +386,11 @@ def register():
if User.query.filter_by(username=username).first(): if User.query.filter_by(username=username).first():
flash(_('Username already exists.'), 'danger') flash(_('Username already exists.'), 'danger')
captcha = generate_captcha() return _rerender_registration(form_data)
form_data.pop('password', None)
form_data.pop('confirm_password', None)
return render_template(
'pages/register.html',
esport_games=ESPORT_GAMES,
captcha=captcha,
form_data=form_data,
)
if User.query.filter_by(email=email).first(): if User.query.filter_by(email=email).first():
flash(_('Email already registered.'), 'danger') flash(_('Email already registered.'), 'danger')
captcha = generate_captcha() return _rerender_registration(form_data)
form_data.pop('password', None)
form_data.pop('confirm_password', None)
return render_template(
'pages/register.html',
esport_games=ESPORT_GAMES,
captcha=captcha,
form_data=form_data,
)
hashed_password = hash_password(password) hashed_password = hash_password(password)
user = Player( user = Player(
@@ -404,6 +429,7 @@ def register():
# Clear Discord OAuth data from session after successful registration # Clear Discord OAuth data from session after successful registration
session.pop('discord_oauth', None) session.pop('discord_oauth', None)
session.pop(REGISTRATION_ISSUED_KEY, None)
log_auth_event('account.registered', username=user.username, user_id=user.id) log_auth_event('account.registered', username=user.username, user_id=user.id)
@@ -411,13 +437,8 @@ def register():
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
# GET request — render empty form # GET request — render empty form
captcha = generate_captcha() issue_registration_challenge()
return render_template( return _rerender_registration({})
'pages/register.html',
esport_games=ESPORT_GAMES,
captcha=captcha,
form_data={},
)
@auth_bp.route('/discord/login') @auth_bp.route('/discord/login')
+9
View File
@@ -2021,3 +2021,12 @@ a:hover { color: var(--primary-dark); }
white-space: nowrap; white-space: nowrap;
border: 0; border: 0;
} }
/* Honeypot: hidden from everyone, filled in only by a robot (SEC-AUTH-008).
The opposite of .sr-only above — that one hides from the eye and keeps the
announcement, this one has to hide from both. display:none is deliberate:
an off-screen input is still reachable by keyboard and by a screen reader,
and a person who lands in it gets refused with no idea why. */
.honeypot {
display: none;
}
+8 -3
View File
@@ -146,9 +146,14 @@
required> required>
</div> </div>
<div class="form-group"> {# Honeypot (SEC-AUTH-008). Not a field anyone is meant to see or fill:
<label for="captcha_answer"><i class="fas fa-calculator"></i> {{ captcha.question }}</label> hidden in the stylesheet, kept out of the tab order, and told to
<input type="number" id="captcha_answer" name="captcha_answer" placeholder="{{ _('Answer') }}" required autocomplete="off"> screen readers to skip. A submission that carries a value here is a
robot filling every input it can find.
Do not add a label, do not translate the name, do not remove
aria-hidden — each of those turns it into a trap for a person. #}
<div class="honeypot" aria-hidden="true">
<input type="text" name="{{ honeypot_field }}" tabindex="-1" autocomplete="off" value="">
</div> </div>
<button type="submit" class="btn btn-primary btn-block">{{ _('Create Account') }}</button> <button type="submit" class="btn btn-primary btn-block">{{ _('Create Account') }}</button>
Binary file not shown.
+237 -233
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: team-tryouts VERSION\n" "Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-08-08 15:57-0400\n" "POT-Creation-Date: 2026-08-11 12:05-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -19,7 +19,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#: app/validators.py:49 #: app/validators.py:50
msgid "" msgid ""
"Password must be at least 8 characters with uppercase, lowercase, and a " "Password must be at least 8 characters with uppercase, lowercase, and a "
"number." "number."
@@ -27,95 +27,95 @@ msgstr ""
"Password must be at least 8 characters with uppercase, lowercase, and a " "Password must be at least 8 characters with uppercase, lowercase, and a "
"number." "number."
#: app/validators.py:67 #: app/validators.py:68
msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
#: app/validators.py:86 #: app/validators.py:87
msgid "Invalid Discord username format." msgid "Invalid Discord username format."
msgstr "Invalid Discord username format." msgstr "Invalid Discord username format."
#: app/validators.py:103 #: app/validators.py:104
msgid "Discord User ID must be a 17-20 digit number." msgid "Discord User ID must be a 17-20 digit number."
msgstr "Discord User ID must be a 17-20 digit number." msgstr "Discord User ID must be a 17-20 digit number."
#: app/validators.py:121 #: app/validators.py:122
msgid "Invalid phone number format." msgid "Invalid phone number format."
msgstr "Invalid phone number format." msgstr "Invalid phone number format."
#: app/validators.py:163 #: app/validators.py:164
msgid "Username is required." msgid "Username is required."
msgstr "Username is required." msgstr "Username is required."
#: app/validators.py:167 #: app/validators.py:168
msgid "Password is required." msgid "Password is required."
msgstr "Password is required." msgstr "Password is required."
#: app/validators.py:189 app/validators.py:261 #: app/validators.py:190 app/validators.py:262
msgid "Username must be 3-80 characters." msgid "Username must be 3-80 characters."
msgstr "Username must be 3-80 characters." msgstr "Username must be 3-80 characters."
#: app/validators.py:195 #: app/validators.py:196
msgid "Email must be 120 characters or less." msgid "Email must be 120 characters or less."
msgstr "Email must be 120 characters or less." msgstr "Email must be 120 characters or less."
#: app/validators.py:208 app/validators.py:276 app/validators.py:307 #: app/validators.py:209 app/validators.py:277 app/validators.py:308
#: app/validators.py:371 #: app/validators.py:372
msgid "Full name is required." msgid "Full name is required."
msgstr "Full name is required." msgstr "Full name is required."
#: app/validators.py:243 #: app/validators.py:244
msgid "Passwords do not match." msgid "Passwords do not match."
msgstr "Passwords do not match." msgstr "Passwords do not match."
#: app/validators.py:280 app/validators.py:315 #: app/validators.py:281 app/validators.py:316
msgid "Invalid role selected." msgid "Invalid role selected."
msgstr "Invalid role selected." msgstr "Invalid role selected."
#: app/validators.py:416 #: app/validators.py:417
msgid "Player must be selected." msgid "Player must be selected."
msgstr "Player must be selected." msgstr "Player must be selected."
#: app/validators.py:419 #: app/validators.py:420
msgid "Notes must be 2000 characters or less." msgid "Notes must be 2000 characters or less."
msgstr "Notes must be 2000 characters or less." msgstr "Notes must be 2000 characters or less."
#: app/validators.py:438 #: app/validators.py:439
msgid "Date must be in YYYY-MM-DD format." msgid "Date must be in YYYY-MM-DD format."
msgstr "Date must be in YYYY-MM-DD format." msgstr "Date must be in YYYY-MM-DD format."
#: app/validators.py:443 app/validators.py:470 #: app/validators.py:444 app/validators.py:471
msgid "Start time must be in HH:MM format." msgid "Start time must be in HH:MM format."
msgstr "Start time must be in HH:MM format." msgstr "Start time must be in HH:MM format."
#: app/validators.py:447 #: app/validators.py:448
msgid "End time must be in HH:MM format." msgid "End time must be in HH:MM format."
msgstr "End time must be in HH:MM format." msgstr "End time must be in HH:MM format."
#: app/validators.py:450 #: app/validators.py:451
msgid "Points must be 2000 characters or less." msgid "Points must be 2000 characters or less."
msgstr "Points must be 2000 characters or less." msgstr "Points must be 2000 characters or less."
#: app/validators.py:466 #: app/validators.py:467
msgid "Day must be 0 (Monday) to 6 (Sunday)." msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Day must be 0 (Monday) to 6 (Sunday)." msgstr "Day must be 0 (Monday) to 6 (Sunday)."
#: app/routes/auth.py:186 app/routes/auth.py:322 app/routes/users.py:106 #: app/routes/auth.py:224 app/routes/auth.py:374 app/routes/users/_shared.py:64
#: app/routes/users.py:821 #: app/routes/users/contracts.py:95
#, python-format #, python-format
msgid "%(field)s: %(msg)s" msgid "%(field)s: %(msg)s"
msgstr "%(field)s: %(msg)s" msgstr "%(field)s: %(msg)s"
#: app/routes/auth.py:203 #: app/routes/auth.py:241
msgid "This account has been deactivated." msgid "This account has been deactivated."
msgstr "This account has been deactivated." msgstr "This account has been deactivated."
#: app/routes/auth.py:238 #: app/routes/auth.py:276
#, python-format #, python-format
msgid "Welcome back, %(username)s!" msgid "Welcome back, %(username)s!"
msgstr "Welcome back, %(username)s!" msgstr "Welcome back, %(username)s!"
#: app/routes/auth.py:268 #: app/routes/auth.py:306
msgid "" msgid ""
"Login unsuccessful. Please check your username and password, or ask a " "Login unsuccessful. Please check your username and password, or ask a "
"president for help." "president for help."
@@ -123,27 +123,27 @@ msgstr ""
"Login unsuccessful. Please check your username and password, or ask a " "Login unsuccessful. Please check your username and password, or ask a "
"president for help." "president for help."
#: app/routes/auth.py:303 #: app/routes/auth.py:363
msgid "Incorrect CAPTCHA answer. Please try again." msgid "Your registration could not be processed. Please try again."
msgstr "Incorrect CAPTCHA answer. Please try again." msgstr "Your registration could not be processed. Please try again."
#: app/routes/auth.py:345 app/routes/users.py:436 #: app/routes/auth.py:388 app/routes/users/accounts.py:308
msgid "Username already exists." msgid "Username already exists."
msgstr "Username already exists." msgstr "Username already exists."
#: app/routes/auth.py:357 app/routes/users.py:440 #: app/routes/auth.py:392 app/routes/users/accounts.py:312
msgid "Email already registered." msgid "Email already registered."
msgstr "Email already registered." msgstr "Email already registered."
#: app/routes/auth.py:404 #: app/routes/auth.py:436
msgid "Your account has been created! You can now log in." msgid "Your account has been created! You can now log in."
msgstr "Your account has been created! You can now log in." msgstr "Your account has been created! You can now log in."
#: app/routes/auth.py:430 #: app/routes/auth.py:457
msgid "Discord OAuth2 is not configured." msgid "Discord OAuth2 is not configured."
msgstr "Discord OAuth2 is not configured." msgstr "Discord OAuth2 is not configured."
#: app/routes/auth.py:471 #: app/routes/auth.py:498
msgid "" msgid ""
"Discord authorization could not be verified. Please start the connection " "Discord authorization could not be verified. Please start the connection "
"again from this page." "again from this page."
@@ -151,418 +151,419 @@ msgstr ""
"Discord authorization could not be verified. Please start the connection " "Discord authorization could not be verified. Please start the connection "
"again from this page." "again from this page."
#: app/routes/auth.py:480 #: app/routes/auth.py:507
msgid "Discord authorization failed. No code received." msgid "Discord authorization failed. No code received."
msgstr "Discord authorization failed. No code received." msgstr "Discord authorization failed. No code received."
#: app/routes/auth.py:504 #: app/routes/auth.py:531
msgid "Failed to connect to Discord. Please try again." msgid "Failed to connect to Discord. Please try again."
msgstr "Failed to connect to Discord. Please try again." msgstr "Failed to connect to Discord. Please try again."
#: app/routes/auth.py:508 #: app/routes/auth.py:535
msgid "Failed to obtain Discord access token." msgid "Failed to obtain Discord access token."
msgstr "Failed to obtain Discord access token." msgstr "Failed to obtain Discord access token."
#: app/routes/auth.py:523 #: app/routes/auth.py:550
msgid "Failed to fetch Discord user profile." msgid "Failed to fetch Discord user profile."
msgstr "Failed to fetch Discord user profile." msgstr "Failed to fetch Discord user profile."
#: app/routes/auth.py:570 #: app/routes/auth.py:597
msgid "Discord account connected! Your profile has been pre-filled." msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Discord account connected! Your profile has been pre-filled." msgstr "Discord account connected! Your profile has been pre-filled."
#: app/routes/auth.py:598 #: app/routes/auth.py:625
msgid "You have been logged out." msgid "You have been logged out."
msgstr "You have been logged out." msgstr "You have been logged out."
#: app/routes/evaluations.py:45 #: app/routes/evaluations.py:46
msgid "You do not have permission to view evaluations." msgid "You do not have permission to view evaluations."
msgstr "You do not have permission to view evaluations." msgstr "You do not have permission to view evaluations."
#: app/routes/evaluations.py:135 #: app/routes/evaluations.py:136
msgid "You do not have permission to evaluate players." msgid "You do not have permission to evaluate players."
msgstr "You do not have permission to evaluate players." msgstr "You do not have permission to evaluate players."
#: app/routes/evaluations.py:140 app/routes/evaluations.py:263 #: app/routes/evaluations.py:141 app/routes/evaluations.py:264
msgid "You do not have permission to evaluate players in this tryout." msgid "You do not have permission to evaluate players in this tryout."
msgstr "You do not have permission to evaluate players in this tryout." msgstr "You do not have permission to evaluate players in this tryout."
#: app/routes/evaluations.py:151 #: app/routes/evaluations.py:152
msgid "Player is not registered for this tryout." msgid "Player is not registered for this tryout."
msgstr "Player is not registered for this tryout." msgstr "Player is not registered for this tryout."
#: app/routes/evaluations.py:156 #: app/routes/evaluations.py:157
msgid "Can only evaluate players." msgid "Can only evaluate players."
msgstr "Can only evaluate players." msgstr "Can only evaluate players."
#: app/routes/evaluations.py:208 #: app/routes/evaluations.py:209
msgid "Evaluation updated!" msgid "Evaluation updated!"
msgstr "Evaluation updated!" msgstr "Evaluation updated!"
#: app/routes/evaluations.py:228 #: app/routes/evaluations.py:229
msgid "Evaluation submitted successfully!" msgid "Evaluation submitted successfully!"
msgstr "Evaluation submitted successfully!" msgstr "Evaluation submitted successfully!"
#: app/routes/evaluations.py:258 app/routes/teams.py:268 #: app/routes/evaluations.py:259 app/routes/teams.py:270
#: app/routes/teams.py:309 app/routes/teams.py:350 app/routes/teams.py:375 #: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
#: app/routes/teams.py:400 app/routes/teams.py:435 app/routes/tryouts.py:472 #: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:505
#: app/routes/tryouts.py:488 app/routes/tryouts.py:508 #: app/routes/tryouts.py:521 app/routes/tryouts.py:541
#: app/routes/tryouts.py:547 app/routes/tryouts.py:583 #: app/routes/tryouts.py:580 app/routes/tryouts.py:616
#: app/routes/tryouts.py:602 #: app/routes/tryouts.py:635
msgid "Permission denied." msgid "Permission denied."
msgstr "Permission denied." msgstr "Permission denied."
#: app/routes/main.py:53 #: app/routes/main.py:55
msgid "That language is not available." msgid "That language is not available."
msgstr "That language is not available." msgstr "That language is not available."
#: app/routes/matches.py:245 #: app/routes/matches.py:307
msgid "You do not have permission to schedule matches for this tryout." msgid "You do not have permission to schedule matches for this tryout."
msgstr "You do not have permission to schedule matches for this tryout." msgstr "You do not have permission to schedule matches for this tryout."
#: app/routes/matches.py:249 app/routes/matches.py:422 #: app/routes/matches.py:311 app/routes/matches.py:473
msgid "This tryout has ended. Matches can no longer be created or modified." msgid "This tryout has ended. Matches can no longer be created or modified."
msgstr "This tryout has ended. Matches can no longer be created or modified." msgstr "This tryout has ended. Matches can no longer be created or modified."
#: app/routes/matches.py:270 #: app/routes/matches.py:332
msgid "Start time is required. Please select a time slot." msgid "Start time is required. Please select a time slot."
msgstr "Start time is required. Please select a time slot." msgstr "Start time is required. Please select a time slot."
#: app/routes/matches.py:282 app/routes/matches.py:445 #: app/routes/matches.py:344 app/routes/matches.py:496
#: app/routes/team_matches.py:151 app/routes/team_matches.py:255 #: app/routes/team_matches.py:153 app/routes/team_matches.py:247
msgid "Invalid date format." msgid "Invalid date format."
msgstr "Invalid date format." msgstr "Invalid date format."
#: app/routes/matches.py:302 app/routes/team_matches.py:172 #: app/routes/matches.py:364 app/routes/team_matches.py:174
msgid "Invalid time format." msgid "Invalid time format."
msgstr "Invalid time format." msgstr "Invalid time format."
#: app/routes/matches.py:398 #: app/routes/matches.py:449
msgid "Match scheduled successfully!" msgid "Match scheduled successfully!"
msgstr "Match scheduled successfully!" msgstr "Match scheduled successfully!"
#: app/routes/matches.py:418 app/routes/team_matches.py:242 #: app/routes/matches.py:469 app/routes/team_matches.py:234
msgid "You do not have permission to edit this match." msgid "You do not have permission to edit this match."
msgstr "You do not have permission to edit this match." msgstr "You do not have permission to edit this match."
#: app/routes/matches.py:456 #: app/routes/matches.py:507
msgid "Start time is required." msgid "Start time is required."
msgstr "Start time is required." msgstr "Start time is required."
#: app/routes/matches.py:578 app/routes/team_matches.py:284 #: app/routes/matches.py:616 app/routes/team_matches.py:276
msgid "Match updated successfully!" msgid "Match updated successfully!"
msgstr "Match updated successfully!" msgstr "Match updated successfully!"
#: app/routes/matches.py:631 app/routes/team_matches.py:299 #: app/routes/matches.py:669 app/routes/team_matches.py:291
msgid "You do not have permission to delete this match." msgid "You do not have permission to delete this match."
msgstr "You do not have permission to delete this match." msgstr "You do not have permission to delete this match."
#: app/routes/matches.py:634 #: app/routes/matches.py:672
msgid "This tryout has ended. Matches can no longer be deleted." msgid "This tryout has ended. Matches can no longer be deleted."
msgstr "This tryout has ended. Matches can no longer be deleted." msgstr "This tryout has ended. Matches can no longer be deleted."
#: app/routes/matches.py:647 app/routes/team_matches.py:303 #: app/routes/matches.py:685 app/routes/team_matches.py:295
msgid "Match deleted successfully." msgid "Match deleted successfully."
msgstr "Match deleted successfully." msgstr "Match deleted successfully."
#: app/routes/team_matches.py:98 #: app/routes/team_matches.py:100
msgid "You do not have permission to schedule matches for this team." msgid "You do not have permission to schedule matches for this team."
msgstr "You do not have permission to schedule matches for this team." msgstr "You do not have permission to schedule matches for this team."
#: app/routes/team_matches.py:140 #: app/routes/team_matches.py:142
msgid "Date is required." msgid "Date is required."
msgstr "Date is required." msgstr "Date is required."
#: app/routes/team_matches.py:228 #: app/routes/team_matches.py:220
#, python-format #, python-format
msgid "Team match \"%(title)s\" scheduled successfully!" msgid "Team match \"%(title)s\" scheduled successfully!"
msgstr "Team match \"%(title)s\" scheduled successfully!" msgstr "Team match \"%(title)s\" scheduled successfully!"
#: app/routes/team_matches.py:267 #: app/routes/team_matches.py:259
msgid "Invalid start time format." msgid "Invalid start time format."
msgstr "Invalid start time format." msgstr "Invalid start time format."
#: app/routes/team_matches.py:275 #: app/routes/team_matches.py:267
msgid "Invalid end time format." msgid "Invalid end time format."
msgstr "Invalid end time format." msgstr "Invalid end time format."
#: app/routes/teams.py:38 #: app/routes/teams.py:40
msgid "Use My Team(s) to view your teams." msgid "Use My Team(s) to view your teams."
msgstr "Use My Team(s) to view your teams." msgstr "Use My Team(s) to view your teams."
#: app/routes/teams.py:41 #: app/routes/teams.py:43
msgid "You do not have permission to view teams." msgid "You do not have permission to view teams."
msgstr "You do not have permission to view teams." msgstr "You do not have permission to view teams."
#: app/routes/teams.py:68 #: app/routes/teams.py:70
msgid "This page is for players." msgid "This page is for players."
msgstr "This page is for players." msgstr "This page is for players."
#: app/routes/teams.py:121 #: app/routes/teams.py:123
msgid "You do not have permission to create teams." msgid "You do not have permission to create teams."
msgstr "You do not have permission to create teams." msgstr "You do not have permission to create teams."
#: app/routes/teams.py:129 app/routes/teams.py:174 #: app/routes/teams.py:131 app/routes/teams.py:176
msgid "Team name is required." msgid "Team name is required."
msgstr "Team name is required." msgstr "Team name is required."
#: app/routes/teams.py:134 app/routes/teams.py:179 #: app/routes/teams.py:136 app/routes/teams.py:181
#, python-format #, python-format
msgid "Team \"%(name)s\" already exists." msgid "Team \"%(name)s\" already exists."
msgstr "Team \"%(name)s\" already exists." msgstr "Team \"%(name)s\" already exists."
#: app/routes/teams.py:156 #: app/routes/teams.py:158
#, python-format #, python-format
msgid "Team \"%(name)s\" created successfully!" msgid "Team \"%(name)s\" created successfully!"
msgstr "Team \"%(name)s\" created successfully!" msgstr "Team \"%(name)s\" created successfully!"
#: app/routes/teams.py:166 #: app/routes/teams.py:168
msgid "You do not have permission to edit this team." msgid "You do not have permission to edit this team."
msgstr "You do not have permission to edit this team." msgstr "You do not have permission to edit this team."
#: app/routes/teams.py:217 #: app/routes/teams.py:219
#, python-format #, python-format
msgid "Team \"%(name)s\" updated successfully!" msgid "Team \"%(name)s\" updated successfully!"
msgstr "Team \"%(name)s\" updated successfully!" msgstr "Team \"%(name)s\" updated successfully!"
#: app/routes/teams.py:226 #: app/routes/teams.py:228
msgid "You do not have permission to delete teams." msgid "You do not have permission to delete teams."
msgstr "You do not have permission to delete teams." msgstr "You do not have permission to delete teams."
#: app/routes/teams.py:258 #: app/routes/teams.py:260
#, python-format #, python-format
msgid "Team \"%(name)s\" deleted successfully." msgid "Team \"%(name)s\" deleted successfully."
msgstr "Team \"%(name)s\" deleted successfully." msgstr "Team \"%(name)s\" deleted successfully."
#: app/routes/teams.py:273 #: app/routes/teams.py:275
msgid "Please select a coach." msgid "Please select a coach."
msgstr "Please select a coach." msgstr "Please select a coach."
#: app/routes/teams.py:278 #: app/routes/teams.py:280
msgid "Only coaches can be assigned as coach." msgid "Only coaches can be assigned as coach."
msgstr "Only coaches can be assigned as coach." msgstr "Only coaches can be assigned as coach."
#: app/routes/teams.py:284 #: app/routes/teams.py:286
#, python-format #, python-format
msgid "%(username)s is already a coach of %(name)s." msgid "%(username)s is already a coach of %(name)s."
msgstr "%(username)s is already a coach of %(name)s." msgstr "%(username)s is already a coach of %(name)s."
#: app/routes/teams.py:297 #: app/routes/teams.py:299
#, python-format #, python-format
msgid "%(username)s added as coach of %(name)s." msgid "%(username)s added as coach of %(name)s."
msgstr "%(username)s added as coach of %(name)s." msgstr "%(username)s added as coach of %(name)s."
#: app/routes/teams.py:314 #: app/routes/teams.py:316
msgid "Please select a manager." msgid "Please select a manager."
msgstr "Please select a manager." msgstr "Please select a manager."
#: app/routes/teams.py:319 #: app/routes/teams.py:321
msgid "Only managers can be assigned as manager." msgid "Only managers can be assigned as manager."
msgstr "Only managers can be assigned as manager." msgstr "Only managers can be assigned as manager."
#: app/routes/teams.py:325 #: app/routes/teams.py:327
#, python-format #, python-format
msgid "%(username)s is already a manager of %(name)s." msgid "%(username)s is already a manager of %(name)s."
msgstr "%(username)s is already a manager of %(name)s." msgstr "%(username)s is already a manager of %(name)s."
#: app/routes/teams.py:338 #: app/routes/teams.py:340
#, python-format #, python-format
msgid "%(username)s added as manager of %(name)s." msgid "%(username)s added as manager of %(name)s."
msgstr "%(username)s added as manager of %(name)s." msgstr "%(username)s added as manager of %(name)s."
#: app/routes/teams.py:365 #: app/routes/teams.py:367
#, python-format #, python-format
msgid "Coach removed from %(name)s." msgid "Coach removed from %(name)s."
msgstr "Coach removed from %(name)s." msgstr "Coach removed from %(name)s."
#: app/routes/teams.py:390 #: app/routes/teams.py:392
#, python-format #, python-format
msgid "Manager removed from %(name)s." msgid "Manager removed from %(name)s."
msgstr "Manager removed from %(name)s." msgstr "Manager removed from %(name)s."
#: app/routes/teams.py:406 app/routes/tryouts.py:512 app/routes/tryouts.py:613 #: app/routes/teams.py:408 app/routes/tryouts.py:545 app/routes/tryouts.py:646
msgid "Please select a player." msgid "Please select a player."
msgstr "Please select a player." msgstr "Please select a player."
#: app/routes/teams.py:411 #: app/routes/teams.py:413
msgid "Can only assign players to teams." msgid "Can only assign players to teams."
msgstr "Can only assign players to teams." msgstr "Can only assign players to teams."
#: app/routes/teams.py:417 #: app/routes/teams.py:419
#, python-format #, python-format
msgid "%(username)s is already on %(name)s." msgid "%(username)s is already on %(name)s."
msgstr "%(username)s is already on %(name)s." msgstr "%(username)s is already on %(name)s."
#: app/routes/teams.py:425 #: app/routes/teams.py:427
#, python-format #, python-format
msgid "%(username)s added to %(name)s!" msgid "%(username)s added to %(name)s!"
msgstr "%(username)s added to %(name)s!" msgstr "%(username)s added to %(name)s!"
#: app/routes/teams.py:442 app/routes/teams.py:515 #: app/routes/teams.py:444 app/routes/teams.py:517
#, python-format #, python-format
msgid "%(username)s is not on %(name)s." msgid "%(username)s is not on %(name)s."
msgstr "%(username)s is not on %(name)s." msgstr "%(username)s is not on %(name)s."
#: app/routes/teams.py:450 #: app/routes/teams.py:452
#, python-format #, python-format
msgid "%(username)s removed from %(name)s." msgid "%(username)s removed from %(name)s."
msgstr "%(username)s removed from %(name)s." msgstr "%(username)s removed from %(name)s."
#: app/routes/teams.py:486 app/routes/teams.py:504 #: app/routes/teams.py:488 app/routes/teams.py:506
msgid "You do not have permission to add notes to this team." msgid "You do not have permission to add notes to this team."
msgstr "You do not have permission to add notes to this team." msgstr "You do not have permission to add notes to this team."
#: app/routes/teams.py:494 #: app/routes/teams.py:496
msgid "Team notes added successfully!" msgid "Team notes added successfully!"
msgstr "Team notes added successfully!" msgstr "Team notes added successfully!"
#: app/routes/teams.py:509 app/routes/users.py:1530 app/routes/users.py:1573 #: app/routes/teams.py:511 app/routes/users/notes.py:207
#: app/routes/users/notes.py:250
msgid "Can only add notes for players." msgid "Can only add notes for players."
msgstr "Can only add notes for players." msgstr "Can only add notes for players."
#: app/routes/teams.py:525 #: app/routes/teams.py:527
#, python-format #, python-format
msgid "Note added for %(username)s!" msgid "Note added for %(username)s!"
msgstr "Note added for %(username)s!" msgstr "Note added for %(username)s!"
#: app/routes/tryouts.py:56 #: app/routes/tryouts.py:77
msgid "You do not have permission to create tryouts." msgid "You do not have permission to create tryouts."
msgstr "You do not have permission to create tryouts." msgstr "You do not have permission to create tryouts."
#: app/routes/tryouts.py:82 app/routes/tryouts.py:189 #: app/routes/tryouts.py:103 app/routes/tryouts.py:210
msgid "Invalid start date format." msgid "Invalid start date format."
msgstr "Invalid start date format." msgstr "Invalid start date format."
#: app/routes/tryouts.py:97 app/routes/tryouts.py:204 #: app/routes/tryouts.py:118 app/routes/tryouts.py:225
msgid "End date cannot be before start date." msgid "End date cannot be before start date."
msgstr "End date cannot be before start date." msgstr "End date cannot be before start date."
#: app/routes/tryouts.py:107 app/routes/tryouts.py:214 #: app/routes/tryouts.py:128 app/routes/tryouts.py:235
msgid "Invalid end date format." msgid "Invalid end date format."
msgstr "Invalid end date format." msgstr "Invalid end date format."
#: app/routes/tryouts.py:139 #: app/routes/tryouts.py:160
msgid "Tryout created successfully!" msgid "Tryout created successfully!"
msgstr "Tryout created successfully!" msgstr "Tryout created successfully!"
#: app/routes/tryouts.py:159 #: app/routes/tryouts.py:180
msgid "You do not have permission to edit this tryout." msgid "You do not have permission to edit this tryout."
msgstr "You do not have permission to edit this tryout." msgstr "You do not have permission to edit this tryout."
#: app/routes/tryouts.py:163 #: app/routes/tryouts.py:184
msgid "This tryout has ended and can no longer be modified." msgid "This tryout has ended and can no longer be modified."
msgstr "This tryout has ended and can no longer be modified." msgstr "This tryout has ended and can no longer be modified."
#: app/routes/tryouts.py:242 #: app/routes/tryouts.py:263
msgid "Tryout updated successfully!" msgid "Tryout updated successfully!"
msgstr "Tryout updated successfully!" msgstr "Tryout updated successfully!"
#: app/routes/tryouts.py:289 #: app/routes/tryouts.py:310
msgid "You do not have permission to view this tryout." msgid "You do not have permission to view this tryout."
msgstr "You do not have permission to view this tryout." msgstr "You do not have permission to view this tryout."
#: app/routes/tryouts.py:439 #: app/routes/tryouts.py:472
msgid "Only players can register for tryouts." msgid "Only players can register for tryouts."
msgstr "Only players can register for tryouts." msgstr "Only players can register for tryouts."
#: app/routes/tryouts.py:443 #: app/routes/tryouts.py:476
msgid "This tryout is not accepting registrations." msgid "This tryout is not accepting registrations."
msgstr "This tryout is not accepting registrations." msgstr "This tryout is not accepting registrations."
#: app/routes/tryouts.py:450 #: app/routes/tryouts.py:483
msgid "You are already registered for this tryout." msgid "You are already registered for this tryout."
msgstr "You are already registered for this tryout." msgstr "You are already registered for this tryout."
#: app/routes/tryouts.py:456 app/routes/tryouts.py:531 #: app/routes/tryouts.py:489 app/routes/tryouts.py:564
msgid "This tryout is full." msgid "This tryout is full."
msgstr "This tryout is full." msgstr "This tryout is full."
#: app/routes/tryouts.py:462 #: app/routes/tryouts.py:495
msgid "Successfully registered for tryout!" msgid "Successfully registered for tryout!"
msgstr "Successfully registered for tryout!" msgstr "Successfully registered for tryout!"
#: app/routes/tryouts.py:478 #: app/routes/tryouts.py:511
#, python-format #, python-format
msgid "Tryout status updated to %(new_status)s." msgid "Tryout status updated to %(new_status)s."
msgstr "Tryout status updated to %(new_status)s." msgstr "Tryout status updated to %(new_status)s."
#: app/routes/tryouts.py:498 #: app/routes/tryouts.py:531
msgid "Registration status updated." msgid "Registration status updated."
msgstr "Registration status updated." msgstr "Registration status updated."
#: app/routes/tryouts.py:517 #: app/routes/tryouts.py:550
msgid "Can only register players." msgid "Can only register players."
msgstr "Can only register players." msgstr "Can only register players."
#: app/routes/tryouts.py:523 #: app/routes/tryouts.py:556
#, python-format #, python-format
msgid "%(username)s is already registered for this tryout." msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s is already registered for this tryout." msgstr "%(username)s is already registered for this tryout."
#: app/routes/tryouts.py:537 #: app/routes/tryouts.py:570
#, python-format #, python-format
msgid "%(username)s registered for tryout!" msgid "%(username)s registered for tryout!"
msgstr "%(username)s registered for tryout!" msgstr "%(username)s registered for tryout!"
#: app/routes/tryouts.py:573 #: app/routes/tryouts.py:606
#, python-format #, python-format
msgid "%(username)s removed from tryout." msgid "%(username)s removed from tryout."
msgstr "%(username)s removed from tryout." msgstr "%(username)s removed from tryout."
#: app/routes/tryouts.py:591 #: app/routes/tryouts.py:624
#, python-format #, python-format
msgid "Team \"%(team_name)s\" created!" msgid "Team \"%(team_name)s\" created!"
msgstr "Team \"%(team_name)s\" created!" msgstr "Team \"%(team_name)s\" created!"
#: app/routes/tryouts.py:622 #: app/routes/tryouts.py:655
msgid "That player is not registered for this tryout." msgid "That player is not registered for this tryout."
msgstr "That player is not registered for this tryout." msgstr "That player is not registered for this tryout."
#: app/routes/tryouts.py:628 #: app/routes/tryouts.py:661
msgid "Player is already on this team." msgid "Player is already on this team."
msgstr "Player is already on this team." msgstr "Player is already on this team."
#: app/routes/tryouts.py:633 #: app/routes/tryouts.py:666
msgid "Player added to team!" msgid "Player added to team!"
msgstr "Player added to team!" msgstr "Player added to team!"
#: app/routes/tryouts.py:643 #: app/routes/tryouts.py:676
msgid "You do not have permission to delete this tryout." msgid "You do not have permission to delete this tryout."
msgstr "You do not have permission to delete this tryout." msgstr "You do not have permission to delete this tryout."
#: app/routes/tryouts.py:679 #: app/routes/tryouts.py:712
msgid "Tryout deleted successfully." msgid "Tryout deleted successfully."
msgstr "Tryout deleted successfully." msgstr "Tryout deleted successfully."
#: app/routes/users.py:83 #: app/routes/users/_shared.py:46
msgid "No file selected." msgid "No file selected."
msgstr "No file selected." msgstr "No file selected."
#: app/routes/users.py:87 #: app/routes/users/_shared.py:50
msgid "Only PDF files are allowed for contracts." msgid "Only PDF files are allowed for contracts."
msgstr "Only PDF files are allowed for contracts." msgstr "Only PDF files are allowed for contracts."
#: app/routes/users.py:92 #: app/routes/users/_shared.py:55
msgid "That file is not a PDF, whatever its name says." msgid "That file is not a PDF, whatever its name says."
msgstr "That file is not a PDF, whatever its name says." msgstr "That file is not a PDF, whatever its name says."
#: app/routes/users.py:181 #: app/routes/users/accounts.py:53
msgid "Only the president can manage users." msgid "Only the president can manage users."
msgstr "Only the president can manage users." msgstr "Only the president can manage users."
#: app/routes/users.py:193 #: app/routes/users/accounts.py:65
msgid "Only the president can edit users." msgid "Only the president can edit users."
msgstr "Only the president can edit users." msgstr "Only the president can edit users."
#: app/routes/users.py:234 #: app/routes/users/accounts.py:106
msgid "Email already in use by another account." msgid "Email already in use by another account."
msgstr "Email already in use by another account." msgstr "Email already in use by another account."
#: app/routes/users.py:245 #: app/routes/users/accounts.py:117
msgid "You cannot change your own role. Ask another president to do it." msgid "You cannot change your own role. Ask another president to do it."
msgstr "You cannot change your own role. Ask another president to do it." msgstr "You cannot change your own role. Ask another president to do it."
#: app/routes/users.py:258 #: app/routes/users/accounts.py:130
msgid "" msgid ""
"This is the last active president. Promote another account before " "This is the last active president. Promote another account before "
"changing this one." "changing this one."
@@ -570,175 +571,176 @@ msgstr ""
"This is the last active president. Promote another account before " "This is the last active president. Promote another account before "
"changing this one." "changing this one."
#: app/routes/users.py:337 #: app/routes/users/accounts.py:209
#, python-format #, python-format
msgid "User %(username)s updated successfully!" msgid "User %(username)s updated successfully!"
msgstr "User %(username)s updated successfully!" msgstr "User %(username)s updated successfully!"
#: app/routes/users.py:358 #: app/routes/users/accounts.py:230
msgid "Only the president can delete users." msgid "Only the president can delete users."
msgstr "Only the president can delete users." msgstr "Only the president can delete users."
#: app/routes/users.py:362 #: app/routes/users/accounts.py:234
msgid "You cannot delete your own account." msgid "You cannot delete your own account."
msgstr "You cannot delete your own account." msgstr "You cannot delete your own account."
#: app/routes/users.py:405 #: app/routes/users/accounts.py:277
#, python-format #, python-format
msgid "User %(deleted_username)s has been removed." msgid "User %(deleted_username)s has been removed."
msgstr "User %(deleted_username)s has been removed." msgstr "User %(deleted_username)s has been removed."
#: app/routes/users.py:416 #: app/routes/users/accounts.py:288
msgid "Only the president can create users." msgid "Only the president can create users."
msgstr "Only the president can create users." msgstr "Only the president can create users."
#: app/routes/users.py:464 #: app/routes/users/accounts.py:336
#, python-format #, python-format
msgid "User %(full_name)s created as %(role)s!" msgid "User %(full_name)s created as %(role)s!"
msgstr "User %(full_name)s created as %(role)s!" msgstr "User %(full_name)s created as %(role)s!"
#: app/routes/users.py:534 #: app/routes/users/availability.py:179
msgid "Username already taken." msgid "Only coaches can manage availability."
msgstr "Username already taken." msgstr "Only coaches can manage availability."
#: app/routes/users.py:544 #: app/routes/users/contracts.py:83
msgid "Email already in use."
msgstr "Email already in use."
#: app/routes/users.py:574
msgid "Profile updated successfully!"
msgstr "Profile updated successfully!"
#: app/routes/users.py:809
msgid "Only presidents, managers, and coaches can upload contracts." msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Only presidents, managers, and coaches can upload contracts." msgstr "Only presidents, managers, and coaches can upload contracts."
#: app/routes/users.py:828 #: app/routes/users/contracts.py:102
msgid "You do not have permission to upload a contract for this player." msgid "You do not have permission to upload a contract for this player."
msgstr "You do not have permission to upload a contract for this player." msgstr "You do not have permission to upload a contract for this player."
#: app/routes/users.py:869 #: app/routes/users/contracts.py:143
#, python-format #, python-format
msgid "Contract uploaded successfully for %(username)s!" msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contract uploaded successfully for %(username)s!" msgstr "Contract uploaded successfully for %(username)s!"
#: app/routes/users.py:883 #: app/routes/users/contracts.py:157
msgid "Only the player can upload their signed contract." msgid "Only the player can upload their signed contract."
msgstr "Only the player can upload their signed contract." msgstr "Only the player can upload their signed contract."
#: app/routes/users.py:902 #: app/routes/users/contracts.py:176
msgid "Signed contract uploaded successfully!" msgid "Signed contract uploaded successfully!"
msgstr "Signed contract uploaded successfully!" msgstr "Signed contract uploaded successfully!"
#: app/routes/users.py:912 app/routes/users.py:925 #: app/routes/users/contracts.py:186 app/routes/users/contracts.py:199
msgid "You do not have permission to download this contract." msgid "You do not have permission to download this contract."
msgstr "You do not have permission to download this contract." msgstr "You do not have permission to download this contract."
#: app/routes/users.py:928 #: app/routes/users/contracts.py:202
msgid "No signed contract available." msgid "No signed contract available."
msgstr "No signed contract available." msgstr "No signed contract available."
#: app/routes/users.py:1034 #: app/routes/users/notes.py:34
msgid "Only players can request One on One sessions."
msgstr "Only players can request One on One sessions."
#: app/routes/users.py:1047
msgid "You do not have a coach assigned to your team."
msgstr "You do not have a coach assigned to your team."
#: app/routes/users.py:1082
msgid "Cannot request One on One - no coach assigned."
msgstr "Cannot request One on One - no coach assigned."
#: app/routes/users.py:1090
msgid "Invalid date or time format."
msgstr "Invalid date or time format."
#: app/routes/users.py:1104
msgid "The requested time is not within the coach's availability."
msgstr "The requested time is not within the coach's availability."
#: app/routes/users.py:1132
msgid "Your One on One request has been submitted!"
msgstr "Your One on One request has been submitted!"
#: app/routes/users.py:1176
msgid "Only coaches can accept One on One requests."
msgstr "Only coaches can accept One on One requests."
#: app/routes/users.py:1182 app/routes/users.py:1232
msgid "This request is not for you."
msgstr "This request is not for you."
#: app/routes/users.py:1186 app/routes/users.py:1236
msgid "This request has already been processed."
msgstr "This request has already been processed."
#: app/routes/users.py:1213
#, python-format
msgid "One on One request from %(player)s has been approved!"
msgstr "One on One request from %(player)s has been approved!"
#: app/routes/users.py:1226
msgid "Only coaches can reject One on One requests."
msgstr "Only coaches can reject One on One requests."
#: app/routes/users.py:1268
#, python-format
msgid "One on One request from %(player)s has been rejected."
msgstr "One on One request from %(player)s has been rejected."
#: app/routes/users.py:1286
msgid "This page is for players only." msgid "This page is for players only."
msgstr "This page is for players only." msgstr "This page is for players only."
#: app/routes/users.py:1328 #: app/routes/users/notes.py:71
msgid "Only coaches can manage availability."
msgstr "Only coaches can manage availability."
#: app/routes/users.py:1394
msgid "Only coaches can access the notes dashboard." msgid "Only coaches can access the notes dashboard."
msgstr "Only coaches can access the notes dashboard." msgstr "Only coaches can access the notes dashboard."
#: app/routes/users.py:1484 #: app/routes/users/notes.py:161
msgid "Only coaches can manage team notes." msgid "Only coaches can manage team notes."
msgstr "Only coaches can manage team notes." msgstr "Only coaches can manage team notes."
#: app/routes/users.py:1490 #: app/routes/users/notes.py:167
msgid "You are not assigned to a team." msgid "You are not assigned to a team."
msgstr "You are not assigned to a team." msgstr "You are not assigned to a team."
#: app/routes/users.py:1503 #: app/routes/users/notes.py:180
msgid "Team notes saved successfully!" msgid "Team notes saved successfully!"
msgstr "Team notes saved successfully!" msgstr "Team notes saved successfully!"
#: app/routes/users.py:1518 #: app/routes/users/notes.py:195
msgid "Only coaches can manage personal notes." msgid "Only coaches can manage personal notes."
msgstr "Only coaches can manage personal notes." msgstr "Only coaches can manage personal notes."
#: app/routes/users.py:1525 app/routes/users.py:1568 app/routes/users.py:1619 #: app/routes/users/notes.py:202 app/routes/users/notes.py:245
#: app/routes/users.py:1673 #: app/routes/users/notes.py:296 app/routes/users/notes.py:350
msgid "Player and content are required." msgid "Player and content are required."
msgstr "Player and content are required." msgstr "Player and content are required."
#: app/routes/users.py:1534 app/routes/users.py:1577 app/routes/users.py:1623 #: app/routes/users/notes.py:211 app/routes/users/notes.py:254
#: app/routes/users.py:1677 #: app/routes/users/notes.py:300 app/routes/users/notes.py:354
msgid "You can only write notes about players you work with." msgid "You can only write notes about players you work with."
msgstr "You can only write notes about players you work with." msgstr "You can only write notes about players you work with."
#: app/routes/users.py:1544 app/routes/users.py:1590 #: app/routes/users/notes.py:221 app/routes/users/notes.py:267
#, python-format #, python-format
msgid "Note added for %(username)s." msgid "Note added for %(username)s."
msgstr "Note added for %(username)s." msgstr "Note added for %(username)s."
#: app/routes/users.py:1558 app/routes/users.py:1604 app/routes/users.py:1657 #: app/routes/users/notes.py:235 app/routes/users/notes.py:281
#: app/routes/users/notes.py:334
msgid "Only coaches can add personal notes." msgid "Only coaches can add personal notes."
msgstr "Only coaches can add personal notes." msgstr "Only coaches can add personal notes."
#: app/routes/users.py:1634 app/routes/users.py:1688 #: app/routes/users/notes.py:311 app/routes/users/notes.py:365
msgid "Note added successfully." msgid "Note added successfully."
msgstr "Note added successfully." msgstr "Note added successfully."
#: app/routes/users/one_on_one.py:20
msgid "Only players can request One on One sessions."
msgstr "Only players can request One on One sessions."
#: app/routes/users/one_on_one.py:33
msgid "You do not have a coach assigned to your team."
msgstr "You do not have a coach assigned to your team."
#: app/routes/users/one_on_one.py:68
msgid "Cannot request One on One - no coach assigned."
msgstr "Cannot request One on One - no coach assigned."
#: app/routes/users/one_on_one.py:76
msgid "Invalid date or time format."
msgstr "Invalid date or time format."
#: app/routes/users/one_on_one.py:90
msgid "The requested time is not within the coach's availability."
msgstr "The requested time is not within the coach's availability."
#: app/routes/users/one_on_one.py:118
msgid "Your One on One request has been submitted!"
msgstr "Your One on One request has been submitted!"
#: app/routes/users/one_on_one.py:163
msgid "Only coaches can accept One on One requests."
msgstr "Only coaches can accept One on One requests."
#: app/routes/users/one_on_one.py:169 app/routes/users/one_on_one.py:219
msgid "This request is not for you."
msgstr "This request is not for you."
#: app/routes/users/one_on_one.py:173 app/routes/users/one_on_one.py:223
msgid "This request has already been processed."
msgstr "This request has already been processed."
#: app/routes/users/one_on_one.py:200
#, python-format
msgid "One on One request from %(player)s has been approved!"
msgstr "One on One request from %(player)s has been approved!"
#: app/routes/users/one_on_one.py:213
msgid "Only coaches can reject One on One requests."
msgstr "Only coaches can reject One on One requests."
#: app/routes/users/one_on_one.py:255
#, python-format
msgid "One on One request from %(player)s has been rejected."
msgstr "One on One request from %(player)s has been rejected."
#: app/routes/users/profile.py:83
msgid "Username already taken."
msgstr "Username already taken."
#: app/routes/users/profile.py:93
msgid "Email already in use."
msgstr "Email already in use."
#: app/routes/users/profile.py:123
msgid "Profile updated successfully!"
msgstr "Profile updated successfully!"
#: app/templates/errors/400.html:2 #: app/templates/errors/400.html:2
msgid "400 Bad Request" msgid "400 Bad Request"
msgstr "400 Bad Request" msgstr "400 Bad Request"
@@ -2372,11 +2374,7 @@ msgstr "Confirm Password"
msgid "Confirm your password" msgid "Confirm your password"
msgstr "Confirm your password" msgstr "Confirm your password"
#: app/templates/pages/register.html:151 #: app/templates/pages/register.html:159
msgid "Answer"
msgstr "Answer"
#: app/templates/pages/register.html:154
msgid "Create Account" msgid "Create Account"
msgstr "Create Account" msgstr "Create Account"
@@ -2876,3 +2874,9 @@ msgstr "View Profile"
#~ msgid "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout." #~ msgid "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout."
#~ msgstr "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout." #~ msgstr "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout."
#~ msgid "Incorrect CAPTCHA answer. Please try again."
#~ msgstr "Incorrect CAPTCHA answer. Please try again."
#~ msgid "Answer"
#~ msgstr "Answer"
Binary file not shown.
+237 -233
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: team-tryouts VERSION\n" "Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-08-08 15:57-0400\n" "POT-Creation-Date: 2026-08-11 12:05-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n" "Language: fr\n"
@@ -19,7 +19,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n" "Generated-By: Babel 2.18.0\n"
#: app/validators.py:49 #: app/validators.py:50
msgid "" msgid ""
"Password must be at least 8 characters with uppercase, lowercase, and a " "Password must be at least 8 characters with uppercase, lowercase, and a "
"number." "number."
@@ -27,97 +27,97 @@ msgstr ""
"Le mot de passe doit compter au moins 8 caractères, dont une majuscule, " "Le mot de passe doit compter au moins 8 caractères, dont une majuscule, "
"une minuscule et un chiffre." "une minuscule et un chiffre."
#: app/validators.py:67 #: app/validators.py:68
msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)."
msgstr "" msgstr ""
"Le nom dutilisateur doit compter de 3 à 30 caractères (lettres, " "Le nom dutilisateur doit compter de 3 à 30 caractères (lettres, "
"chiffres, tiret bas, trait dunion)." "chiffres, tiret bas, trait dunion)."
#: app/validators.py:86 #: app/validators.py:87
msgid "Invalid Discord username format." msgid "Invalid Discord username format."
msgstr "Format de nom dutilisateur Discord invalide." msgstr "Format de nom dutilisateur Discord invalide."
#: app/validators.py:103 #: app/validators.py:104
msgid "Discord User ID must be a 17-20 digit number." msgid "Discord User ID must be a 17-20 digit number."
msgstr "Lidentifiant Discord doit être un nombre de 17 à 20 chiffres." msgstr "Lidentifiant Discord doit être un nombre de 17 à 20 chiffres."
#: app/validators.py:121 #: app/validators.py:122
msgid "Invalid phone number format." msgid "Invalid phone number format."
msgstr "Format de numéro de téléphone invalide." msgstr "Format de numéro de téléphone invalide."
#: app/validators.py:163 #: app/validators.py:164
msgid "Username is required." msgid "Username is required."
msgstr "Le nom dutilisateur est obligatoire." msgstr "Le nom dutilisateur est obligatoire."
#: app/validators.py:167 #: app/validators.py:168
msgid "Password is required." msgid "Password is required."
msgstr "Le mot de passe est obligatoire." msgstr "Le mot de passe est obligatoire."
#: app/validators.py:189 app/validators.py:261 #: app/validators.py:190 app/validators.py:262
msgid "Username must be 3-80 characters." msgid "Username must be 3-80 characters."
msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères." msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères."
#: app/validators.py:195 #: app/validators.py:196
msgid "Email must be 120 characters or less." msgid "Email must be 120 characters or less."
msgstr "Ladresse courriel ne doit pas dépasser 120 caractères." msgstr "Ladresse courriel ne doit pas dépasser 120 caractères."
#: app/validators.py:208 app/validators.py:276 app/validators.py:307 #: app/validators.py:209 app/validators.py:277 app/validators.py:308
#: app/validators.py:371 #: app/validators.py:372
msgid "Full name is required." msgid "Full name is required."
msgstr "Le nom complet est obligatoire." msgstr "Le nom complet est obligatoire."
#: app/validators.py:243 #: app/validators.py:244
msgid "Passwords do not match." msgid "Passwords do not match."
msgstr "Les mots de passe ne concordent pas." msgstr "Les mots de passe ne concordent pas."
#: app/validators.py:280 app/validators.py:315 #: app/validators.py:281 app/validators.py:316
msgid "Invalid role selected." msgid "Invalid role selected."
msgstr "Rôle sélectionné invalide." msgstr "Rôle sélectionné invalide."
#: app/validators.py:416 #: app/validators.py:417
msgid "Player must be selected." msgid "Player must be selected."
msgstr "Vous devez choisir un joueur." msgstr "Vous devez choisir un joueur."
#: app/validators.py:419 #: app/validators.py:420
msgid "Notes must be 2000 characters or less." msgid "Notes must be 2000 characters or less."
msgstr "Les notes ne doivent pas dépasser 2000 caractères." msgstr "Les notes ne doivent pas dépasser 2000 caractères."
#: app/validators.py:438 #: app/validators.py:439
msgid "Date must be in YYYY-MM-DD format." msgid "Date must be in YYYY-MM-DD format."
msgstr "La date doit être au format AAAA-MM-JJ." msgstr "La date doit être au format AAAA-MM-JJ."
#: app/validators.py:443 app/validators.py:470 #: app/validators.py:444 app/validators.py:471
msgid "Start time must be in HH:MM format." msgid "Start time must be in HH:MM format."
msgstr "Lheure de début doit être au format HH:MM." msgstr "Lheure de début doit être au format HH:MM."
#: app/validators.py:447 #: app/validators.py:448
msgid "End time must be in HH:MM format." msgid "End time must be in HH:MM format."
msgstr "Lheure de fin doit être au format HH:MM." msgstr "Lheure de fin doit être au format HH:MM."
#: app/validators.py:450 #: app/validators.py:451
msgid "Points must be 2000 characters or less." msgid "Points must be 2000 characters or less."
msgstr "Les points ne doivent pas dépasser 2000 caractères." msgstr "Les points ne doivent pas dépasser 2000 caractères."
#: app/validators.py:466 #: app/validators.py:467
msgid "Day must be 0 (Monday) to 6 (Sunday)." msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)." msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
#: app/routes/auth.py:186 app/routes/auth.py:322 app/routes/users.py:106 #: app/routes/auth.py:224 app/routes/auth.py:374 app/routes/users/_shared.py:64
#: app/routes/users.py:821 #: app/routes/users/contracts.py:95
#, python-format #, python-format
msgid "%(field)s: %(msg)s" msgid "%(field)s: %(msg)s"
msgstr "%(field)s : %(msg)s" msgstr "%(field)s : %(msg)s"
#: app/routes/auth.py:203 #: app/routes/auth.py:241
msgid "This account has been deactivated." msgid "This account has been deactivated."
msgstr "Ce compte a été désactivé." msgstr "Ce compte a été désactivé."
#: app/routes/auth.py:238 #: app/routes/auth.py:276
#, python-format #, python-format
msgid "Welcome back, %(username)s!" msgid "Welcome back, %(username)s!"
msgstr "Bon retour, %(username)s !" msgstr "Bon retour, %(username)s !"
#: app/routes/auth.py:268 #: app/routes/auth.py:306
msgid "" msgid ""
"Login unsuccessful. Please check your username and password, or ask a " "Login unsuccessful. Please check your username and password, or ask a "
"president for help." "president for help."
@@ -125,27 +125,27 @@ msgstr ""
"Échec de la connexion. Vérifiez le nom dutilisateur et le mot de passe, " "Échec de la connexion. Vérifiez le nom dutilisateur et le mot de passe, "
"ou demandez de laide à un président." "ou demandez de laide à un président."
#: app/routes/auth.py:303 #: app/routes/auth.py:363
msgid "Incorrect CAPTCHA answer. Please try again." msgid "Your registration could not be processed. Please try again."
msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer." msgstr "Votre inscription n'a pas pu être traitée. Veuillez réessayer."
#: app/routes/auth.py:345 app/routes/users.py:436 #: app/routes/auth.py:388 app/routes/users/accounts.py:308
msgid "Username already exists." msgid "Username already exists."
msgstr "Ce nom dutilisateur est déjà pris." msgstr "Ce nom dutilisateur est déjà pris."
#: app/routes/auth.py:357 app/routes/users.py:440 #: app/routes/auth.py:392 app/routes/users/accounts.py:312
msgid "Email already registered." msgid "Email already registered."
msgstr "Cette adresse courriel est déjà enregistrée." msgstr "Cette adresse courriel est déjà enregistrée."
#: app/routes/auth.py:404 #: app/routes/auth.py:436
msgid "Your account has been created! You can now log in." msgid "Your account has been created! You can now log in."
msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter." msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter."
#: app/routes/auth.py:430 #: app/routes/auth.py:457
msgid "Discord OAuth2 is not configured." msgid "Discord OAuth2 is not configured."
msgstr "La connexion Discord nest pas configurée." msgstr "La connexion Discord nest pas configurée."
#: app/routes/auth.py:471 #: app/routes/auth.py:498
msgid "" msgid ""
"Discord authorization could not be verified. Please start the connection " "Discord authorization could not be verified. Please start the connection "
"again from this page." "again from this page."
@@ -153,422 +153,423 @@ msgstr ""
"Lautorisation Discord na pas pu être vérifiée. Relancez la connexion " "Lautorisation Discord na pas pu être vérifiée. Relancez la connexion "
"depuis cette page." "depuis cette page."
#: app/routes/auth.py:480 #: app/routes/auth.py:507
msgid "Discord authorization failed. No code received." msgid "Discord authorization failed. No code received."
msgstr "Lautorisation Discord a échoué : aucun code reçu." msgstr "Lautorisation Discord a échoué : aucun code reçu."
#: app/routes/auth.py:504 #: app/routes/auth.py:531
msgid "Failed to connect to Discord. Please try again." msgid "Failed to connect to Discord. Please try again."
msgstr "Impossible de joindre Discord. Veuillez réessayer." msgstr "Impossible de joindre Discord. Veuillez réessayer."
#: app/routes/auth.py:508 #: app/routes/auth.py:535
msgid "Failed to obtain Discord access token." msgid "Failed to obtain Discord access token."
msgstr "Impossible dobtenir le jeton daccès Discord." msgstr "Impossible dobtenir le jeton daccès Discord."
#: app/routes/auth.py:523 #: app/routes/auth.py:550
msgid "Failed to fetch Discord user profile." msgid "Failed to fetch Discord user profile."
msgstr "Impossible de récupérer le profil Discord." msgstr "Impossible de récupérer le profil Discord."
#: app/routes/auth.py:570 #: app/routes/auth.py:597
msgid "Discord account connected! Your profile has been pre-filled." msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Compte Discord connecté. Votre profil a été pré-rempli." msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
#: app/routes/auth.py:598 #: app/routes/auth.py:625
msgid "You have been logged out." msgid "You have been logged out."
msgstr "Vous avez été déconnecté." msgstr "Vous avez été déconnecté."
#: app/routes/evaluations.py:45 #: app/routes/evaluations.py:46
msgid "You do not have permission to view evaluations." msgid "You do not have permission to view evaluations."
msgstr "Vous navez pas les droits pour consulter les évaluations." msgstr "Vous navez pas les droits pour consulter les évaluations."
#: app/routes/evaluations.py:135 #: app/routes/evaluations.py:136
msgid "You do not have permission to evaluate players." msgid "You do not have permission to evaluate players."
msgstr "Vous navez pas les droits pour évaluer des joueurs." msgstr "Vous navez pas les droits pour évaluer des joueurs."
#: app/routes/evaluations.py:140 app/routes/evaluations.py:263 #: app/routes/evaluations.py:141 app/routes/evaluations.py:264
msgid "You do not have permission to evaluate players in this tryout." msgid "You do not have permission to evaluate players in this tryout."
msgstr "Vous navez pas les droits pour évaluer des joueurs dans cette sélection." msgstr "Vous navez pas les droits pour évaluer des joueurs dans cette sélection."
#: app/routes/evaluations.py:151 #: app/routes/evaluations.py:152
msgid "Player is not registered for this tryout." msgid "Player is not registered for this tryout."
msgstr "Ce joueur nest pas inscrit à cette sélection." msgstr "Ce joueur nest pas inscrit à cette sélection."
#: app/routes/evaluations.py:156 #: app/routes/evaluations.py:157
msgid "Can only evaluate players." msgid "Can only evaluate players."
msgstr "Seuls des joueurs peuvent être évalués." msgstr "Seuls des joueurs peuvent être évalués."
#: app/routes/evaluations.py:208 #: app/routes/evaluations.py:209
msgid "Evaluation updated!" msgid "Evaluation updated!"
msgstr "Évaluation mise à jour." msgstr "Évaluation mise à jour."
#: app/routes/evaluations.py:228 #: app/routes/evaluations.py:229
msgid "Evaluation submitted successfully!" msgid "Evaluation submitted successfully!"
msgstr "Évaluation enregistrée." msgstr "Évaluation enregistrée."
#: app/routes/evaluations.py:258 app/routes/teams.py:268 #: app/routes/evaluations.py:259 app/routes/teams.py:270
#: app/routes/teams.py:309 app/routes/teams.py:350 app/routes/teams.py:375 #: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
#: app/routes/teams.py:400 app/routes/teams.py:435 app/routes/tryouts.py:472 #: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:505
#: app/routes/tryouts.py:488 app/routes/tryouts.py:508 #: app/routes/tryouts.py:521 app/routes/tryouts.py:541
#: app/routes/tryouts.py:547 app/routes/tryouts.py:583 #: app/routes/tryouts.py:580 app/routes/tryouts.py:616
#: app/routes/tryouts.py:602 #: app/routes/tryouts.py:635
msgid "Permission denied." msgid "Permission denied."
msgstr "Accès refusé." msgstr "Accès refusé."
#: app/routes/main.py:53 #: app/routes/main.py:55
msgid "That language is not available." msgid "That language is not available."
msgstr "Cette langue nest pas disponible." msgstr "Cette langue nest pas disponible."
#: app/routes/matches.py:245 #: app/routes/matches.py:307
msgid "You do not have permission to schedule matches for this tryout." msgid "You do not have permission to schedule matches for this tryout."
msgstr "Vous navez pas les droits pour planifier des matchs pour cette sélection." msgstr "Vous navez pas les droits pour planifier des matchs pour cette sélection."
#: app/routes/matches.py:249 app/routes/matches.py:422 #: app/routes/matches.py:311 app/routes/matches.py:473
msgid "This tryout has ended. Matches can no longer be created or modified." msgid "This tryout has ended. Matches can no longer be created or modified."
msgstr "" msgstr ""
"Cette sélection est terminée. Les matchs ne peuvent plus être créés ni " "Cette sélection est terminée. Les matchs ne peuvent plus être créés ni "
"modifiés." "modifiés."
#: app/routes/matches.py:270 #: app/routes/matches.py:332
msgid "Start time is required. Please select a time slot." msgid "Start time is required. Please select a time slot."
msgstr "Lheure de début est obligatoire. Choisissez une plage horaire." msgstr "Lheure de début est obligatoire. Choisissez une plage horaire."
#: app/routes/matches.py:282 app/routes/matches.py:445 #: app/routes/matches.py:344 app/routes/matches.py:496
#: app/routes/team_matches.py:151 app/routes/team_matches.py:255 #: app/routes/team_matches.py:153 app/routes/team_matches.py:247
msgid "Invalid date format." msgid "Invalid date format."
msgstr "Format de date invalide." msgstr "Format de date invalide."
#: app/routes/matches.py:302 app/routes/team_matches.py:172 #: app/routes/matches.py:364 app/routes/team_matches.py:174
msgid "Invalid time format." msgid "Invalid time format."
msgstr "Format dheure invalide." msgstr "Format dheure invalide."
#: app/routes/matches.py:398 #: app/routes/matches.py:449
msgid "Match scheduled successfully!" msgid "Match scheduled successfully!"
msgstr "Match planifié." msgstr "Match planifié."
#: app/routes/matches.py:418 app/routes/team_matches.py:242 #: app/routes/matches.py:469 app/routes/team_matches.py:234
msgid "You do not have permission to edit this match." msgid "You do not have permission to edit this match."
msgstr "Vous navez pas les droits pour modifier ce match." msgstr "Vous navez pas les droits pour modifier ce match."
#: app/routes/matches.py:456 #: app/routes/matches.py:507
msgid "Start time is required." msgid "Start time is required."
msgstr "Lheure de début est obligatoire." msgstr "Lheure de début est obligatoire."
#: app/routes/matches.py:578 app/routes/team_matches.py:284 #: app/routes/matches.py:616 app/routes/team_matches.py:276
msgid "Match updated successfully!" msgid "Match updated successfully!"
msgstr "Match mis à jour." msgstr "Match mis à jour."
#: app/routes/matches.py:631 app/routes/team_matches.py:299 #: app/routes/matches.py:669 app/routes/team_matches.py:291
msgid "You do not have permission to delete this match." msgid "You do not have permission to delete this match."
msgstr "Vous navez pas les droits pour supprimer ce match." msgstr "Vous navez pas les droits pour supprimer ce match."
#: app/routes/matches.py:634 #: app/routes/matches.py:672
msgid "This tryout has ended. Matches can no longer be deleted." 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." msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés."
#: app/routes/matches.py:647 app/routes/team_matches.py:303 #: app/routes/matches.py:685 app/routes/team_matches.py:295
msgid "Match deleted successfully." msgid "Match deleted successfully."
msgstr "Match supprimé." msgstr "Match supprimé."
#: app/routes/team_matches.py:98 #: app/routes/team_matches.py:100
msgid "You do not have permission to schedule matches for this team." msgid "You do not have permission to schedule matches for this team."
msgstr "Vous navez pas les droits pour planifier des matchs pour cette équipe." msgstr "Vous navez pas les droits pour planifier des matchs pour cette équipe."
#: app/routes/team_matches.py:140 #: app/routes/team_matches.py:142
msgid "Date is required." msgid "Date is required."
msgstr "La date est obligatoire." msgstr "La date est obligatoire."
#: app/routes/team_matches.py:228 #: app/routes/team_matches.py:220
#, python-format #, python-format
msgid "Team match \"%(title)s\" scheduled successfully!" msgid "Team match \"%(title)s\" scheduled successfully!"
msgstr "Match d’équipe « %(title)s » planifié." msgstr "Match d’équipe « %(title)s » planifié."
#: app/routes/team_matches.py:267 #: app/routes/team_matches.py:259
msgid "Invalid start time format." msgid "Invalid start time format."
msgstr "Format dheure de début invalide." msgstr "Format dheure de début invalide."
#: app/routes/team_matches.py:275 #: app/routes/team_matches.py:267
msgid "Invalid end time format." msgid "Invalid end time format."
msgstr "Format dheure de fin invalide." msgstr "Format dheure de fin invalide."
#: app/routes/teams.py:38 #: app/routes/teams.py:40
msgid "Use My Team(s) to view your teams." msgid "Use My Team(s) to view your teams."
msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes." msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
#: app/routes/teams.py:41 #: app/routes/teams.py:43
msgid "You do not have permission to view teams." msgid "You do not have permission to view teams."
msgstr "Vous navez pas les droits pour consulter les équipes." msgstr "Vous navez pas les droits pour consulter les équipes."
#: app/routes/teams.py:68 #: app/routes/teams.py:70
msgid "This page is for players." msgid "This page is for players."
msgstr "Cette page est réservée aux joueurs." msgstr "Cette page est réservée aux joueurs."
#: app/routes/teams.py:121 #: app/routes/teams.py:123
msgid "You do not have permission to create teams." msgid "You do not have permission to create teams."
msgstr "Vous navez pas les droits pour créer une équipe." msgstr "Vous navez pas les droits pour créer une équipe."
#: app/routes/teams.py:129 app/routes/teams.py:174 #: app/routes/teams.py:131 app/routes/teams.py:176
msgid "Team name is required." msgid "Team name is required."
msgstr "Le nom de l’équipe est obligatoire." msgstr "Le nom de l’équipe est obligatoire."
#: app/routes/teams.py:134 app/routes/teams.py:179 #: app/routes/teams.py:136 app/routes/teams.py:181
#, python-format #, python-format
msgid "Team \"%(name)s\" already exists." msgid "Team \"%(name)s\" already exists."
msgstr "L’équipe « %(name)s » existe déjà." msgstr "L’équipe « %(name)s » existe déjà."
#: app/routes/teams.py:156 #: app/routes/teams.py:158
#, python-format #, python-format
msgid "Team \"%(name)s\" created successfully!" msgid "Team \"%(name)s\" created successfully!"
msgstr "Équipe « %(name)s » créée." msgstr "Équipe « %(name)s » créée."
#: app/routes/teams.py:166 #: app/routes/teams.py:168
msgid "You do not have permission to edit this team." msgid "You do not have permission to edit this team."
msgstr "Vous navez pas les droits pour modifier cette équipe." msgstr "Vous navez pas les droits pour modifier cette équipe."
#: app/routes/teams.py:217 #: app/routes/teams.py:219
#, python-format #, python-format
msgid "Team \"%(name)s\" updated successfully!" msgid "Team \"%(name)s\" updated successfully!"
msgstr "Équipe « %(name)s » mise à jour." msgstr "Équipe « %(name)s » mise à jour."
#: app/routes/teams.py:226 #: app/routes/teams.py:228
msgid "You do not have permission to delete teams." msgid "You do not have permission to delete teams."
msgstr "Vous navez pas les droits pour supprimer une équipe." msgstr "Vous navez pas les droits pour supprimer une équipe."
#: app/routes/teams.py:258 #: app/routes/teams.py:260
#, python-format #, python-format
msgid "Team \"%(name)s\" deleted successfully." msgid "Team \"%(name)s\" deleted successfully."
msgstr "Équipe « %(name)s » supprimée." msgstr "Équipe « %(name)s » supprimée."
#: app/routes/teams.py:273 #: app/routes/teams.py:275
msgid "Please select a coach." msgid "Please select a coach."
msgstr "Veuillez choisir un coach." msgstr "Veuillez choisir un coach."
#: app/routes/teams.py:278 #: app/routes/teams.py:280
msgid "Only coaches can be assigned as coach." msgid "Only coaches can be assigned as coach."
msgstr "Seuls les coachs peuvent être assignés comme coach." msgstr "Seuls les coachs peuvent être assignés comme coach."
#: app/routes/teams.py:284 #: app/routes/teams.py:286
#, python-format #, python-format
msgid "%(username)s is already a coach of %(name)s." msgid "%(username)s is already a coach of %(name)s."
msgstr "%(username)s est déjà coach de %(name)s." msgstr "%(username)s est déjà coach de %(name)s."
#: app/routes/teams.py:297 #: app/routes/teams.py:299
#, python-format #, python-format
msgid "%(username)s added as coach of %(name)s." msgid "%(username)s added as coach of %(name)s."
msgstr "%(username)s a été ajouté comme coach de %(name)s." msgstr "%(username)s a été ajouté comme coach de %(name)s."
#: app/routes/teams.py:314 #: app/routes/teams.py:316
msgid "Please select a manager." msgid "Please select a manager."
msgstr "Veuillez choisir un gérant." msgstr "Veuillez choisir un gérant."
#: app/routes/teams.py:319 #: app/routes/teams.py:321
msgid "Only managers can be assigned as manager." msgid "Only managers can be assigned as manager."
msgstr "Seuls les gérants peuvent être assignés comme gérant." msgstr "Seuls les gérants peuvent être assignés comme gérant."
#: app/routes/teams.py:325 #: app/routes/teams.py:327
#, python-format #, python-format
msgid "%(username)s is already a manager of %(name)s." msgid "%(username)s is already a manager of %(name)s."
msgstr "%(username)s est déjà gérant de %(name)s." msgstr "%(username)s est déjà gérant de %(name)s."
#: app/routes/teams.py:338 #: app/routes/teams.py:340
#, python-format #, python-format
msgid "%(username)s added as manager of %(name)s." msgid "%(username)s added as manager of %(name)s."
msgstr "%(username)s a été ajouté comme gérant de %(name)s." msgstr "%(username)s a été ajouté comme gérant de %(name)s."
#: app/routes/teams.py:365 #: app/routes/teams.py:367
#, python-format #, python-format
msgid "Coach removed from %(name)s." msgid "Coach removed from %(name)s."
msgstr "Coach retiré de %(name)s." msgstr "Coach retiré de %(name)s."
#: app/routes/teams.py:390 #: app/routes/teams.py:392
#, python-format #, python-format
msgid "Manager removed from %(name)s." msgid "Manager removed from %(name)s."
msgstr "Gérant retiré de %(name)s." msgstr "Gérant retiré de %(name)s."
#: app/routes/teams.py:406 app/routes/tryouts.py:512 app/routes/tryouts.py:613 #: app/routes/teams.py:408 app/routes/tryouts.py:545 app/routes/tryouts.py:646
msgid "Please select a player." msgid "Please select a player."
msgstr "Veuillez choisir un joueur." msgstr "Veuillez choisir un joueur."
#: app/routes/teams.py:411 #: app/routes/teams.py:413
msgid "Can only assign players to teams." msgid "Can only assign players to teams."
msgstr "Seuls des joueurs peuvent être assignés à une équipe." msgstr "Seuls des joueurs peuvent être assignés à une équipe."
#: app/routes/teams.py:417 #: app/routes/teams.py:419
#, python-format #, python-format
msgid "%(username)s is already on %(name)s." msgid "%(username)s is already on %(name)s."
msgstr "%(username)s fait déjà partie de %(name)s." msgstr "%(username)s fait déjà partie de %(name)s."
#: app/routes/teams.py:425 #: app/routes/teams.py:427
#, python-format #, python-format
msgid "%(username)s added to %(name)s!" msgid "%(username)s added to %(name)s!"
msgstr "%(username)s a été ajouté à %(name)s." msgstr "%(username)s a été ajouté à %(name)s."
#: app/routes/teams.py:442 app/routes/teams.py:515 #: app/routes/teams.py:444 app/routes/teams.py:517
#, python-format #, python-format
msgid "%(username)s is not on %(name)s." msgid "%(username)s is not on %(name)s."
msgstr "%(username)s ne fait pas partie de %(name)s." msgstr "%(username)s ne fait pas partie de %(name)s."
#: app/routes/teams.py:450 #: app/routes/teams.py:452
#, python-format #, python-format
msgid "%(username)s removed from %(name)s." msgid "%(username)s removed from %(name)s."
msgstr "%(username)s a été retiré de %(name)s." msgstr "%(username)s a été retiré de %(name)s."
#: app/routes/teams.py:486 app/routes/teams.py:504 #: app/routes/teams.py:488 app/routes/teams.py:506
msgid "You do not have permission to add notes to this team." msgid "You do not have permission to add notes to this team."
msgstr "Vous navez pas les droits pour ajouter des notes à cette équipe." msgstr "Vous navez pas les droits pour ajouter des notes à cette équipe."
#: app/routes/teams.py:494 #: app/routes/teams.py:496
msgid "Team notes added successfully!" msgid "Team notes added successfully!"
msgstr "Notes d’équipe ajoutées." msgstr "Notes d’équipe ajoutées."
#: app/routes/teams.py:509 app/routes/users.py:1530 app/routes/users.py:1573 #: app/routes/teams.py:511 app/routes/users/notes.py:207
#: app/routes/users/notes.py:250
msgid "Can only add notes for players." msgid "Can only add notes for players."
msgstr "Il nest possible dajouter des notes que pour des joueurs." msgstr "Il nest possible dajouter des notes que pour des joueurs."
#: app/routes/teams.py:525 #: app/routes/teams.py:527
#, python-format #, python-format
msgid "Note added for %(username)s!" msgid "Note added for %(username)s!"
msgstr "Note ajoutée pour %(username)s." msgstr "Note ajoutée pour %(username)s."
#: app/routes/tryouts.py:56 #: app/routes/tryouts.py:77
msgid "You do not have permission to create tryouts." msgid "You do not have permission to create tryouts."
msgstr "Vous navez pas les droits pour créer une sélection." msgstr "Vous navez pas les droits pour créer une sélection."
#: app/routes/tryouts.py:82 app/routes/tryouts.py:189 #: app/routes/tryouts.py:103 app/routes/tryouts.py:210
msgid "Invalid start date format." msgid "Invalid start date format."
msgstr "Format de date de début invalide." msgstr "Format de date de début invalide."
#: app/routes/tryouts.py:97 app/routes/tryouts.py:204 #: app/routes/tryouts.py:118 app/routes/tryouts.py:225
msgid "End date cannot be before start date." msgid "End date cannot be before start date."
msgstr "La date de fin ne peut pas précéder la date de début." msgstr "La date de fin ne peut pas précéder la date de début."
#: app/routes/tryouts.py:107 app/routes/tryouts.py:214 #: app/routes/tryouts.py:128 app/routes/tryouts.py:235
msgid "Invalid end date format." msgid "Invalid end date format."
msgstr "Format de date de fin invalide." msgstr "Format de date de fin invalide."
#: app/routes/tryouts.py:139 #: app/routes/tryouts.py:160
msgid "Tryout created successfully!" msgid "Tryout created successfully!"
msgstr "Sélection créée." msgstr "Sélection créée."
#: app/routes/tryouts.py:159 #: app/routes/tryouts.py:180
msgid "You do not have permission to edit this tryout." msgid "You do not have permission to edit this tryout."
msgstr "Vous navez pas les droits pour modifier cette sélection." msgstr "Vous navez pas les droits pour modifier cette sélection."
#: app/routes/tryouts.py:163 #: app/routes/tryouts.py:184
msgid "This tryout has ended and can no longer be modified." 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." msgstr "Cette sélection est terminée et ne peut plus être modifiée."
#: app/routes/tryouts.py:242 #: app/routes/tryouts.py:263
msgid "Tryout updated successfully!" msgid "Tryout updated successfully!"
msgstr "Sélection mise à jour." msgstr "Sélection mise à jour."
#: app/routes/tryouts.py:289 #: app/routes/tryouts.py:310
msgid "You do not have permission to view this tryout." msgid "You do not have permission to view this tryout."
msgstr "Vous navez pas les droits pour consulter cette sélection." msgstr "Vous navez pas les droits pour consulter cette sélection."
#: app/routes/tryouts.py:439 #: app/routes/tryouts.py:472
msgid "Only players can register for tryouts." msgid "Only players can register for tryouts."
msgstr "Seuls les joueurs peuvent sinscrire à une sélection." msgstr "Seuls les joueurs peuvent sinscrire à une sélection."
#: app/routes/tryouts.py:443 #: app/routes/tryouts.py:476
msgid "This tryout is not accepting registrations." msgid "This tryout is not accepting registrations."
msgstr "Cette sélection naccepte pas dinscriptions." msgstr "Cette sélection naccepte pas dinscriptions."
#: app/routes/tryouts.py:450 #: app/routes/tryouts.py:483
msgid "You are already registered for this tryout." msgid "You are already registered for this tryout."
msgstr "Vous êtes déjà inscrit à cette sélection." msgstr "Vous êtes déjà inscrit à cette sélection."
#: app/routes/tryouts.py:456 app/routes/tryouts.py:531 #: app/routes/tryouts.py:489 app/routes/tryouts.py:564
msgid "This tryout is full." msgid "This tryout is full."
msgstr "Cette sélection est complète." msgstr "Cette sélection est complète."
#: app/routes/tryouts.py:462 #: app/routes/tryouts.py:495
msgid "Successfully registered for tryout!" msgid "Successfully registered for tryout!"
msgstr "Inscription à la sélection réussie." msgstr "Inscription à la sélection réussie."
#: app/routes/tryouts.py:478 #: app/routes/tryouts.py:511
#, python-format #, python-format
msgid "Tryout status updated to %(new_status)s." msgid "Tryout status updated to %(new_status)s."
msgstr "Statut de la sélection mis à jour : %(new_status)s." msgstr "Statut de la sélection mis à jour : %(new_status)s."
#: app/routes/tryouts.py:498 #: app/routes/tryouts.py:531
msgid "Registration status updated." msgid "Registration status updated."
msgstr "Statut dinscription mis à jour." msgstr "Statut dinscription mis à jour."
#: app/routes/tryouts.py:517 #: app/routes/tryouts.py:550
msgid "Can only register players." msgid "Can only register players."
msgstr "Seuls des joueurs peuvent être inscrits." msgstr "Seuls des joueurs peuvent être inscrits."
#: app/routes/tryouts.py:523 #: app/routes/tryouts.py:556
#, python-format #, python-format
msgid "%(username)s is already registered for this tryout." msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s est déjà inscrit à cette sélection." msgstr "%(username)s est déjà inscrit à cette sélection."
#: app/routes/tryouts.py:537 #: app/routes/tryouts.py:570
#, python-format #, python-format
msgid "%(username)s registered for tryout!" msgid "%(username)s registered for tryout!"
msgstr "%(username)s est inscrit à la sélection." msgstr "%(username)s est inscrit à la sélection."
#: app/routes/tryouts.py:573 #: app/routes/tryouts.py:606
#, python-format #, python-format
msgid "%(username)s removed from tryout." msgid "%(username)s removed from tryout."
msgstr "%(username)s a été retiré de la sélection." msgstr "%(username)s a été retiré de la sélection."
#: app/routes/tryouts.py:591 #: app/routes/tryouts.py:624
#, python-format #, python-format
msgid "Team \"%(team_name)s\" created!" msgid "Team \"%(team_name)s\" created!"
msgstr "Équipe « %(team_name)s » créée." msgstr "Équipe « %(team_name)s » créée."
#: app/routes/tryouts.py:622 #: app/routes/tryouts.py:655
msgid "That player is not registered for this tryout." msgid "That player is not registered for this tryout."
msgstr "Ce joueur nest pas inscrit à cette sélection." msgstr "Ce joueur nest pas inscrit à cette sélection."
#: app/routes/tryouts.py:628 #: app/routes/tryouts.py:661
msgid "Player is already on this team." msgid "Player is already on this team."
msgstr "Ce joueur est déjà dans cette équipe." msgstr "Ce joueur est déjà dans cette équipe."
#: app/routes/tryouts.py:633 #: app/routes/tryouts.py:666
msgid "Player added to team!" msgid "Player added to team!"
msgstr "Joueur ajouté à l’équipe." msgstr "Joueur ajouté à l’équipe."
#: app/routes/tryouts.py:643 #: app/routes/tryouts.py:676
msgid "You do not have permission to delete this tryout." msgid "You do not have permission to delete this tryout."
msgstr "Vous navez pas les droits pour supprimer cette sélection." msgstr "Vous navez pas les droits pour supprimer cette sélection."
#: app/routes/tryouts.py:679 #: app/routes/tryouts.py:712
msgid "Tryout deleted successfully." msgid "Tryout deleted successfully."
msgstr "Sélection supprimée." msgstr "Sélection supprimée."
#: app/routes/users.py:83 #: app/routes/users/_shared.py:46
msgid "No file selected." msgid "No file selected."
msgstr "Aucun fichier sélectionné." msgstr "Aucun fichier sélectionné."
#: app/routes/users.py:87 #: app/routes/users/_shared.py:50
msgid "Only PDF files are allowed for contracts." msgid "Only PDF files are allowed for contracts."
msgstr "Seuls les fichiers PDF sont acceptés pour les contrats." msgstr "Seuls les fichiers PDF sont acceptés pour les contrats."
#: app/routes/users.py:92 #: app/routes/users/_shared.py:55
msgid "That file is not a PDF, whatever its name says." msgid "That file is not a PDF, whatever its name says."
msgstr "Ce fichier nest pas un PDF, quel que soit son nom." msgstr "Ce fichier nest pas un PDF, quel que soit son nom."
#: app/routes/users.py:181 #: app/routes/users/accounts.py:53
msgid "Only the president can manage users." msgid "Only the president can manage users."
msgstr "Seul le président peut gérer les utilisateurs." msgstr "Seul le président peut gérer les utilisateurs."
#: app/routes/users.py:193 #: app/routes/users/accounts.py:65
msgid "Only the president can edit users." msgid "Only the president can edit users."
msgstr "Seul le président peut modifier des utilisateurs." msgstr "Seul le président peut modifier des utilisateurs."
#: app/routes/users.py:234 #: app/routes/users/accounts.py:106
msgid "Email already in use by another account." msgid "Email already in use by another account."
msgstr "Cette adresse courriel est déjà utilisée par un autre compte." msgstr "Cette adresse courriel est déjà utilisée par un autre compte."
#: app/routes/users.py:245 #: app/routes/users/accounts.py:117
msgid "You cannot change your own role. Ask another president to do it." msgid "You cannot change your own role. Ask another president to do it."
msgstr "" msgstr ""
"Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre " "Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre "
"président de le faire." "président de le faire."
#: app/routes/users.py:258 #: app/routes/users/accounts.py:130
msgid "" msgid ""
"This is the last active president. Promote another account before " "This is the last active president. Promote another account before "
"changing this one." "changing this one."
@@ -576,177 +577,178 @@ msgstr ""
"Cest le dernier président actif. Promouvez un autre compte avant de " "Cest le dernier président actif. Promouvez un autre compte avant de "
"modifier celui-ci." "modifier celui-ci."
#: app/routes/users.py:337 #: app/routes/users/accounts.py:209
#, python-format #, python-format
msgid "User %(username)s updated successfully!" msgid "User %(username)s updated successfully!"
msgstr "Utilisateur %(username)s mis à jour." msgstr "Utilisateur %(username)s mis à jour."
#: app/routes/users.py:358 #: app/routes/users/accounts.py:230
msgid "Only the president can delete users." msgid "Only the president can delete users."
msgstr "Seul le président peut supprimer des utilisateurs." msgstr "Seul le président peut supprimer des utilisateurs."
#: app/routes/users.py:362 #: app/routes/users/accounts.py:234
msgid "You cannot delete your own account." msgid "You cannot delete your own account."
msgstr "Vous ne pouvez pas supprimer votre propre compte." msgstr "Vous ne pouvez pas supprimer votre propre compte."
#: app/routes/users.py:405 #: app/routes/users/accounts.py:277
#, python-format #, python-format
msgid "User %(deleted_username)s has been removed." msgid "User %(deleted_username)s has been removed."
msgstr "Lutilisateur %(deleted_username)s a été supprimé." msgstr "Lutilisateur %(deleted_username)s a été supprimé."
#: app/routes/users.py:416 #: app/routes/users/accounts.py:288
msgid "Only the president can create users." msgid "Only the president can create users."
msgstr "Seul le président peut créer des utilisateurs." msgstr "Seul le président peut créer des utilisateurs."
#: app/routes/users.py:464 #: app/routes/users/accounts.py:336
#, python-format #, python-format
msgid "User %(full_name)s created as %(role)s!" msgid "User %(full_name)s created as %(role)s!"
msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s." msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
#: app/routes/users.py:534 #: app/routes/users/availability.py:179
msgid "Username already taken." msgid "Only coaches can manage availability."
msgstr "Ce nom dutilisateur est déjà pris." msgstr "Seuls les coachs peuvent gérer leurs disponibilités."
#: app/routes/users.py:544 #: app/routes/users/contracts.py:83
msgid "Email already in use."
msgstr "Cette adresse courriel est déjà utilisée."
#: app/routes/users.py:574
msgid "Profile updated successfully!"
msgstr "Profil mis à jour."
#: app/routes/users.py:809
msgid "Only presidents, managers, and coaches can upload contracts." msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat." msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat."
#: app/routes/users.py:828 #: app/routes/users/contracts.py:102
msgid "You do not have permission to upload a contract for this player." msgid "You do not have permission to upload a contract for this player."
msgstr "Vous navez pas les droits pour téléverser un contrat pour ce joueur." msgstr "Vous navez pas les droits pour téléverser un contrat pour ce joueur."
#: app/routes/users.py:869 #: app/routes/users/contracts.py:143
#, python-format #, python-format
msgid "Contract uploaded successfully for %(username)s!" msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contrat téléversé pour %(username)s." msgstr "Contrat téléversé pour %(username)s."
#: app/routes/users.py:883 #: app/routes/users/contracts.py:157
msgid "Only the player can upload their signed contract." msgid "Only the player can upload their signed contract."
msgstr "Seul le joueur peut téléverser son contrat signé." msgstr "Seul le joueur peut téléverser son contrat signé."
#: app/routes/users.py:902 #: app/routes/users/contracts.py:176
msgid "Signed contract uploaded successfully!" msgid "Signed contract uploaded successfully!"
msgstr "Contrat signé téléversé." msgstr "Contrat signé téléversé."
#: app/routes/users.py:912 app/routes/users.py:925 #: app/routes/users/contracts.py:186 app/routes/users/contracts.py:199
msgid "You do not have permission to download this contract." msgid "You do not have permission to download this contract."
msgstr "Vous navez pas les droits pour télécharger ce contrat." msgstr "Vous navez pas les droits pour télécharger ce contrat."
#: app/routes/users.py:928 #: app/routes/users/contracts.py:202
msgid "No signed contract available." msgid "No signed contract available."
msgstr "Aucun contrat signé disponible." msgstr "Aucun contrat signé disponible."
#: app/routes/users.py:1034 #: app/routes/users/notes.py:34
msgid "Only players can request One on One sessions."
msgstr "Seuls les joueurs peuvent demander une rencontre individuelle."
#: app/routes/users.py:1047
msgid "You do not have a coach assigned to your team."
msgstr "Aucun coach nest assigné à votre équipe."
#: app/routes/users.py:1082
msgid "Cannot request One on One - no coach assigned."
msgstr "Impossible de demander une rencontre : aucun coach assigné."
#: app/routes/users.py:1090
msgid "Invalid date or time format."
msgstr "Format de date ou dheure invalide."
#: app/routes/users.py:1104
msgid "The requested time is not within the coach's availability."
msgstr "Lhoraire demandé ne correspond à aucune disponibilité du coach."
#: app/routes/users.py:1132
msgid "Your One on One request has been submitted!"
msgstr "Votre demande de rencontre a été envoyée."
#: app/routes/users.py:1176
msgid "Only coaches can accept One on One requests."
msgstr "Seuls les coachs peuvent accepter une demande de rencontre."
#: app/routes/users.py:1182 app/routes/users.py:1232
msgid "This request is not for you."
msgstr "Cette demande ne vous est pas destinée."
#: app/routes/users.py:1186 app/routes/users.py:1236
msgid "This request has already been processed."
msgstr "Cette demande a déjà été traitée."
#: app/routes/users.py:1213
#, 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.py:1226
msgid "Only coaches can reject One on One requests."
msgstr "Seuls les coachs peuvent refuser une demande de rencontre."
#: app/routes/users.py:1268
#, 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."
#: app/routes/users.py:1286
msgid "This page is for players only." msgid "This page is for players only."
msgstr "Cette page est réservée aux joueurs." msgstr "Cette page est réservée aux joueurs."
#: app/routes/users.py:1328 #: app/routes/users/notes.py:71
msgid "Only coaches can manage availability."
msgstr "Seuls les coachs peuvent gérer leurs disponibilités."
#: app/routes/users.py:1394
msgid "Only coaches can access the notes dashboard." msgid "Only coaches can access the notes dashboard."
msgstr "Seuls les coachs ont accès au tableau des notes." msgstr "Seuls les coachs ont accès au tableau des notes."
#: app/routes/users.py:1484 #: app/routes/users/notes.py:161
msgid "Only coaches can manage team notes." msgid "Only coaches can manage team notes."
msgstr "Seuls les coachs peuvent gérer les notes d’équipe." msgstr "Seuls les coachs peuvent gérer les notes d’équipe."
#: app/routes/users.py:1490 #: app/routes/users/notes.py:167
msgid "You are not assigned to a team." msgid "You are not assigned to a team."
msgstr "Vous n’êtes assigné à aucune équipe." msgstr "Vous n’êtes assigné à aucune équipe."
#: app/routes/users.py:1503 #: app/routes/users/notes.py:180
msgid "Team notes saved successfully!" msgid "Team notes saved successfully!"
msgstr "Notes d’équipe enregistrées." msgstr "Notes d’équipe enregistrées."
#: app/routes/users.py:1518 #: app/routes/users/notes.py:195
msgid "Only coaches can manage personal notes." msgid "Only coaches can manage personal notes."
msgstr "Seuls les coachs peuvent gérer les notes personnelles." msgstr "Seuls les coachs peuvent gérer les notes personnelles."
#: app/routes/users.py:1525 app/routes/users.py:1568 app/routes/users.py:1619 #: app/routes/users/notes.py:202 app/routes/users/notes.py:245
#: app/routes/users.py:1673 #: app/routes/users/notes.py:296 app/routes/users/notes.py:350
msgid "Player and content are required." msgid "Player and content are required."
msgstr "Le joueur et le contenu sont obligatoires." msgstr "Le joueur et le contenu sont obligatoires."
#: app/routes/users.py:1534 app/routes/users.py:1577 app/routes/users.py:1623 #: app/routes/users/notes.py:211 app/routes/users/notes.py:254
#: app/routes/users.py:1677 #: app/routes/users/notes.py:300 app/routes/users/notes.py:354
msgid "You can only write notes about players you work with." msgid "You can only write notes about players you work with."
msgstr "" msgstr ""
"Vous ne pouvez écrire des notes que sur les joueurs avec qui vous " "Vous ne pouvez écrire des notes que sur les joueurs avec qui vous "
"travaillez." "travaillez."
#: app/routes/users.py:1544 app/routes/users.py:1590 #: app/routes/users/notes.py:221 app/routes/users/notes.py:267
#, python-format #, python-format
msgid "Note added for %(username)s." msgid "Note added for %(username)s."
msgstr "Note ajoutée pour %(username)s." msgstr "Note ajoutée pour %(username)s."
#: app/routes/users.py:1558 app/routes/users.py:1604 app/routes/users.py:1657 #: app/routes/users/notes.py:235 app/routes/users/notes.py:281
#: app/routes/users/notes.py:334
msgid "Only coaches can add personal notes." msgid "Only coaches can add personal notes."
msgstr "Seuls les coachs peuvent ajouter des notes personnelles." msgstr "Seuls les coachs peuvent ajouter des notes personnelles."
#: app/routes/users.py:1634 app/routes/users.py:1688 #: app/routes/users/notes.py:311 app/routes/users/notes.py:365
msgid "Note added successfully." msgid "Note added successfully."
msgstr "Note ajoutée." msgstr "Note ajoutée."
#: app/routes/users/one_on_one.py:20
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:33
msgid "You do not have a coach assigned to your team."
msgstr "Aucun coach nest assigné à votre équipe."
#: app/routes/users/one_on_one.py:68
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:76
msgid "Invalid date or time format."
msgstr "Format de date ou dheure invalide."
#: app/routes/users/one_on_one.py:90
msgid "The requested time is not within the coach's availability."
msgstr "Lhoraire demandé ne correspond à aucune disponibilité du coach."
#: app/routes/users/one_on_one.py:118
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:163
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:169 app/routes/users/one_on_one.py:219
msgid "This request is not for you."
msgstr "Cette demande ne vous est pas destinée."
#: app/routes/users/one_on_one.py:173 app/routes/users/one_on_one.py:223
msgid "This request has already been processed."
msgstr "Cette demande a déjà été traitée."
#: app/routes/users/one_on_one.py:200
#, 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:213
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:255
#, 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."
#: app/routes/users/profile.py:83
msgid "Username already taken."
msgstr "Ce nom dutilisateur est déjà pris."
#: app/routes/users/profile.py:93
msgid "Email already in use."
msgstr "Cette adresse courriel est déjà utilisée."
#: app/routes/users/profile.py:123
msgid "Profile updated successfully!"
msgstr "Profil mis à jour."
#: app/templates/errors/400.html:2 #: app/templates/errors/400.html:2
msgid "400 Bad Request" msgid "400 Bad Request"
msgstr "400 Requête incorrecte" msgstr "400 Requête incorrecte"
@@ -2387,11 +2389,7 @@ msgstr "Confirmer le mot de passe"
msgid "Confirm your password" msgid "Confirm your password"
msgstr "Confirmez votre mot de passe" msgstr "Confirmez votre mot de passe"
#: app/templates/pages/register.html:151 #: app/templates/pages/register.html:159
msgid "Answer"
msgstr "Réponse"
#: app/templates/pages/register.html:154
msgid "Create Account" msgid "Create Account"
msgstr "Créer le compte" msgstr "Créer le compte"
@@ -2900,3 +2898,9 @@ msgstr "Voir le profil"
#~ "%(remaining)s tentative(s) avant le " #~ "%(remaining)s tentative(s) avant le "
#~ "verrouillage." #~ "verrouillage."
#~ msgid "Incorrect CAPTCHA answer. Please try again."
#~ msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer."
#~ msgid "Answer"
#~ msgstr "Réponse"
+217
View File
@@ -0,0 +1,217 @@
"""What now stands between a robot and a new account (SEC-AUTH-008).
The arithmetic CAPTCHA it replaces had nineteen possible answers and could
be solved by reading the question as a string. It stopped nothing, cost every
human a step, and — being counted as a protection — was worse than nothing.
Two checks took its place: a honeypot input, and a floor on how fast the
form can come back. Both are invisible to a person filling in the form. Both
are honest about their ceiling: they stop commodity spam, not somebody who
reads the page.
The tests that matter most here are the ones asserting a *legitimate*
sign-up still works. A screening rule that quietly refuses real people is a
worse outcome than the CAPTCHA was.
"""
import time
from app.routes.auth import (
MIN_REGISTRATION_SECONDS,
REGISTRATION_HONEYPOT_FIELD,
REGISTRATION_ISSUED_KEY,
)
FORM = {
'username': 'brandnew',
'email': '[email protected]',
'password': 'Password123',
'confirm_password': 'Password123',
'full_name': 'Brand New',
}
def _issued_long_ago(client, seconds_ago=None):
"""Pretend the form was handed out a while back.
Backdated rather than slept through: waiting three real seconds per test
would add a minute to the suite for nothing.
"""
if seconds_ago is None:
seconds_ago = MIN_REGISTRATION_SECONDS + 1
with client.session_transaction() as session:
session[REGISTRATION_ISSUED_KEY] = time.time() - seconds_ago
def _account_exists(app, username='brandnew'):
from app.models import User
with app.app_context():
return User.query.filter_by(username=username).first() is not None
class TestLegitimateSignUp:
def test_a_person_filling_the_form_gets_an_account(self, app, client):
client.get('/auth/register')
_issued_long_ago(client)
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
assert _account_exists(app)
def test_an_empty_honeypot_is_not_a_refusal(self, app, client):
"""A browser submits the hidden input as an empty string, not absent."""
_issued_long_ago(client)
client.post(
'/auth/register',
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: ''}),
follow_redirects=True,
)
assert _account_exists(app)
def test_correcting_a_typo_does_not_restart_the_clock(self, app, client):
"""The dwell timer must survive a failed attempt.
Reissuing it on every re-render would refuse the second submission of
anyone who fixes a mistake quickly — a rule that fires on real people
and not on robots, which is the wrong way round.
"""
client.get('/auth/register')
_issued_long_ago(client)
# First attempt fails validation: passwords do not match.
client.post(
'/auth/register',
data=dict(FORM, confirm_password='Different123'),
follow_redirects=True,
)
# Corrected and sent straight back, well inside the dwell floor.
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
assert _account_exists(app)
class TestScreening:
def test_a_filled_honeypot_is_refused(self, app, client):
_issued_long_ago(client)
client.post(
'/auth/register',
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: 'http://spam.example'}),
follow_redirects=True,
)
assert not _account_exists(app)
def test_a_form_returned_instantly_is_refused(self, app, client):
client.get('/auth/register')
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
assert not _account_exists(app)
def test_a_post_that_never_fetched_the_form_is_refused(self, app, client):
"""The strongest signal available: no form was ever issued."""
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
assert not _account_exists(app)
def test_the_two_rules_are_indistinguishable_to_the_sender(self, app, client):
"""Naming the rule tells whoever tripped it how to avoid it.
Both refusals must read identically from outside. The log, which the
sender cannot see, is where they are told apart.
One client per scenario, on purpose: the dwell stamp lives in the
session and is deliberately kept across a refusal, so reusing a single
client would carry the first scenario's backdated stamp into the
second and the "too fast" case would never fire.
"""
_issued_long_ago(client)
tripped_honeypot = client.post(
'/auth/register',
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: 'spam'}),
follow_redirects=True,
).get_data(as_text=True)
fresh = app.test_client()
fresh.get('/auth/register')
too_fast = fresh.post('/auth/register', data=dict(FORM), follow_redirects=True).get_data(
as_text=True
)
def flashes(page):
return [line for line in page.splitlines() if 'alert-danger' in line]
assert flashes(tripped_honeypot) == flashes(too_fast)
assert flashes(tripped_honeypot), 'no message at all is not the same as a uniform one'
class TestTheFormItself:
def test_no_arithmetic_question_is_asked(self, client):
page = client.get('/auth/register').get_data(as_text=True)
assert 'captcha' not in page.lower()
def test_the_honeypot_is_hidden_from_assistive_technology(self, client):
"""An input a screen reader announces is a trap for a person.
aria-hidden, tabindex=-1 and display:none all have to hold. The CSS
rule lives in the stylesheet rather than in a style attribute so that
it survives a future tightening of style-src.
"""
page = client.get('/auth/register').get_data(as_text=True)
assert 'class="honeypot" aria-hidden="true"' in page
assert f'name="{REGISTRATION_HONEYPOT_FIELD}" tabindex="-1"' in page
def test_the_stylesheet_actually_hides_it(self, app):
import os
with open(os.path.join(app.static_folder, 'css', 'style.css'), encoding='utf-8') as handle:
css = handle.read()
block = css.split('.honeypot')[-1]
assert 'display: none' in block.split('}')[0]
class TestRefusalIsVisible:
def test_a_refusal_is_written_to_the_audit_log(self, app, client, monkeypatch):
"""Sign-up abuse leaves a trace or it is not happening, as far as
anyone can tell."""
recorded = []
from app.routes import auth as auth_module
monkeypatch.setattr(
auth_module,
'log_auth_event',
lambda event, **fields: recorded.append((event, fields)),
)
_issued_long_ago(client)
client.post(
'/auth/register',
data=dict(FORM, **{REGISTRATION_HONEYPOT_FIELD: 'spam'}),
follow_redirects=True,
)
assert recorded, 'the double was never called — the test would pass on nothing'
assert recorded[0][0] == 'account.registration_refused'
assert recorded[0][1]['reason'] == 'honeypot'
def test_the_reason_distinguishes_the_two_rules(self, app, client, monkeypatch):
recorded = []
from app.routes import auth as auth_module
monkeypatch.setattr(
auth_module,
'log_auth_event',
lambda event, **fields: recorded.append((event, fields)),
)
client.get('/auth/register')
client.post('/auth/register', data=dict(FORM), follow_redirects=True)
assert recorded and recorded[0][1]['reason'] == 'too-fast'
+9 -2
View File
@@ -19,6 +19,8 @@ These tests state the guarantee, so that removing the rollback or
reintroducing a mid-operation commit fails loudly. reintroducing a mid-operation commit fails loudly.
""" """
import time
import pytest import pytest
from app.models import User, UserGamertag from app.models import User, UserGamertag
@@ -78,9 +80,14 @@ class TestRegistrationIsOneOperation:
} }
def _submit(self, client, app, **overrides): def _submit(self, client, app, **overrides):
# Registration is screened for robots (SEC-AUTH-008): the form has to
# have been issued, and long enough ago. Backdated here rather than
# slept through, so the suite does not pay three seconds per call.
from app.routes.auth import MIN_REGISTRATION_SECONDS, REGISTRATION_ISSUED_KEY
with client.session_transaction() as session: with client.session_transaction() as session:
session['captcha_answer'] = 4 session[REGISTRATION_ISSUED_KEY] = time.time() - MIN_REGISTRATION_SECONDS - 1
payload = dict(self.FORM, captcha_answer='4') payload = dict(self.FORM)
payload.update(overrides) payload.update(overrides)
return client.post('/auth/register', data=payload, follow_redirects=True) return client.post('/auth/register', data=payload, follow_redirects=True)