3 Commits
19 changed files with 906 additions and 381 deletions
+5 -2
View File
@@ -101,8 +101,11 @@ Ce qui **n'est pas** fait, pour que personne ne s'y fie :
- **Les comptes créés par le formulaire d'inscription sont actifs - **Les comptes créés par le formulaire d'inscription sont actifs
immédiatement** : il n'y a pas d'étape de validation par le staff. Décision immédiatement** : il n'y a pas d'étape de validation par le staff. Décision
de produit en attente, voir `docs/roles-and-permissions.md`. de produit en attente, voir `docs/roles-and-permissions.md`.
- **L'identité Discord** transite encore par un champ caché du formulaire - **L'unicité Discord n'est pas encore garantie par PostgreSQL.** L'identité
d'inscription : elle n'est pas prouvée par le passage OAuth2. OAuth reste désormais côté serveur, le profil ne peut plus réécrire le
snowflake et l'application refuse les nouvelles collisions. Les doublons
historiques doivent être relevés puis corrigés avant la contrainte
`UNIQUE` (`schema_report.py --check-discord-identities`).
`docs/security-checklist.md` détaille la liste avant mise en production. `docs/security-checklist.md` détaille la liste avant mise en production.
+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.i18n import LOCALE_SESSION_KEY
from app.logging_config import log_auth_event from app.logging_config import log_auth_event
from app.models import ESPORT_GAMES, Player, User 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. #: Session key holding the pending OAuth2 anti-forgery token.
DISCORD_STATE_KEY = 'discord_oauth_state' 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 # 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). # 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 = dict(request.form)
form_data['games'] = request.form.getlist('games') 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) refusal = check_registration_challenge(request.form)
if refusal is not None: if refusal is not None:
# Logged, because this is the only place abuse of the sign-up # Logged, because this is the only place abuse of the sign-up
@@ -380,8 +393,18 @@ def register():
full_name = validated['full_name'] full_name = validated['full_name']
phone = validated.get('phone') phone = validated.get('phone')
selected_games = validated.get('games', []) selected_games = validated.get('games', [])
discord_username = validated.get('discord_username') # The OAuth identity is server-side state. It used to be copied into
discord_user_id = validated.get('discord_user_id') # 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') league_os_profile = validated.get('league_os_profile')
if User.query.filter_by(username=username).first(): if User.query.filter_by(username=username).first():
@@ -392,6 +415,13 @@ def register():
flash(_('Email already registered.'), 'danger') flash(_('Email already registered.'), 'danger')
return _rerender_registration(form_data) 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) hashed_password = hash_password(password)
user = Player( user = Player(
username=username, username=username,
@@ -451,11 +481,15 @@ def discord_login():
Returns: Returns:
Response: Redirect to Discord authorization URL. 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 # DISCORD_REDIRECT_URI is checked too: quoting it when unset used to
# raise inside the query builder rather than report a configuration error. # raise inside the query builder rather than report a configuration error.
if not DISCORD_CLIENT_ID or not DISCORD_REDIRECT_URI: if not DISCORD_CLIENT_ID or not DISCORD_REDIRECT_URI:
flash(_('Discord OAuth2 is not configured.'), 'danger') 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 # Anti-forgery token, required by RFC 6749 §10.12. Without it, an
# attacker could have the victim's browser consume an authorization code # attacker could have the victim's browser consume an authorization code
@@ -479,16 +513,24 @@ def discord_login():
def discord_callback(): def discord_callback():
"""Handle the OAuth2 callback from Discord. """Handle the OAuth2 callback from Discord.
Exchanges the authorization code for an access token, then fetches Exchanges the authorization code for an access token, then fetches the
the user's profile (/users/@me) and connections (/users/@me/connections). user's profile. During registration, connected game accounts are also
Results are stored in the session and the user is redirected back to loaded into server-side draft state. For a signed-in profile relink, the
the registration form where fields will be pre-filled. verified identity is written directly without passing through a form.
Returns: 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 # The state is consumed whatever happens next: a token is single-use, and
# leaving it in the session would allow a replay. # 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) expected_state = session.pop(DISCORD_STATE_KEY, None)
received_state = request.args.get('state', '') received_state = request.args.get('state', '')
@@ -500,12 +542,12 @@ def discord_callback():
), ),
'danger', 'danger',
) )
return redirect(url_for('auth.register')) return redirect(url_for(return_endpoint))
code = request.args.get('code') code = request.args.get('code')
if not code: if not code:
flash(_('Discord authorization failed. No code received.'), 'danger') 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 # Exchange the authorization code for an access token
token_data = { token_data = {
@@ -529,11 +571,11 @@ def discord_callback():
access_token = token_json.get('access_token') access_token = token_json.get('access_token')
except requests.RequestException: except requests.RequestException:
flash(_('Failed to connect to Discord. Please try again.'), 'danger') 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: if not access_token:
flash(_('Failed to obtain Discord access token.'), 'danger') 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}'} auth_headers = {'Authorization': f'Bearer {access_token}'}
@@ -548,7 +590,43 @@ def discord_callback():
user_data = user_response.json() user_data = user_response.json()
except requests.RequestException: except requests.RequestException:
flash(_('Failed to fetch Discord user profile.'), 'danger') 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 # Fetch the user's connected gaming accounts
connections = [] connections = []
@@ -587,7 +665,7 @@ def discord_callback():
# Store in session for the registration form to use # Store in session for the registration form to use
session['discord_oauth'] = { session['discord_oauth'] = {
'id': user_data.get('id'), 'id': discord_user_id,
'username': user_data.get('username'), 'username': user_data.get('username'),
'avatar': user_data.get('avatar'), 'avatar': user_data.get('avatar'),
'gamertag_suggestions': gamertag_suggestions, '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') flash(_('Email already in use by another account.'), 'danger')
return _rerender() 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 role_changed = user.role != role
previous_role = user.role previous_role = user.role
+25 -25
View File
@@ -141,41 +141,41 @@ def add_disponibility():
@json_endpoint @json_endpoint
@login_required @login_required
def add_disponibilities_bulk(): def add_disponibilities_bulk():
"""Add multiple disponibility blocks at once.""" """Replace the current player's disponibility blocks atomically."""
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
accepted, rejected = _load_slots(data.get('slots')) accepted, rejected = _load_slots(data.get('slots'))
if rejected:
return jsonify(
{
'error': 'Invalid slots; nothing was changed.',
'rejected': rejected,
}
), 400
PlayerDisponibility.query.filter_by(player_id=current_user.id).delete()
created = [] created = []
for slot in accepted: for slot in accepted:
start_time = slot['start_time'] start_time = slot['start_time']
existing = PlayerDisponibility.query.filter_by( disponibility = PlayerDisponibility(
player_id=current_user.id, player_id=current_user.id,
day_of_week=slot['day_of_week'], day_of_week=slot['day_of_week'],
start_time=start_time, start_time=start_time,
).first() end_time=slot_end(start_time),
if not existing: )
disponibility = PlayerDisponibility( db.session.add(disponibility)
player_id=current_user.id, db.session.flush()
day_of_week=slot['day_of_week'], created.append(
start_time=start_time, {
end_time=slot_end(start_time), 'id': disponibility.id,
) 'day_of_week': disponibility.day_of_week,
db.session.add(disponibility) 'day_name': day_name(disponibility.day_of_week),
db.session.flush() 'start_time': disponibility.start_time.strftime('%H:%M'),
created.append( }
{ )
'id': disponibility.id,
'day_of_week': disponibility.day_of_week,
'day_name': day_name(disponibility.day_of_week),
'start_time': disponibility.start_time.strftime('%H:%M'),
}
)
db.session.commit() db.session.commit()
# `rejected` is reported rather than swallowed. What was accepted is return jsonify({'success': True, 'created': created, 'rejected': []})
# still saved — dropping a whole batch because one cell was malformed
# would be its own kind of surprise — but the client can now tell the
# difference between "nine slots saved" and "ten sent, nine saved".
return jsonify({'success': True, 'created': created, 'rejected': rejected})
@users_bp.route('/disponibilities/clear', methods=['POST']) @users_bp.route('/disponibilities/clear', methods=['POST'])
-2
View File
@@ -76,7 +76,6 @@ def edit_profile():
phone = validated.get('phone') phone = validated.get('phone')
selected_games = validated.get('games', []) selected_games = validated.get('games', [])
discord_username = validated.get('discord_username') discord_username = validated.get('discord_username')
discord_user_id = validated.get('discord_user_id')
league_os_profile = validated.get('league_os_profile') league_os_profile = validated.get('league_os_profile')
if username != current_user.username and User.query.filter_by(username=username).first(): 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.phone = phone
current_user.games = ','.join(selected_games) if selected_games else None current_user.games = ','.join(selected_games) if selected_games else None
current_user.discord_username = discord_username or 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 current_user.league_os_profile = league_os_profile or None
update_user_gamertags(current_user, selected_games) 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) # Also look for the seeded admin/password account (SEC-003)
python app/supporting_scripts/schema_report.py --check-seed-accounts 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 Exit codes
---------- ----------
0 the live schema matches the models 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 2 could not connect or read the catalogue
Reading the output Reading the output
@@ -315,6 +318,42 @@ def find_seed_accounts(engine):
return results 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): def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.split('\n')[0]) parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
parser.add_argument( parser.add_argument(
@@ -327,6 +366,11 @@ def main(argv=None):
action='store_true', action='store_true',
help='Also look for the admin/password account seeded by clear_db.py (SEC-003).', 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) args = parser.parse_args(argv)
if not args.url: if not args.url:
@@ -375,9 +419,24 @@ def main(argv=None):
) )
print(f' {username} ({role}): {verdict}') 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) blocking = sum(1 for f in findings if f.severity == BLOCKING)
print(f'\n{len(findings)} finding(s), {blocking} 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 if __name__ == '__main__': # pragma: no cover
+5 -6
View File
@@ -79,14 +79,13 @@
</div> </div>
<div class="form-row"> <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> <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') }}"> <input type="text" id="discord_username" name="discord_username" value="{{ user.discord_username or '' }}" placeholder="{{ _('e.g. Name#1234') }}">
</div> <a href="{{ url_for('auth.discord_login') }}" class="btn btn-secondary mt-2">
<div class="form-group col-6"> <i class="fab fa-discord"></i>
<label for="discord_user_id"><i class="fab fa-discord"></i> Discord User ID <small>{{ _('(for DMs)') }}</small></label> {% if user.discord_user_id %}{{ _('Reconnect') }}{% else %}{{ _('Connect Discord Account') }}{% endif %}
<input type="text" id="discord_user_id" name="discord_user_id" value="{{ user.discord_user_id or '' }}" placeholder="{{ _('Numeric ID (e.g. 123456789012345678)') }}"> </a>
<small class="text-muted">{{ _('Enable Developer Mode in Discord → Right-click profile → Copy ID') }}</small>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
+2 -6
View File
@@ -193,9 +193,6 @@
<p class="text-muted">{{ _('Loading...') }}</p> <p class="text-muted">{{ _('Loading...') }}</p>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" data-action="save-disponibilities">
<i class="fas fa-save"></i> {{ _('Save Disponibilities') }}
</button>
<button type="button" class="btn btn-secondary" data-action="clear-disponibilities"> <button type="button" class="btn btn-secondary" data-action="clear-disponibilities">
<i class="fas fa-trash"></i> {{ _('Clear All') }} <i class="fas fa-trash"></i> {{ _('Clear All') }}
</button> </button>
@@ -369,7 +366,7 @@ function saveCoachAvailability() {
const msg = document.createElement('div'); const msg = document.createElement('div');
msg.className = 'alert alert-success'; msg.className = 'alert alert-success';
msg.style.marginTop = '10px'; msg.style.marginTop = '10px';
msg.innerHTML = '<i class="fas fa-check"></i> Availability saved!'; msg.innerHTML = '<span><i class="fas fa-check"></i> Availability saved!</span>';
document.getElementById('availability-grid').appendChild(msg); document.getElementById('availability-grid').appendChild(msg);
setTimeout(() => msg.remove(), 3000); setTimeout(() => msg.remove(), 3000);
} }
@@ -571,7 +568,7 @@ function saveDisponibilities() {
var msg = document.createElement('div'); var msg = document.createElement('div');
msg.className = 'alert alert-success'; msg.className = 'alert alert-success';
msg.style.marginTop = '10px'; msg.style.marginTop = '10px';
msg.innerHTML = '<i class="fas fa-check"></i> Disponibilities saved successfully!'; msg.innerHTML = '<span><i class="fas fa-check"></i> Disponibilities saved successfully!</span>';
document.getElementById('disponibilities-grid').appendChild(msg); document.getElementById('disponibilities-grid').appendChild(msg);
setTimeout(function() { msg.remove(); }, 3000); setTimeout(function() { msg.remove(); }, 3000);
} }
@@ -610,7 +607,6 @@ document.addEventListener('DOMContentLoaded', function() {
// dispatched by the delegated listener in main.js. This replaces inline // dispatched by the delegated listener in main.js. This replaces inline
// onclick attributes, which no CSP nonce is able to authorise. // onclick attributes, which no CSP nonce is able to authorise.
registerActions({ registerActions({
'save-disponibilities': saveDisponibilities,
'clear-disponibilities': clearDisponibilities, 'clear-disponibilities': clearDisponibilities,
'clear-availability': clearAllAvailability, 'clear-availability': clearAllAvailability,
}); });
-2
View File
@@ -57,8 +57,6 @@
<i class="fas fa-sync-alt"></i> {{ _('Reconnect') }} <i class="fas fa-sync-alt"></i> {{ _('Reconnect') }}
</a> </a>
</div> </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"> <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.') }} <i class="fas fa-check-circle"></i> {{ _('Discord connected. Game connections have been used to pre-fill your profile below.') }}
</small> </small>
Binary file not shown.
+164 -147
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: team-tryouts VERSION\n" "Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-08-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" "PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -23,8 +23,8 @@ msgstr ""
msgid "Please log in to access this page." msgid "Please log in to access this page."
msgstr "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/forms.py:37 app/routes/auth.py:228 app/routes/auth.py:387
#: app/routes/users/contracts.py:96 #: app/routes/users/contracts.py:98
#, python-format #, python-format
msgid "%(field)s: %(msg)s" msgid "%(field)s: %(msg)s"
msgstr "%(field)s: %(msg)s" msgstr "%(field)s: %(msg)s"
@@ -61,7 +61,7 @@ msgstr "Username is required."
msgid "Password is required." msgid "Password is required."
msgstr "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." msgid "Username must be 3-80 characters."
msgstr "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." msgid "Email must be 120 characters or less."
msgstr "Email must be 120 characters or less." msgstr "Email must be 120 characters or less."
#: app/validators.py:246 app/validators.py:314 app/validators.py:345 #: app/validators.py:246 app/validators.py:309 app/validators.py:340
#: app/validators.py:409 #: app/validators.py:403
msgid "Full name is required." msgid "Full name is required."
msgstr "Full name is required." msgstr "Full name is required."
#: app/validators.py:281 #: app/validators.py:276
msgid "Passwords do not match." msgid "Passwords do not match."
msgstr "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." msgid "Invalid role selected."
msgstr "Invalid role selected." msgstr "Invalid role selected."
#: app/validators.py:454 #: app/validators.py:443
msgid "Player must be selected." msgid "Player must be selected."
msgstr "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." msgid "Notes must be 2000 characters or less."
msgstr "Notes must be 2000 characters or less." msgstr "Notes must be 2000 characters or less."
#: app/validators.py:494 app/validators.py:853 #: app/validators.py:483 app/validators.py:854
msgid "Invalid coach selection." msgid "Invalid coach selection."
msgstr "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." msgid "Invalid manager selection."
msgstr "Invalid manager selection." msgstr "Invalid manager selection."
#: app/validators.py:511 #: app/validators.py:507
msgid "Invalid player selection." msgid "Invalid player selection."
msgstr "Invalid player selection." msgstr "Invalid player selection."
#: app/validators.py:515 #: app/validators.py:516
msgid "Unknown roster status." msgid "Unknown roster status."
msgstr "Unknown roster status." msgstr "Unknown roster status."
#: app/validators.py:538 #: app/validators.py:539
msgid "Date must be in YYYY-MM-DD format." msgid "Date must be in YYYY-MM-DD format."
msgstr "Date must be in YYYY-MM-DD format." msgstr "Date must be in YYYY-MM-DD format."
#: app/validators.py:539 #: app/validators.py:540
msgid "A date is required." msgid "A date is required."
msgstr "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." msgid "Start time must be in HH:MM format."
msgstr "Start time must be in HH:MM format." msgstr "Start time must be in HH:MM format."
#: app/validators.py:546 app/validators.py:611 #: app/validators.py:547 app/validators.py:612
msgid "A start time is required." msgid "A start time is required."
msgstr "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." msgid "End time must be in HH:MM format."
msgstr "End time must be in HH:MM format." msgstr "End time must be in HH:MM format."
#: app/validators.py:553 #: app/validators.py:554
msgid "An end time is required." msgid "An end time is required."
msgstr "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." msgid "Points must be 2000 characters or less."
msgstr "Points must be 2000 characters or less." msgstr "Points must be 2000 characters or less."
#: app/validators.py:572 #: app/validators.py:573
msgid "End time must be after start time." msgid "End time must be after start time."
msgstr "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)." msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Day must be 0 (Monday) to 6 (Sunday)." msgstr "Day must be 0 (Monday) to 6 (Sunday)."
#: app/validators.py:604 #: app/validators.py:605
msgid "A day is required." msgid "A day is required."
msgstr "A day is required." msgstr "A day is required."
#: app/validators.py:639 #: app/validators.py:640
msgid "Player selection is malformed." msgid "Player selection is malformed."
msgstr "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." msgid "A title is required."
msgstr "A title is required." msgstr "A title is required."
#: app/validators.py:674 #: app/validators.py:675
msgid "Invalid date format." msgid "Invalid date format."
msgstr "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." msgid "Invalid time format."
msgstr "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." msgid "Start time is required. Please select a time slot."
msgstr "Start time is required. Please select a time slot." msgstr "Start time is required. Please select a time slot."
#: app/validators.py:694 #: app/validators.py:695
msgid "Unknown match status." msgid "Unknown match status."
msgstr "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." msgid "The end time must come after the start time."
msgstr "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." msgid "Unknown match type."
msgstr "Unknown match type." msgstr "Unknown match type."
#: app/validators.py:736 #: app/validators.py:737
msgid "A team cannot play against itself." msgid "A team cannot play against itself."
msgstr "A team cannot play against itself." msgstr "A team cannot play against itself."
#: app/validators.py:784 #: app/validators.py:785
msgid "Unknown game." msgid "Unknown game."
msgstr "Unknown game." msgstr "Unknown game."
#: app/validators.py:789 #: app/validators.py:790
msgid "Invalid start date format." msgid "Invalid start date format."
msgstr "Invalid start date format." msgstr "Invalid start date format."
#: app/validators.py:790 #: app/validators.py:791
msgid "A start date is required." msgid "A start date is required."
msgstr "A start date is required." msgstr "A start date is required."
#: app/validators.py:796 #: app/validators.py:797
msgid "Invalid end date format." msgid "Invalid end date format."
msgstr "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." msgid "A tryout must allow at least one player."
msgstr "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." msgid "The player limit must be a whole number."
msgstr "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." msgid "End date cannot be before start date."
msgstr "End date cannot be before start date." msgstr "End date cannot be before start date."
#: app/validators.py:846 app/validators.py:847 #: app/validators.py:847 app/validators.py:848
msgid "Team name is required." msgid "Team name is required."
msgstr "Team name is required." msgstr "Team name is required."
#: app/validators.py:871 #: app/validators.py:872
msgid "Scores run from 1 to 10." msgid "Scores run from 1 to 10."
msgstr "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." msgid "A score must be a whole number from 1 to 10."
msgstr "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." msgid "This account has been deactivated."
msgstr "This account has been deactivated." msgstr "This account has been deactivated."
#: app/routes/auth.py:276 #: app/routes/auth.py:280
#, python-format #, python-format
msgid "Welcome back, %(username)s!" msgid "Welcome back, %(username)s!"
msgstr "Welcome back, %(username)s!" msgstr "Welcome back, %(username)s!"
#: app/routes/auth.py:306 #: app/routes/auth.py:310
msgid "" msgid ""
"Login unsuccessful. Please check your username and password, or ask a " "Login unsuccessful. Please check your username and password, or ask a "
"president for help." "president for help."
@@ -239,27 +239,32 @@ msgstr ""
"Login unsuccessful. Please check your username and password, or ask a " "Login unsuccessful. Please check your username and password, or ask a "
"president for help." "president for help."
#: app/routes/auth.py:363 #: app/routes/auth.py:376
msgid "Your registration could not be processed. Please try again." msgid "Your registration could not be processed. Please try again."
msgstr "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." msgid "Username already exists."
msgstr "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." msgid "Email already registered."
msgstr "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." msgid "Your account has been created! You can now log in."
msgstr "Your account has been created! You can now log in." msgstr "Your account has been created! You can now log in."
#: app/routes/auth.py:457 #: app/routes/auth.py:491
msgid "Discord OAuth2 is not configured." msgid "Discord OAuth2 is not configured."
msgstr "Discord OAuth2 is not configured." msgstr "Discord OAuth2 is not configured."
#: app/routes/auth.py:498 #: app/routes/auth.py:540
msgid "" msgid ""
"Discord authorization could not be verified. Please start the connection " "Discord authorization could not be verified. Please start the connection "
"again from this page." "again from this page."
@@ -267,27 +272,35 @@ msgstr ""
"Discord authorization could not be verified. Please start the connection " "Discord authorization could not be verified. Please start the connection "
"again from this page." "again from this page."
#: app/routes/auth.py:507 #: app/routes/auth.py:549
msgid "Discord authorization failed. No code received." msgid "Discord authorization failed. No code received."
msgstr "Discord authorization failed. No code received." msgstr "Discord authorization failed. No code received."
#: app/routes/auth.py:531 #: app/routes/auth.py:573
msgid "Failed to connect to Discord. Please try again." msgid "Failed to connect to Discord. Please try again."
msgstr "Failed to connect to Discord. Please try again." msgstr "Failed to connect to Discord. Please try again."
#: app/routes/auth.py:535 #: app/routes/auth.py:577
msgid "Failed to obtain Discord access token." msgid "Failed to obtain Discord access token."
msgstr "Failed to obtain Discord access token." msgstr "Failed to obtain Discord access token."
#: app/routes/auth.py:550 #: app/routes/auth.py:592 app/routes/auth.py:602
msgid "Failed to fetch Discord user profile." msgid "Failed to fetch Discord user profile."
msgstr "Failed to fetch Discord user profile." msgstr "Failed to fetch Discord user profile."
#: app/routes/auth.py: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." msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Discord account connected! Your profile has been pre-filled." msgstr "Discord account connected! Your profile has been pre-filled."
#: app/routes/auth.py:625 #: app/routes/auth.py:703
msgid "You have been logged out." msgid "You have been logged out."
msgstr "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: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/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:460 app/routes/tryouts.py:480
#: app/routes/tryouts.py:519 app/routes/tryouts.py:555 #: app/routes/tryouts.py:527 app/routes/tryouts.py:563
#: app/routes/tryouts.py:574 #: app/routes/tryouts.py:582
msgid "Permission denied." msgid "Permission denied."
msgstr "Permission denied." msgstr "Permission denied."
@@ -463,7 +476,7 @@ msgstr "Coach removed from %(name)s."
msgid "Manager removed from %(name)s." msgid "Manager removed from %(name)s."
msgstr "Manager removed from %(name)s." msgstr "Manager removed from %(name)s."
#: app/routes/teams.py: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." msgid "Please select a player."
msgstr "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." msgid "You are already registered for this tryout."
msgstr "You are already registered for this tryout." msgstr "You are already registered for this tryout."
#: app/routes/tryouts.py:428 app/routes/tryouts.py:503 #: app/routes/tryouts.py:428 app/routes/tryouts.py:511
msgid "This tryout is full." msgid "This tryout is full."
msgstr "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." msgid "Registration status updated."
msgstr "Registration status updated." msgstr "Registration status updated."
#: app/routes/tryouts.py:489 #: app/routes/tryouts.py:497
msgid "Can only register players." msgid "Can only register players."
msgstr "Can only register players." msgstr "Can only register players."
#: app/routes/tryouts.py:495 #: app/routes/tryouts.py:503
#, python-format #, python-format
msgid "%(username)s is already registered for this tryout." msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s is already registered for this tryout." msgstr "%(username)s is already registered for this tryout."
#: app/routes/tryouts.py:509 #: app/routes/tryouts.py:517
#, python-format #, python-format
msgid "%(username)s registered for tryout!" msgid "%(username)s registered for tryout!"
msgstr "%(username)s registered for tryout!" msgstr "%(username)s registered for tryout!"
#: app/routes/tryouts.py:545 #: app/routes/tryouts.py:553
#, python-format #, python-format
msgid "%(username)s removed from tryout." msgid "%(username)s removed from tryout."
msgstr "%(username)s removed from tryout." msgstr "%(username)s removed from tryout."
#: app/routes/tryouts.py:563 #: app/routes/tryouts.py:571
#, python-format #, python-format
msgid "Team \"%(team_name)s\" created!" msgid "Team \"%(team_name)s\" created!"
msgstr "Team \"%(team_name)s\" created!" msgstr "Team \"%(team_name)s\" created!"
#: app/routes/tryouts.py:594 #: app/routes/tryouts.py:602
msgid "That player is not registered for this tryout." msgid "That player is not registered for this tryout."
msgstr "That player is not registered for this tryout." msgstr "That player is not registered for this tryout."
#: app/routes/tryouts.py:600 #: app/routes/tryouts.py:608
msgid "Player is already on this team." msgid "Player is already on this team."
msgstr "Player is already on this team." msgstr "Player is already on this team."
#: app/routes/tryouts.py:605 #: app/routes/tryouts.py:613
msgid "Player added to team!" msgid "Player added to team!"
msgstr "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." msgid "You do not have permission to delete this tryout."
msgstr "You do not have permission to delete this tryout." msgstr "You do not have permission to delete this tryout."
#: app/routes/tryouts.py:651 #: app/routes/tryouts.py:659
msgid "Tryout deleted successfully." msgid "Tryout deleted successfully."
msgstr "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." msgid "Email already in use by another account."
msgstr "Email already in use by another account." msgstr "Email already in use by another account."
#: app/routes/users/accounts.py:122 #: app/routes/users/accounts.py:132
msgid "You cannot change your own role. Ask another president to do it." msgid "You cannot change your own role. Ask another president to do it."
msgstr "You cannot change your own role. Ask another president to do it." msgstr "You cannot change your own role. Ask another president to do it."
#: app/routes/users/accounts.py:135 #: app/routes/users/accounts.py:145
msgid "" msgid ""
"This is the last active president. Promote another account before " "This is the last active president. Promote another account before "
"changing this one." "changing this one."
@@ -642,29 +655,29 @@ msgstr ""
"This is the last active president. Promote another account before " "This is the last active president. Promote another account before "
"changing this one." "changing this one."
#: app/routes/users/accounts.py:214 #: app/routes/users/accounts.py:224
#, python-format #, python-format
msgid "User %(username)s updated successfully!" msgid "User %(username)s updated successfully!"
msgstr "User %(username)s updated successfully!" msgstr "User %(username)s updated successfully!"
#: app/routes/users/accounts.py:235 #: app/routes/users/accounts.py:245
msgid "Only the president can delete users." msgid "Only the president can delete users."
msgstr "Only the president can delete users." msgstr "Only the president can delete users."
#: app/routes/users/accounts.py:239 #: app/routes/users/accounts.py:249
msgid "You cannot delete your own account." msgid "You cannot delete your own account."
msgstr "You cannot delete your own account." msgstr "You cannot delete your own account."
#: app/routes/users/accounts.py:298 #: app/routes/users/accounts.py:308
#, python-format #, python-format
msgid "User %(deleted_username)s has been removed." msgid "User %(deleted_username)s has been removed."
msgstr "User %(deleted_username)s has been removed." msgstr "User %(deleted_username)s has been removed."
#: app/routes/users/accounts.py:309 #: app/routes/users/accounts.py:319
msgid "Only the president can create users." msgid "Only the president can create users."
msgstr "Only the president can create users." msgstr "Only the president can create users."
#: app/routes/users/accounts.py:357 #: app/routes/users/accounts.py:367
#, python-format #, python-format
msgid "User %(full_name)s created as %(role)s!" msgid "User %(full_name)s created as %(role)s!"
msgstr "User %(full_name)s created as %(role)s!" msgstr "User %(full_name)s created as %(role)s!"
@@ -673,32 +686,32 @@ msgstr "User %(full_name)s created as %(role)s!"
msgid "Only coaches can manage availability." msgid "Only coaches can manage availability."
msgstr "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." msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Only presidents, managers, and coaches can upload contracts." msgstr "Only presidents, managers, and coaches can upload contracts."
#: app/routes/users/contracts.py:103 #: app/routes/users/contracts.py:105
msgid "You do not have permission to upload a contract for this player." msgid "You do not have permission to upload a contract for this player."
msgstr "You do not have permission to upload a contract for this player." msgstr "You do not have permission to upload a contract for this player."
#: app/routes/users/contracts.py:143 #: app/routes/users/contracts.py:145
#, python-format #, python-format
msgid "Contract uploaded successfully for %(username)s!" msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contract uploaded successfully for %(username)s!" msgstr "Contract uploaded successfully for %(username)s!"
#: app/routes/users/contracts.py:157 #: app/routes/users/contracts.py:159
msgid "Only the player can upload their signed contract." msgid "Only the player can upload their signed contract."
msgstr "Only the player can upload their signed contract." msgstr "Only the player can upload their signed contract."
#: app/routes/users/contracts.py:175 #: app/routes/users/contracts.py:177
msgid "Signed contract uploaded successfully!" msgid "Signed contract uploaded successfully!"
msgstr "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." msgid "You do not have permission to download this contract."
msgstr "You do not have permission to download this contract." msgstr "You do not have permission to download this contract."
#: app/routes/users/contracts.py:203 #: app/routes/users/contracts.py:205
msgid "No signed contract available." msgid "No signed contract available."
msgstr "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." msgid "One on One request from %(player)s has been rejected."
msgstr "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." msgid "Username already taken."
msgstr "Username already taken." msgstr "Username already taken."
#: app/routes/users/profile.py:93 #: app/routes/users/profile.py:92
msgid "Email already in use." msgid "Email already in use."
msgstr "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!" msgid "Profile updated successfully!"
msgstr "Profile updated successfully!" msgstr "Profile updated successfully!"
@@ -953,7 +966,7 @@ msgstr "%(total)s in total"
msgid "Next" msgid "Next"
msgstr "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 #: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3
msgid "Dashboard" msgid "Dashboard"
msgstr "Dashboard" msgstr "Dashboard"
@@ -995,38 +1008,34 @@ msgid "My Notes"
msgstr "My Notes" msgstr "My Notes"
#: app/templates/layouts/base.html:106 #: app/templates/layouts/base.html:106
msgid "Availability"
msgstr "Availability"
#: app/templates/layouts/base.html:112
msgid "Notes & One on One" msgid "Notes & One on One"
msgstr "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 #: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9
msgid "Contracts" msgid "Contracts"
msgstr "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 #: app/templates/pages/profile.html:3
msgid "My Profile" msgid "My Profile"
msgstr "My Profile" msgstr "My Profile"
#: app/templates/layouts/base.html:137 #: app/templates/layouts/base.html:131
msgid "Logout" msgid "Logout"
msgstr "Logout" msgstr "Logout"
#: app/templates/layouts/base.html:158 #: app/templates/layouts/base.html:152
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "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" msgid "Dismiss"
msgstr "Dismiss" msgstr "Dismiss"
#: app/templates/layouts/base.html:199 #: app/templates/layouts/base.html:193
msgid "Team Tryout Management System" msgid "UdeS team manager"
msgstr "Team Tryout Management System" msgstr "UdeS team manager"
#: app/templates/layouts/macros.html:116 #: app/templates/layouts/macros.html:116
msgid "Close" msgid "Close"
@@ -1209,6 +1218,10 @@ msgid "Loading availability grid..."
msgstr "Loading availability grid..." msgstr "Loading availability grid..."
#: app/templates/pages/coach_availability.html:19 #: 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 #: app/templates/pages/profile.html:200 app/templates/pages/profile.html:220
msgid "Clear All" msgid "Clear All"
msgstr "Clear All" msgstr "Clear All"
@@ -1372,7 +1385,7 @@ msgid "Role"
msgstr "Role" msgstr "Role"
#: app/templates/pages/create_user.html:41 app/templates/pages/login.html:11 #: 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" msgid "Password"
msgstr "Password" msgstr "Password"
@@ -1535,7 +1548,7 @@ msgstr "Edit Profile"
#: app/templates/pages/edit_profile.html:33 #: app/templates/pages/edit_profile.html:33
#: app/templates/pages/edit_user.html:43 app/templates/pages/profile.html:63 #: 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" msgid "E-Sports Profile"
msgstr "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." msgstr "Update your competitive gaming profile for tryouts."
#: app/templates/pages/edit_profile.html:37 #: app/templates/pages/edit_profile.html:37
#: app/templates/pages/register.html:87 #: app/templates/pages/register.html:85
msgid "Games You Play" msgid "Games You Play"
msgstr "Games You Play" msgstr "Games You Play"
#: app/templates/pages/edit_profile.html:47 #: 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." msgid "Select all games you're signing in for."
msgstr "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" msgstr "e.g. Name#1234"
#: app/templates/pages/edit_profile.html:87 #: app/templates/pages/edit_profile.html:87
#: app/templates/pages/edit_user.html:95 #: app/templates/pages/register.html:57
msgid "(for DMs)" msgid "Reconnect"
msgstr "(for DMs)" msgstr "Reconnect"
#: app/templates/pages/edit_profile.html:88 #: app/templates/pages/edit_profile.html:87
#: app/templates/pages/edit_user.html:96 #: app/templates/pages/register.html:67
msgid "Numeric ID (e.g. 123456789012345678)" msgid "Connect Discord Account"
msgstr "Numeric ID (e.g. 123456789012345678)" msgstr "Connect Discord Account"
#: app/templates/pages/edit_profile.html:89 #: app/templates/pages/edit_profile.html:93
#: 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_user.html:100 #: app/templates/pages/edit_user.html:100
msgid "League OS Connection" msgid "League OS Connection"
msgstr "League OS Connection" msgstr "League OS Connection"
#: app/templates/pages/edit_profile.html:95 #: app/templates/pages/edit_profile.html:94
#: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:131 #: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:129
msgid "League OS profile link or ID" msgid "League OS profile link or ID"
msgstr "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" msgid "Change Password"
msgstr "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." msgid "Leave blank to keep your current password."
msgstr "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" msgid "New Password"
msgstr "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 #: app/templates/pages/edit_user.html:107
msgid "Enter new password" msgid "Enter new password"
msgstr "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" msgid "Save Changes"
msgstr "Save Changes" msgstr "Save Changes"
@@ -1650,6 +1658,18 @@ msgstr "Games"
msgid "Enter gamertag for each selected game to link to Tracker Network." msgid "Enter gamertag for each selected game to link to Tracker Network."
msgstr "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 #: app/templates/pages/edit_user.html:106
msgid "(leave blank to keep current)" msgid "(leave blank to keep current)"
msgstr "(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 " "Select time slots when you're available for One on One sessions (8am to "
"10pm)." "10pm)."
#: app/templates/pages/profile.html:454 #: app/templates/pages/profile.html:460
msgid "Click or click-and-drag to select your available hours" msgid "Click or click-and-drag to select your available hours"
msgstr "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" msgid "Connected"
msgstr "Connected" msgstr "Connected"
#: app/templates/pages/register.html:57 #: app/templates/pages/register.html:61
msgid "Reconnect"
msgstr "Reconnect"
#: app/templates/pages/register.html:63
msgid "" msgid ""
"Discord connected. Game connections have been used to pre-fill your " "Discord connected. Game connections have been used to pre-fill your "
"profile below." "profile below."
@@ -2426,50 +2442,46 @@ msgstr ""
"profile below." "profile below."
#: app/templates/pages/register.html:69 #: 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." 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." 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)" msgid "Discord Username (Manual)"
msgstr "Discord Username (Manual)" msgstr "Discord Username (Manual)"
#: app/templates/pages/register.html:75 #: app/templates/pages/register.html:73
msgid "e.g. YourName" msgid "e.g. YourName"
msgstr "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." msgid "Set up your competitive gaming profile for tryouts."
msgstr "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)" msgid "League OS Connection (Optional)"
msgstr "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." msgid "Connect your League OS profile for organized play."
msgstr "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" msgid "Security"
msgstr "Security" msgstr "Security"
#: app/templates/pages/register.html:140 #: app/templates/pages/register.html:138
msgid "Create a password" msgid "Create a password"
msgstr "Create a password" msgstr "Create a password"
#: app/templates/pages/register.html:144 #: app/templates/pages/register.html:142
msgid "Confirm Password" msgid "Confirm Password"
msgstr "Confirm Password" msgstr "Confirm Password"
#: app/templates/pages/register.html:145 #: app/templates/pages/register.html:143
msgid "Confirm your password" msgid "Confirm your password"
msgstr "Confirm your password" msgstr "Confirm your password"
#: app/templates/pages/register.html:159 #: app/templates/pages/register.html:157
msgid "Create Account" msgid "Create Account"
msgstr "Create Account" msgstr "Create Account"
@@ -2984,3 +2996,8 @@ msgstr "View Profile"
#~ msgid "Invalid date or time format." #~ msgid "Invalid date or time format."
#~ msgstr "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 "" msgstr ""
"Project-Id-Version: team-tryouts VERSION\n" "Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-08-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" "PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n" "Language: fr\n"
@@ -23,8 +23,8 @@ msgstr ""
msgid "Please log in to access this page." msgid "Please log in to access this page."
msgstr "Veuillez vous connecter pour accéder à cette 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/forms.py:37 app/routes/auth.py:228 app/routes/auth.py:387
#: app/routes/users/contracts.py:96 #: app/routes/users/contracts.py:98
#, python-format #, python-format
msgid "%(field)s: %(msg)s" msgid "%(field)s: %(msg)s"
msgstr "%(field)s : %(msg)s" msgstr "%(field)s : %(msg)s"
@@ -63,7 +63,7 @@ msgstr "Le nom dutilisateur est obligatoire."
msgid "Password is required." msgid "Password is required."
msgstr "Le mot de passe est obligatoire." msgstr "Le mot de passe est obligatoire."
#: app/validators.py:227 app/validators.py:299 #: app/validators.py:227 app/validators.py:294
msgid "Username must be 3-80 characters." msgid "Username must be 3-80 characters."
msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères." msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères."
@@ -71,169 +71,169 @@ msgstr "Le nom dutilisateur doit compter de 3 à 80 caractères."
msgid "Email must be 120 characters or less." msgid "Email must be 120 characters or less."
msgstr "Ladresse courriel ne doit pas dépasser 120 caractères." msgstr "Ladresse courriel ne doit pas dépasser 120 caractères."
#: app/validators.py:246 app/validators.py:314 app/validators.py:345 #: app/validators.py:246 app/validators.py:309 app/validators.py:340
#: app/validators.py:409 #: app/validators.py:403
msgid "Full name is required." msgid "Full name is required."
msgstr "Le nom complet est obligatoire." msgstr "Le nom complet est obligatoire."
#: app/validators.py:281 #: app/validators.py:276
msgid "Passwords do not match." msgid "Passwords do not match."
msgstr "Les mots de passe ne concordent pas." msgstr "Les mots de passe ne concordent pas."
#: app/validators.py:318 app/validators.py:353 #: app/validators.py:313 app/validators.py:348
msgid "Invalid role selected." msgid "Invalid role selected."
msgstr "Rôle sélectionné invalide." msgstr "Rôle sélectionné invalide."
#: app/validators.py:454 #: app/validators.py:443
msgid "Player must be selected." msgid "Player must be selected."
msgstr "Vous devez choisir un joueur." msgstr "Vous devez choisir un joueur."
#: app/validators.py:457 #: app/validators.py:446
msgid "Notes must be 2000 characters or less." msgid "Notes must be 2000 characters or less."
msgstr "Les notes ne doivent pas dépasser 2000 caractères." msgstr "Les notes ne doivent pas dépasser 2000 caractères."
#: app/validators.py:494 app/validators.py:853 #: app/validators.py:483 app/validators.py:854
msgid "Invalid coach selection." msgid "Invalid coach selection."
msgstr "Sélection de coach invalide." 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." msgid "Invalid manager selection."
msgstr "Sélection de gérant invalide." msgstr "Sélection de gérant invalide."
#: app/validators.py:511 #: app/validators.py:507
msgid "Invalid player selection." msgid "Invalid player selection."
msgstr "Sélection de joueur invalide." msgstr "Sélection de joueur invalide."
#: app/validators.py:515 #: app/validators.py:516
msgid "Unknown roster status." msgid "Unknown roster status."
msgstr "Statut d'effectif inconnu." msgstr "Statut d'effectif inconnu."
#: app/validators.py:538 #: app/validators.py:539
msgid "Date must be in YYYY-MM-DD format." msgid "Date must be in YYYY-MM-DD format."
msgstr "La date doit être au format AAAA-MM-JJ." msgstr "La date doit être au format AAAA-MM-JJ."
#: app/validators.py:539 #: app/validators.py:540
msgid "A date is required." msgid "A date is required."
msgstr "Une date est requise." 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." msgid "Start time must be in HH:MM format."
msgstr "Lheure de début doit être au format HH:MM." msgstr "Lheure de début doit être au format HH:MM."
#: app/validators.py:546 app/validators.py:611 #: app/validators.py:547 app/validators.py:612
msgid "A start time is required." msgid "A start time is required."
msgstr "Une heure de début est requise." 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." msgid "End time must be in HH:MM format."
msgstr "Lheure de fin doit être au format HH:MM." msgstr "Lheure de fin doit être au format HH:MM."
#: app/validators.py:553 #: app/validators.py:554
msgid "An end time is required." msgid "An end time is required."
msgstr "Une heure de fin est requise." msgstr "Une heure de fin est requise."
#: app/validators.py:557 #: app/validators.py:558
msgid "Points must be 2000 characters or less." msgid "Points must be 2000 characters or less."
msgstr "Les points ne doivent pas dépasser 2000 caractères." msgstr "Les points ne doivent pas dépasser 2000 caractères."
#: app/validators.py:572 #: app/validators.py:573
msgid "End time must be after start time." msgid "End time must be after start time."
msgstr "L'heure de fin doit être postérieure à l'heure de début." 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)." msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)." msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
#: app/validators.py:604 #: app/validators.py:605
msgid "A day is required." msgid "A day is required."
msgstr "Un jour est requis." msgstr "Un jour est requis."
#: app/validators.py:639 #: app/validators.py:640
msgid "Player selection is malformed." msgid "Player selection is malformed."
msgstr "La sélection de joueurs est mal formée." 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." msgid "A title is required."
msgstr "Un titre est requis." msgstr "Un titre est requis."
#: app/validators.py:674 #: app/validators.py:675
msgid "Invalid date format." msgid "Invalid date format."
msgstr "Format de date invalide." 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." msgid "Invalid time format."
msgstr "Format dheure invalide." msgstr "Format dheure invalide."
#: app/validators.py:680 #: app/validators.py:681
msgid "Start time is required. Please select a time slot." msgid "Start time is required. Please select a time slot."
msgstr "Lheure de début est obligatoire. Choisissez une plage horaire." msgstr "Lheure de début est obligatoire. Choisissez une plage horaire."
#: app/validators.py:694 #: app/validators.py:695
msgid "Unknown match status." msgid "Unknown match status."
msgstr "Statut de match inconnu." msgstr "Statut de match inconnu."
#: app/validators.py:710 #: app/validators.py:711
msgid "The end time must come after the start time." msgid "The end time must come after the start time."
msgstr "L'heure de fin doit être postérieure à l'heure de début." 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." msgid "Unknown match type."
msgstr "Type de match inconnu." msgstr "Type de match inconnu."
#: app/validators.py:736 #: app/validators.py:737
msgid "A team cannot play against itself." msgid "A team cannot play against itself."
msgstr "Une équipe ne peut pas jouer contre elle-même." msgstr "Une équipe ne peut pas jouer contre elle-même."
#: app/validators.py:784 #: app/validators.py:785
msgid "Unknown game." msgid "Unknown game."
msgstr "Jeu inconnu." msgstr "Jeu inconnu."
#: app/validators.py:789 #: app/validators.py:790
msgid "Invalid start date format." msgid "Invalid start date format."
msgstr "Format de date de début invalide." msgstr "Format de date de début invalide."
#: app/validators.py:790 #: app/validators.py:791
msgid "A start date is required." msgid "A start date is required."
msgstr "Une date de début est requise." msgstr "Une date de début est requise."
#: app/validators.py:796 #: app/validators.py:797
msgid "Invalid end date format." msgid "Invalid end date format."
msgstr "Format de date de fin invalide." msgstr "Format de date de fin invalide."
#: app/validators.py:804 #: app/validators.py:805
msgid "A tryout must allow at least one player." msgid "A tryout must allow at least one player."
msgstr "Une sélection doit accepter au moins un joueur." 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." msgid "The player limit must be a whole number."
msgstr "La limite de joueurs doit être un nombre entier." 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." msgid "End date cannot be before start date."
msgstr "La date de fin ne peut pas précéder la date de début." msgstr "La date de fin ne peut pas précéder la date de début."
#: app/validators.py:846 app/validators.py:847 #: app/validators.py:847 app/validators.py:848
msgid "Team name is required." msgid "Team name is required."
msgstr "Le nom de l’équipe est obligatoire." msgstr "Le nom de l’équipe est obligatoire."
#: app/validators.py:871 #: app/validators.py:872
msgid "Scores run from 1 to 10." msgid "Scores run from 1 to 10."
msgstr "Les notes vont de 1 à 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." msgid "A score must be a whole number from 1 to 10."
msgstr "Une note doit être un nombre entier de 1 à 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." msgid "This account has been deactivated."
msgstr "Ce compte a été désactivé." msgstr "Ce compte a été désactivé."
#: app/routes/auth.py:276 #: app/routes/auth.py:280
#, python-format #, python-format
msgid "Welcome back, %(username)s!" msgid "Welcome back, %(username)s!"
msgstr "Bon retour, %(username)s !" msgstr "Bon retour, %(username)s !"
#: app/routes/auth.py:306 #: app/routes/auth.py:310
msgid "" msgid ""
"Login unsuccessful. Please check your username and password, or ask a " "Login unsuccessful. Please check your username and password, or ask a "
"president for help." "president for help."
@@ -241,27 +241,32 @@ msgstr ""
"Échec de la connexion. Vérifiez le nom dutilisateur et le mot de passe, " "Échec de la connexion. Vérifiez le nom dutilisateur et le mot de passe, "
"ou demandez de laide à un président." "ou demandez de laide à un président."
#: app/routes/auth.py:363 #: app/routes/auth.py:376
msgid "Your registration could not be processed. Please try again." msgid "Your registration could not be processed. Please try again."
msgstr "Votre inscription n'a pas pu être traitée. Veuillez réessayer." 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." msgid "Username already exists."
msgstr "Ce nom dutilisateur est déjà pris." 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." msgid "Email already registered."
msgstr "Cette adresse courriel est déjà enregistrée." msgstr "Cette adresse courriel est déjà enregistrée."
#: app/routes/auth.py: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." msgid "Your account has been created! You can now log in."
msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter." msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter."
#: app/routes/auth.py:457 #: app/routes/auth.py:491
msgid "Discord OAuth2 is not configured." msgid "Discord OAuth2 is not configured."
msgstr "La connexion Discord nest pas configurée." msgstr "La connexion Discord nest pas configurée."
#: app/routes/auth.py:498 #: app/routes/auth.py:540
msgid "" msgid ""
"Discord authorization could not be verified. Please start the connection " "Discord authorization could not be verified. Please start the connection "
"again from this page." "again from this page."
@@ -269,27 +274,35 @@ msgstr ""
"Lautorisation Discord na pas pu être vérifiée. Relancez la connexion " "Lautorisation Discord na pas pu être vérifiée. Relancez la connexion "
"depuis cette page." "depuis cette page."
#: app/routes/auth.py:507 #: app/routes/auth.py:549
msgid "Discord authorization failed. No code received." msgid "Discord authorization failed. No code received."
msgstr "Lautorisation Discord a échoué : aucun code reçu." msgstr "Lautorisation Discord a échoué : aucun code reçu."
#: app/routes/auth.py:531 #: app/routes/auth.py:573
msgid "Failed to connect to Discord. Please try again." msgid "Failed to connect to Discord. Please try again."
msgstr "Impossible de joindre Discord. Veuillez réessayer." msgstr "Impossible de joindre Discord. Veuillez réessayer."
#: app/routes/auth.py:535 #: app/routes/auth.py:577
msgid "Failed to obtain Discord access token." msgid "Failed to obtain Discord access token."
msgstr "Impossible dobtenir le jeton daccès Discord." msgstr "Impossible dobtenir le jeton daccès Discord."
#: app/routes/auth.py:550 #: app/routes/auth.py:592 app/routes/auth.py:602
msgid "Failed to fetch Discord user profile." msgid "Failed to fetch Discord user profile."
msgstr "Impossible de récupérer le profil Discord." msgstr "Impossible de récupérer le profil Discord."
#: app/routes/auth.py: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." msgid "Discord account connected! Your profile has been pre-filled."
msgstr "Compte Discord connecté. Votre profil a été pré-rempli." msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
#: app/routes/auth.py:625 #: app/routes/auth.py:703
msgid "You have been logged out." msgid "You have been logged out."
msgstr "Vous avez été déconnecté." 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: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/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:460 app/routes/tryouts.py:480
#: app/routes/tryouts.py:519 app/routes/tryouts.py:555 #: app/routes/tryouts.py:527 app/routes/tryouts.py:563
#: app/routes/tryouts.py:574 #: app/routes/tryouts.py:582
msgid "Permission denied." msgid "Permission denied."
msgstr "Accès refusé." msgstr "Accès refusé."
@@ -467,7 +480,7 @@ msgstr "Coach retiré de %(name)s."
msgid "Manager removed from %(name)s." msgid "Manager removed from %(name)s."
msgstr "Gérant retiré de %(name)s." msgstr "Gérant retiré de %(name)s."
#: app/routes/teams.py: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." msgid "Please select a player."
msgstr "Veuillez choisir un joueur." msgstr "Veuillez choisir un joueur."
@@ -549,7 +562,7 @@ msgstr "Cette sélection naccepte pas dinscriptions."
msgid "You are already registered for this tryout." msgid "You are already registered for this tryout."
msgstr "Vous êtes déjà inscrit à cette sélection." msgstr "Vous êtes déjà inscrit à cette sélection."
#: app/routes/tryouts.py:428 app/routes/tryouts.py:503 #: app/routes/tryouts.py:428 app/routes/tryouts.py:511
msgid "This tryout is full." msgid "This tryout is full."
msgstr "Cette sélection est complète." 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." msgid "Registration status updated."
msgstr "Statut dinscription mis à jour." msgstr "Statut dinscription mis à jour."
#: app/routes/tryouts.py:489 #: app/routes/tryouts.py:497
msgid "Can only register players." msgid "Can only register players."
msgstr "Seuls des joueurs peuvent être inscrits." msgstr "Seuls des joueurs peuvent être inscrits."
#: app/routes/tryouts.py:495 #: app/routes/tryouts.py:503
#, python-format #, python-format
msgid "%(username)s is already registered for this tryout." msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s est déjà inscrit à cette sélection." msgstr "%(username)s est déjà inscrit à cette sélection."
#: app/routes/tryouts.py:509 #: app/routes/tryouts.py:517
#, python-format #, python-format
msgid "%(username)s registered for tryout!" msgid "%(username)s registered for tryout!"
msgstr "%(username)s est inscrit à la sélection." msgstr "%(username)s est inscrit à la sélection."
#: app/routes/tryouts.py:545 #: app/routes/tryouts.py:553
#, python-format #, python-format
msgid "%(username)s removed from tryout." msgid "%(username)s removed from tryout."
msgstr "%(username)s a été retiré de la sélection." msgstr "%(username)s a été retiré de la sélection."
#: app/routes/tryouts.py:563 #: app/routes/tryouts.py:571
#, python-format #, python-format
msgid "Team \"%(team_name)s\" created!" msgid "Team \"%(team_name)s\" created!"
msgstr "Équipe « %(team_name)s » créée." msgstr "Équipe « %(team_name)s » créée."
#: app/routes/tryouts.py:594 #: app/routes/tryouts.py:602
msgid "That player is not registered for this tryout." msgid "That player is not registered for this tryout."
msgstr "Ce joueur nest pas inscrit à cette sélection." msgstr "Ce joueur nest pas inscrit à cette sélection."
#: app/routes/tryouts.py:600 #: app/routes/tryouts.py:608
msgid "Player is already on this team." msgid "Player is already on this team."
msgstr "Ce joueur est déjà dans cette équipe." msgstr "Ce joueur est déjà dans cette équipe."
#: app/routes/tryouts.py:605 #: app/routes/tryouts.py:613
msgid "Player added to team!" msgid "Player added to team!"
msgstr "Joueur ajouté à l’équipe." 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." msgid "You do not have permission to delete this tryout."
msgstr "Vous navez pas les droits pour supprimer cette sélection." msgstr "Vous navez pas les droits pour supprimer cette sélection."
#: app/routes/tryouts.py:651 #: app/routes/tryouts.py:659
msgid "Tryout deleted successfully." msgid "Tryout deleted successfully."
msgstr "Sélection supprimée." 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." msgid "Email already in use by another account."
msgstr "Cette adresse courriel est déjà utilisée par un autre compte." msgstr "Cette adresse courriel est déjà utilisée par un autre compte."
#: app/routes/users/accounts.py:122 #: app/routes/users/accounts.py:132
msgid "You cannot change your own role. Ask another president to do it." msgid "You cannot change your own role. Ask another president to do it."
msgstr "" msgstr ""
"Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre " "Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre "
"président de le faire." "président de le faire."
#: app/routes/users/accounts.py:135 #: app/routes/users/accounts.py:145
msgid "" msgid ""
"This is the last active president. Promote another account before " "This is the last active president. Promote another account before "
"changing this one." "changing this one."
@@ -648,29 +661,29 @@ msgstr ""
"Cest le dernier président actif. Promouvez un autre compte avant de " "Cest le dernier président actif. Promouvez un autre compte avant de "
"modifier celui-ci." "modifier celui-ci."
#: app/routes/users/accounts.py:214 #: app/routes/users/accounts.py:224
#, python-format #, python-format
msgid "User %(username)s updated successfully!" msgid "User %(username)s updated successfully!"
msgstr "Utilisateur %(username)s mis à jour." msgstr "Utilisateur %(username)s mis à jour."
#: app/routes/users/accounts.py:235 #: app/routes/users/accounts.py:245
msgid "Only the president can delete users." msgid "Only the president can delete users."
msgstr "Seul le président peut supprimer des utilisateurs." msgstr "Seul le président peut supprimer des utilisateurs."
#: app/routes/users/accounts.py:239 #: app/routes/users/accounts.py:249
msgid "You cannot delete your own account." msgid "You cannot delete your own account."
msgstr "Vous ne pouvez pas supprimer votre propre compte." msgstr "Vous ne pouvez pas supprimer votre propre compte."
#: app/routes/users/accounts.py:298 #: app/routes/users/accounts.py:308
#, python-format #, python-format
msgid "User %(deleted_username)s has been removed." msgid "User %(deleted_username)s has been removed."
msgstr "Lutilisateur %(deleted_username)s a été supprimé." msgstr "Lutilisateur %(deleted_username)s a été supprimé."
#: app/routes/users/accounts.py:309 #: app/routes/users/accounts.py:319
msgid "Only the president can create users." msgid "Only the president can create users."
msgstr "Seul le président peut créer des utilisateurs." msgstr "Seul le président peut créer des utilisateurs."
#: app/routes/users/accounts.py:357 #: app/routes/users/accounts.py:367
#, python-format #, python-format
msgid "User %(full_name)s created as %(role)s!" msgid "User %(full_name)s created as %(role)s!"
msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s." msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
@@ -679,32 +692,32 @@ msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
msgid "Only coaches can manage availability." msgid "Only coaches can manage availability."
msgstr "Seuls les coachs peuvent gérer leurs disponibilités." 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." msgid "Only presidents, managers, and coaches can upload contracts."
msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat." msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat."
#: app/routes/users/contracts.py:103 #: app/routes/users/contracts.py:105
msgid "You do not have permission to upload a contract for this player." msgid "You do not have permission to upload a contract for this player."
msgstr "Vous navez pas les droits pour téléverser un contrat pour ce joueur." msgstr "Vous navez pas les droits pour téléverser un contrat pour ce joueur."
#: app/routes/users/contracts.py:143 #: app/routes/users/contracts.py:145
#, python-format #, python-format
msgid "Contract uploaded successfully for %(username)s!" msgid "Contract uploaded successfully for %(username)s!"
msgstr "Contrat téléversé pour %(username)s." msgstr "Contrat téléversé pour %(username)s."
#: app/routes/users/contracts.py:157 #: app/routes/users/contracts.py:159
msgid "Only the player can upload their signed contract." msgid "Only the player can upload their signed contract."
msgstr "Seul le joueur peut téléverser son contrat signé." msgstr "Seul le joueur peut téléverser son contrat signé."
#: app/routes/users/contracts.py:175 #: app/routes/users/contracts.py:177
msgid "Signed contract uploaded successfully!" msgid "Signed contract uploaded successfully!"
msgstr "Contrat signé téléversé." msgstr "Contrat signé téléversé."
#: app/routes/users/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." msgid "You do not have permission to download this contract."
msgstr "Vous navez pas les droits pour télécharger ce contrat." msgstr "Vous navez pas les droits pour télécharger ce contrat."
#: app/routes/users/contracts.py:203 #: app/routes/users/contracts.py:205
msgid "No signed contract available." msgid "No signed contract available."
msgstr "Aucun contrat signé disponible." 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." msgid "One on One request from %(player)s has been rejected."
msgstr "La demande de rencontre de %(player)s a été refusée." 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." msgid "Username already taken."
msgstr "Ce nom dutilisateur est déjà pris." 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." msgid "Email already in use."
msgstr "Cette adresse courriel est déjà utilisée." 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!" msgid "Profile updated successfully!"
msgstr "Profil mis à jour." msgstr "Profil mis à jour."
@@ -959,7 +972,7 @@ msgstr "%(total)s au total"
msgid "Next" msgid "Next"
msgstr "Suivant" 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 #: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3
msgid "Dashboard" msgid "Dashboard"
msgstr "Tableau de bord" msgstr "Tableau de bord"
@@ -1001,38 +1014,34 @@ msgid "My Notes"
msgstr "Mes notes" msgstr "Mes notes"
#: app/templates/layouts/base.html:106 #: app/templates/layouts/base.html:106
msgid "Availability"
msgstr "Disponibilités"
#: app/templates/layouts/base.html:112
msgid "Notes & One on One" msgid "Notes & One on One"
msgstr "Notes et rencontres individuelles" 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 #: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9
msgid "Contracts" msgid "Contracts"
msgstr "Contrats" 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 #: app/templates/pages/profile.html:3
msgid "My Profile" msgid "My Profile"
msgstr "Mon profil" msgstr "Mon profil"
#: app/templates/layouts/base.html:137 #: app/templates/layouts/base.html:131
msgid "Logout" msgid "Logout"
msgstr "Déconnexion" msgstr "Déconnexion"
#: app/templates/layouts/base.html:158 #: app/templates/layouts/base.html:152
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "Basculer le mode sombre" 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" msgid "Dismiss"
msgstr "Fermer" msgstr "Fermer"
#: app/templates/layouts/base.html:199 #: app/templates/layouts/base.html:193
msgid "Team Tryout Management System" msgid "UdeS team manager"
msgstr "Système de gestion des sélections d’équipe" msgstr "UdeS team manager"
#: app/templates/layouts/macros.html:116 #: app/templates/layouts/macros.html:116
msgid "Close" msgid "Close"
@@ -1217,6 +1226,10 @@ msgid "Loading availability grid..."
msgstr "Chargement de la grille de disponibilités..." msgstr "Chargement de la grille de disponibilités..."
#: app/templates/pages/coach_availability.html:19 #: 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 #: app/templates/pages/profile.html:200 app/templates/pages/profile.html:220
msgid "Clear All" msgid "Clear All"
msgstr "Tout effacer" msgstr "Tout effacer"
@@ -1378,7 +1391,7 @@ msgid "Role"
msgstr "Rôle" msgstr "Rôle"
#: app/templates/pages/create_user.html:41 app/templates/pages/login.html:11 #: 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" msgid "Password"
msgstr "Mot de passe" msgstr "Mot de passe"
@@ -1541,7 +1554,7 @@ msgstr "Modifier le profil"
#: app/templates/pages/edit_profile.html:33 #: app/templates/pages/edit_profile.html:33
#: app/templates/pages/edit_user.html:43 app/templates/pages/profile.html:63 #: 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" msgid "E-Sports Profile"
msgstr "Profil e-sport" 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." msgstr "Mettez à jour votre profil de joueur compétitif pour les sélections."
#: app/templates/pages/edit_profile.html:37 #: app/templates/pages/edit_profile.html:37
#: app/templates/pages/register.html:87 #: app/templates/pages/register.html:85
msgid "Games You Play" msgid "Games You Play"
msgstr "Jeux auxquels vous jouez" msgstr "Jeux auxquels vous jouez"
#: app/templates/pages/edit_profile.html:47 #: 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." msgid "Select all games you're signing in for."
msgstr "Sélectionnez tous les jeux pour lesquels vous vous inscrivez." msgstr "Sélectionnez tous les jeux pour lesquels vous vous inscrivez."
@@ -1599,50 +1612,43 @@ msgid "e.g. Name#1234"
msgstr "ex. : Nom#1234" msgstr "ex. : Nom#1234"
#: app/templates/pages/edit_profile.html:87 #: app/templates/pages/edit_profile.html:87
#: app/templates/pages/edit_user.html:95 #: app/templates/pages/register.html:57
msgid "(for DMs)" msgid "Reconnect"
msgstr "(pour les messages privés)" msgstr "Reconnecter"
#: app/templates/pages/edit_profile.html:88 #: app/templates/pages/edit_profile.html:87
#: app/templates/pages/edit_user.html:96 #: app/templates/pages/register.html:67
msgid "Numeric ID (e.g. 123456789012345678)" msgid "Connect Discord Account"
msgstr "Identifiant numérique (ex. : 123456789012345678)" msgstr "Connecter un compte Discord"
#: app/templates/pages/edit_profile.html:89 #: app/templates/pages/edit_profile.html:93
#: 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_user.html:100 #: app/templates/pages/edit_user.html:100
msgid "League OS Connection" msgid "League OS Connection"
msgstr "Connexion League OS" msgstr "Connexion League OS"
#: app/templates/pages/edit_profile.html:95 #: app/templates/pages/edit_profile.html:94
#: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:131 #: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:129
msgid "League OS profile link or ID" msgid "League OS profile link or ID"
msgstr "Lien ou identifiant du profil League OS" 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" msgid "Change Password"
msgstr "Changer le mot de passe" 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." msgid "Leave blank to keep your current password."
msgstr "Laissez vide pour conserver votre mot de passe actuel." 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" msgid "New Password"
msgstr "Nouveau mot de passe" 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 #: app/templates/pages/edit_user.html:107
msgid "Enter new password" msgid "Enter new password"
msgstr "Saisir le nouveau mot de passe" 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" msgid "Save Changes"
msgstr "Enregistrer les modifications" msgstr "Enregistrer les modifications"
@@ -1658,6 +1664,20 @@ msgstr "Jeux"
msgid "Enter gamertag for each selected game to link to Tracker Network." 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." 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 #: app/templates/pages/edit_user.html:106
msgid "(leave blank to keep current)" msgid "(leave blank to keep current)"
msgstr "(laisser vide pour conserver lactuel)" msgstr "(laisser vide pour conserver lactuel)"
@@ -2378,7 +2398,7 @@ msgstr ""
"Choisissez les plages où vous êtes disponible pour des rencontres " "Choisissez les plages où vous êtes disponible pour des rencontres "
"individuelles (8 h à 22 h)." "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" msgid "Click or click-and-drag to select your available hours"
msgstr "Cliquez ou faites glisser pour choisir vos heures de disponibilité" msgstr "Cliquez ou faites glisser pour choisir vos heures de disponibilité"
@@ -2426,11 +2446,7 @@ msgstr ""
msgid "Connected" msgid "Connected"
msgstr "Connecté" msgstr "Connecté"
#: app/templates/pages/register.html:57 #: app/templates/pages/register.html:61
msgid "Reconnect"
msgstr "Reconnecter"
#: app/templates/pages/register.html:63
msgid "" msgid ""
"Discord connected. Game connections have been used to pre-fill your " "Discord connected. Game connections have been used to pre-fill your "
"profile below." "profile below."
@@ -2439,52 +2455,48 @@ msgstr ""
"ci-dessous." "ci-dessous."
#: app/templates/pages/register.html:69 #: 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." msgid "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc."
msgstr "" msgstr ""
"Connectez-vous pour pré-remplir vos pseudos depuis Steam, Battle.net, " "Connectez-vous pour pré-remplir vos pseudos depuis Steam, Battle.net, "
"Xbox, etc." "Xbox, etc."
#: app/templates/pages/register.html:74 #: app/templates/pages/register.html:72
msgid "Discord Username (Manual)" msgid "Discord Username (Manual)"
msgstr "Nom dutilisateur Discord (saisie manuelle)" msgstr "Nom dutilisateur Discord (saisie manuelle)"
#: app/templates/pages/register.html:75 #: app/templates/pages/register.html:73
msgid "e.g. YourName" msgid "e.g. YourName"
msgstr "ex. : VotrePseudo" msgstr "ex. : VotrePseudo"
#: app/templates/pages/register.html:84 #: app/templates/pages/register.html:82
msgid "Set up your competitive gaming profile for tryouts." msgid "Set up your competitive gaming profile for tryouts."
msgstr "Configurez votre profil de joueur compétitif pour les sélections." 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)" msgid "League OS Connection (Optional)"
msgstr "Connexion League OS (facultative)" 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." msgid "Connect your League OS profile for organized play."
msgstr "Liez votre profil League OS pour le jeu organisé." msgstr "Liez votre profil League OS pour le jeu organisé."
#: app/templates/pages/register.html:137 #: app/templates/pages/register.html:135
msgid "Security" msgid "Security"
msgstr "Sécurité" msgstr "Sécurité"
#: app/templates/pages/register.html:140 #: app/templates/pages/register.html:138
msgid "Create a password" msgid "Create a password"
msgstr "Créez un mot de passe" msgstr "Créez un mot de passe"
#: app/templates/pages/register.html:144 #: app/templates/pages/register.html:142
msgid "Confirm Password" msgid "Confirm Password"
msgstr "Confirmer le mot de passe" msgstr "Confirmer le mot de passe"
#: app/templates/pages/register.html:145 #: app/templates/pages/register.html:143
msgid "Confirm your password" msgid "Confirm your password"
msgstr "Confirmez votre mot de passe" msgstr "Confirmez votre mot de passe"
#: app/templates/pages/register.html:159 #: app/templates/pages/register.html:157
msgid "Create Account" msgid "Create Account"
msgstr "Créer le compte" msgstr "Créer le compte"
@@ -3008,3 +3020,8 @@ msgstr "Voir le profil"
#~ msgid "Invalid date or time format." #~ msgid "Invalid date or time format."
#~ msgstr "Format de date ou dheure invalide." #~ 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, allow_none=True,
load_default=None, load_default=None,
) )
discord_user_id = fields.String(
validate=validate_discord_user_id,
allow_none=True,
load_default=None,
)
league_os_profile = fields.String( league_os_profile = fields.String(
validate=validate.Length(max=256), validate=validate.Length(max=256),
allow_none=True, allow_none=True,
@@ -392,7 +387,6 @@ class EditProfileSchema(StripMixin):
phone: Optional. phone: Optional.
password: Optional (only if changing). password: Optional (only if changing).
discord_username: Optional. discord_username: Optional.
discord_user_id: Optional.
league_os_profile: Optional. league_os_profile: Optional.
games: Optional list. games: Optional list.
""" """
@@ -428,11 +422,6 @@ class EditProfileSchema(StripMixin):
allow_none=True, allow_none=True,
load_default=None, load_default=None,
) )
discord_user_id = fields.String(
validate=validate_discord_user_id,
allow_none=True,
load_default=None,
)
league_os_profile = fields.String( league_os_profile = fields.String(
validate=validate.Length(max=256), validate=validate.Length(max=256),
allow_none=True, allow_none=True,
+13 -4
View File
@@ -1,8 +1,9 @@
# Le schéma réel, et comment sortir de `create_all()` # Le schéma réel, et comment sortir de `create_all()`
> **État au 2026-08-11** : l'outil de relevé existe et est testé. Le relevé > **État au 2026-08-16** : l'outil de relevé existe et est testé, y compris
> lui-même n'a pas été exécuté — il demande un accès à la base de production, > pour les collisions d'identité Discord. Le relevé lui-même n'a pas été
> qui ne peut pas venir du dépôt. Tout ce qui suit attend cette exécution. > exécuté — il demande un accès à la base de production, qui ne peut pas venir
> du dépôt. Tout changement de schéma ci-dessous attend cette exécution.
## Pourquoi c'est le nœud ## Pourquoi c'est le nœud
@@ -60,6 +61,14 @@ existe-t-il encore ? (`SEC-003`) :
python app/supporting_scripts/schema_report.py --check-seed-accounts python app/supporting_scripts/schema_report.py --check-seed-accounts
``` ```
L'identité Discord est désormais conservée côté serveur et toute nouvelle
collision est refusée par l'application. Les doublons historiques restent à
identifier avant d'ajouter la contrainte `UNIQUE` de `SEC-012` :
```bash
python app/supporting_scripts/schema_report.py --check-discord-identities
```
## Étape 3 — Alembic, décrivant le schéma **réel** (`DB-002`) ## Étape 3 — Alembic, décrivant le schéma **réel** (`DB-002`)
Le piège de cette étape tient en une phrase : **la migration initiale doit Le piège de cette étape tient en une phrase : **la migration initiale doit
@@ -111,7 +120,7 @@ Dans cet ordre, parce qu'ils dépendent tous de `DB-002` :
| `DB-008` | Trancher `attendance_confirmed` côté tryout | `discord_bot.py` écrit un attribut fantôme ; aujourd'hui journalisé en avertissement | | `DB-008` | Trancher `attendance_confirmed` côté tryout | `discord_bot.py` écrit un attribut fantôme ; aujourd'hui journalisé en avertissement |
| `DB-009` | Horodatages avec fuseau | `datetime.utcnow` partout, déprécié en 3.12 | | `DB-009` | Horodatages avec fuseau | `datetime.utcnow` partout, déprécié en 3.12 |
| `ARCH-001` | Fusionner coach/équipe sur la relation m2m | Migration de données ; `app/permissions.py` rend la duplication inoffensive **en lecture** seulement, l'écriture crée toujours les deux | | `ARCH-001` | Fusionner coach/équipe sur la relation m2m | Migration de données ; `app/permissions.py` rend la duplication inoffensive **en lecture** seulement, l'écriture crée toujours les deux |
| `SEC-012` | Identité Discord côté serveur, `unique=True` | La colonne doit être unique, donc dédoublonnée d'abord | | `SEC-012` | Ajouter `unique=True` sur l'identité Discord | La valeur OAuth reste côté serveur et les nouvelles collisions sont refusées ; les lignes historiques doivent être dédoublonnées d'abord |
## Ce qu'on ne fait pas ## Ce qu'on ne fait pas
+222
View File
@@ -0,0 +1,222 @@
"""The Discord snowflake is a verified identity, not profile text.
SEC-AUTH-005. Discord OAuth used to put its result in two hidden inputs;
registration then trusted those client-controlled values, and edit_profile
let the account owner replace the snowflake later. The bot uses that value to
route private messages and authorize reaction-driven writes.
"""
import time
from urllib.parse import parse_qs, urlparse
from app.routes import auth as auth_module
from app.routes.auth import (
MIN_REGISTRATION_SECONDS,
REGISTRATION_ISSUED_KEY,
)
FORM = {
'username': 'brandnew',
'email': '[email protected]',
'password': 'Password123',
'confirm_password': 'Password123',
'full_name': 'Brand New',
}
def _allow_registration(client):
with client.session_transaction() as session:
session[REGISTRATION_ISSUED_KEY] = time.time() - MIN_REGISTRATION_SECONDS - 1
def _user(app, username='brandnew'):
from app.models import User
with app.app_context():
return User.query.filter_by(username=username).first()
class _DiscordResponse:
def __init__(self, payload):
self.payload = payload
def raise_for_status(self):
return None
def json(self):
return self.payload
def _complete_profile_oauth(client, monkeypatch, discord_user_id):
monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_ID', 'client-id')
monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_SECRET', 'client-secret')
monkeypatch.setattr(
auth_module,
'DISCORD_REDIRECT_URI',
'https://example.test/auth/discord/callback',
)
start = client.get('/auth/discord/login')
state = parse_qs(urlparse(start.headers['Location']).query)['state'][0]
monkeypatch.setattr(
auth_module.requests,
'post',
lambda *args, **kwargs: _DiscordResponse({'access_token': 'token'}),
)
monkeypatch.setattr(
auth_module.requests,
'get',
lambda *args, **kwargs: _DiscordResponse(
{'id': discord_user_id, 'username': 'verified-user'}
),
)
return client.get(f'/auth/discord/callback?code=code&state={state}')
class TestRegistrationIdentity:
def test_a_posted_snowflake_without_oauth_is_ignored(self, app, client):
_allow_registration(client)
client.post(
'/auth/register',
data=dict(FORM, discord_user_id='111111111111111111'),
)
assert _user(app).discord_user_id is None
def test_the_server_side_oauth_identity_wins_over_the_form(self, app, client):
verified = '222222222222222222'
_allow_registration(client)
with client.session_transaction() as session:
session['discord_oauth'] = {
'id': verified,
'username': 'verified-user',
}
client.post(
'/auth/register',
data=dict(
FORM,
discord_user_id='111111111111111111',
discord_username='forged-user',
),
)
created = _user(app)
assert created.discord_user_id == verified
assert created.discord_username == 'verified-user'
def test_an_oauth_identity_already_in_use_is_refused(self, app, client, make_user):
verified = '222222222222222222'
make_user('player', discord_user_id=verified)
_allow_registration(client)
with client.session_transaction() as session:
session['discord_oauth'] = {
'id': verified,
'username': 'verified-user',
}
client.post('/auth/register', data=FORM)
assert _user(app) is None
def test_the_registration_page_has_no_client_identity_field(self, client):
with client.session_transaction() as session:
session['discord_oauth'] = {
'id': '222222222222222222',
'username': 'verified-user',
}
page = client.get('/auth/register').get_data(as_text=True)
assert 'name="discord_user_id"' not in page
class TestProfileIdentity:
def test_a_profile_post_cannot_replace_the_verified_snowflake(self, app, client, as_role):
original = '222222222222222222'
user_id = as_role('player', discord_user_id=original)
client.post(
'/users/profile/edit',
data={
'username': 'player1',
'full_name': 'Player 1',
'email': '[email protected]',
'discord_user_id': '111111111111111111',
},
)
from app.models import User
with app.app_context():
assert (
app.extensions['sqlalchemy'].session.get(User, user_id).discord_user_id == original
)
def test_the_profile_form_does_not_offer_the_snowflake(self, client, as_role):
as_role('player', discord_user_id='222222222222222222')
page = client.get('/users/profile/edit').get_data(as_text=True)
assert 'name="discord_user_id"' not in page
assert '/auth/discord/login' in page
def test_a_signed_in_user_can_relink_only_through_oauth(
self, app, client, as_role, monkeypatch
):
user_id = as_role('player', discord_user_id='111111111111111111')
response = _complete_profile_oauth(
client,
monkeypatch,
discord_user_id='222222222222222222',
)
from app.models import User
assert '/users/profile/edit' in response.headers['Location']
with app.app_context():
user = app.extensions['sqlalchemy'].session.get(User, user_id)
assert user.discord_user_id == '222222222222222222'
assert user.discord_username == 'verified-user'
def test_profile_oauth_refuses_an_identity_owned_by_someone_else(
self, app, client, as_role, make_user, monkeypatch
):
taken = '222222222222222222'
make_user('player', discord_user_id=taken)
user_id = as_role('player', discord_user_id='111111111111111111')
_complete_profile_oauth(client, monkeypatch, discord_user_id=taken)
from app.models import User
with app.app_context():
assert (
app.extensions['sqlalchemy'].session.get(User, user_id).discord_user_id
== '111111111111111111'
)
class TestAdministrativeFallback:
def test_an_admin_cannot_assign_a_snowflake_twice(self, app, client, as_role, make_user):
taken = '222222222222222222'
make_user('player', discord_user_id=taken)
target_id = make_user('player')
as_role('admin')
client.post(
f'/users/{target_id}/edit',
data={
'full_name': 'Target Player',
'email': '[email protected]',
'role': 'player',
'discord_user_id': taken,
},
)
from app.models import User
with app.app_context():
assert app.extensions['sqlalchemy'].session.get(User, target_id).discord_user_id is None
+61
View File
@@ -202,6 +202,67 @@ class TestCatalogueIntegrity:
f'{len(untranslated)} untranslated string(s) in {locale}: {untranslated[:5]}' f'{len(untranslated)} untranslated string(s) in {locale}: {untranslated[:5]}'
) )
# Babel keeps its guessed translation when it marks an entry fuzzy,
# but gettext deliberately ignores that guess at runtime. Merely
# checking m.string therefore let five English fallbacks through after
# the branding merge, including a dangerously wrong French label.
fuzzy = [m.id for m in catalog if m.id and 'fuzzy' in m.flags]
assert not fuzzy, f'{len(fuzzy)} fuzzy string(s) in {locale}: {fuzzy[:5]}'
def test_the_catalogue_contains_every_message_in_the_source(self, tmp_path):
"""A translated PO can still be stale.
The previous guard only inspected entries already in the catalogue.
A new `_()` in Python or Jinja therefore stayed English without any
failure until somebody happened to run extraction by hand.
"""
import os
import subprocess
import sys
from babel.messages.pofile import read_po
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
extracted_path = tmp_path / 'messages.pot'
subprocess.run(
[
sys.executable,
'-m',
'babel.messages.frontend',
'extract',
'-F',
'babel.cfg',
'-k',
'_l',
'-o',
str(extracted_path),
'.',
],
cwd=root,
check=True,
capture_output=True,
text=True,
)
with open(extracted_path, encoding='utf-8') as handle:
source_ids = {message.id for message in read_po(handle) if message.id}
for locale in SUPPORTED_LOCALES:
path = os.path.join(
root,
'app',
'translations',
locale,
'LC_MESSAGES',
'messages.po',
)
with open(path, encoding='utf-8') as handle:
catalogue_ids = {message.id for message in read_po(handle) if message.id}
missing = source_ids - catalogue_ids
assert not missing, (
f'{len(missing)} source string(s) absent from {locale}: '
f'{sorted(missing, key=str)[:5]}'
)
class TestSelectorUnit: class TestSelectorUnit:
def test_select_locale_returns_a_supported_code(self, app): def test_select_locale_returns_a_supported_code(self, app):
+33
View File
@@ -257,3 +257,36 @@ class TestSeedAccountCheck:
output = capsys.readouterr().out output = capsys.readouterr().out
assert 'password has been changed' in output assert 'password has been changed' in output
assert 'PASSWORD IS STILL' not in output assert 'PASSWORD IS STILL' not in output
class TestDiscordIdentityCheck:
def test_it_finds_identity_collisions_before_the_unique_migration(self, live_db, app, capsys):
engine, _tamper = live_db
path = str(engine.url).replace('sqlite:///', '')
connection = sqlite3.connect(path)
for username in ('alice', 'bob'):
connection.execute(
'INSERT INTO users '
'(username, password_hash, role, full_name, email, '
'is_active_account, discord_user_id) '
"VALUES (?, 'hash', 'player', ?, ?, 1, '222222222222222222')",
(username, username.title(), f'{username}@example.test'),
)
connection.commit()
connection.close()
with app.app_context():
result = main(['--url', str(engine.url), '--check-discord-identities'])
output = capsys.readouterr().out
assert result == 1
assert '222222222222222222: 2 accounts (alice, bob)' in output
def test_a_clean_identity_set_does_not_change_the_exit_code(self, live_db, app, capsys):
engine, _tamper = live_db
with app.app_context():
result = main(['--url', str(engine.url), '--check-discord-identities'])
assert result == 0
assert 'No Discord identity is shared' in capsys.readouterr().out
+46 -10
View File
@@ -92,7 +92,7 @@ class TestPlayerAvailability:
assert PlayerDisponibility.query.count() == 0 assert PlayerDisponibility.query.count() == 0
class TestBulkAvailabilityReportsWhatItDropped: class TestBulkAvailabilityReplacementIsAtomic:
def test_valid_slots_are_saved(self, app, client, as_role): def test_valid_slots_are_saved(self, app, client, as_role):
from app.models import PlayerDisponibility from app.models import PlayerDisponibility
@@ -107,28 +107,64 @@ class TestBulkAvailabilityReportsWhatItDropped:
with app.app_context(): with app.app_context():
assert PlayerDisponibility.query.count() == 1 assert PlayerDisponibility.query.count() == 1
def test_a_dropped_slot_is_named(self, app, client, as_role): def test_a_good_payload_replaces_the_slots(self, app, client, as_role):
"""It used to `continue` and answer success, so the client could not
tell nine saved from ten sent."""
from app.models import PlayerDisponibility from app.models import PlayerDisponibility
as_role('player') player_id = as_role('player')
client.post(
'/users/disponibilities/add_bulk',
json={'slots': [{'day_of_week': 1, 'start_time': '09:00'}]},
)
response = client.post(
'/users/disponibilities/add_bulk',
json={'slots': [{'day_of_week': 3, 'start_time': '18:00'}]},
)
assert response.status_code == 200
with app.app_context():
slots = PlayerDisponibility.query.filter_by(player_id=player_id).all()
assert [(s.day_of_week, s.start_time) for s in slots] == [(3, time(18, 0))]
def test_an_empty_payload_clears_the_slots(self, app, client, as_role):
from app.models import PlayerDisponibility
player_id = as_role('player')
client.post(
'/users/disponibilities/add_bulk',
json={'slots': [{'day_of_week': 1, 'start_time': '09:00'}]},
)
response = client.post('/users/disponibilities/add_bulk', json={'slots': []})
assert response.status_code == 200
with app.app_context():
assert PlayerDisponibility.query.filter_by(player_id=player_id).count() == 0
def test_a_malformed_slot_changes_nothing(self, app, client, as_role):
from app.models import PlayerDisponibility
player_id = as_role('player')
client.post(
'/users/disponibilities/add_bulk',
json={'slots': [{'day_of_week': 1, 'start_time': '09:00'}]},
)
response = client.post( response = client.post(
'/users/disponibilities/add_bulk', '/users/disponibilities/add_bulk',
json={ json={
'slots': [ 'slots': [
{'day_of_week': 1, 'start_time': '09:00'}, {'day_of_week': 3, 'start_time': '18:00'},
{'day_of_week': 1, 'start_time': 'nope'}, {'day_of_week': 1, 'start_time': 'nope'},
] ]
}, },
) )
body = response.get_json() assert response.status_code == 400
assert len(body['created']) == 1 assert len(response.get_json()['rejected']) == 1
assert len(body['rejected']) == 1, 'the caller must learn a slot was dropped'
with app.app_context(): with app.app_context():
assert PlayerDisponibility.query.count() == 1 slots = PlayerDisponibility.query.filter_by(player_id=player_id).all()
assert [(s.day_of_week, s.start_time) for s in slots] == [(1, time(9, 0))]
class TestCoachAvailabilityIsNotWipedByABadPayload: class TestCoachAvailabilityIsNotWipedByABadPayload: