fix(auth): garder l identite Discord du cote verifie

This commit is contained in:
GGThed
2026-08-16 23:36:30 -04:00
parent 437b229c82
commit 9647003c3f
16 changed files with 833 additions and 340 deletions
+93 -15
View File
@@ -21,10 +21,14 @@ from app.extensions import check_password, db, hash_password, limiter
from app.i18n import LOCALE_SESSION_KEY
from app.logging_config import log_auth_event
from app.models import ESPORT_GAMES, Player, User
from app.validators import LoginSchema, RegisterSchema
from app.validators import LoginSchema, RegisterSchema, validate_discord_user_id
#: Session key holding the pending OAuth2 anti-forgery token.
DISCORD_STATE_KEY = 'discord_oauth_state'
#: Whether the OAuth result should create a registration draft or relink the
#: signed-in account. Kept server-side and covered by the same signed session
#: as the anti-forgery state.
DISCORD_PURPOSE_KEY = 'discord_oauth_purpose'
# Failed-attempt tracking. The tally is kept for the audit trail and for the
# cool-off marker below; it no longer refuses a correct password (SEC-018).
@@ -354,6 +358,15 @@ def register():
form_data = dict(request.form)
form_data['games'] = request.form.getlist('games')
# Once Discord has authenticated the identity, neither its display
# name nor its snowflake is input data anymore. Remove any client
# copies before validation as well as before persistence: otherwise a
# forged, malformed hidden value can still make the verified flow fail.
discord_oauth = session.get('discord_oauth') or {}
if discord_oauth.get('id'):
form_data.pop('discord_username', None)
form_data.pop('discord_user_id', None)
refusal = check_registration_challenge(request.form)
if refusal is not None:
# Logged, because this is the only place abuse of the sign-up
@@ -380,8 +393,18 @@ def register():
full_name = validated['full_name']
phone = validated.get('phone')
selected_games = validated.get('games', [])
discord_username = validated.get('discord_username')
discord_user_id = validated.get('discord_user_id')
# The OAuth identity is server-side state. It used to be copied into
# hidden inputs and read back from request.form, which let anyone
# replace the verified Discord account before submitting (SEC-AUTH-005).
# A manual registration may still provide a display name, but never a
# Discord snowflake: that identifier is an authentication factor for
# bot reactions and must come from Discord itself.
discord_user_id = discord_oauth.get('id')
if discord_user_id:
discord_user_id = str(discord_user_id)
discord_username = (
discord_oauth.get('username') if discord_user_id else validated.get('discord_username')
)
league_os_profile = validated.get('league_os_profile')
if User.query.filter_by(username=username).first():
@@ -392,6 +415,13 @@ def register():
flash(_('Email already registered.'), 'danger')
return _rerender_registration(form_data)
# The database constraint belongs to DB-002, after production has
# been backed up and deduplicated. Refuse new duplicates now instead
# of leaving the critical impersonation path open until then.
if discord_user_id and User.query.filter_by(discord_user_id=discord_user_id).first():
flash(_('This Discord account is already linked to another account.'), 'danger')
return _rerender_registration(form_data)
hashed_password = hash_password(password)
user = Player(
username=username,
@@ -451,11 +481,15 @@ def discord_login():
Returns:
Response: Redirect to Discord authorization URL.
"""
purpose = 'profile' if current_user.is_authenticated else 'registration'
session[DISCORD_PURPOSE_KEY] = purpose
return_endpoint = 'users.edit_profile' if purpose == 'profile' else 'auth.register'
# DISCORD_REDIRECT_URI is checked too: quoting it when unset used to
# raise inside the query builder rather than report a configuration error.
if not DISCORD_CLIENT_ID or not DISCORD_REDIRECT_URI:
flash(_('Discord OAuth2 is not configured.'), 'danger')
return redirect(url_for('auth.register'))
return redirect(url_for(return_endpoint))
# Anti-forgery token, required by RFC 6749 §10.12. Without it, an
# attacker could have the victim's browser consume an authorization code
@@ -479,16 +513,24 @@ def discord_login():
def discord_callback():
"""Handle the OAuth2 callback from Discord.
Exchanges the authorization code for an access token, then fetches
the user's profile (/users/@me) and connections (/users/@me/connections).
Results are stored in the session and the user is redirected back to
the registration form where fields will be pre-filled.
Exchanges the authorization code for an access token, then fetches the
user's profile. During registration, connected game accounts are also
loaded into server-side draft state. For a signed-in profile relink, the
verified identity is written directly without passing through a form.
Returns:
Response: Redirect to registration page.
Response: Redirect to the registration form or profile editor.
"""
# The state is consumed whatever happens next: a token is single-use, and
# leaving it in the session would allow a replay.
purpose = session.pop(DISCORD_PURPOSE_KEY, 'registration')
if purpose == 'profile' and current_user.is_authenticated:
return_endpoint = 'users.edit_profile'
elif purpose == 'profile':
return_endpoint = 'auth.login'
else:
return_endpoint = 'auth.register'
expected_state = session.pop(DISCORD_STATE_KEY, None)
received_state = request.args.get('state', '')
@@ -500,12 +542,12 @@ def discord_callback():
),
'danger',
)
return redirect(url_for('auth.register'))
return redirect(url_for(return_endpoint))
code = request.args.get('code')
if not code:
flash(_('Discord authorization failed. No code received.'), 'danger')
return redirect(url_for('auth.register'))
return redirect(url_for(return_endpoint))
# Exchange the authorization code for an access token
token_data = {
@@ -529,11 +571,11 @@ def discord_callback():
access_token = token_json.get('access_token')
except requests.RequestException:
flash(_('Failed to connect to Discord. Please try again.'), 'danger')
return redirect(url_for('auth.register'))
return redirect(url_for(return_endpoint))
if not access_token:
flash(_('Failed to obtain Discord access token.'), 'danger')
return redirect(url_for('auth.register'))
return redirect(url_for(return_endpoint))
auth_headers = {'Authorization': f'Bearer {access_token}'}
@@ -548,7 +590,43 @@ def discord_callback():
user_data = user_response.json()
except requests.RequestException:
flash(_('Failed to fetch Discord user profile.'), 'danger')
return redirect(url_for('auth.register'))
return redirect(url_for(return_endpoint))
discord_user_id = user_data.get('id')
try:
if not discord_user_id:
raise ValidationError('missing Discord user id')
discord_user_id = str(discord_user_id)
validate_discord_user_id(discord_user_id)
except ValidationError:
flash(_('Failed to fetch Discord user profile.'), 'danger')
return redirect(url_for(return_endpoint))
if purpose == 'profile':
# If the session expired while Discord was open, do not turn a profile
# relink into registration state for an anonymous browser.
if not current_user.is_authenticated:
flash(_('Please log in to connect your Discord account.'), 'danger')
return redirect(url_for('auth.login'))
clash = User.query.filter(
User.discord_user_id == discord_user_id,
User.id != current_user.id,
).first()
if clash:
flash(_('This Discord account is already linked to another account.'), 'danger')
return redirect(url_for('users.edit_profile'))
current_user.discord_user_id = discord_user_id
current_user.discord_username = user_data.get('username') or None
db.session.commit()
log_auth_event(
'account.discord_linked',
username=current_user.username,
user_id=current_user.id,
)
flash(_('Discord account connected!'), 'success')
return redirect(url_for('users.edit_profile'))
# Fetch the user's connected gaming accounts
connections = []
@@ -587,7 +665,7 @@ def discord_callback():
# Store in session for the registration form to use
session['discord_oauth'] = {
'id': user_data.get('id'),
'id': discord_user_id,
'username': user_data.get('username'),
'avatar': user_data.get('avatar'),
'gamertag_suggestions': gamertag_suggestions,
+10
View File
@@ -111,6 +111,16 @@ def edit_user(user_id):
flash(_('Email already in use by another account.'), 'danger')
return _rerender()
discord_clash = None
if discord_user_id:
discord_clash = User.query.filter(
User.discord_user_id == discord_user_id,
User.id != user.id,
).first()
if discord_clash:
flash(_('This Discord account is already linked to another account.'), 'danger')
return _rerender()
role_changed = user.role != role
previous_role = user.role
-2
View File
@@ -76,7 +76,6 @@ def edit_profile():
phone = validated.get('phone')
selected_games = validated.get('games', [])
discord_username = validated.get('discord_username')
discord_user_id = validated.get('discord_user_id')
league_os_profile = validated.get('league_os_profile')
if username != current_user.username and User.query.filter_by(username=username).first():
@@ -105,7 +104,6 @@ def edit_profile():
current_user.phone = phone
current_user.games = ','.join(selected_games) if selected_games else None
current_user.discord_username = discord_username or None
current_user.discord_user_id = discord_user_id or None
current_user.league_os_profile = league_os_profile or None
update_user_gamertags(current_user, selected_games)
+61 -2
View File
@@ -29,10 +29,13 @@ Usage
# Also look for the seeded admin/password account (SEC-003)
python app/supporting_scripts/schema_report.py --check-seed-accounts
# Find Discord identities that must be reconciled before UNIQUE (SEC-012)
python app/supporting_scripts/schema_report.py --check-discord-identities
Exit codes
----------
0 the live schema matches the models
1 drift found — the report says what
1 drift or requested data risk found — the report says what
2 could not connect or read the catalogue
Reading the output
@@ -315,6 +318,42 @@ def find_seed_accounts(engine):
return results
def find_duplicate_discord_identities(engine):
"""Discord snowflakes claimed by more than one account (SEC-012).
New links are now refused in application code, but existing production
rows predate that guard. These groups must be reconciled before Alembic
can add the database-level UNIQUE constraint.
Returns:
list[tuple]: (discord_user_id, comma-separated usernames, count).
"""
from sqlalchemy import text
with engine.connect() as connection:
rows = connection.execute(
text(
'SELECT discord_user_id, COUNT(*) AS account_count '
'FROM users '
"WHERE discord_user_id IS NOT NULL AND discord_user_id <> '' "
'GROUP BY discord_user_id HAVING COUNT(*) > 1 '
'ORDER BY discord_user_id'
)
).fetchall()
duplicates = []
for discord_user_id, account_count in rows:
usernames = connection.execute(
text(
'SELECT username FROM users '
'WHERE discord_user_id = :discord_user_id ORDER BY username'
),
{'discord_user_id': discord_user_id},
).scalars()
duplicates.append((discord_user_id, ', '.join(usernames), account_count))
return duplicates
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
parser.add_argument(
@@ -327,6 +366,11 @@ def main(argv=None):
action='store_true',
help='Also look for the admin/password account seeded by clear_db.py (SEC-003).',
)
parser.add_argument(
'--check-discord-identities',
action='store_true',
help='Find duplicate Discord IDs that block the SEC-012 UNIQUE constraint.',
)
args = parser.parse_args(argv)
if not args.url:
@@ -375,9 +419,24 @@ def main(argv=None):
)
print(f' {username} ({role}): {verdict}')
duplicate_discord_identities = []
if args.check_discord_identities:
print('\n' + '=' * 78)
print('Duplicate Discord identities (SEC-012)')
print('=' * 78)
try:
duplicate_discord_identities = find_duplicate_discord_identities(engine)
except SQLAlchemyError as exc:
print(f'Could not check: {exc}')
else:
if not duplicate_discord_identities:
print('No Discord identity is shared by multiple accounts.')
for discord_user_id, usernames, account_count in duplicate_discord_identities:
print(f' {discord_user_id}: {account_count} accounts ({usernames})')
blocking = sum(1 for f in findings if f.severity == BLOCKING)
print(f'\n{len(findings)} finding(s), {blocking} blocking.')
return 1 if findings else 0
return 1 if findings or duplicate_discord_identities else 0
if __name__ == '__main__': # pragma: no cover
+5 -6
View File
@@ -79,14 +79,13 @@
</div>
<div class="form-row">
<div class="form-group col-6">
<div class="form-group col-12">
<label for="discord_username"><i class="fab fa-discord"></i> {{ _('Discord Username') }}</label>
<input type="text" id="discord_username" name="discord_username" value="{{ user.discord_username or '' }}" placeholder="{{ _('e.g. Name#1234') }}">
</div>
<div class="form-group col-6">
<label for="discord_user_id"><i class="fab fa-discord"></i> Discord User ID <small>{{ _('(for DMs)') }}</small></label>
<input type="text" id="discord_user_id" name="discord_user_id" value="{{ user.discord_user_id or '' }}" placeholder="{{ _('Numeric ID (e.g. 123456789012345678)') }}">
<small class="text-muted">{{ _('Enable Developer Mode in Discord → Right-click profile → Copy ID') }}</small>
<a href="{{ url_for('auth.discord_login') }}" class="btn btn-secondary mt-2">
<i class="fab fa-discord"></i>
{% if user.discord_user_id %}{{ _('Reconnect') }}{% else %}{{ _('Connect Discord Account') }}{% endif %}
</a>
</div>
</div>
<div class="form-row">
-2
View File
@@ -57,8 +57,6 @@
<i class="fas fa-sync-alt"></i> {{ _('Reconnect') }}
</a>
</div>
<input type="hidden" name="discord_username" value="{{ discord_data.username }}">
<input type="hidden" name="discord_user_id" value="{{ discord_data.id }}">
<small class="form-text text-success">
<i class="fas fa-check-circle"></i> {{ _('Discord connected. Game connections have been used to pre-fill your profile below.') }}
</small>
Binary file not shown.
+164 -147
View File
@@ -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-11 20:39-0400\n"
"POT-Creation-Date: 2026-08-16 23:22-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@@ -23,8 +23,8 @@ msgstr ""
msgid "Please log in to access this page."
msgstr "Please log in to access this page."
#: app/forms.py:37 app/routes/auth.py:224 app/routes/auth.py:374
#: app/routes/users/contracts.py:96
#: app/forms.py:37 app/routes/auth.py:228 app/routes/auth.py:387
#: app/routes/users/contracts.py:98
#, python-format
msgid "%(field)s: %(msg)s"
msgstr "%(field)s: %(msg)s"
@@ -61,7 +61,7 @@ msgstr "Username is required."
msgid "Password is required."
msgstr "Password is required."
#: app/validators.py:227 app/validators.py:299
#: app/validators.py:227 app/validators.py:294
msgid "Username must be 3-80 characters."
msgstr "Username must be 3-80 characters."
@@ -69,169 +69,169 @@ msgstr "Username must be 3-80 characters."
msgid "Email must be 120 characters or less."
msgstr "Email must be 120 characters or less."
#: app/validators.py:246 app/validators.py:314 app/validators.py:345
#: app/validators.py:409
#: app/validators.py:246 app/validators.py:309 app/validators.py:340
#: app/validators.py:403
msgid "Full name is required."
msgstr "Full name is required."
#: app/validators.py:281
#: app/validators.py:276
msgid "Passwords do not match."
msgstr "Passwords do not match."
#: app/validators.py:318 app/validators.py:353
#: app/validators.py:313 app/validators.py:348
msgid "Invalid role selected."
msgstr "Invalid role selected."
#: app/validators.py:454
#: app/validators.py:443
msgid "Player must be selected."
msgstr "Player must be selected."
#: app/validators.py:457
#: app/validators.py:446
msgid "Notes must be 2000 characters or less."
msgstr "Notes must be 2000 characters or less."
#: app/validators.py:494 app/validators.py:853
#: app/validators.py:483 app/validators.py:854
msgid "Invalid coach selection."
msgstr "Invalid coach selection."
#: app/validators.py:500 app/validators.py:859
#: app/validators.py:489 app/validators.py:860
msgid "Invalid manager selection."
msgstr "Invalid manager selection."
#: app/validators.py:511
#: app/validators.py:507
msgid "Invalid player selection."
msgstr "Invalid player selection."
#: app/validators.py:515
#: app/validators.py:516
msgid "Unknown roster status."
msgstr "Unknown roster status."
#: app/validators.py:538
#: app/validators.py:539
msgid "Date must be in YYYY-MM-DD format."
msgstr "Date must be in YYYY-MM-DD format."
#: app/validators.py:539
#: app/validators.py:540
msgid "A date is required."
msgstr "A date is required."
#: app/validators.py:545 app/validators.py:610
#: app/validators.py:546 app/validators.py:611
msgid "Start time must be in HH:MM format."
msgstr "Start time must be in HH:MM format."
#: app/validators.py:546 app/validators.py:611
#: app/validators.py:547 app/validators.py:612
msgid "A start time is required."
msgstr "A start time is required."
#: app/validators.py:552
#: app/validators.py:553
msgid "End time must be in HH:MM format."
msgstr "End time must be in HH:MM format."
#: app/validators.py:553
#: app/validators.py:554
msgid "An end time is required."
msgstr "An end time is required."
#: app/validators.py:557
#: app/validators.py:558
msgid "Points must be 2000 characters or less."
msgstr "Points must be 2000 characters or less."
#: app/validators.py:572
#: app/validators.py:573
msgid "End time must be after start time."
msgstr "End time must be after start time."
#: app/validators.py:601 app/validators.py:603
#: app/validators.py:602 app/validators.py:604
msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
#: app/validators.py:604
#: app/validators.py:605
msgid "A day is required."
msgstr "A day is required."
#: app/validators.py:639
#: app/validators.py:640
msgid "Player selection is malformed."
msgstr "Player selection is malformed."
#: app/validators.py:665 app/validators.py:775
#: app/validators.py:666 app/validators.py:776
msgid "A title is required."
msgstr "A title is required."
#: app/validators.py:674
#: app/validators.py:675
msgid "Invalid date format."
msgstr "Invalid date format."
#: app/validators.py:679 app/validators.py:686
#: app/validators.py:680 app/validators.py:687
msgid "Invalid time format."
msgstr "Invalid time format."
#: app/validators.py:680
#: app/validators.py:681
msgid "Start time is required. Please select a time slot."
msgstr "Start time is required. Please select a time slot."
#: app/validators.py:694
#: app/validators.py:695
msgid "Unknown match status."
msgstr "Unknown match status."
#: app/validators.py:710
#: app/validators.py:711
msgid "The end time must come after the start time."
msgstr "The end time must come after the start time."
#: app/validators.py:724
#: app/validators.py:725
msgid "Unknown match type."
msgstr "Unknown match type."
#: app/validators.py:736
#: app/validators.py:737
msgid "A team cannot play against itself."
msgstr "A team cannot play against itself."
#: app/validators.py:784
#: app/validators.py:785
msgid "Unknown game."
msgstr "Unknown game."
#: app/validators.py:789
#: app/validators.py:790
msgid "Invalid start date format."
msgstr "Invalid start date format."
#: app/validators.py:790
#: app/validators.py:791
msgid "A start date is required."
msgstr "A start date is required."
#: app/validators.py:796
#: app/validators.py:797
msgid "Invalid end date format."
msgstr "Invalid end date format."
#: app/validators.py:804
#: app/validators.py:805
msgid "A tryout must allow at least one player."
msgstr "A tryout must allow at least one player."
#: app/validators.py:807
#: app/validators.py:808
msgid "The player limit must be a whole number."
msgstr "The player limit must be a whole number."
#: app/validators.py:819
#: app/validators.py:820
msgid "End date cannot be before start date."
msgstr "End date cannot be before start date."
#: app/validators.py:846 app/validators.py:847
#: app/validators.py:847 app/validators.py:848
msgid "Team name is required."
msgstr "Team name is required."
#: app/validators.py:871
#: app/validators.py:872
msgid "Scores run from 1 to 10."
msgstr "Scores run from 1 to 10."
#: app/validators.py:872
#: app/validators.py:873
msgid "A score must be a whole number from 1 to 10."
msgstr "A score must be a whole number from 1 to 10."
#: app/routes/auth.py:241
#: app/routes/auth.py:245
msgid "This account has been deactivated."
msgstr "This account has been deactivated."
#: app/routes/auth.py:276
#: app/routes/auth.py:280
#, python-format
msgid "Welcome back, %(username)s!"
msgstr "Welcome back, %(username)s!"
#: app/routes/auth.py:306
#: app/routes/auth.py:310
msgid ""
"Login unsuccessful. Please check your username and password, or ask a "
"president for help."
@@ -239,27 +239,32 @@ msgstr ""
"Login unsuccessful. Please check your username and password, or ask a "
"president for help."
#: app/routes/auth.py:363
#: app/routes/auth.py:376
msgid "Your registration could not be processed. Please try again."
msgstr "Your registration could not be processed. Please try again."
#: app/routes/auth.py:388 app/routes/users/accounts.py:329
#: app/routes/auth.py:411 app/routes/users/accounts.py:339
msgid "Username already exists."
msgstr "Username already exists."
#: app/routes/auth.py:392 app/routes/users/accounts.py:333
#: app/routes/auth.py:415 app/routes/users/accounts.py:343
msgid "Email already registered."
msgstr "Email already registered."
#: app/routes/auth.py:436
#: app/routes/auth.py:422 app/routes/auth.py:617
#: app/routes/users/accounts.py:121
msgid "This Discord account is already linked to another account."
msgstr "This Discord account is already linked to another account."
#: app/routes/auth.py:466
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:457
#: app/routes/auth.py:491
msgid "Discord OAuth2 is not configured."
msgstr "Discord OAuth2 is not configured."
#: app/routes/auth.py:498
#: app/routes/auth.py:540
msgid ""
"Discord authorization could not be verified. Please start the connection "
"again from this page."
@@ -267,27 +272,35 @@ msgstr ""
"Discord authorization could not be verified. Please start the connection "
"again from this page."
#: app/routes/auth.py:507
#: app/routes/auth.py:549
msgid "Discord authorization failed. No code received."
msgstr "Discord authorization failed. No code received."
#: app/routes/auth.py:531
#: app/routes/auth.py:573
msgid "Failed to connect to Discord. Please try again."
msgstr "Failed to connect to Discord. Please try again."
#: app/routes/auth.py:535
#: app/routes/auth.py:577
msgid "Failed to obtain Discord access token."
msgstr "Failed to obtain Discord access token."
#: app/routes/auth.py:550
#: app/routes/auth.py:592 app/routes/auth.py:602
msgid "Failed to fetch Discord user profile."
msgstr "Failed to fetch Discord user profile."
#: app/routes/auth.py:597
#: app/routes/auth.py:609
msgid "Please log in to connect your Discord account."
msgstr "Please log in to connect your Discord account."
#: app/routes/auth.py:628
msgid "Discord account connected!"
msgstr "Discord account connected!"
#: app/routes/auth.py:675
msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Discord account connected! Your profile has been pre-filled."
#: app/routes/auth.py:625
#: app/routes/auth.py:703
msgid "You have been logged out."
msgstr "You have been logged out."
@@ -323,8 +336,8 @@ msgstr "Evaluation updated!"
#: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455
#: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:444
#: app/routes/tryouts.py:460 app/routes/tryouts.py:480
#: app/routes/tryouts.py:519 app/routes/tryouts.py:555
#: app/routes/tryouts.py:574
#: app/routes/tryouts.py:527 app/routes/tryouts.py:563
#: app/routes/tryouts.py:582
msgid "Permission denied."
msgstr "Permission denied."
@@ -463,7 +476,7 @@ msgstr "Coach removed from %(name)s."
msgid "Manager removed from %(name)s."
msgstr "Manager removed from %(name)s."
#: app/routes/teams.py:490 app/routes/tryouts.py:484 app/routes/tryouts.py:585
#: app/routes/teams.py:490 app/routes/tryouts.py:489 app/routes/tryouts.py:593
msgid "Please select a player."
msgstr "Please select a player."
@@ -545,7 +558,7 @@ msgstr "This tryout is not accepting registrations."
msgid "You are already registered for this tryout."
msgstr "You are already registered for this tryout."
#: app/routes/tryouts.py:428 app/routes/tryouts.py:503
#: app/routes/tryouts.py:428 app/routes/tryouts.py:511
msgid "This tryout is full."
msgstr "This tryout is full."
@@ -562,47 +575,47 @@ msgstr "Tryout status updated to %(new_status)s."
msgid "Registration status updated."
msgstr "Registration status updated."
#: app/routes/tryouts.py:489
#: app/routes/tryouts.py:497
msgid "Can only register players."
msgstr "Can only register players."
#: app/routes/tryouts.py:495
#: app/routes/tryouts.py:503
#, python-format
msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s is already registered for this tryout."
#: app/routes/tryouts.py:509
#: app/routes/tryouts.py:517
#, python-format
msgid "%(username)s registered for tryout!"
msgstr "%(username)s registered for tryout!"
#: app/routes/tryouts.py:545
#: app/routes/tryouts.py:553
#, python-format
msgid "%(username)s removed from tryout."
msgstr "%(username)s removed from tryout."
#: app/routes/tryouts.py:563
#: app/routes/tryouts.py:571
#, python-format
msgid "Team \"%(team_name)s\" created!"
msgstr "Team \"%(team_name)s\" created!"
#: app/routes/tryouts.py:594
#: app/routes/tryouts.py:602
msgid "That player is not registered for this tryout."
msgstr "That player is not registered for this tryout."
#: app/routes/tryouts.py:600
#: app/routes/tryouts.py:608
msgid "Player is already on this team."
msgstr "Player is already on this team."
#: app/routes/tryouts.py:605
#: app/routes/tryouts.py:613
msgid "Player added to team!"
msgstr "Player added to team!"
#: app/routes/tryouts.py:615
#: app/routes/tryouts.py:623
msgid "You do not have permission to delete this tryout."
msgstr "You do not have permission to delete this tryout."
#: app/routes/tryouts.py:651
#: app/routes/tryouts.py:659
msgid "Tryout deleted successfully."
msgstr "Tryout deleted successfully."
@@ -630,11 +643,11 @@ msgstr "Only the president can edit users."
msgid "Email already in use by another account."
msgstr "Email already in use by another account."
#: app/routes/users/accounts.py:122
#: app/routes/users/accounts.py:132
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/accounts.py:135
#: app/routes/users/accounts.py:145
msgid ""
"This is the last active president. Promote another account before "
"changing this one."
@@ -642,29 +655,29 @@ msgstr ""
"This is the last active president. Promote another account before "
"changing this one."
#: app/routes/users/accounts.py:214
#: app/routes/users/accounts.py:224
#, python-format
msgid "User %(username)s updated successfully!"
msgstr "User %(username)s updated successfully!"
#: app/routes/users/accounts.py:235
#: app/routes/users/accounts.py:245
msgid "Only the president can delete users."
msgstr "Only the president can delete users."
#: app/routes/users/accounts.py:239
#: app/routes/users/accounts.py:249
msgid "You cannot delete your own account."
msgstr "You cannot delete your own account."
#: app/routes/users/accounts.py:298
#: app/routes/users/accounts.py:308
#, python-format
msgid "User %(deleted_username)s has been removed."
msgstr "User %(deleted_username)s has been removed."
#: app/routes/users/accounts.py:309
#: app/routes/users/accounts.py:319
msgid "Only the president can create users."
msgstr "Only the president can create users."
#: app/routes/users/accounts.py:357
#: app/routes/users/accounts.py:367
#, python-format
msgid "User %(full_name)s created as %(role)s!"
msgstr "User %(full_name)s created as %(role)s!"
@@ -673,32 +686,32 @@ msgstr "User %(full_name)s created as %(role)s!"
msgid "Only coaches can manage availability."
msgstr "Only coaches can manage availability."
#: app/routes/users/contracts.py:84
#: app/routes/users/contracts.py:86
msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Only presidents, managers, and coaches can upload contracts."
#: app/routes/users/contracts.py:103
#: app/routes/users/contracts.py:105
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/contracts.py:143
#: app/routes/users/contracts.py:145
#, python-format
msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contract uploaded successfully for %(username)s!"
#: app/routes/users/contracts.py:157
#: app/routes/users/contracts.py:159
msgid "Only the player can upload their signed contract."
msgstr "Only the player can upload their signed contract."
#: app/routes/users/contracts.py:175
#: app/routes/users/contracts.py:177
msgid "Signed contract uploaded successfully!"
msgstr "Signed contract uploaded successfully!"
#: app/routes/users/contracts.py:185 app/routes/users/contracts.py:200
#: app/routes/users/contracts.py:187 app/routes/users/contracts.py:202
msgid "You do not have permission to download this contract."
msgstr "You do not have permission to download this contract."
#: app/routes/users/contracts.py:203
#: app/routes/users/contracts.py:205
msgid "No signed contract available."
msgstr "No signed contract available."
@@ -796,15 +809,15 @@ msgstr "Only coaches can reject One on One requests."
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
#: app/routes/users/profile.py:82
msgid "Username already taken."
msgstr "Username already taken."
#: app/routes/users/profile.py:93
#: app/routes/users/profile.py:92
msgid "Email already in use."
msgstr "Email already in use."
#: app/routes/users/profile.py:123
#: app/routes/users/profile.py:121
msgid "Profile updated successfully!"
msgstr "Profile updated successfully!"
@@ -953,7 +966,7 @@ msgstr "%(total)s in total"
msgid "Next"
msgstr "Next"
#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:154
#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:148
#: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3
msgid "Dashboard"
msgstr "Dashboard"
@@ -995,38 +1008,34 @@ msgid "My Notes"
msgstr "My Notes"
#: app/templates/layouts/base.html:106
msgid "Availability"
msgstr "Availability"
#: app/templates/layouts/base.html:112
msgid "Notes & One on One"
msgstr "Notes & One on One"
#: app/templates/layouts/base.html:119 app/templates/pages/contracts.html:2
#: app/templates/layouts/base.html:113 app/templates/pages/contracts.html:2
#: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9
msgid "Contracts"
msgstr "Contracts"
#: app/templates/layouts/base.html:126 app/templates/pages/profile.html:2
#: app/templates/layouts/base.html:120 app/templates/pages/profile.html:2
#: app/templates/pages/profile.html:3
msgid "My Profile"
msgstr "My Profile"
#: app/templates/layouts/base.html:137
#: app/templates/layouts/base.html:131
msgid "Logout"
msgstr "Logout"
#: app/templates/layouts/base.html:158
#: app/templates/layouts/base.html:152
msgid "Toggle dark mode"
msgstr "Toggle dark mode"
#: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189
#: app/templates/layouts/base.html:164 app/templates/layouts/base.html:183
msgid "Dismiss"
msgstr "Dismiss"
#: app/templates/layouts/base.html:199
msgid "Team Tryout Management System"
msgstr "Team Tryout Management System"
#: app/templates/layouts/base.html:193
msgid "UdeS team manager"
msgstr "UdeS team manager"
#: app/templates/layouts/macros.html:116
msgid "Close"
@@ -1209,6 +1218,10 @@ msgid "Loading availability grid..."
msgstr "Loading availability grid..."
#: app/templates/pages/coach_availability.html:19
msgid "Save Availability"
msgstr "Save Availability"
#: app/templates/pages/coach_availability.html:22
#: app/templates/pages/profile.html:200 app/templates/pages/profile.html:220
msgid "Clear All"
msgstr "Clear All"
@@ -1372,7 +1385,7 @@ msgid "Role"
msgstr "Role"
#: app/templates/pages/create_user.html:41 app/templates/pages/login.html:11
#: app/templates/pages/register.html:139
#: app/templates/pages/register.html:137
msgid "Password"
msgstr "Password"
@@ -1535,7 +1548,7 @@ msgstr "Edit Profile"
#: app/templates/pages/edit_profile.html:33
#: app/templates/pages/edit_user.html:43 app/templates/pages/profile.html:63
#: app/templates/pages/register.html:83
#: app/templates/pages/register.html:81
msgid "E-Sports Profile"
msgstr "E-Sports Profile"
@@ -1544,12 +1557,12 @@ msgid "Update your competitive gaming profile for tryouts."
msgstr "Update your competitive gaming profile for tryouts."
#: app/templates/pages/edit_profile.html:37
#: app/templates/pages/register.html:87
#: app/templates/pages/register.html:85
msgid "Games You Play"
msgstr "Games You Play"
#: app/templates/pages/edit_profile.html:47
#: app/templates/pages/register.html:101
#: app/templates/pages/register.html:99
msgid "Select all games you're signing in for."
msgstr "Select all games you're signing in for."
@@ -1593,48 +1606,43 @@ msgid "e.g. Name#1234"
msgstr "e.g. Name#1234"
#: app/templates/pages/edit_profile.html:87
#: app/templates/pages/edit_user.html:95
msgid "(for DMs)"
msgstr "(for DMs)"
#: app/templates/pages/register.html:57
msgid "Reconnect"
msgstr "Reconnect"
#: app/templates/pages/edit_profile.html:88
#: app/templates/pages/edit_user.html:96
msgid "Numeric ID (e.g. 123456789012345678)"
msgstr "Numeric ID (e.g. 123456789012345678)"
#: app/templates/pages/edit_profile.html:87
#: app/templates/pages/register.html:67
msgid "Connect Discord Account"
msgstr "Connect Discord Account"
#: app/templates/pages/edit_profile.html:89
#: app/templates/pages/edit_user.html:97
msgid "Enable Developer Mode in Discord → Right-click profile → Copy ID"
msgstr "Enable Developer Mode in Discord → Right-click profile → Copy ID"
#: app/templates/pages/edit_profile.html:94
#: app/templates/pages/edit_profile.html:93
#: app/templates/pages/edit_user.html:100
msgid "League OS Connection"
msgstr "League OS Connection"
#: app/templates/pages/edit_profile.html:95
#: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:131
#: app/templates/pages/edit_profile.html:94
#: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:129
msgid "League OS profile link or ID"
msgstr "League OS profile link or ID"
#: app/templates/pages/edit_profile.html:100
#: app/templates/pages/edit_profile.html:99
msgid "Change Password"
msgstr "Change Password"
#: app/templates/pages/edit_profile.html:101
#: app/templates/pages/edit_profile.html:100
msgid "Leave blank to keep your current password."
msgstr "Leave blank to keep your current password."
#: app/templates/pages/edit_profile.html:103
#: app/templates/pages/edit_profile.html:102
msgid "New Password"
msgstr "New Password"
#: app/templates/pages/edit_profile.html:104
#: app/templates/pages/edit_profile.html:103
#: app/templates/pages/edit_user.html:107
msgid "Enter new password"
msgstr "Enter new password"
#: app/templates/pages/edit_profile.html:109 app/templates/pages/teams.html:296
#: app/templates/pages/edit_profile.html:108 app/templates/pages/teams.html:296
msgid "Save Changes"
msgstr "Save Changes"
@@ -1650,6 +1658,18 @@ msgstr "Games"
msgid "Enter gamertag for each selected game to link to Tracker Network."
msgstr "Enter gamertag for each selected game to link to Tracker Network."
#: app/templates/pages/edit_user.html:95
msgid "(for DMs)"
msgstr "(for DMs)"
#: app/templates/pages/edit_user.html:96
msgid "Numeric ID (e.g. 123456789012345678)"
msgstr "Numeric ID (e.g. 123456789012345678)"
#: app/templates/pages/edit_user.html:97
msgid "Enable Developer Mode in Discord → Right-click profile → Copy ID"
msgstr "Enable Developer Mode in Discord → Right-click profile → Copy ID"
#: app/templates/pages/edit_user.html:106
msgid "(leave blank to keep current)"
msgstr "(leave blank to keep current)"
@@ -2365,7 +2385,7 @@ msgstr ""
"Select time slots when you're available for One on One sessions (8am to "
"10pm)."
#: app/templates/pages/profile.html:454
#: app/templates/pages/profile.html:460
msgid "Click or click-and-drag to select your available hours"
msgstr "Click or click-and-drag to select your available hours"
@@ -2413,11 +2433,7 @@ msgstr ""
msgid "Connected"
msgstr "Connected"
#: app/templates/pages/register.html:57
msgid "Reconnect"
msgstr "Reconnect"
#: app/templates/pages/register.html:63
#: app/templates/pages/register.html:61
msgid ""
"Discord connected. Game connections have been used to pre-fill your "
"profile below."
@@ -2426,50 +2442,46 @@ msgstr ""
"profile below."
#: app/templates/pages/register.html:69
msgid "Connect Discord Account"
msgstr "Connect Discord Account"
#: app/templates/pages/register.html:71
msgid "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc."
msgstr "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc."
#: app/templates/pages/register.html:74
#: app/templates/pages/register.html:72
msgid "Discord Username (Manual)"
msgstr "Discord Username (Manual)"
#: app/templates/pages/register.html:75
#: app/templates/pages/register.html:73
msgid "e.g. YourName"
msgstr "e.g. YourName"
#: app/templates/pages/register.html:84
#: app/templates/pages/register.html:82
msgid "Set up your competitive gaming profile for tryouts."
msgstr "Set up your competitive gaming profile for tryouts."
#: app/templates/pages/register.html:129
#: app/templates/pages/register.html:127
msgid "League OS Connection (Optional)"
msgstr "League OS Connection (Optional)"
#: app/templates/pages/register.html:133
#: app/templates/pages/register.html:131
msgid "Connect your League OS profile for organized play."
msgstr "Connect your League OS profile for organized play."
#: app/templates/pages/register.html:137
#: app/templates/pages/register.html:135
msgid "Security"
msgstr "Security"
#: app/templates/pages/register.html:140
#: app/templates/pages/register.html:138
msgid "Create a password"
msgstr "Create a password"
#: app/templates/pages/register.html:144
#: app/templates/pages/register.html:142
msgid "Confirm Password"
msgstr "Confirm Password"
#: app/templates/pages/register.html:145
#: app/templates/pages/register.html:143
msgid "Confirm your password"
msgstr "Confirm your password"
#: app/templates/pages/register.html:159
#: app/templates/pages/register.html:157
msgid "Create Account"
msgstr "Create Account"
@@ -2984,3 +2996,8 @@ msgstr "View Profile"
#~ msgid "Invalid date or time format."
#~ msgstr "Invalid date or time format."
#~ msgid "Availability"
#~ msgstr "Availability"
#~ msgid "Team Tryout Management System"
#~ msgstr "Team Tryout Management System"
Binary file not shown.
+166 -149
View File
@@ -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-11 20:39-0400\n"
"POT-Creation-Date: 2026-08-16 23:22-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
@@ -23,8 +23,8 @@ msgstr ""
msgid "Please log in to access this page."
msgstr "Veuillez vous connecter pour accéder à cette page."
#: app/forms.py:37 app/routes/auth.py:224 app/routes/auth.py:374
#: app/routes/users/contracts.py:96
#: app/forms.py:37 app/routes/auth.py:228 app/routes/auth.py:387
#: app/routes/users/contracts.py:98
#, python-format
msgid "%(field)s: %(msg)s"
msgstr "%(field)s : %(msg)s"
@@ -63,7 +63,7 @@ msgstr "Le nom dutilisateur est obligatoire."
msgid "Password is required."
msgstr "Le mot de passe est obligatoire."
#: app/validators.py:227 app/validators.py:299
#: app/validators.py:227 app/validators.py:294
msgid "Username must be 3-80 characters."
msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères."
@@ -71,169 +71,169 @@ msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères."
msgid "Email must be 120 characters or less."
msgstr "Ladresse courriel ne doit pas dépasser 120 caractères."
#: app/validators.py:246 app/validators.py:314 app/validators.py:345
#: app/validators.py:409
#: app/validators.py:246 app/validators.py:309 app/validators.py:340
#: app/validators.py:403
msgid "Full name is required."
msgstr "Le nom complet est obligatoire."
#: app/validators.py:281
#: app/validators.py:276
msgid "Passwords do not match."
msgstr "Les mots de passe ne concordent pas."
#: app/validators.py:318 app/validators.py:353
#: app/validators.py:313 app/validators.py:348
msgid "Invalid role selected."
msgstr "Rôle sélectionné invalide."
#: app/validators.py:454
#: app/validators.py:443
msgid "Player must be selected."
msgstr "Vous devez choisir un joueur."
#: app/validators.py:457
#: app/validators.py:446
msgid "Notes must be 2000 characters or less."
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
#: app/validators.py:494 app/validators.py:853
#: app/validators.py:483 app/validators.py:854
msgid "Invalid coach selection."
msgstr "Sélection de coach invalide."
#: app/validators.py:500 app/validators.py:859
#: app/validators.py:489 app/validators.py:860
msgid "Invalid manager selection."
msgstr "Sélection de gérant invalide."
#: app/validators.py:511
#: app/validators.py:507
msgid "Invalid player selection."
msgstr "Sélection de joueur invalide."
#: app/validators.py:515
#: app/validators.py:516
msgid "Unknown roster status."
msgstr "Statut d'effectif inconnu."
#: app/validators.py:538
#: app/validators.py:539
msgid "Date must be in YYYY-MM-DD format."
msgstr "La date doit être au format AAAA-MM-JJ."
#: app/validators.py:539
#: app/validators.py:540
msgid "A date is required."
msgstr "Une date est requise."
#: app/validators.py:545 app/validators.py:610
#: app/validators.py:546 app/validators.py:611
msgid "Start time must be in HH:MM format."
msgstr "Lheure de début doit être au format HH:MM."
#: app/validators.py:546 app/validators.py:611
#: app/validators.py:547 app/validators.py:612
msgid "A start time is required."
msgstr "Une heure de début est requise."
#: app/validators.py:552
#: app/validators.py:553
msgid "End time must be in HH:MM format."
msgstr "Lheure de fin doit être au format HH:MM."
#: app/validators.py:553
#: app/validators.py:554
msgid "An end time is required."
msgstr "Une heure de fin est requise."
#: app/validators.py:557
#: app/validators.py:558
msgid "Points must be 2000 characters or less."
msgstr "Les points ne doivent pas dépasser 2000 caractères."
#: app/validators.py:572
#: app/validators.py:573
msgid "End time must be after start time."
msgstr "L'heure de fin doit être postérieure à l'heure de début."
#: app/validators.py:601 app/validators.py:603
#: app/validators.py:602 app/validators.py:604
msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
#: app/validators.py:604
#: app/validators.py:605
msgid "A day is required."
msgstr "Un jour est requis."
#: app/validators.py:639
#: app/validators.py:640
msgid "Player selection is malformed."
msgstr "La sélection de joueurs est mal formée."
#: app/validators.py:665 app/validators.py:775
#: app/validators.py:666 app/validators.py:776
msgid "A title is required."
msgstr "Un titre est requis."
#: app/validators.py:674
#: app/validators.py:675
msgid "Invalid date format."
msgstr "Format de date invalide."
#: app/validators.py:679 app/validators.py:686
#: app/validators.py:680 app/validators.py:687
msgid "Invalid time format."
msgstr "Format dheure invalide."
#: app/validators.py:680
#: app/validators.py:681
msgid "Start time is required. Please select a time slot."
msgstr "Lheure de début est obligatoire. Choisissez une plage horaire."
#: app/validators.py:694
#: app/validators.py:695
msgid "Unknown match status."
msgstr "Statut de match inconnu."
#: app/validators.py:710
#: app/validators.py:711
msgid "The end time must come after the start time."
msgstr "L'heure de fin doit être postérieure à l'heure de début."
#: app/validators.py:724
#: app/validators.py:725
msgid "Unknown match type."
msgstr "Type de match inconnu."
#: app/validators.py:736
#: app/validators.py:737
msgid "A team cannot play against itself."
msgstr "Une équipe ne peut pas jouer contre elle-même."
#: app/validators.py:784
#: app/validators.py:785
msgid "Unknown game."
msgstr "Jeu inconnu."
#: app/validators.py:789
#: app/validators.py:790
msgid "Invalid start date format."
msgstr "Format de date de début invalide."
#: app/validators.py:790
#: app/validators.py:791
msgid "A start date is required."
msgstr "Une date de début est requise."
#: app/validators.py:796
#: app/validators.py:797
msgid "Invalid end date format."
msgstr "Format de date de fin invalide."
#: app/validators.py:804
#: app/validators.py:805
msgid "A tryout must allow at least one player."
msgstr "Une sélection doit accepter au moins un joueur."
#: app/validators.py:807
#: app/validators.py:808
msgid "The player limit must be a whole number."
msgstr "La limite de joueurs doit être un nombre entier."
#: app/validators.py:819
#: app/validators.py:820
msgid "End date cannot be before start date."
msgstr "La date de fin ne peut pas précéder la date de début."
#: app/validators.py:846 app/validators.py:847
#: app/validators.py:847 app/validators.py:848
msgid "Team name is required."
msgstr "Le nom de l’équipe est obligatoire."
#: app/validators.py:871
#: app/validators.py:872
msgid "Scores run from 1 to 10."
msgstr "Les notes vont de 1 à 10."
#: app/validators.py:872
#: app/validators.py:873
msgid "A score must be a whole number from 1 to 10."
msgstr "Une note doit être un nombre entier de 1 à 10."
#: app/routes/auth.py:241
#: app/routes/auth.py:245
msgid "This account has been deactivated."
msgstr "Ce compte a été désactivé."
#: app/routes/auth.py:276
#: app/routes/auth.py:280
#, python-format
msgid "Welcome back, %(username)s!"
msgstr "Bon retour, %(username)s !"
#: app/routes/auth.py:306
#: app/routes/auth.py:310
msgid ""
"Login unsuccessful. Please check your username and password, or ask a "
"president for help."
@@ -241,27 +241,32 @@ msgstr ""
"Échec de la connexion. Vérifiez le nom dutilisateur et le mot de passe, "
"ou demandez de laide à un président."
#: app/routes/auth.py:363
#: app/routes/auth.py:376
msgid "Your registration could not be processed. Please try again."
msgstr "Votre inscription n'a pas pu être traitée. Veuillez réessayer."
#: app/routes/auth.py:388 app/routes/users/accounts.py:329
#: app/routes/auth.py:411 app/routes/users/accounts.py:339
msgid "Username already exists."
msgstr "Ce nom dutilisateur est déjà pris."
#: app/routes/auth.py:392 app/routes/users/accounts.py:333
#: app/routes/auth.py:415 app/routes/users/accounts.py:343
msgid "Email already registered."
msgstr "Cette adresse courriel est déjà enregistrée."
#: app/routes/auth.py:436
#: app/routes/auth.py:422 app/routes/auth.py:617
#: app/routes/users/accounts.py:121
msgid "This Discord account is already linked to another account."
msgstr "Ce compte Discord est déjà lié à un autre compte."
#: app/routes/auth.py:466
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:457
#: app/routes/auth.py:491
msgid "Discord OAuth2 is not configured."
msgstr "La connexion Discord nest pas configurée."
#: app/routes/auth.py:498
#: app/routes/auth.py:540
msgid ""
"Discord authorization could not be verified. Please start the connection "
"again from this page."
@@ -269,27 +274,35 @@ msgstr ""
"Lautorisation Discord na pas pu être vérifiée. Relancez la connexion "
"depuis cette page."
#: app/routes/auth.py:507
#: app/routes/auth.py:549
msgid "Discord authorization failed. No code received."
msgstr "Lautorisation Discord a échoué : aucun code reçu."
#: app/routes/auth.py:531
#: app/routes/auth.py:573
msgid "Failed to connect to Discord. Please try again."
msgstr "Impossible de joindre Discord. Veuillez réessayer."
#: app/routes/auth.py:535
#: app/routes/auth.py:577
msgid "Failed to obtain Discord access token."
msgstr "Impossible dobtenir le jeton daccès Discord."
#: app/routes/auth.py:550
#: app/routes/auth.py:592 app/routes/auth.py:602
msgid "Failed to fetch Discord user profile."
msgstr "Impossible de récupérer le profil Discord."
#: app/routes/auth.py:597
#: app/routes/auth.py:609
msgid "Please log in to connect your Discord account."
msgstr "Veuillez vous connecter pour lier votre compte Discord."
#: app/routes/auth.py:628
msgid "Discord account connected!"
msgstr "Compte Discord connecté !"
#: app/routes/auth.py:675
msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
#: app/routes/auth.py:625
#: app/routes/auth.py:703
msgid "You have been logged out."
msgstr "Vous avez été déconnecté."
@@ -325,8 +338,8 @@ msgstr "Évaluation mise à jour."
#: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455
#: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:444
#: app/routes/tryouts.py:460 app/routes/tryouts.py:480
#: app/routes/tryouts.py:519 app/routes/tryouts.py:555
#: app/routes/tryouts.py:574
#: app/routes/tryouts.py:527 app/routes/tryouts.py:563
#: app/routes/tryouts.py:582
msgid "Permission denied."
msgstr "Accès refusé."
@@ -467,7 +480,7 @@ msgstr "Coach retiré de %(name)s."
msgid "Manager removed from %(name)s."
msgstr "Gérant retiré de %(name)s."
#: app/routes/teams.py:490 app/routes/tryouts.py:484 app/routes/tryouts.py:585
#: app/routes/teams.py:490 app/routes/tryouts.py:489 app/routes/tryouts.py:593
msgid "Please select a player."
msgstr "Veuillez choisir un joueur."
@@ -549,7 +562,7 @@ msgstr "Cette sélection naccepte pas dinscriptions."
msgid "You are already registered for this tryout."
msgstr "Vous êtes déjà inscrit à cette sélection."
#: app/routes/tryouts.py:428 app/routes/tryouts.py:503
#: app/routes/tryouts.py:428 app/routes/tryouts.py:511
msgid "This tryout is full."
msgstr "Cette sélection est complète."
@@ -566,47 +579,47 @@ msgstr "Statut de la sélection mis à jour : %(new_status)s."
msgid "Registration status updated."
msgstr "Statut dinscription mis à jour."
#: app/routes/tryouts.py:489
#: app/routes/tryouts.py:497
msgid "Can only register players."
msgstr "Seuls des joueurs peuvent être inscrits."
#: app/routes/tryouts.py:495
#: app/routes/tryouts.py:503
#, 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:509
#: app/routes/tryouts.py:517
#, python-format
msgid "%(username)s registered for tryout!"
msgstr "%(username)s est inscrit à la sélection."
#: app/routes/tryouts.py:545
#: app/routes/tryouts.py:553
#, python-format
msgid "%(username)s removed from tryout."
msgstr "%(username)s a été retiré de la sélection."
#: app/routes/tryouts.py:563
#: app/routes/tryouts.py:571
#, python-format
msgid "Team \"%(team_name)s\" created!"
msgstr "Équipe « %(team_name)s » créée."
#: app/routes/tryouts.py:594
#: app/routes/tryouts.py:602
msgid "That player is not registered for this tryout."
msgstr "Ce joueur nest pas inscrit à cette sélection."
#: app/routes/tryouts.py:600
#: app/routes/tryouts.py:608
msgid "Player is already on this team."
msgstr "Ce joueur est déjà dans cette équipe."
#: app/routes/tryouts.py:605
#: app/routes/tryouts.py:613
msgid "Player added to team!"
msgstr "Joueur ajouté à l’équipe."
#: app/routes/tryouts.py:615
#: app/routes/tryouts.py:623
msgid "You do not have permission to delete this tryout."
msgstr "Vous navez pas les droits pour supprimer cette sélection."
#: app/routes/tryouts.py:651
#: app/routes/tryouts.py:659
msgid "Tryout deleted successfully."
msgstr "Sélection supprimée."
@@ -634,13 +647,13 @@ msgstr "Seul le président peut modifier des utilisateurs."
msgid "Email already in use by another account."
msgstr "Cette adresse courriel est déjà utilisée par un autre compte."
#: app/routes/users/accounts.py:122
#: app/routes/users/accounts.py:132
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/accounts.py:135
#: app/routes/users/accounts.py:145
msgid ""
"This is the last active president. Promote another account before "
"changing this one."
@@ -648,29 +661,29 @@ msgstr ""
"Cest le dernier président actif. Promouvez un autre compte avant de "
"modifier celui-ci."
#: app/routes/users/accounts.py:214
#: app/routes/users/accounts.py:224
#, python-format
msgid "User %(username)s updated successfully!"
msgstr "Utilisateur %(username)s mis à jour."
#: app/routes/users/accounts.py:235
#: app/routes/users/accounts.py:245
msgid "Only the president can delete users."
msgstr "Seul le président peut supprimer des utilisateurs."
#: app/routes/users/accounts.py:239
#: app/routes/users/accounts.py:249
msgid "You cannot delete your own account."
msgstr "Vous ne pouvez pas supprimer votre propre compte."
#: app/routes/users/accounts.py:298
#: app/routes/users/accounts.py:308
#, python-format
msgid "User %(deleted_username)s has been removed."
msgstr "Lutilisateur %(deleted_username)s a été supprimé."
#: app/routes/users/accounts.py:309
#: app/routes/users/accounts.py:319
msgid "Only the president can create users."
msgstr "Seul le président peut créer des utilisateurs."
#: app/routes/users/accounts.py:357
#: app/routes/users/accounts.py:367
#, python-format
msgid "User %(full_name)s created as %(role)s!"
msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
@@ -679,32 +692,32 @@ msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
msgid "Only coaches can manage availability."
msgstr "Seuls les coachs peuvent gérer leurs disponibilités."
#: app/routes/users/contracts.py:84
#: app/routes/users/contracts.py:86
msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat."
#: app/routes/users/contracts.py:103
#: app/routes/users/contracts.py:105
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."
#: app/routes/users/contracts.py:143
#: app/routes/users/contracts.py:145
#, python-format
msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contrat téléversé pour %(username)s."
#: app/routes/users/contracts.py:157
#: app/routes/users/contracts.py:159
msgid "Only the player can upload their signed contract."
msgstr "Seul le joueur peut téléverser son contrat signé."
#: app/routes/users/contracts.py:175
#: app/routes/users/contracts.py:177
msgid "Signed contract uploaded successfully!"
msgstr "Contrat signé téléversé."
#: app/routes/users/contracts.py:185 app/routes/users/contracts.py:200
#: app/routes/users/contracts.py:187 app/routes/users/contracts.py:202
msgid "You do not have permission to download this contract."
msgstr "Vous navez pas les droits pour télécharger ce contrat."
#: app/routes/users/contracts.py:203
#: app/routes/users/contracts.py:205
msgid "No signed contract available."
msgstr "Aucun contrat signé disponible."
@@ -804,15 +817,15 @@ msgstr "Seuls les coachs peuvent refuser une demande de rencontre."
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
#: app/routes/users/profile.py:82
msgid "Username already taken."
msgstr "Ce nom dutilisateur est déjà pris."
#: app/routes/users/profile.py:93
#: app/routes/users/profile.py:92
msgid "Email already in use."
msgstr "Cette adresse courriel est déjà utilisée."
#: app/routes/users/profile.py:123
#: app/routes/users/profile.py:121
msgid "Profile updated successfully!"
msgstr "Profil mis à jour."
@@ -959,7 +972,7 @@ msgstr "%(total)s au total"
msgid "Next"
msgstr "Suivant"
#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:154
#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:148
#: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3
msgid "Dashboard"
msgstr "Tableau de bord"
@@ -1001,38 +1014,34 @@ msgid "My Notes"
msgstr "Mes notes"
#: app/templates/layouts/base.html:106
msgid "Availability"
msgstr "Disponibilités"
#: app/templates/layouts/base.html:112
msgid "Notes & One on One"
msgstr "Notes et rencontres individuelles"
#: app/templates/layouts/base.html:119 app/templates/pages/contracts.html:2
#: app/templates/layouts/base.html:113 app/templates/pages/contracts.html:2
#: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9
msgid "Contracts"
msgstr "Contrats"
#: app/templates/layouts/base.html:126 app/templates/pages/profile.html:2
#: app/templates/layouts/base.html:120 app/templates/pages/profile.html:2
#: app/templates/pages/profile.html:3
msgid "My Profile"
msgstr "Mon profil"
#: app/templates/layouts/base.html:137
#: app/templates/layouts/base.html:131
msgid "Logout"
msgstr "Déconnexion"
#: app/templates/layouts/base.html:158
#: app/templates/layouts/base.html:152
msgid "Toggle dark mode"
msgstr "Basculer le mode sombre"
#: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189
#: app/templates/layouts/base.html:164 app/templates/layouts/base.html:183
msgid "Dismiss"
msgstr "Fermer"
#: app/templates/layouts/base.html:199
msgid "Team Tryout Management System"
msgstr "Système de gestion des sélections d’équipe"
#: app/templates/layouts/base.html:193
msgid "UdeS team manager"
msgstr "UdeS team manager"
#: app/templates/layouts/macros.html:116
msgid "Close"
@@ -1217,6 +1226,10 @@ msgid "Loading availability grid..."
msgstr "Chargement de la grille de disponibilités..."
#: app/templates/pages/coach_availability.html:19
msgid "Save Availability"
msgstr "Enregistrer les disponibilités"
#: app/templates/pages/coach_availability.html:22
#: app/templates/pages/profile.html:200 app/templates/pages/profile.html:220
msgid "Clear All"
msgstr "Tout effacer"
@@ -1378,7 +1391,7 @@ msgid "Role"
msgstr "Rôle"
#: app/templates/pages/create_user.html:41 app/templates/pages/login.html:11
#: app/templates/pages/register.html:139
#: app/templates/pages/register.html:137
msgid "Password"
msgstr "Mot de passe"
@@ -1541,7 +1554,7 @@ msgstr "Modifier le profil"
#: app/templates/pages/edit_profile.html:33
#: app/templates/pages/edit_user.html:43 app/templates/pages/profile.html:63
#: app/templates/pages/register.html:83
#: app/templates/pages/register.html:81
msgid "E-Sports Profile"
msgstr "Profil e-sport"
@@ -1550,12 +1563,12 @@ msgid "Update your competitive gaming profile for tryouts."
msgstr "Mettez à jour votre profil de joueur compétitif pour les sélections."
#: app/templates/pages/edit_profile.html:37
#: app/templates/pages/register.html:87
#: app/templates/pages/register.html:85
msgid "Games You Play"
msgstr "Jeux auxquels vous jouez"
#: app/templates/pages/edit_profile.html:47
#: app/templates/pages/register.html:101
#: app/templates/pages/register.html:99
msgid "Select all games you're signing in for."
msgstr "Sélectionnez tous les jeux pour lesquels vous vous inscrivez."
@@ -1599,50 +1612,43 @@ msgid "e.g. Name#1234"
msgstr "ex. : Nom#1234"
#: app/templates/pages/edit_profile.html:87
#: app/templates/pages/edit_user.html:95
msgid "(for DMs)"
msgstr "(pour les messages privés)"
#: app/templates/pages/register.html:57
msgid "Reconnect"
msgstr "Reconnecter"
#: app/templates/pages/edit_profile.html:88
#: app/templates/pages/edit_user.html:96
msgid "Numeric ID (e.g. 123456789012345678)"
msgstr "Identifiant numérique (ex. : 123456789012345678)"
#: app/templates/pages/edit_profile.html:87
#: app/templates/pages/register.html:67
msgid "Connect Discord Account"
msgstr "Connecter un compte Discord"
#: app/templates/pages/edit_profile.html:89
#: app/templates/pages/edit_user.html:97
msgid "Enable Developer Mode in Discord → Right-click profile → Copy ID"
msgstr ""
"Activez le mode développeur dans Discord → clic droit sur le profil → "
"Copier lidentifiant"
#: app/templates/pages/edit_profile.html:94
#: app/templates/pages/edit_profile.html:93
#: app/templates/pages/edit_user.html:100
msgid "League OS Connection"
msgstr "Connexion League OS"
#: app/templates/pages/edit_profile.html:95
#: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:131
#: app/templates/pages/edit_profile.html:94
#: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:129
msgid "League OS profile link or ID"
msgstr "Lien ou identifiant du profil League OS"
#: app/templates/pages/edit_profile.html:100
#: app/templates/pages/edit_profile.html:99
msgid "Change Password"
msgstr "Changer le mot de passe"
#: app/templates/pages/edit_profile.html:101
#: app/templates/pages/edit_profile.html:100
msgid "Leave blank to keep your current password."
msgstr "Laissez vide pour conserver votre mot de passe actuel."
#: app/templates/pages/edit_profile.html:103
#: app/templates/pages/edit_profile.html:102
msgid "New Password"
msgstr "Nouveau mot de passe"
#: app/templates/pages/edit_profile.html:104
#: app/templates/pages/edit_profile.html:103
#: app/templates/pages/edit_user.html:107
msgid "Enter new password"
msgstr "Saisir le nouveau mot de passe"
#: app/templates/pages/edit_profile.html:109 app/templates/pages/teams.html:296
#: app/templates/pages/edit_profile.html:108 app/templates/pages/teams.html:296
msgid "Save Changes"
msgstr "Enregistrer les modifications"
@@ -1658,6 +1664,20 @@ msgstr "Jeux"
msgid "Enter gamertag for each selected game to link to Tracker Network."
msgstr "Saisissez un pseudo par jeu sélectionné pour le lier à Tracker Network."
#: app/templates/pages/edit_user.html:95
msgid "(for DMs)"
msgstr "(pour les messages privés)"
#: app/templates/pages/edit_user.html:96
msgid "Numeric ID (e.g. 123456789012345678)"
msgstr "Identifiant numérique (ex. : 123456789012345678)"
#: app/templates/pages/edit_user.html:97
msgid "Enable Developer Mode in Discord → Right-click profile → Copy ID"
msgstr ""
"Activez le mode développeur dans Discord → clic droit sur le profil → "
"Copier lidentifiant"
#: app/templates/pages/edit_user.html:106
msgid "(leave blank to keep current)"
msgstr "(laisser vide pour conserver lactuel)"
@@ -2378,7 +2398,7 @@ msgstr ""
"Choisissez les plages où vous êtes disponible pour des rencontres "
"individuelles (8 h à 22 h)."
#: app/templates/pages/profile.html:454
#: app/templates/pages/profile.html:460
msgid "Click or click-and-drag to select your available hours"
msgstr "Cliquez ou faites glisser pour choisir vos heures de disponibilité"
@@ -2426,11 +2446,7 @@ msgstr ""
msgid "Connected"
msgstr "Connecté"
#: app/templates/pages/register.html:57
msgid "Reconnect"
msgstr "Reconnecter"
#: app/templates/pages/register.html:63
#: app/templates/pages/register.html:61
msgid ""
"Discord connected. Game connections have been used to pre-fill your "
"profile below."
@@ -2439,52 +2455,48 @@ msgstr ""
"ci-dessous."
#: app/templates/pages/register.html:69
msgid "Connect Discord Account"
msgstr "Connecter un compte Discord"
#: app/templates/pages/register.html:71
msgid "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc."
msgstr ""
"Connectez-vous pour pré-remplir vos pseudos depuis Steam, Battle.net, "
"Xbox, etc."
#: app/templates/pages/register.html:74
#: app/templates/pages/register.html:72
msgid "Discord Username (Manual)"
msgstr "Nom dutilisateur Discord (saisie manuelle)"
#: app/templates/pages/register.html:75
#: app/templates/pages/register.html:73
msgid "e.g. YourName"
msgstr "ex. : VotrePseudo"
#: app/templates/pages/register.html:84
#: app/templates/pages/register.html:82
msgid "Set up your competitive gaming profile for tryouts."
msgstr "Configurez votre profil de joueur compétitif pour les sélections."
#: app/templates/pages/register.html:129
#: app/templates/pages/register.html:127
msgid "League OS Connection (Optional)"
msgstr "Connexion League OS (facultative)"
#: app/templates/pages/register.html:133
#: app/templates/pages/register.html:131
msgid "Connect your League OS profile for organized play."
msgstr "Liez votre profil League OS pour le jeu organisé."
#: app/templates/pages/register.html:137
#: app/templates/pages/register.html:135
msgid "Security"
msgstr "Sécurité"
#: app/templates/pages/register.html:140
#: app/templates/pages/register.html:138
msgid "Create a password"
msgstr "Créez un mot de passe"
#: app/templates/pages/register.html:144
#: app/templates/pages/register.html:142
msgid "Confirm Password"
msgstr "Confirmer le mot de passe"
#: app/templates/pages/register.html:145
#: app/templates/pages/register.html:143
msgid "Confirm your password"
msgstr "Confirmez votre mot de passe"
#: app/templates/pages/register.html:159
#: app/templates/pages/register.html:157
msgid "Create Account"
msgstr "Créer le compte"
@@ -3008,3 +3020,8 @@ msgstr "Voir le profil"
#~ msgid "Invalid date or time format."
#~ msgstr "Format de date ou dheure invalide."
#~ msgid "Availability"
#~ msgstr "Disponibilités"
#~ msgid "Team Tryout Management System"
#~ msgstr "Système de gestion des sélections d’équipe"
-11
View File
@@ -256,11 +256,6 @@ class RegisterSchema(StripMixin):
allow_none=True,
load_default=None,
)
discord_user_id = fields.String(
validate=validate_discord_user_id,
allow_none=True,
load_default=None,
)
league_os_profile = fields.String(
validate=validate.Length(max=256),
allow_none=True,
@@ -392,7 +387,6 @@ class EditProfileSchema(StripMixin):
phone: Optional.
password: Optional (only if changing).
discord_username: Optional.
discord_user_id: Optional.
league_os_profile: Optional.
games: Optional list.
"""
@@ -428,11 +422,6 @@ class EditProfileSchema(StripMixin):
allow_none=True,
load_default=None,
)
discord_user_id = fields.String(
validate=validate_discord_user_id,
allow_none=True,
load_default=None,
)
league_os_profile = fields.String(
validate=validate.Length(max=256),
allow_none=True,