From 8e3865f5576fed0fb824e052fa1e211ad18d1eb8 Mon Sep 17 00:00:00 2001 From: GGThed Date: Sat, 8 Aug 2026 15:59:40 -0400 Subject: [PATCH] chore(lint): elargir les regles ruff et rendre le format bloquant en CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QUA-002, seconde moitie. Le depot etant formate, l elargissement porte sur des defauts et non sur du brassage. Ajoute a la selection : B (bugbear), C4, RET, SIM, UP. Le lot entier n a produit que 24 signalements sur 76 fichiers -- le code etait plus propre que l audit ne le craignait. Neuf corriges automatiquement, quinze a la main. SIM108 est ignore : forcer un ternaire se lit moins bien que le if/else qu il remplace, au seul endroit ou il se declenche. isort (I) n est PAS active. Il reordonnerait les imports de 48 fichiers, soit une seconde passe de pur brassage juste apres le commit de formatage. A faire, mais seul. Deux vrais defauts trouves par les nouvelles regles - team_matches.edit_match faisait `except ValueError: pass` sur l heure de debut et l heure de fin, trois lignes sous un champ date qui, lui, signale et redirige. Une heure mal saisie etait donc acceptee par le formulaire, jetee, l ancienne valeur conservee -- et la page annoncait la reussite. Meme traitement que la date desormais. - backup.py levait BackupError depuis deux blocs `except` sans `from`, ce qui perdait la cause d origine dans la trace. Ainsi que : un `return` explicite dans force_https, `%`-formatage remplace dans log_auth_event (operations de chaine avant journalisation, pas des gabarits de logger -- la redaction n est pas affectee), une compréhension inutile, un `set(...)` en compréhension d ensemble, `open(..., 'r')`, une variable de boucle inutilisee, et `contextlib.suppress` dans conftest. CI : `ruff format --check` remplace le commentaire qui expliquait pourquoi il etait absent. 263 tests passent. Les deux nouveaux messages sont traduits ; attention, pybabel les avait apparies en `fuzzy` avec des entrees « date » existantes, et une entree fuzzy est ignoree a l execution. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 9 +- app/app.py | 9 +- app/discord_bot.py | 2 +- app/logging_config.py | 8 +- app/models/user_gamertag.py | 4 +- app/models/user_model/player.py | 2 +- app/routes/auth.py | 55 ++- app/routes/team_matches.py | 16 +- app/supporting_scripts/backup.py | 8 +- app/supporting_scripts/security_scan.py | 53 ++- app/translations/en/LC_MESSAGES/messages.mo | Bin 43726 -> 43862 bytes app/translations/en/LC_MESSAGES/messages.po | 356 ++++++++++---------- app/translations/fr/LC_MESSAGES/messages.mo | Bin 47910 -> 48065 bytes app/translations/fr/LC_MESSAGES/messages.po | 356 ++++++++++---------- pyproject.toml | 24 +- tests/conftest.py | 9 +- tests/test_i18n.py | 3 +- 17 files changed, 475 insertions(+), 439 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf4c24e..c6fce70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,10 +61,11 @@ jobs: - name: Run ruff linter run: ruff check . --output-format=github - # `ruff format --check` is deliberately absent for now: the codebase has - # never been formatted, so it would fail on 62 of 64 files for reasons - # unrelated to correctness. Reformatting in one isolated commit and then - # enforcing it here is tracked as QUA-002. + # Enabled now that the repository has been formatted once, in its own + # commit (QUA-002). Reaching this step before that would have failed on + # 72 of 76 files for reasons unrelated to correctness. + - name: Check formatting + run: ruff format --check . security-scan: name: Security Scan diff --git a/app/app.py b/app/app.py index c37103d..299b448 100644 --- a/app/app.py +++ b/app/app.py @@ -333,10 +333,17 @@ def create_app(config=None): Respects the X-Forwarded-Proto header from reverse proxies. Can be disabled via FORCE_HTTPS environment variable. + + Returns: + Response | None: A redirect, or None to let the request through. """ if not app.debug and app.config['FORCE_HTTPS']: - if not request.is_secure and request.headers.get('X-Forwarded-Proto') != 'https': + already_secure = ( + request.is_secure or request.headers.get('X-Forwarded-Proto') == 'https' + ) + if not already_secure: return redirect(request.url.replace('http://', 'https://'), code=301) + return None # ========================================================================= # Health Check Endpoint diff --git a/app/discord_bot.py b/app/discord_bot.py index a2823bc..46d5fb4 100644 --- a/app/discord_bot.py +++ b/app/discord_bot.py @@ -69,7 +69,7 @@ class TeamTryoutsBot(commands.Bot): """Load pending requests from the JSON file.""" try: if os.path.exists(PENDING_FILE): - with open(PENDING_FILE, 'r') as f: + with open(PENDING_FILE) as f: data = json.load(f) # Convert string keys back to int self.pending_requests = {int(k): v for k, v in data.items()} diff --git a/app/logging_config.py b/app/logging_config.py index 35004e9..003365c 100644 --- a/app/logging_config.py +++ b/app/logging_config.py @@ -211,10 +211,10 @@ def log_auth_event(event, **fields): """ from flask import has_request_context, request - parts = ['event=%s' % event] + parts = [f'event={event}'] if has_request_context(): - parts.append('ip=%s' % request.remote_addr) - parts.append('path=%s' % request.path) - parts.extend('%s=%s' % (key, value) for key, value in fields.items()) + parts.append(f'ip={request.remote_addr}') + parts.append(f'path={request.path}') + parts.extend(f'{key}={value}' for key, value in fields.items()) get_auth_logger().info(' '.join(parts)) diff --git a/app/models/user_gamertag.py b/app/models/user_gamertag.py index b6f194b..8c2c591 100644 --- a/app/models/user_gamertag.py +++ b/app/models/user_gamertag.py @@ -34,11 +34,11 @@ class UserGamertag(db.Model): platform.lower().replace(' ', '-') if platform else '', ) return url.format(platform_code=platform_code, username=encoded_gamertag) - elif '{platform}' in url and '{username}' in url: + if '{platform}' in url and '{username}' in url: return url.format( platform=platform.lower().replace(' ', '-') if platform else '', username=encoded_gamertag, ) - elif '{username}' in url: + if '{username}' in url: return url.format(username=encoded_gamertag) return url diff --git a/app/models/user_model/player.py b/app/models/user_model/player.py index 74c0067..30229c0 100644 --- a/app/models/user_model/player.py +++ b/app/models/user_model/player.py @@ -29,7 +29,7 @@ class Player(User): ) .all() ) - extra_ids = set(m.tryout_id for m in player_matches) + extra_ids = {m.tryout_id for m in player_matches} extra = ( Tryout.query.filter( Tryout.id.in_(extra_ids), diff --git a/app/routes/auth.py b/app/routes/auth.py index e6b790a..c75e273 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -237,40 +237,39 @@ def login(): next_page = None flash(_('Welcome back, %(username)s!', username=user.username), 'success') return redirect(next_page) if next_page else redirect(url_for('main.dashboard')) - else: - # One message for every failure. The old code said "N attempts - # remaining" to a real account and "check username and password" - # to an unknown one, which listed the club's accounts to anyone - # who asked (SEC-017). - if user: - user.failed_login_attempts += 1 + # One message for every failure. The old code said "N attempts + # remaining" to a real account and "check username and password" + # to an unknown one, which listed the club's accounts to anyone + # who asked (SEC-017). + if user: + user.failed_login_attempts += 1 + log_auth_event( + 'login.failure', + username=username, + user_id=user.id, + attempts=user.failed_login_attempts, + ) + if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS: + minutes = cooloff_minutes(user.failed_login_attempts) + user.locked_until = datetime.utcnow() + timedelta(minutes=minutes) log_auth_event( - 'login.failure', + 'account.throttled', username=username, user_id=user.id, + minutes=minutes, attempts=user.failed_login_attempts, ) - if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS: - minutes = cooloff_minutes(user.failed_login_attempts) - user.locked_until = datetime.utcnow() + timedelta(minutes=minutes) - log_auth_event( - 'account.throttled', - username=username, - user_id=user.id, - minutes=minutes, - attempts=user.failed_login_attempts, - ) - db.session.commit() - else: - log_auth_event('login.failure.unknown_user', username=username) + db.session.commit() + else: + log_auth_event('login.failure.unknown_user', username=username) - flash( - _( - 'Login unsuccessful. Please check your username and ' - 'password, or ask a president for help.' - ), - 'danger', - ) + flash( + _( + 'Login unsuccessful. Please check your username and ' + 'password, or ask a president for help.' + ), + 'danger', + ) return render_template('pages/login.html') diff --git a/app/routes/team_matches.py b/app/routes/team_matches.py index 0ef562e..fd65b27 100644 --- a/app/routes/team_matches.py +++ b/app/routes/team_matches.py @@ -98,7 +98,7 @@ def create_match(team_id): flash(_('You do not have permission to schedule matches for this team.'), 'danger') return redirect(url_for('team_matches.list_matches')) - team_players = [tp for tp in TeamPlayer.query.filter_by(org_team_id=team_id).all()] + team_players = TeamPlayer.query.filter_by(org_team_id=team_id).all() prefill_date = request.args.get('date', '') is_practice = request.args.get('type') == 'practice' default_title = 'Practice' if is_practice else f'Team Match — {team.name}' @@ -255,19 +255,25 @@ def edit_match(match_id): flash(_('Invalid date format.'), 'danger') return redirect(url_for('team_matches.edit_match', match_id=match_id)) + # Both used to `except ValueError: pass`, three lines below a date + # field that flashes and redirects. A mistyped time was therefore + # accepted by the form, discarded, and the old value kept — with the + # page reporting success. Same treatment as the date now. start_time_str = request.form.get('start_time') if start_time_str: try: team_match.start_time = datetime.strptime(start_time_str, '%H:%M').time() - except ValueError: - pass + except (ValueError, TypeError): + flash(_('Invalid start time format.'), 'danger') + return redirect(url_for('team_matches.edit_match', match_id=match_id)) end_time_str = request.form.get('end_time') if end_time_str: try: team_match.end_time = datetime.strptime(end_time_str, '%H:%M').time() - except ValueError: - pass + except (ValueError, TypeError): + flash(_('Invalid end time format.'), 'danger') + return redirect(url_for('team_matches.edit_match', match_id=match_id)) team_match.location = request.form.get('location', '') or None status = request.form.get('status') diff --git a/app/supporting_scripts/backup.py b/app/supporting_scripts/backup.py index bf89988..1126226 100644 --- a/app/supporting_scripts/backup.py +++ b/app/supporting_scripts/backup.py @@ -167,13 +167,13 @@ def backup_database(conn): text=True, timeout=900, ) - except FileNotFoundError: + except FileNotFoundError as err: raise BackupError( f'{PG_DUMP} not found. Install the PostgreSQL client tools, or set ' 'PG_DUMP to its full path.' - ) - except subprocess.TimeoutExpired: - raise BackupError('pg_dump timed out after 15 minutes.') + ) from err + except subprocess.TimeoutExpired as err: + raise BackupError('pg_dump timed out after 15 minutes.') from err if result.returncode != 0: raise BackupError(f'pg_dump failed: {result.stderr.strip()}') diff --git a/app/supporting_scripts/security_scan.py b/app/supporting_scripts/security_scan.py index b817b14..e783462 100644 --- a/app/supporting_scripts/security_scan.py +++ b/app/supporting_scripts/security_scan.py @@ -176,29 +176,27 @@ def check_dependencies(): if result.returncode == 0: print('[OK] No known vulnerabilities found') return True - else: - try: - data = json.loads(result.stdout) - # pip-audit's "dependencies" array lists EVERY dependency, each - # carrying a "vulns" list that is empty when the package is - # clean. Treating the array itself as the vulnerability list - # reported all ~45 installed packages as vulnerable on every - # run, which is why this check was pure noise. - affected = [dep for dep in data.get('dependencies', []) if dep.get('vulns')] - if affected: - for dep in affected: - ids = ', '.join(v.get('id', '?') for v in dep.get('vulns', [])) - print(f'[FAIL] {dep["name"]}=={dep["version"]}: {ids}') - return False - else: - print('[OK] No vulnerabilities found') - return True - except json.JSONDecodeError: - if result.stdout: - print(f'[INFO] {result.stdout.strip()}') - if result.stderr: - print(f'[WARN] {result.stderr.strip()}') - return True + try: + data = json.loads(result.stdout) + # pip-audit's "dependencies" array lists EVERY dependency, each + # carrying a "vulns" list that is empty when the package is + # clean. Treating the array itself as the vulnerability list + # reported all ~45 installed packages as vulnerable on every + # run, which is why this check was pure noise. + affected = [dep for dep in data.get('dependencies', []) if dep.get('vulns')] + if affected: + for dep in affected: + ids = ', '.join(v.get('id', '?') for v in dep.get('vulns', [])) + print(f'[FAIL] {dep["name"]}=={dep["version"]}: {ids}') + return False + print('[OK] No vulnerabilities found') + return True + except json.JSONDecodeError: + if result.stdout: + print(f'[INFO] {result.stdout.strip()}') + if result.stderr: + print(f'[WARN] {result.stderr.strip()}') + return True except FileNotFoundError: print('[SKIP] pip-audit not installed. Run: pip install pip-audit') return True @@ -223,7 +221,7 @@ def check_file_permissions(): gitignore_path = os.path.join(os.getcwd(), '.gitignore') if os.path.exists(gitignore_path): required_patterns = ['.env', 'instance/', '*.db', '*.log'] - with open(gitignore_path, 'r') as f: + with open(gitignore_path) as f: content = f.read() for pattern in required_patterns: @@ -245,7 +243,7 @@ def check_file_permissions(): # Check for leftover .pyc or __pycache__ pycache_count = 0 - for root, dirs, files in os.walk(os.getcwd()): + for _root, dirs, files in os.walk(os.getcwd()): if '__pycache__' in dirs: pycache_count += 1 for f in files: @@ -402,9 +400,8 @@ def main(): if failed == 0: print('\n[OK] All security checks passed!') return 0 - else: - print(f'\n[WARN] {failed} check(s) failed. Review the output above.') - return 1 + print(f'\n[WARN] {failed} check(s) failed. Review the output above.') + return 1 if __name__ == '__main__': diff --git a/app/translations/en/LC_MESSAGES/messages.mo b/app/translations/en/LC_MESSAGES/messages.mo index 7555c4e3ab01413520ea2781510e6838b9150cbb..87c678d579cab70e39682040c6e608f99fc2e429 100644 GIT binary patch delta 10228 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 XJ==L>!}Ho~-8lZCD*x{jof`fJq}4Ns delta 10162 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 DTlf*2 diff --git a/app/translations/en/LC_MESSAGES/messages.po b/app/translations/en/LC_MESSAGES/messages.po index fb448dd..4cd5b2d 100644 --- a/app/translations/en/LC_MESSAGES/messages.po +++ b/app/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: team-tryouts VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-08 15:07-0400\n" +"POT-Creation-Date: 2026-08-08 15:57-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: app/validators.py:43 +#: app/validators.py:49 msgid "" "Password must be at least 8 characters with uppercase, lowercase, and a " "number." @@ -27,72 +27,72 @@ msgstr "" "Password must be at least 8 characters with uppercase, lowercase, and a " "number." -#: app/validators.py:62 +#: app/validators.py:67 msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." -#: app/validators.py:81 +#: app/validators.py:86 msgid "Invalid Discord username format." msgstr "Invalid Discord username format." -#: app/validators.py:98 +#: app/validators.py:103 msgid "Discord User ID must be a 17-20 digit number." msgstr "Discord User ID must be a 17-20 digit number." -#: app/validators.py:116 +#: app/validators.py:121 msgid "Invalid phone number format." msgstr "Invalid phone number format." -#: app/validators.py:155 +#: app/validators.py:163 msgid "Username is required." msgstr "Username is required." -#: app/validators.py:159 +#: app/validators.py:167 msgid "Password is required." msgstr "Password is required." -#: app/validators.py:180 app/validators.py:251 +#: app/validators.py:189 app/validators.py:261 msgid "Username must be 3-80 characters." msgstr "Username must be 3-80 characters." -#: app/validators.py:186 +#: app/validators.py:195 msgid "Email must be 120 characters or less." msgstr "Email must be 120 characters or less." -#: app/validators.py:199 app/validators.py:266 app/validators.py:299 -#: app/validators.py:365 +#: app/validators.py:208 app/validators.py:276 app/validators.py:307 +#: app/validators.py:371 msgid "Full name is required." msgstr "Full name is required." -#: app/validators.py:234 +#: app/validators.py:243 msgid "Passwords do not match." msgstr "Passwords do not match." -#: app/validators.py:272 app/validators.py:309 +#: app/validators.py:280 app/validators.py:315 msgid "Invalid role selected." msgstr "Invalid role selected." -#: app/validators.py:409 +#: app/validators.py:416 msgid "Player must be selected." msgstr "Player must be selected." -#: app/validators.py:412 +#: app/validators.py:419 msgid "Notes must be 2000 characters or less." msgstr "Notes must be 2000 characters or less." -#: app/validators.py:431 +#: app/validators.py:438 msgid "Date must be in YYYY-MM-DD format." msgstr "Date must be in YYYY-MM-DD format." -#: app/validators.py:438 app/validators.py:473 +#: app/validators.py:443 app/validators.py:470 msgid "Start time must be in HH:MM format." msgstr "Start time must be in HH:MM format." -#: app/validators.py:445 +#: app/validators.py:447 msgid "End time must be in HH:MM format." msgstr "End time must be in HH:MM format." -#: app/validators.py:449 +#: app/validators.py:450 msgid "Points must be 2000 characters or less." msgstr "Points must be 2000 characters or less." @@ -100,22 +100,22 @@ msgstr "Points must be 2000 characters or less." msgid "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:314 app/routes/users.py:83 -#: app/routes/users.py:687 +#: app/routes/auth.py:186 app/routes/auth.py:322 app/routes/users.py:106 +#: app/routes/users.py:821 #, python-format msgid "%(field)s: %(msg)s" msgstr "%(field)s: %(msg)s" -#: app/routes/auth.py:204 +#: app/routes/auth.py:203 msgid "This account has been deactivated." msgstr "This account has been deactivated." -#: app/routes/auth.py:242 +#: app/routes/auth.py:238 #, python-format msgid "Welcome back, %(username)s!" msgstr "Welcome back, %(username)s!" -#: app/routes/auth.py:263 +#: app/routes/auth.py:268 msgid "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." @@ -123,27 +123,27 @@ msgstr "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." -#: app/routes/auth.py:295 +#: app/routes/auth.py:303 msgid "Incorrect CAPTCHA answer. Please try again." msgstr "Incorrect CAPTCHA answer. Please try again." -#: app/routes/auth.py:337 app/routes/users.py:370 +#: app/routes/auth.py:345 app/routes/users.py:436 msgid "Username already exists." msgstr "Username already exists." -#: app/routes/auth.py:349 app/routes/users.py:374 +#: app/routes/auth.py:357 app/routes/users.py:440 msgid "Email already registered." msgstr "Email already registered." -#: app/routes/auth.py:395 +#: app/routes/auth.py:404 msgid "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:421 +#: app/routes/auth.py:430 msgid "Discord OAuth2 is not configured." msgstr "Discord OAuth2 is not configured." -#: app/routes/auth.py:461 +#: app/routes/auth.py:471 msgid "" "Discord authorization could not be verified. Please start the connection " "again from this page." @@ -151,410 +151,418 @@ msgstr "" "Discord authorization could not be verified. Please start the connection " "again from this page." -#: app/routes/auth.py:469 +#: app/routes/auth.py:480 msgid "Discord authorization failed. No code received." msgstr "Discord authorization failed. No code received." -#: app/routes/auth.py:493 +#: app/routes/auth.py:504 msgid "Failed to connect to Discord. Please try again." msgstr "Failed to connect to Discord. Please try again." -#: app/routes/auth.py:497 +#: app/routes/auth.py:508 msgid "Failed to obtain Discord access token." msgstr "Failed to obtain Discord access token." -#: app/routes/auth.py:512 +#: app/routes/auth.py:523 msgid "Failed to fetch Discord user profile." msgstr "Failed to fetch Discord user profile." -#: app/routes/auth.py:559 +#: app/routes/auth.py:570 msgid "Discord account connected! Your profile has been pre-filled." msgstr "Discord account connected! Your profile has been pre-filled." -#: app/routes/auth.py:587 +#: app/routes/auth.py:598 msgid "You have been logged out." msgstr "You have been logged out." -#: app/routes/evaluations.py:41 +#: app/routes/evaluations.py:45 msgid "You do not have permission to view evaluations." msgstr "You do not have permission to view evaluations." -#: app/routes/evaluations.py:122 +#: app/routes/evaluations.py:135 msgid "You do not have permission to evaluate players." msgstr "You do not have permission to evaluate players." -#: app/routes/evaluations.py:127 app/routes/evaluations.py:220 +#: app/routes/evaluations.py:140 app/routes/evaluations.py:263 msgid "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:134 +#: app/routes/evaluations.py:151 msgid "Player is not registered for this tryout." msgstr "Player is not registered for this tryout." -#: app/routes/evaluations.py:139 +#: app/routes/evaluations.py:156 msgid "Can only evaluate players." msgstr "Can only evaluate players." -#: app/routes/evaluations.py:177 +#: app/routes/evaluations.py:208 msgid "Evaluation updated!" msgstr "Evaluation updated!" -#: app/routes/evaluations.py:190 +#: app/routes/evaluations.py:228 msgid "Evaluation submitted successfully!" msgstr "Evaluation submitted successfully!" -#: app/routes/evaluations.py:215 app/routes/teams.py:236 -#: app/routes/teams.py:267 app/routes/teams.py:298 app/routes/teams.py:323 -#: app/routes/teams.py:348 app/routes/teams.py:380 app/routes/tryouts.py:341 -#: app/routes/tryouts.py:357 app/routes/tryouts.py:376 -#: app/routes/tryouts.py:413 app/routes/tryouts.py:448 -#: app/routes/tryouts.py:467 +#: app/routes/evaluations.py:258 app/routes/teams.py:268 +#: app/routes/teams.py:309 app/routes/teams.py:350 app/routes/teams.py:375 +#: app/routes/teams.py:400 app/routes/teams.py:435 app/routes/tryouts.py:472 +#: app/routes/tryouts.py:488 app/routes/tryouts.py:508 +#: app/routes/tryouts.py:547 app/routes/tryouts.py:583 +#: app/routes/tryouts.py:602 msgid "Permission denied." msgstr "Permission denied." -#: app/routes/main.py:44 +#: app/routes/main.py:53 msgid "That language is not available." msgstr "That language is not available." -#: app/routes/matches.py:201 +#: app/routes/matches.py:245 msgid "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:205 app/routes/matches.py:336 +#: app/routes/matches.py:249 app/routes/matches.py:422 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." -#: app/routes/matches.py:224 +#: app/routes/matches.py:270 msgid "Start time is required. Please select a time slot." msgstr "Start time is required. Please select a time slot." -#: app/routes/matches.py:231 app/routes/matches.py:359 -#: app/routes/team_matches.py:123 app/routes/team_matches.py:206 +#: app/routes/matches.py:282 app/routes/matches.py:445 +#: app/routes/team_matches.py:151 app/routes/team_matches.py:255 msgid "Invalid date format." msgstr "Invalid date format." -#: app/routes/matches.py:246 app/routes/team_matches.py:140 +#: app/routes/matches.py:302 app/routes/team_matches.py:172 msgid "Invalid time format." msgstr "Invalid time format." -#: app/routes/matches.py:317 +#: app/routes/matches.py:398 msgid "Match scheduled successfully!" msgstr "Match scheduled successfully!" -#: app/routes/matches.py:332 app/routes/team_matches.py:193 +#: app/routes/matches.py:418 app/routes/team_matches.py:242 msgid "You do not have permission to edit this match." msgstr "You do not have permission to edit this match." -#: app/routes/matches.py:365 +#: app/routes/matches.py:456 msgid "Start time is required." msgstr "Start time is required." -#: app/routes/matches.py:461 app/routes/team_matches.py:229 +#: app/routes/matches.py:578 app/routes/team_matches.py:284 msgid "Match updated successfully!" msgstr "Match updated successfully!" -#: app/routes/matches.py:506 app/routes/team_matches.py:243 +#: app/routes/matches.py:631 app/routes/team_matches.py:299 msgid "You do not have permission to delete this match." msgstr "You do not have permission to delete this match." -#: app/routes/matches.py:509 +#: app/routes/matches.py:634 msgid "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:521 app/routes/team_matches.py:247 +#: app/routes/matches.py:647 app/routes/team_matches.py:303 msgid "Match deleted successfully." msgstr "Match deleted successfully." -#: app/routes/team_matches.py:81 +#: app/routes/team_matches.py:98 msgid "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:116 +#: app/routes/team_matches.py:140 msgid "Date is required." msgstr "Date is required." -#: app/routes/team_matches.py:179 +#: app/routes/team_matches.py:228 #, python-format msgid "Team match \"%(title)s\" scheduled successfully!" msgstr "Team match \"%(title)s\" scheduled successfully!" -#: app/routes/teams.py:28 +#: app/routes/team_matches.py:267 +msgid "Invalid start time format." +msgstr "Invalid start time format." + +#: app/routes/team_matches.py:275 +msgid "Invalid end time format." +msgstr "Invalid end time format." + +#: app/routes/teams.py:38 msgid "Use My Team(s) to view your teams." msgstr "Use My Team(s) to view your teams." -#: app/routes/teams.py:31 +#: app/routes/teams.py:41 msgid "You do not have permission to view teams." msgstr "You do not have permission to view teams." -#: app/routes/teams.py:48 +#: app/routes/teams.py:68 msgid "This page is for players." msgstr "This page is for players." -#: app/routes/teams.py:90 +#: app/routes/teams.py:121 msgid "You do not have permission to create teams." msgstr "You do not have permission to create teams." -#: app/routes/teams.py:98 app/routes/teams.py:143 +#: app/routes/teams.py:129 app/routes/teams.py:174 msgid "Team name is required." msgstr "Team name is required." -#: app/routes/teams.py:103 app/routes/teams.py:148 +#: app/routes/teams.py:134 app/routes/teams.py:179 #, python-format msgid "Team \"%(name)s\" already exists." msgstr "Team \"%(name)s\" already exists." -#: app/routes/teams.py:125 +#: app/routes/teams.py:156 #, python-format msgid "Team \"%(name)s\" created successfully!" msgstr "Team \"%(name)s\" created successfully!" -#: app/routes/teams.py:135 +#: app/routes/teams.py:166 msgid "You do not have permission to edit this team." msgstr "You do not have permission to edit this team." -#: app/routes/teams.py:186 +#: app/routes/teams.py:217 #, python-format msgid "Team \"%(name)s\" updated successfully!" msgstr "Team \"%(name)s\" updated successfully!" -#: app/routes/teams.py:195 +#: app/routes/teams.py:226 msgid "You do not have permission to delete teams." msgstr "You do not have permission to delete teams." -#: app/routes/teams.py:226 +#: app/routes/teams.py:258 #, python-format msgid "Team \"%(name)s\" deleted successfully." msgstr "Team \"%(name)s\" deleted successfully." -#: app/routes/teams.py:241 +#: app/routes/teams.py:273 msgid "Please select a coach." msgstr "Please select a coach." -#: app/routes/teams.py:246 +#: app/routes/teams.py:278 msgid "Only coaches can be assigned as coach." msgstr "Only coaches can be assigned as coach." -#: app/routes/teams.py:250 +#: app/routes/teams.py:284 #, python-format msgid "%(username)s is already a coach of %(name)s." msgstr "%(username)s is already a coach of %(name)s." -#: app/routes/teams.py:257 +#: app/routes/teams.py:297 #, python-format msgid "%(username)s added as coach of %(name)s." msgstr "%(username)s added as coach of %(name)s." -#: app/routes/teams.py:272 +#: app/routes/teams.py:314 msgid "Please select a manager." msgstr "Please select a manager." -#: app/routes/teams.py:277 +#: app/routes/teams.py:319 msgid "Only managers can be assigned as manager." msgstr "Only managers can be assigned as manager." -#: app/routes/teams.py:281 +#: app/routes/teams.py:325 #, python-format msgid "%(username)s is already a manager of %(name)s." msgstr "%(username)s is already a manager of %(name)s." -#: app/routes/teams.py:288 +#: app/routes/teams.py:338 #, python-format msgid "%(username)s added as manager of %(name)s." msgstr "%(username)s added as manager of %(name)s." -#: app/routes/teams.py:313 +#: app/routes/teams.py:365 #, python-format msgid "Coach removed from %(name)s." msgstr "Coach removed from %(name)s." -#: app/routes/teams.py:338 +#: app/routes/teams.py:390 #, python-format msgid "Manager removed from %(name)s." msgstr "Manager removed from %(name)s." -#: app/routes/teams.py:354 app/routes/tryouts.py:380 app/routes/tryouts.py:478 +#: app/routes/teams.py:406 app/routes/tryouts.py:512 app/routes/tryouts.py:613 msgid "Please select a player." msgstr "Please select a player." -#: app/routes/teams.py:359 +#: app/routes/teams.py:411 msgid "Can only assign players to teams." msgstr "Can only assign players to teams." -#: app/routes/teams.py:364 +#: app/routes/teams.py:417 #, python-format msgid "%(username)s is already on %(name)s." msgstr "%(username)s is already on %(name)s." -#: app/routes/teams.py:370 +#: app/routes/teams.py:425 #, python-format msgid "%(username)s added to %(name)s!" msgstr "%(username)s added to %(name)s!" -#: app/routes/teams.py:386 app/routes/teams.py:449 +#: app/routes/teams.py:442 app/routes/teams.py:515 #, python-format msgid "%(username)s is not on %(name)s." msgstr "%(username)s is not on %(name)s." -#: app/routes/teams.py:391 +#: app/routes/teams.py:450 #, python-format msgid "%(username)s removed from %(name)s." msgstr "%(username)s removed from %(name)s." -#: app/routes/teams.py:421 app/routes/teams.py:439 +#: app/routes/teams.py:486 app/routes/teams.py:504 msgid "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:429 +#: app/routes/teams.py:494 msgid "Team notes added successfully!" msgstr "Team notes added successfully!" -#: app/routes/teams.py:444 app/routes/users.py:1266 app/routes/users.py:1308 +#: app/routes/teams.py:509 app/routes/users.py:1530 app/routes/users.py:1573 msgid "Can only add notes for players." msgstr "Can only add notes for players." -#: app/routes/teams.py:457 +#: app/routes/teams.py:525 #, python-format msgid "Note added for %(username)s!" msgstr "Note added for %(username)s!" -#: app/routes/tryouts.py:43 +#: app/routes/tryouts.py:56 msgid "You do not have permission to create tryouts." msgstr "You do not have permission to create tryouts." -#: app/routes/tryouts.py:65 app/routes/tryouts.py:140 +#: app/routes/tryouts.py:82 app/routes/tryouts.py:189 msgid "Invalid start date format." msgstr "Invalid start date format." -#: app/routes/tryouts.py:74 app/routes/tryouts.py:149 +#: app/routes/tryouts.py:97 app/routes/tryouts.py:204 msgid "End date cannot be before start date." msgstr "End date cannot be before start date." -#: app/routes/tryouts.py:78 app/routes/tryouts.py:153 +#: app/routes/tryouts.py:107 app/routes/tryouts.py:214 msgid "Invalid end date format." msgstr "Invalid end date format." -#: app/routes/tryouts.py:100 +#: app/routes/tryouts.py:139 msgid "Tryout created successfully!" msgstr "Tryout created successfully!" -#: app/routes/tryouts.py:114 +#: app/routes/tryouts.py:159 msgid "You do not have permission to edit this tryout." msgstr "You do not have permission to edit this tryout." -#: app/routes/tryouts.py:118 +#: app/routes/tryouts.py:163 msgid "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:175 +#: app/routes/tryouts.py:242 msgid "Tryout updated successfully!" msgstr "Tryout updated successfully!" -#: app/routes/tryouts.py:207 +#: app/routes/tryouts.py:289 msgid "You do not have permission to view this tryout." msgstr "You do not have permission to view this tryout." -#: app/routes/tryouts.py:309 +#: app/routes/tryouts.py:439 msgid "Only players can register for tryouts." msgstr "Only players can register for tryouts." -#: app/routes/tryouts.py:313 +#: app/routes/tryouts.py:443 msgid "This tryout is not accepting registrations." msgstr "This tryout is not accepting registrations." -#: app/routes/tryouts.py:319 +#: app/routes/tryouts.py:450 msgid "You are already registered for this tryout." msgstr "You are already registered for this tryout." -#: app/routes/tryouts.py:325 app/routes/tryouts.py:397 +#: app/routes/tryouts.py:456 app/routes/tryouts.py:531 msgid "This tryout is full." msgstr "This tryout is full." -#: app/routes/tryouts.py:331 +#: app/routes/tryouts.py:462 msgid "Successfully registered for tryout!" msgstr "Successfully registered for tryout!" -#: app/routes/tryouts.py:347 +#: app/routes/tryouts.py:478 #, python-format msgid "Tryout status updated to %(new_status)s." msgstr "Tryout status updated to %(new_status)s." -#: app/routes/tryouts.py:366 +#: app/routes/tryouts.py:498 msgid "Registration status updated." msgstr "Registration status updated." -#: app/routes/tryouts.py:385 +#: app/routes/tryouts.py:517 msgid "Can only register players." msgstr "Can only register players." -#: app/routes/tryouts.py:391 +#: app/routes/tryouts.py:523 #, python-format msgid "%(username)s is already registered for this tryout." msgstr "%(username)s is already registered for this tryout." -#: app/routes/tryouts.py:403 +#: app/routes/tryouts.py:537 #, python-format msgid "%(username)s registered for tryout!" msgstr "%(username)s registered for tryout!" -#: app/routes/tryouts.py:438 +#: app/routes/tryouts.py:573 #, python-format msgid "%(username)s removed from tryout." msgstr "%(username)s removed from tryout." -#: app/routes/tryouts.py:456 +#: app/routes/tryouts.py:591 #, python-format msgid "Team \"%(team_name)s\" created!" msgstr "Team \"%(team_name)s\" created!" -#: app/routes/tryouts.py:485 +#: app/routes/tryouts.py:622 msgid "That player is not registered for this tryout." msgstr "That player is not registered for this tryout." -#: app/routes/tryouts.py:491 +#: app/routes/tryouts.py:628 msgid "Player is already on this team." msgstr "Player is already on this team." -#: app/routes/tryouts.py:496 +#: app/routes/tryouts.py:633 msgid "Player added to team!" msgstr "Player added to team!" -#: app/routes/tryouts.py:506 +#: app/routes/tryouts.py:643 msgid "You do not have permission to delete this tryout." msgstr "You do not have permission to delete this tryout." -#: app/routes/tryouts.py:541 +#: app/routes/tryouts.py:679 msgid "Tryout deleted successfully." msgstr "Tryout deleted successfully." -#: app/routes/users.py:61 +#: app/routes/users.py:83 msgid "No file selected." msgstr "No file selected." -#: app/routes/users.py:65 +#: app/routes/users.py:87 msgid "Only PDF files are allowed for contracts." msgstr "Only PDF files are allowed for contracts." -#: app/routes/users.py:70 +#: app/routes/users.py:92 msgid "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:153 +#: app/routes/users.py:181 msgid "Only the president can manage users." msgstr "Only the president can manage users." -#: app/routes/users.py:165 +#: app/routes/users.py:193 msgid "Only the president can edit users." msgstr "Only the president can edit users." -#: app/routes/users.py:202 +#: app/routes/users.py:234 msgid "Email already in use by another account." msgstr "Email already in use by another account." -#: app/routes/users.py:212 +#: app/routes/users.py:245 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." -#: app/routes/users.py:223 +#: app/routes/users.py:258 msgid "" "This is the last active president. Promote another account before " "changing this one." @@ -562,172 +570,172 @@ msgstr "" "This is the last active president. Promote another account before " "changing this one." -#: app/routes/users.py:285 +#: app/routes/users.py:337 #, python-format msgid "User %(username)s updated successfully!" msgstr "User %(username)s updated successfully!" -#: app/routes/users.py:300 +#: app/routes/users.py:358 msgid "Only the president can delete users." msgstr "Only the president can delete users." -#: app/routes/users.py:304 +#: app/routes/users.py:362 msgid "You cannot delete your own account." msgstr "You cannot delete your own account." -#: app/routes/users.py:341 +#: app/routes/users.py:405 #, python-format msgid "User %(deleted_username)s has been removed." msgstr "User %(deleted_username)s has been removed." -#: app/routes/users.py:350 +#: app/routes/users.py:416 msgid "Only the president can create users." msgstr "Only the president can create users." -#: app/routes/users.py:389 +#: app/routes/users.py:464 #, python-format msgid "User %(full_name)s created as %(role)s!" msgstr "User %(full_name)s created as %(role)s!" -#: app/routes/users.py:446 +#: app/routes/users.py:534 msgid "Username already taken." msgstr "Username already taken." -#: app/routes/users.py:452 +#: app/routes/users.py:544 msgid "Email already in use." msgstr "Email already in use." -#: app/routes/users.py:477 +#: app/routes/users.py:574 msgid "Profile updated successfully!" msgstr "Profile updated successfully!" -#: app/routes/users.py:675 +#: app/routes/users.py:809 msgid "Only presidents, managers, and coaches can upload contracts." msgstr "Only presidents, managers, and coaches can upload contracts." -#: app/routes/users.py:694 +#: app/routes/users.py:828 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." -#: app/routes/users.py:733 +#: app/routes/users.py:869 #, python-format msgid "Contract uploaded successfully for %(username)s!" msgstr "Contract uploaded successfully for %(username)s!" -#: app/routes/users.py:745 +#: app/routes/users.py:883 msgid "Only the player can upload their signed contract." msgstr "Only the player can upload their signed contract." -#: app/routes/users.py:762 +#: app/routes/users.py:902 msgid "Signed contract uploaded successfully!" msgstr "Signed contract uploaded successfully!" -#: app/routes/users.py:772 app/routes/users.py:783 +#: app/routes/users.py:912 app/routes/users.py:925 msgid "You do not have permission to download this contract." msgstr "You do not have permission to download this contract." -#: app/routes/users.py:786 +#: app/routes/users.py:928 msgid "No signed contract available." msgstr "No signed contract available." -#: app/routes/users.py:853 +#: app/routes/users.py:1034 msgid "Only players can request One on One sessions." msgstr "Only players can request One on One sessions." -#: app/routes/users.py:866 +#: 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:893 +#: 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:901 +#: app/routes/users.py:1090 msgid "Invalid date or time format." msgstr "Invalid date or time format." -#: app/routes/users.py:915 +#: 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:938 +#: 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:973 +#: 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:979 app/routes/users.py:1020 +#: 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:983 app/routes/users.py:1024 +#: 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:1005 +#: 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:1014 +#: 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:1051 +#: 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:1064 +#: app/routes/users.py:1286 msgid "This page is for players only." msgstr "This page is for players only." -#: app/routes/users.py:1095 +#: app/routes/users.py:1328 msgid "Only coaches can manage availability." msgstr "Only coaches can manage availability." -#: app/routes/users.py:1157 +#: app/routes/users.py:1394 msgid "Only coaches can access the notes dashboard." msgstr "Only coaches can access the notes dashboard." -#: app/routes/users.py:1221 +#: app/routes/users.py:1484 msgid "Only coaches can manage team notes." msgstr "Only coaches can manage team notes." -#: app/routes/users.py:1227 +#: app/routes/users.py:1490 msgid "You are not assigned to a team." msgstr "You are not assigned to a team." -#: app/routes/users.py:1240 +#: app/routes/users.py:1503 msgid "Team notes saved successfully!" msgstr "Team notes saved successfully!" -#: app/routes/users.py:1254 +#: app/routes/users.py:1518 msgid "Only coaches can manage personal notes." msgstr "Only coaches can manage personal notes." -#: app/routes/users.py:1261 app/routes/users.py:1303 app/routes/users.py:1353 -#: app/routes/users.py:1404 +#: app/routes/users.py:1525 app/routes/users.py:1568 app/routes/users.py:1619 +#: app/routes/users.py:1673 msgid "Player and content are required." msgstr "Player and content are required." -#: app/routes/users.py:1270 app/routes/users.py:1312 app/routes/users.py:1357 -#: app/routes/users.py:1408 +#: app/routes/users.py:1534 app/routes/users.py:1577 app/routes/users.py:1623 +#: app/routes/users.py:1677 msgid "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:1280 app/routes/users.py:1325 +#: app/routes/users.py:1544 app/routes/users.py:1590 #, python-format msgid "Note added for %(username)s." msgstr "Note added for %(username)s." -#: app/routes/users.py:1293 app/routes/users.py:1338 app/routes/users.py:1388 +#: app/routes/users.py:1558 app/routes/users.py:1604 app/routes/users.py:1657 msgid "Only coaches can add personal notes." msgstr "Only coaches can add personal notes." -#: app/routes/users.py:1368 app/routes/users.py:1419 +#: app/routes/users.py:1634 app/routes/users.py:1688 msgid "Note added successfully." msgstr "Note added successfully." diff --git a/app/translations/fr/LC_MESSAGES/messages.mo b/app/translations/fr/LC_MESSAGES/messages.mo index b6f33c874c10381718f160a02a4365bbd4caecc2..6936029d68087e94423bcb235dc58de5fb00caf4 100644 GIT binary patch delta 10222 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 t-@OVns?+e@)wj`\n" "Language: fr\n" @@ -19,7 +19,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: app/validators.py:43 +#: app/validators.py:49 msgid "" "Password must be at least 8 characters with uppercase, lowercase, and a " "number." @@ -27,74 +27,74 @@ msgstr "" "Le mot de passe doit compter au moins 8 caractères, dont une majuscule, " "une minuscule et un chiffre." -#: app/validators.py:62 +#: app/validators.py:67 msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgstr "" "Le nom d’utilisateur doit compter de 3 à 30 caractères (lettres, " "chiffres, tiret bas, trait d’union)." -#: app/validators.py:81 +#: app/validators.py:86 msgid "Invalid Discord username format." msgstr "Format de nom d’utilisateur Discord invalide." -#: app/validators.py:98 +#: app/validators.py:103 msgid "Discord User ID must be a 17-20 digit number." msgstr "L’identifiant Discord doit être un nombre de 17 à 20 chiffres." -#: app/validators.py:116 +#: app/validators.py:121 msgid "Invalid phone number format." msgstr "Format de numéro de téléphone invalide." -#: app/validators.py:155 +#: app/validators.py:163 msgid "Username is required." msgstr "Le nom d’utilisateur est obligatoire." -#: app/validators.py:159 +#: app/validators.py:167 msgid "Password is required." msgstr "Le mot de passe est obligatoire." -#: app/validators.py:180 app/validators.py:251 +#: app/validators.py:189 app/validators.py:261 msgid "Username must be 3-80 characters." msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères." -#: app/validators.py:186 +#: app/validators.py:195 msgid "Email must be 120 characters or less." msgstr "L’adresse courriel ne doit pas dépasser 120 caractères." -#: app/validators.py:199 app/validators.py:266 app/validators.py:299 -#: app/validators.py:365 +#: app/validators.py:208 app/validators.py:276 app/validators.py:307 +#: app/validators.py:371 msgid "Full name is required." msgstr "Le nom complet est obligatoire." -#: app/validators.py:234 +#: app/validators.py:243 msgid "Passwords do not match." msgstr "Les mots de passe ne concordent pas." -#: app/validators.py:272 app/validators.py:309 +#: app/validators.py:280 app/validators.py:315 msgid "Invalid role selected." msgstr "Rôle sélectionné invalide." -#: app/validators.py:409 +#: app/validators.py:416 msgid "Player must be selected." msgstr "Vous devez choisir un joueur." -#: app/validators.py:412 +#: app/validators.py:419 msgid "Notes must be 2000 characters or less." msgstr "Les notes ne doivent pas dépasser 2000 caractères." -#: app/validators.py:431 +#: app/validators.py:438 msgid "Date must be in YYYY-MM-DD format." msgstr "La date doit être au format AAAA-MM-JJ." -#: app/validators.py:438 app/validators.py:473 +#: app/validators.py:443 app/validators.py:470 msgid "Start time must be in HH:MM format." msgstr "L’heure de début doit être au format HH:MM." -#: app/validators.py:445 +#: app/validators.py:447 msgid "End time must be in HH:MM format." msgstr "L’heure de fin doit être au format HH:MM." -#: app/validators.py:449 +#: app/validators.py:450 msgid "Points must be 2000 characters or less." msgstr "Les points ne doivent pas dépasser 2000 caractères." @@ -102,22 +102,22 @@ msgstr "Les points ne doivent pas dépasser 2000 caractères." msgid "Day must be 0 (Monday) to 6 (Sunday)." msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)." -#: app/routes/auth.py:186 app/routes/auth.py:314 app/routes/users.py:83 -#: app/routes/users.py:687 +#: app/routes/auth.py:186 app/routes/auth.py:322 app/routes/users.py:106 +#: app/routes/users.py:821 #, python-format msgid "%(field)s: %(msg)s" msgstr "%(field)s : %(msg)s" -#: app/routes/auth.py:204 +#: app/routes/auth.py:203 msgid "This account has been deactivated." msgstr "Ce compte a été désactivé." -#: app/routes/auth.py:242 +#: app/routes/auth.py:238 #, python-format msgid "Welcome back, %(username)s!" msgstr "Bon retour, %(username)s !" -#: app/routes/auth.py:263 +#: app/routes/auth.py:268 msgid "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." @@ -125,27 +125,27 @@ msgstr "" "Échec de la connexion. Vérifiez le nom d’utilisateur et le mot de passe, " "ou demandez de l’aide à un président." -#: app/routes/auth.py:295 +#: app/routes/auth.py:303 msgid "Incorrect CAPTCHA answer. Please try again." msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer." -#: app/routes/auth.py:337 app/routes/users.py:370 +#: app/routes/auth.py:345 app/routes/users.py:436 msgid "Username already exists." msgstr "Ce nom d’utilisateur est déjà pris." -#: app/routes/auth.py:349 app/routes/users.py:374 +#: app/routes/auth.py:357 app/routes/users.py:440 msgid "Email already registered." msgstr "Cette adresse courriel est déjà enregistrée." -#: app/routes/auth.py:395 +#: app/routes/auth.py:404 msgid "Your account has been created! You can now log in." msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter." -#: app/routes/auth.py:421 +#: app/routes/auth.py:430 msgid "Discord OAuth2 is not configured." msgstr "La connexion Discord n’est pas configurée." -#: app/routes/auth.py:461 +#: app/routes/auth.py:471 msgid "" "Discord authorization could not be verified. Please start the connection " "again from this page." @@ -153,414 +153,422 @@ msgstr "" "L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion " "depuis cette page." -#: app/routes/auth.py:469 +#: app/routes/auth.py:480 msgid "Discord authorization failed. No code received." msgstr "L’autorisation Discord a échoué : aucun code reçu." -#: app/routes/auth.py:493 +#: app/routes/auth.py:504 msgid "Failed to connect to Discord. Please try again." msgstr "Impossible de joindre Discord. Veuillez réessayer." -#: app/routes/auth.py:497 +#: app/routes/auth.py:508 msgid "Failed to obtain Discord access token." msgstr "Impossible d’obtenir le jeton d’accès Discord." -#: app/routes/auth.py:512 +#: app/routes/auth.py:523 msgid "Failed to fetch Discord user profile." msgstr "Impossible de récupérer le profil Discord." -#: app/routes/auth.py:559 +#: app/routes/auth.py:570 msgid "Discord account connected! Your profile has been pre-filled." msgstr "Compte Discord connecté. Votre profil a été pré-rempli." -#: app/routes/auth.py:587 +#: app/routes/auth.py:598 msgid "You have been logged out." msgstr "Vous avez été déconnecté." -#: app/routes/evaluations.py:41 +#: app/routes/evaluations.py:45 msgid "You do not have permission to view evaluations." msgstr "Vous n’avez pas les droits pour consulter les évaluations." -#: app/routes/evaluations.py:122 +#: app/routes/evaluations.py:135 msgid "You do not have permission to evaluate players." msgstr "Vous n’avez pas les droits pour évaluer des joueurs." -#: app/routes/evaluations.py:127 app/routes/evaluations.py:220 +#: app/routes/evaluations.py:140 app/routes/evaluations.py:263 msgid "You do not have permission to evaluate players in this tryout." msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection." -#: app/routes/evaluations.py:134 +#: app/routes/evaluations.py:151 msgid "Player is not registered for this tryout." msgstr "Ce joueur n’est pas inscrit à cette sélection." -#: app/routes/evaluations.py:139 +#: app/routes/evaluations.py:156 msgid "Can only evaluate players." msgstr "Seuls des joueurs peuvent être évalués." -#: app/routes/evaluations.py:177 +#: app/routes/evaluations.py:208 msgid "Evaluation updated!" msgstr "Évaluation mise à jour." -#: app/routes/evaluations.py:190 +#: app/routes/evaluations.py:228 msgid "Evaluation submitted successfully!" msgstr "Évaluation enregistrée." -#: app/routes/evaluations.py:215 app/routes/teams.py:236 -#: app/routes/teams.py:267 app/routes/teams.py:298 app/routes/teams.py:323 -#: app/routes/teams.py:348 app/routes/teams.py:380 app/routes/tryouts.py:341 -#: app/routes/tryouts.py:357 app/routes/tryouts.py:376 -#: app/routes/tryouts.py:413 app/routes/tryouts.py:448 -#: app/routes/tryouts.py:467 +#: app/routes/evaluations.py:258 app/routes/teams.py:268 +#: app/routes/teams.py:309 app/routes/teams.py:350 app/routes/teams.py:375 +#: app/routes/teams.py:400 app/routes/teams.py:435 app/routes/tryouts.py:472 +#: app/routes/tryouts.py:488 app/routes/tryouts.py:508 +#: app/routes/tryouts.py:547 app/routes/tryouts.py:583 +#: app/routes/tryouts.py:602 msgid "Permission denied." msgstr "Accès refusé." -#: app/routes/main.py:44 +#: app/routes/main.py:53 msgid "That language is not available." msgstr "Cette langue n’est pas disponible." -#: app/routes/matches.py:201 +#: app/routes/matches.py:245 msgid "You do not have permission to schedule matches for this tryout." msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection." -#: app/routes/matches.py:205 app/routes/matches.py:336 +#: app/routes/matches.py:249 app/routes/matches.py:422 msgid "This tryout has ended. Matches can no longer be created or modified." msgstr "" "Cette sélection est terminée. Les matchs ne peuvent plus être créés ni " "modifiés." -#: app/routes/matches.py:224 +#: app/routes/matches.py:270 msgid "Start time is required. Please select a time slot." msgstr "L’heure de début est obligatoire. Choisissez une plage horaire." -#: app/routes/matches.py:231 app/routes/matches.py:359 -#: app/routes/team_matches.py:123 app/routes/team_matches.py:206 +#: app/routes/matches.py:282 app/routes/matches.py:445 +#: app/routes/team_matches.py:151 app/routes/team_matches.py:255 msgid "Invalid date format." msgstr "Format de date invalide." -#: app/routes/matches.py:246 app/routes/team_matches.py:140 +#: app/routes/matches.py:302 app/routes/team_matches.py:172 msgid "Invalid time format." msgstr "Format d’heure invalide." -#: app/routes/matches.py:317 +#: app/routes/matches.py:398 msgid "Match scheduled successfully!" msgstr "Match planifié." -#: app/routes/matches.py:332 app/routes/team_matches.py:193 +#: app/routes/matches.py:418 app/routes/team_matches.py:242 msgid "You do not have permission to edit this match." msgstr "Vous n’avez pas les droits pour modifier ce match." -#: app/routes/matches.py:365 +#: app/routes/matches.py:456 msgid "Start time is required." msgstr "L’heure de début est obligatoire." -#: app/routes/matches.py:461 app/routes/team_matches.py:229 +#: app/routes/matches.py:578 app/routes/team_matches.py:284 msgid "Match updated successfully!" msgstr "Match mis à jour." -#: app/routes/matches.py:506 app/routes/team_matches.py:243 +#: app/routes/matches.py:631 app/routes/team_matches.py:299 msgid "You do not have permission to delete this match." msgstr "Vous n’avez pas les droits pour supprimer ce match." -#: app/routes/matches.py:509 +#: app/routes/matches.py:634 msgid "This tryout has ended. Matches can no longer be deleted." msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés." -#: app/routes/matches.py:521 app/routes/team_matches.py:247 +#: app/routes/matches.py:647 app/routes/team_matches.py:303 msgid "Match deleted successfully." msgstr "Match supprimé." -#: app/routes/team_matches.py:81 +#: app/routes/team_matches.py:98 msgid "You do not have permission to schedule matches for this team." msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe." -#: app/routes/team_matches.py:116 +#: app/routes/team_matches.py:140 msgid "Date is required." msgstr "La date est obligatoire." -#: app/routes/team_matches.py:179 +#: app/routes/team_matches.py:228 #, python-format msgid "Team match \"%(title)s\" scheduled successfully!" msgstr "Match d’équipe « %(title)s » planifié." -#: app/routes/teams.py:28 +#: app/routes/team_matches.py:267 +msgid "Invalid start time format." +msgstr "Format d’heure de début invalide." + +#: app/routes/team_matches.py:275 +msgid "Invalid end time format." +msgstr "Format d’heure de fin invalide." + +#: app/routes/teams.py:38 msgid "Use My Team(s) to view your teams." msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes." -#: app/routes/teams.py:31 +#: app/routes/teams.py:41 msgid "You do not have permission to view teams." msgstr "Vous n’avez pas les droits pour consulter les équipes." -#: app/routes/teams.py:48 +#: app/routes/teams.py:68 msgid "This page is for players." msgstr "Cette page est réservée aux joueurs." -#: app/routes/teams.py:90 +#: app/routes/teams.py:121 msgid "You do not have permission to create teams." msgstr "Vous n’avez pas les droits pour créer une équipe." -#: app/routes/teams.py:98 app/routes/teams.py:143 +#: app/routes/teams.py:129 app/routes/teams.py:174 msgid "Team name is required." msgstr "Le nom de l’équipe est obligatoire." -#: app/routes/teams.py:103 app/routes/teams.py:148 +#: app/routes/teams.py:134 app/routes/teams.py:179 #, python-format msgid "Team \"%(name)s\" already exists." msgstr "L’équipe « %(name)s » existe déjà." -#: app/routes/teams.py:125 +#: app/routes/teams.py:156 #, python-format msgid "Team \"%(name)s\" created successfully!" msgstr "Équipe « %(name)s » créée." -#: app/routes/teams.py:135 +#: app/routes/teams.py:166 msgid "You do not have permission to edit this team." msgstr "Vous n’avez pas les droits pour modifier cette équipe." -#: app/routes/teams.py:186 +#: app/routes/teams.py:217 #, python-format msgid "Team \"%(name)s\" updated successfully!" msgstr "Équipe « %(name)s » mise à jour." -#: app/routes/teams.py:195 +#: app/routes/teams.py:226 msgid "You do not have permission to delete teams." msgstr "Vous n’avez pas les droits pour supprimer une équipe." -#: app/routes/teams.py:226 +#: app/routes/teams.py:258 #, python-format msgid "Team \"%(name)s\" deleted successfully." msgstr "Équipe « %(name)s » supprimée." -#: app/routes/teams.py:241 +#: app/routes/teams.py:273 msgid "Please select a coach." msgstr "Veuillez choisir un coach." -#: app/routes/teams.py:246 +#: app/routes/teams.py:278 msgid "Only coaches can be assigned as coach." msgstr "Seuls les coachs peuvent être assignés comme coach." -#: app/routes/teams.py:250 +#: app/routes/teams.py:284 #, python-format msgid "%(username)s is already a coach of %(name)s." msgstr "%(username)s est déjà coach de %(name)s." -#: app/routes/teams.py:257 +#: app/routes/teams.py:297 #, python-format msgid "%(username)s added as coach of %(name)s." msgstr "%(username)s a été ajouté comme coach de %(name)s." -#: app/routes/teams.py:272 +#: app/routes/teams.py:314 msgid "Please select a manager." msgstr "Veuillez choisir un gérant." -#: app/routes/teams.py:277 +#: app/routes/teams.py:319 msgid "Only managers can be assigned as manager." msgstr "Seuls les gérants peuvent être assignés comme gérant." -#: app/routes/teams.py:281 +#: app/routes/teams.py:325 #, python-format msgid "%(username)s is already a manager of %(name)s." msgstr "%(username)s est déjà gérant de %(name)s." -#: app/routes/teams.py:288 +#: app/routes/teams.py:338 #, python-format msgid "%(username)s added as manager of %(name)s." msgstr "%(username)s a été ajouté comme gérant de %(name)s." -#: app/routes/teams.py:313 +#: app/routes/teams.py:365 #, python-format msgid "Coach removed from %(name)s." msgstr "Coach retiré de %(name)s." -#: app/routes/teams.py:338 +#: app/routes/teams.py:390 #, python-format msgid "Manager removed from %(name)s." msgstr "Gérant retiré de %(name)s." -#: app/routes/teams.py:354 app/routes/tryouts.py:380 app/routes/tryouts.py:478 +#: app/routes/teams.py:406 app/routes/tryouts.py:512 app/routes/tryouts.py:613 msgid "Please select a player." msgstr "Veuillez choisir un joueur." -#: app/routes/teams.py:359 +#: app/routes/teams.py:411 msgid "Can only assign players to teams." msgstr "Seuls des joueurs peuvent être assignés à une équipe." -#: app/routes/teams.py:364 +#: app/routes/teams.py:417 #, python-format msgid "%(username)s is already on %(name)s." msgstr "%(username)s fait déjà partie de %(name)s." -#: app/routes/teams.py:370 +#: app/routes/teams.py:425 #, python-format msgid "%(username)s added to %(name)s!" msgstr "%(username)s a été ajouté à %(name)s." -#: app/routes/teams.py:386 app/routes/teams.py:449 +#: app/routes/teams.py:442 app/routes/teams.py:515 #, python-format msgid "%(username)s is not on %(name)s." msgstr "%(username)s ne fait pas partie de %(name)s." -#: app/routes/teams.py:391 +#: app/routes/teams.py:450 #, python-format msgid "%(username)s removed from %(name)s." msgstr "%(username)s a été retiré de %(name)s." -#: app/routes/teams.py:421 app/routes/teams.py:439 +#: app/routes/teams.py:486 app/routes/teams.py:504 msgid "You do not have permission to add notes to this team." msgstr "Vous n’avez pas les droits pour ajouter des notes à cette équipe." -#: app/routes/teams.py:429 +#: app/routes/teams.py:494 msgid "Team notes added successfully!" msgstr "Notes d’équipe ajoutées." -#: app/routes/teams.py:444 app/routes/users.py:1266 app/routes/users.py:1308 +#: app/routes/teams.py:509 app/routes/users.py:1530 app/routes/users.py:1573 msgid "Can only add notes for players." msgstr "Il n’est possible d’ajouter des notes que pour des joueurs." -#: app/routes/teams.py:457 +#: app/routes/teams.py:525 #, python-format msgid "Note added for %(username)s!" msgstr "Note ajoutée pour %(username)s." -#: app/routes/tryouts.py:43 +#: app/routes/tryouts.py:56 msgid "You do not have permission to create tryouts." msgstr "Vous n’avez pas les droits pour créer une sélection." -#: app/routes/tryouts.py:65 app/routes/tryouts.py:140 +#: app/routes/tryouts.py:82 app/routes/tryouts.py:189 msgid "Invalid start date format." msgstr "Format de date de début invalide." -#: app/routes/tryouts.py:74 app/routes/tryouts.py:149 +#: app/routes/tryouts.py:97 app/routes/tryouts.py:204 msgid "End date cannot be before start date." msgstr "La date de fin ne peut pas précéder la date de début." -#: app/routes/tryouts.py:78 app/routes/tryouts.py:153 +#: app/routes/tryouts.py:107 app/routes/tryouts.py:214 msgid "Invalid end date format." msgstr "Format de date de fin invalide." -#: app/routes/tryouts.py:100 +#: app/routes/tryouts.py:139 msgid "Tryout created successfully!" msgstr "Sélection créée." -#: app/routes/tryouts.py:114 +#: app/routes/tryouts.py:159 msgid "You do not have permission to edit this tryout." msgstr "Vous n’avez pas les droits pour modifier cette sélection." -#: app/routes/tryouts.py:118 +#: app/routes/tryouts.py:163 msgid "This tryout has ended and can no longer be modified." msgstr "Cette sélection est terminée et ne peut plus être modifiée." -#: app/routes/tryouts.py:175 +#: app/routes/tryouts.py:242 msgid "Tryout updated successfully!" msgstr "Sélection mise à jour." -#: app/routes/tryouts.py:207 +#: app/routes/tryouts.py:289 msgid "You do not have permission to view this tryout." msgstr "Vous n’avez pas les droits pour consulter cette sélection." -#: app/routes/tryouts.py:309 +#: app/routes/tryouts.py:439 msgid "Only players can register for tryouts." msgstr "Seuls les joueurs peuvent s’inscrire à une sélection." -#: app/routes/tryouts.py:313 +#: app/routes/tryouts.py:443 msgid "This tryout is not accepting registrations." msgstr "Cette sélection n’accepte pas d’inscriptions." -#: app/routes/tryouts.py:319 +#: app/routes/tryouts.py:450 msgid "You are already registered for this tryout." msgstr "Vous êtes déjà inscrit à cette sélection." -#: app/routes/tryouts.py:325 app/routes/tryouts.py:397 +#: app/routes/tryouts.py:456 app/routes/tryouts.py:531 msgid "This tryout is full." msgstr "Cette sélection est complète." -#: app/routes/tryouts.py:331 +#: app/routes/tryouts.py:462 msgid "Successfully registered for tryout!" msgstr "Inscription à la sélection réussie." -#: app/routes/tryouts.py:347 +#: app/routes/tryouts.py:478 #, python-format msgid "Tryout status updated to %(new_status)s." msgstr "Statut de la sélection mis à jour : %(new_status)s." -#: app/routes/tryouts.py:366 +#: app/routes/tryouts.py:498 msgid "Registration status updated." msgstr "Statut d’inscription mis à jour." -#: app/routes/tryouts.py:385 +#: app/routes/tryouts.py:517 msgid "Can only register players." msgstr "Seuls des joueurs peuvent être inscrits." -#: app/routes/tryouts.py:391 +#: app/routes/tryouts.py:523 #, python-format msgid "%(username)s is already registered for this tryout." msgstr "%(username)s est déjà inscrit à cette sélection." -#: app/routes/tryouts.py:403 +#: app/routes/tryouts.py:537 #, python-format msgid "%(username)s registered for tryout!" msgstr "%(username)s est inscrit à la sélection." -#: app/routes/tryouts.py:438 +#: app/routes/tryouts.py:573 #, python-format msgid "%(username)s removed from tryout." msgstr "%(username)s a été retiré de la sélection." -#: app/routes/tryouts.py:456 +#: app/routes/tryouts.py:591 #, python-format msgid "Team \"%(team_name)s\" created!" msgstr "Équipe « %(team_name)s » créée." -#: app/routes/tryouts.py:485 +#: app/routes/tryouts.py:622 msgid "That player is not registered for this tryout." msgstr "Ce joueur n’est pas inscrit à cette sélection." -#: app/routes/tryouts.py:491 +#: app/routes/tryouts.py:628 msgid "Player is already on this team." msgstr "Ce joueur est déjà dans cette équipe." -#: app/routes/tryouts.py:496 +#: app/routes/tryouts.py:633 msgid "Player added to team!" msgstr "Joueur ajouté à l’équipe." -#: app/routes/tryouts.py:506 +#: app/routes/tryouts.py:643 msgid "You do not have permission to delete this tryout." msgstr "Vous n’avez pas les droits pour supprimer cette sélection." -#: app/routes/tryouts.py:541 +#: app/routes/tryouts.py:679 msgid "Tryout deleted successfully." msgstr "Sélection supprimée." -#: app/routes/users.py:61 +#: app/routes/users.py:83 msgid "No file selected." msgstr "Aucun fichier sélectionné." -#: app/routes/users.py:65 +#: app/routes/users.py:87 msgid "Only PDF files are allowed for contracts." msgstr "Seuls les fichiers PDF sont acceptés pour les contrats." -#: app/routes/users.py:70 +#: app/routes/users.py:92 msgid "That file is not a PDF, whatever its name says." msgstr "Ce fichier n’est pas un PDF, quel que soit son nom." -#: app/routes/users.py:153 +#: app/routes/users.py:181 msgid "Only the president can manage users." msgstr "Seul le président peut gérer les utilisateurs." -#: app/routes/users.py:165 +#: app/routes/users.py:193 msgid "Only the president can edit users." msgstr "Seul le président peut modifier des utilisateurs." -#: app/routes/users.py:202 +#: app/routes/users.py:234 msgid "Email already in use by another account." msgstr "Cette adresse courriel est déjà utilisée par un autre compte." -#: app/routes/users.py:212 +#: app/routes/users.py:245 msgid "You cannot change your own role. Ask another president to do it." msgstr "" "Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre " "président de le faire." -#: app/routes/users.py:223 +#: app/routes/users.py:258 msgid "" "This is the last active president. Promote another account before " "changing this one." @@ -568,174 +576,174 @@ msgstr "" "C’est le dernier président actif. Promouvez un autre compte avant de " "modifier celui-ci." -#: app/routes/users.py:285 +#: app/routes/users.py:337 #, python-format msgid "User %(username)s updated successfully!" msgstr "Utilisateur %(username)s mis à jour." -#: app/routes/users.py:300 +#: app/routes/users.py:358 msgid "Only the president can delete users." msgstr "Seul le président peut supprimer des utilisateurs." -#: app/routes/users.py:304 +#: app/routes/users.py:362 msgid "You cannot delete your own account." msgstr "Vous ne pouvez pas supprimer votre propre compte." -#: app/routes/users.py:341 +#: app/routes/users.py:405 #, python-format msgid "User %(deleted_username)s has been removed." msgstr "L’utilisateur %(deleted_username)s a été supprimé." -#: app/routes/users.py:350 +#: app/routes/users.py:416 msgid "Only the president can create users." msgstr "Seul le président peut créer des utilisateurs." -#: app/routes/users.py:389 +#: app/routes/users.py:464 #, python-format msgid "User %(full_name)s created as %(role)s!" msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s." -#: app/routes/users.py:446 +#: app/routes/users.py:534 msgid "Username already taken." msgstr "Ce nom d’utilisateur est déjà pris." -#: app/routes/users.py:452 +#: app/routes/users.py:544 msgid "Email already in use." msgstr "Cette adresse courriel est déjà utilisée." -#: app/routes/users.py:477 +#: app/routes/users.py:574 msgid "Profile updated successfully!" msgstr "Profil mis à jour." -#: app/routes/users.py:675 +#: app/routes/users.py:809 msgid "Only presidents, managers, and coaches can upload contracts." msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat." -#: app/routes/users.py:694 +#: app/routes/users.py:828 msgid "You do not have permission to upload a contract for this player." msgstr "Vous n’avez pas les droits pour téléverser un contrat pour ce joueur." -#: app/routes/users.py:733 +#: app/routes/users.py:869 #, python-format msgid "Contract uploaded successfully for %(username)s!" msgstr "Contrat téléversé pour %(username)s." -#: app/routes/users.py:745 +#: app/routes/users.py:883 msgid "Only the player can upload their signed contract." msgstr "Seul le joueur peut téléverser son contrat signé." -#: app/routes/users.py:762 +#: app/routes/users.py:902 msgid "Signed contract uploaded successfully!" msgstr "Contrat signé téléversé." -#: app/routes/users.py:772 app/routes/users.py:783 +#: app/routes/users.py:912 app/routes/users.py:925 msgid "You do not have permission to download this contract." msgstr "Vous n’avez pas les droits pour télécharger ce contrat." -#: app/routes/users.py:786 +#: app/routes/users.py:928 msgid "No signed contract available." msgstr "Aucun contrat signé disponible." -#: app/routes/users.py:853 +#: app/routes/users.py:1034 msgid "Only players can request One on One sessions." msgstr "Seuls les joueurs peuvent demander une rencontre individuelle." -#: app/routes/users.py:866 +#: app/routes/users.py:1047 msgid "You do not have a coach assigned to your team." msgstr "Aucun coach n’est assigné à votre équipe." -#: app/routes/users.py:893 +#: 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:901 +#: app/routes/users.py:1090 msgid "Invalid date or time format." msgstr "Format de date ou d’heure invalide." -#: app/routes/users.py:915 +#: app/routes/users.py:1104 msgid "The requested time is not within the coach's availability." msgstr "L’horaire demandé ne correspond à aucune disponibilité du coach." -#: app/routes/users.py:938 +#: 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:973 +#: 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:979 app/routes/users.py:1020 +#: 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:983 app/routes/users.py:1024 +#: 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:1005 +#: 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:1014 +#: 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:1051 +#: 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:1064 +#: app/routes/users.py:1286 msgid "This page is for players only." msgstr "Cette page est réservée aux joueurs." -#: app/routes/users.py:1095 +#: app/routes/users.py:1328 msgid "Only coaches can manage availability." msgstr "Seuls les coachs peuvent gérer leurs disponibilités." -#: app/routes/users.py:1157 +#: app/routes/users.py:1394 msgid "Only coaches can access the notes dashboard." msgstr "Seuls les coachs ont accès au tableau des notes." -#: app/routes/users.py:1221 +#: app/routes/users.py:1484 msgid "Only coaches can manage team notes." msgstr "Seuls les coachs peuvent gérer les notes d’équipe." -#: app/routes/users.py:1227 +#: app/routes/users.py:1490 msgid "You are not assigned to a team." msgstr "Vous n’êtes assigné à aucune équipe." -#: app/routes/users.py:1240 +#: app/routes/users.py:1503 msgid "Team notes saved successfully!" msgstr "Notes d’équipe enregistrées." -#: app/routes/users.py:1254 +#: app/routes/users.py:1518 msgid "Only coaches can manage personal notes." msgstr "Seuls les coachs peuvent gérer les notes personnelles." -#: app/routes/users.py:1261 app/routes/users.py:1303 app/routes/users.py:1353 -#: app/routes/users.py:1404 +#: app/routes/users.py:1525 app/routes/users.py:1568 app/routes/users.py:1619 +#: app/routes/users.py:1673 msgid "Player and content are required." msgstr "Le joueur et le contenu sont obligatoires." -#: app/routes/users.py:1270 app/routes/users.py:1312 app/routes/users.py:1357 -#: app/routes/users.py:1408 +#: app/routes/users.py:1534 app/routes/users.py:1577 app/routes/users.py:1623 +#: app/routes/users.py:1677 msgid "You can only write notes about players you work with." msgstr "" "Vous ne pouvez écrire des notes que sur les joueurs avec qui vous " "travaillez." -#: app/routes/users.py:1280 app/routes/users.py:1325 +#: app/routes/users.py:1544 app/routes/users.py:1590 #, python-format msgid "Note added for %(username)s." msgstr "Note ajoutée pour %(username)s." -#: app/routes/users.py:1293 app/routes/users.py:1338 app/routes/users.py:1388 +#: app/routes/users.py:1558 app/routes/users.py:1604 app/routes/users.py:1657 msgid "Only coaches can add personal notes." msgstr "Seuls les coachs peuvent ajouter des notes personnelles." -#: app/routes/users.py:1368 app/routes/users.py:1419 +#: app/routes/users.py:1634 app/routes/users.py:1688 msgid "Note added successfully." msgstr "Note ajoutée." diff --git a/pyproject.toml b/pyproject.toml index 7a37bdc..a29f5f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,11 +32,25 @@ target-version = "py312" exclude = [".venv", "venv", "migrations", "docs"] [tool.ruff.lint] -# Starting from ruff's default rule set (pyflakes + a slice of pycodestyle). -# Widening it — bugbear, isort, pyupgrade — is deliberately deferred until -# the codebase has been formatted once, so that the first enforcement is -# about real defects rather than churn. Tracked as QUA-002. -select = ["E4", "E7", "E9", "F"] +# Widened once the repository had been formatted, so that this enforcement +# is about defects rather than churn (QUA-002). The whole set produced 24 +# findings across 76 files — the code was cleaner than the audit feared. +# +# E4/E7/E9, F pyflakes and the pycodestyle subset ruff enables by default +# B bugbear; B904 alone is worth it (lost exception causes) +# C4 comprehension shortcuts +# RET return-flow tidiness +# SIM obvious simplifications +# UP syntax available on the 3.12 this targets +# +# isort (I) is not enabled yet: it would reorder imports in 48 files, i.e. a +# second sweep of pure churn right after the formatting commit. Worth doing, +# worth doing on its own. +select = ["E4", "E7", "E9", "F", "B", "C4", "RET", "SIM", "UP"] + +# Forcing a ternary reads worse than the if/else it replaces in the one place +# it fires (evaluations.py, choosing a sort direction). +ignore = ["SIM108"] [tool.ruff.lint.per-file-ignores] # Intentional re-export facade: `from app.models import User, Tryout, ...` diff --git a/tests/conftest.py b/tests/conftest.py index 0f0ec73..3183d80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ here: no environment variable is required to run the suite, no database server is needed, and the Discord bot never starts. """ +import contextlib import os import sys import tempfile @@ -87,10 +88,8 @@ def app(): with application.app_context(): _db.session.remove() _db.engine.dispose() - try: + with contextlib.suppress(OSError): os.unlink(db_path) - except OSError: - pass @pytest.fixture @@ -106,10 +105,8 @@ def app_with_csrf(): with application.app_context(): _db.session.remove() _db.engine.dispose() - try: + with contextlib.suppress(OSError): os.unlink(db_path) - except OSError: - pass @pytest.fixture diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 024e8ec..58c92a0 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -182,7 +182,6 @@ class TestCatalogueIntegrity: @pytest.mark.parametrize('locale', SUPPORTED_LOCALES) def test_every_message_is_translated(self, locale): - import io import os from babel.messages.pofile import read_po @@ -195,7 +194,7 @@ class TestCatalogueIntegrity: 'LC_MESSAGES', 'messages.po', ) - with io.open(path, encoding='utf-8') as handle: + with open(path, encoding='utf-8') as handle: catalog = read_po(handle, locale=locale) untranslated = [m.id for m in catalog if m.id and not m.string]