From 820cccb25ed3a488c0950ed68da9fe4bddd3e9ba Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Wed, 26 Aug 2026 20:52:40 -0400 Subject: [PATCH] =?UTF-8?q?changement=20vers=20google=20drive=20storage=20?= =?UTF-8?q?pour=20les=20contrats.=20Tentative=20d'obtenir=20un=20token=20d?= =?UTF-8?q?e=20connection=20=C3=A9choue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _add_drive_test.py | 40 + _add_tests.py | 48 + app/google_drive.py | 41 +- app/routes/users.py | 1312 ------------------- app/routes/users/_shared.py | 20 + app/routes/users/contracts.py | 4 +- app/storage.py | 8 +- app/supporting_scripts/backup.py | 83 +- app/supporting_scripts/get_refresh_token.py | 121 ++ app/templates/pages/contracts.html | 4 +- app/templates/pages/upload_contract.html | 4 +- app/translations/en/LC_MESSAGES/messages.mo | Bin 47712 -> 47844 bytes app/translations/en/LC_MESSAGES/messages.po | 724 +--------- app/translations/fr/LC_MESSAGES/messages.mo | Bin 52215 -> 52350 bytes app/translations/fr/LC_MESSAGES/messages.po | 726 +--------- docs/database-restore.md | 1 + docs/security-checklist.md | 3 +- requirements.txt | 1 + tests/test_contract_uploads.py | 62 +- tests/test_storage.py | 20 +- 20 files changed, 476 insertions(+), 2746 deletions(-) create mode 100644 _add_drive_test.py create mode 100644 _add_tests.py delete mode 100644 app/routes/users.py create mode 100644 app/supporting_scripts/get_refresh_token.py diff --git a/_add_drive_test.py b/_add_drive_test.py new file mode 100644 index 0000000..cf6e291 --- /dev/null +++ b/_add_drive_test.py @@ -0,0 +1,40 @@ +"""Add Drive signed upload test.""" +import pathlib + +p = pathlib.Path('tests/test_contract_uploads.py') +t = p.read_bytes() + +drive_test = ( + b"\n def test_signed_upload_with_google_drive(\n" + b" self, app, client, as_role, make_user, contract_for, monkeypatch\n" + b" ):\n" + b' """Signing with Drive backend stores gdrive:// in signed_file_path."""\n' + b" monkeypatch.setenv('DOCUMENT_STORAGE_BACKEND', 'google_drive')\n" + b" monkeypatch.setattr(\n" + b" 'app.storage.upload_google_drive_file',\n" + b" lambda **_kwargs: 'drive-signed-456',\n" + b" )\n" + b" player_id = as_role('player')\n" + b" admin_id = make_user('admin')\n" + b" contract_id, directory = contract_for(player_id, admin_id)\n\n" + b" client.post(\n" + b" f'/users/contracts/{contract_id}/upload_signed',\n" + b" data={'signed_file': (io.BytesIO(_PDF_BYTES), 'signed.pdf')},\n" + b" content_type='multipart/form-data',\n" + b" follow_redirects=True,\n" + b" )\n\n" + b" with app.app_context():\n" + b" contract = db.session.get(Contract, contract_id)\n" + b" assert contract.status == 'signed'\n" + b" assert contract.signed_file_path == 'gdrive://drive-signed-456'\n\n" +) + +# Add before 'class TestTheHelper:' +marker = b'\nclass TestTheHelper:' +pos = t.find(marker) +if pos >= 0: + t = t[:pos] + drive_test + t[pos:] + p.write_bytes(t) + print('Added Drive signed upload test') +else: + print('Could not find marker') \ No newline at end of file diff --git a/_add_tests.py b/_add_tests.py new file mode 100644 index 0000000..a8a6715 --- /dev/null +++ b/_add_tests.py @@ -0,0 +1,48 @@ +"""Add new tests to test_contract_uploads.py.""" +import pathlib + +p = pathlib.Path('tests/test_contract_uploads.py') +t = p.read_bytes() + +# 1. Header-only test in TestSignedUpload (before 'class TestTheHelper:') +marker = b'\n\nclass TestTheHelper:' +test = ( + b"\n def test_a_header_only_pdf_is_refused(\n" + b" self, app, client, as_role, make_user, contract_for\n" + b" ):\n" + b' """A file with a %PDF- header but no real PDF structure is rejected."""\n' + b" player_id = as_role('player')\n" + b" admin_id = make_user('admin')\n" + b" contract_id, directory = contract_for(player_id, admin_id)\n\n" + b" client.post(\n" + b" f'/users/contracts/{contract_id}/upload_signed',\n" + b" data={'signed_file': (io.BytesIO(PDF_HEADER_ONLY_BYTES), 'signed.pdf')},\n" + b" content_type='multipart/form-data',\n" + b" follow_redirects=True,\n" + b" )\n\n" + b" with app.app_context():\n" + b" contract = db.session.get(Contract, contract_id)\n" + b" assert contract.status != 'signed'\n" + b" assert contract.signed_file_path is None\n\n" +) +pos = t.find(marker) +if pos >= 0: + t = t[:pos] + test + t[pos:] + +# 2. Header-only helper test at end of TestTheHelper +trailing = t.rstrip(b'\n') +idx = t.find(trailing) + len(trailing) +helper = ( + b"\n def test_a_header_only_file_is_refused(self, app):\n" + b' """A stream starting with %PDF- but holding non-PDF data is\n' + b" rejected (the header check is not enough).\"\"\"\n" + b" from werkzeug.datastructures import FileStorage\n\n" + b" from app.routes.users import ALLOWED_CONTRACT_EXTENSIONS, pdf_upload_error\n\n" + b" upload = FileStorage(stream=io.BytesIO(PDF_HEADER_ONLY_BYTES), filename='clever.pdf')\n" + b" with app.test_request_context('/'):\n" + b" assert pdf_upload_error(upload, ALLOWED_CONTRACT_EXTENSIONS)\n" +) +t = t[:idx] + helper + t[idx:] + +p.write_bytes(t) +print('Added new tests') \ No newline at end of file diff --git a/app/google_drive.py b/app/google_drive.py index 3463fb3..e8a4626 100644 --- a/app/google_drive.py +++ b/app/google_drive.py @@ -22,7 +22,9 @@ class GoogleDriveStorageError(RuntimeError): def _setting(name): value = os.getenv(name) if not value: - raise GoogleDriveStorageError(f'{name} must be configured for Google Drive document storage.') + raise GoogleDriveStorageError( + f'{name} must be configured for Google Drive document storage.' + ) return value @@ -47,6 +49,43 @@ def _drive_service(): return build('drive', 'v3', credentials=credentials, cache_discovery=False) +def list_folder_files(): + """Return (file_id, name) tuples for every file in the configured folder. + + Used by the backup script to enumerate Drive-stored contracts. Returns + only files directly in the folder (not nested inside sub-folders). + + Returns: + list[tuple[str, str]]: (file_id, name) for each file found. + """ + try: + folder_id = _setting(GOOGLE_DRIVE_FOLDER_ID_ENV) + page_token = None + results = [] + while True: + response = ( + _drive_service() + .files() + .list( + q=f"'{folder_id}' in parents and trashed=false", + fields='nextPageToken, files(id, name)', + pageToken=page_token, + pageSize=1000, + ) + .execute() + ) + for f in response.get('files', []): + results.append((f['id'], f['name'])) + page_token = response.get('nextPageToken') + if not page_token: + break + return results + except GoogleDriveStorageError: + raise + except Exception as exc: + raise GoogleDriveStorageError('Google Drive could not list folder contents.') from exc + + def upload_file(*, stream, filename, mimetype): """Upload a PDF to the configured owner folder and return its Drive ID.""" try: diff --git a/app/routes/users.py b/app/routes/users.py deleted file mode 100644 index 72ca599..0000000 --- a/app/routes/users.py +++ /dev/null @@ -1,1312 +0,0 @@ -"""User management routes for profiles, disponibilities, and contracts. - -Uses polymorphic isinstance checks instead of role-string comparisons. -""" - -import os -import uuid -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file -from flask_login import login_required, current_user -from app.extensions import db, hash_password, csrf -from app.time_utils import utc_now_naive -from app.models import ( - Admin, Manager, Coach, Player, Scout, - User, USER_TYPES, ESPORT_GAMES, - PlayerDisponibility, UserGamertag, GAME_PLATFORMS, - Contract, OrgTeam, CoachAvailability, - TeamNote, PersonalNote, OneOnOneRequest, - Evaluation, Match, Team, TeamMember, - MatchParticipant, Tryout, TryoutRegistration, TeamPlayer, -) -from werkzeug.utils import secure_filename -from datetime import datetime, timedelta, date as date_type -from marshmallow import ValidationError -from app.validators import ( - CreateUserSchema, EditUserSchema, EditProfileSchema, - UploadContractSchema, OneOnOneRequestSchema, -) -import requests - -ALLOWED_CONTRACT_EXTENSIONS = {'pdf'} -ALLOWED_SIGNED_EXTENSIONS = {'pdf'} - -users_bp = Blueprint('users', __name__, url_prefix='/users') - -# --------------------------------------------------------------------------- -# Gamertag helper (shared) -# --------------------------------------------------------------------------- - -def update_user_gamertags(user, selected_games): - """Update gamertags for a user based on form input.""" - existing_gamertags = {gt.game: gt for gt in user.gamertags} - for game in selected_games: - gamertag = request.form.get(f'gamertag_{game}', '').strip() - platform = request.form.get(f'platform_{game}', '').strip() if GAME_PLATFORMS.get(game) else None - existing = existing_gamertags.get(game) - if gamertag: - if existing: - existing.gamertag = gamertag - existing.platform = platform - else: - gt = UserGamertag(user_id=user.id, game=game, gamertag=gamertag, platform=platform) - db.session.add(gt) - elif existing: - db.session.delete(existing) - for game in existing_gamertags: - if game not in selected_games: - db.session.delete(existing_gamertags[game]) - - -# --------------------------------------------------------------------------- -# USER_TYPE → Model mapping for create_user -# --------------------------------------------------------------------------- - -_USER_CLASS_MAP = { - 'admin': Admin, - 'manager': Manager, - 'coach': Coach, - 'player': Player, - 'scout': Scout, -} - - -# =========================================================================== -# ROUTES -# =========================================================================== - -@users_bp.route('') -@login_required -def list_users(): - """List all users for management (Admin only).""" - if not isinstance(current_user, Admin): - flash('Only the president can manage users.', 'danger') - return redirect(url_for('main.dashboard')) - - users = User.query.order_by(User.role, User.username).all() - return render_template('pages/users.html', users=users, roles=USER_TYPES) - - -@users_bp.route('//edit', methods=['GET', 'POST']) -@login_required -def edit_user(user_id): - """Edit an existing user (Admin only).""" - if not isinstance(current_user, Admin): - flash('Only the president can edit users.', 'danger') - return redirect(url_for('main.dashboard')) - - user = User.query.get_or_404(user_id) - - if request.method == 'POST': - full_name = request.form.get('full_name') - email = request.form.get('email') - phone = request.form.get('phone') - role = request.form.get('role') - is_active = request.form.get('is_active_account') == 'on' - - if role not in USER_TYPES: - flash('Invalid role selected.', 'danger') - return render_template('pages/edit_user.html', user=user, roles=USER_TYPES, - esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS) - - selected_games = request.form.getlist('games') - discord_username = request.form.get('discord_username', '').strip() - discord_user_id = request.form.get('discord_user_id', '').strip() - league_os_profile = request.form.get('league_os_profile', '').strip() - - # Change role via raw SQL to avoid polymorphic identity corruption. - # Must discard the entire session because the polymorphic discriminator - # change invalidates the identity map for this instance and anything - # that references it via relationships. - if user.role != role: - user_id_local = user.id - db.session.execute( - db.text("UPDATE users SET role = :role WHERE id = :id"), - {"role": role, "id": user_id_local} - ) - db.session.commit() - db.session.remove() # discard stale session entirely - user = User.query.get(user_id_local) # fresh session, correct class - - user.full_name = full_name - user.email = email - user.phone = phone - user.is_active_account = is_active - user.games = ','.join(selected_games) if selected_games else None - user.discord_username = discord_username or None - user.discord_user_id = discord_user_id or None - user.league_os_profile = league_os_profile or None - - update_user_gamertags(user, selected_games) - - password = request.form.get('password') - if password: - user.password_hash = hash_password(password) - - db.session.commit() - flash(f'User {user.username} updated successfully!', 'success') - return redirect(url_for('users.list_users')) - - user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} - for gt in user.gamertags} - return render_template('pages/edit_user.html', user=user, roles=USER_TYPES, - esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, - user_gamertags=user_gamertags) - - -@users_bp.route('//delete', methods=['POST']) -@login_required -def delete_user(user_id): - """Delete a user (Admin only).""" - if not isinstance(current_user, Admin): - flash('Only the president can delete users.', 'danger') - return redirect(url_for('main.dashboard')) - - if current_user.id == user_id: - flash('You cannot delete your own account.', 'danger') - return redirect(url_for('users.list_users')) - - user = User.query.get_or_404(user_id) - - Evaluation.query.filter( - db.or_(Evaluation.evaluator_id == user_id, Evaluation.player_id == user_id), - ).delete(synchronize_session=False) - PlayerDisponibility.query.filter_by(player_id=user_id).delete() - CoachAvailability.query.filter_by(coach_id=user_id).delete() - PersonalNote.query.filter( - db.or_(PersonalNote.player_id == user_id, PersonalNote.coach_id == user_id), - ).delete(synchronize_session=False) - TeamNote.query.filter_by(coach_id=user_id).delete() - OneOnOneRequest.query.filter( - db.or_(OneOnOneRequest.player_id == user_id, OneOnOneRequest.coach_id == user_id), - ).delete(synchronize_session=False) - UserGamertag.query.filter_by(user_id=user_id).delete() - Contract.query.filter_by(player_id=user_id).delete() - TryoutRegistration.query.filter_by(player_id=user_id).delete() - TeamPlayer.query.filter_by(player_id=user_id).delete() - TeamMember.query.filter_by(player_id=user_id).delete() - MatchParticipant.query.filter_by(player_id=user_id).delete() - OrgTeam.query.filter_by(coach_id=user_id).update({'coach_id': None}) - OrgTeam.query.filter_by(manager_id=user_id).update({'manager_id': None}) - Tryout.query.filter_by(created_by=user_id).update({'created_by': current_user.id}) - Match.query.filter_by(created_by=user_id).update({'created_by': current_user.id}) - Team.query.filter_by(created_by=user_id).update({'created_by': current_user.id}) - OrgTeam.query.filter_by(created_by=user_id).update({'created_by': current_user.id}) - Contract.query.filter_by(uploaded_by_id=user_id).update({'uploaded_by_id': current_user.id}) - - db.session.delete(user) - db.session.commit() - flash(f'User {user.username} has been removed.', 'success') - return redirect(url_for('users.list_users')) - - -@users_bp.route('/create', methods=['GET', 'POST']) -@login_required -def create_user(): - """Create a new user (Admin only). Uses the correct polymorphic subclass.""" - if not isinstance(current_user, Admin): - flash('Only the president can create users.', 'danger') - return redirect(url_for('main.dashboard')) - - if request.method == 'POST': - username = request.form.get('username') - email = request.form.get('email') - password = request.form.get('password') - full_name = request.form.get('full_name') - phone = request.form.get('phone') - role = request.form.get('role') - - if role not in USER_TYPES: - flash('Invalid role selected.', 'danger') - return render_template('pages/create_user.html', roles=USER_TYPES) - - if User.query.filter_by(username=username).first(): - flash('Username already exists.', 'danger') - return render_template('pages/create_user.html', roles=USER_TYPES) - - if User.query.filter_by(email=email).first(): - flash('Email already registered.', 'danger') - return render_template('pages/create_user.html', roles=USER_TYPES) - - hashed_password = hash_password(password) - user_cls = _USER_CLASS_MAP.get(role, Player) - user = user_cls( - username=username, password_hash=hashed_password, - role=role, full_name=full_name, - email=email, phone=phone, - ) - db.session.add(user) - db.session.commit() - flash(f'User {full_name} created as {role}!', 'success') - return redirect(url_for('users.list_users')) - - return render_template('pages/create_user.html', roles=USER_TYPES) - - -@users_bp.route('//view') -@login_required -def view_user(user_id): - """View a public profile for any user.""" - user = User.query.get_or_404(user_id) - return render_template('pages/view_user.html', profile_user=user) - - -@users_bp.route('/profile') -@login_required -def profile(): - """View the current user's profile.""" - contracts = None - coach_availability = [] - if isinstance(current_user, Player): - contracts = Contract.query.filter_by( - player_id=current_user.id, - ).order_by(Contract.uploaded_at.desc()).all() - - existing_availability = None - if isinstance(current_user, Coach): - existing_availability = CoachAvailability.query.filter_by( - coach_id=current_user.id, - ).all() - - return render_template('pages/profile.html', user=current_user, contracts=contracts, - existing_availability=existing_availability) - - -@users_bp.route('/profile/edit', methods=['GET', 'POST']) -@login_required -def edit_profile(): - """Edit the current user's profile.""" - if request.method == 'POST': - username = request.form.get('username') - full_name = request.form.get('full_name') - email = request.form.get('email') - phone = request.form.get('phone') - - selected_games = request.form.getlist('games') - discord_username = request.form.get('discord_username', '').strip() - discord_user_id = request.form.get('discord_user_id', '').strip() - league_os_profile = request.form.get('league_os_profile', '').strip() - - if username != current_user.username and User.query.filter_by(username=username).first(): - flash('Username already taken.', 'danger') - return render_template('pages/edit_profile.html', user=current_user, - esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, - user_gamertags=current_user.get_gamertags()) - - if email != current_user.email and User.query.filter_by(email=email).first(): - flash('Email already in use.', 'danger') - return render_template('pages/edit_profile.html', user=current_user, - esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, - user_gamertags=current_user.get_gamertags()) - - current_user.username = username - current_user.full_name = full_name - current_user.email = email - current_user.phone = phone - current_user.games = ','.join(selected_games) if selected_games else None - current_user.discord_username = discord_username or None - current_user.discord_user_id = discord_user_id or None - current_user.league_os_profile = league_os_profile or None - - update_user_gamertags(current_user, selected_games) - - password = request.form.get('password') - if password: - current_user.password_hash = hash_password(password) - - db.session.commit() - flash('Profile updated successfully!', 'success') - return redirect(url_for('users.profile')) - - return render_template('pages/edit_profile.html', user=current_user, - esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, - user_gamertags=current_user.get_gamertags()) - - -# --------------------------------------------------------------------------- -# Disponibilities -# --------------------------------------------------------------------------- - -DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - - -def add_30_minutes(t): - return (datetime.combine(datetime.today(), t) + timedelta(minutes=30)).time() - - -@users_bp.route('/disponibilities') -@login_required -def get_disponibilities(): - """API endpoint to get all player disponibilities for scheduling.""" - if not current_user.can_manage_teams() and not current_user.can_schedule_matches(): - return jsonify({'error': 'Unauthorized'}), 403 - - players = User.query.filter_by(role='player', is_active_account=True).order_by(User.username).all() - result = {} - for player in players: - disponibilities = list(player.disponibilities) - result[player.id] = { - 'username': player.username, - 'disponibilities': [ - { - 'id': d.id, 'day_of_week': d.day_of_week, - 'day_name': DAY_NAMES[d.day_of_week], - 'start_time': d.start_time.strftime('%H:%M'), - 'end_time': d.end_time.strftime('%H:%M'), - } - for d in disponibilities - ], - } - return jsonify(result) - - -@users_bp.route('/disponibilities/my') -@login_required -def get_my_disponibilities(): - """API endpoint for players to get their own disponibilities.""" - disponibilities = PlayerDisponibility.query.filter_by(player_id=current_user.id).all() - result = {} - for d in disponibilities: - day = d.day_of_week - if day not in result: - result[day] = [] - result[day].append({ - 'id': d.id, 'day_of_week': d.day_of_week, - 'day_name': DAY_NAMES[d.day_of_week], - 'start_time': d.start_time.strftime('%H:%M'), - 'end_time': d.end_time.strftime('%H:%M'), - }) - return jsonify(result) - - -@users_bp.route('/disponibilities/add', methods=['POST']) -@login_required -def add_disponibility(): - """Add a disponibility block for the current player.""" - day_of_week = request.form.get('day_of_week', type=int) - start_time_str = request.form.get('start_time') - if day_of_week is None or day_of_week < 0 or day_of_week > 6: - return jsonify({'error': 'Invalid day of week'}), 400 - try: - start_time = datetime.strptime(start_time_str, '%H:%M').time() - except (ValueError, TypeError): - return jsonify({'error': 'Invalid time format'}), 400 - - end_time = add_30_minutes(start_time) - disponibility = PlayerDisponibility( - player_id=current_user.id, day_of_week=day_of_week, - start_time=start_time, end_time=end_time, - ) - db.session.add(disponibility) - db.session.commit() - return jsonify({ - 'id': disponibility.id, 'day_of_week': disponibility.day_of_week, - 'day_name': DAY_NAMES[disponibility.day_of_week], - 'start_time': disponibility.start_time.strftime('%H:%M'), - 'end_time': disponibility.end_time.strftime('%H:%M'), - }) - - -@users_bp.route('/disponibilities/add_bulk', methods=['POST']) -@login_required -def add_disponibilities_bulk(): - """Replace the player's disponibilities with the submitted slots. - - Performs a full replace (delete existing + insert submitted) so that - deselected slots are correctly removed, mirroring the coach availability - flow. This keeps the auto-save idempotent and correct. - """ - data = request.get_json() - slots = data.get('slots', []) if data else [] - - # Clear existing disponibilities for the current player. - PlayerDisponibility.query.filter_by(player_id=current_user.id).delete() - - created = [] - for slot in slots: - day_of_week = slot.get('day_of_week') - start_time_str = slot.get('start_time') - if day_of_week is None or day_of_week < 0 or day_of_week > 6: - continue - try: - start_time = datetime.strptime(start_time_str, '%H:%M').time() - except (ValueError, TypeError): - continue - - end_time = add_30_minutes(start_time) - disponibility = PlayerDisponibility( - player_id=current_user.id, day_of_week=day_of_week, - start_time=start_time, end_time=end_time, - ) - db.session.add(disponibility) - db.session.flush() - created.append({ - 'id': disponibility.id, 'day_of_week': disponibility.day_of_week, - 'day_name': DAY_NAMES[disponibility.day_of_week], - 'start_time': disponibility.start_time.strftime('%H:%M'), - }) - db.session.commit() - return jsonify({'success': True, 'created': created}) - - -@users_bp.route('/disponibilities/clear', methods=['POST']) -@login_required -def clear_disponibilities(): - """Clear all disponibilities for the current player.""" - PlayerDisponibility.query.filter_by(player_id=current_user.id).delete() - db.session.commit() - return jsonify({'success': True}) - - -@users_bp.route('/disponibilities//delete', methods=['POST']) -@login_required -def delete_disponibility(disponibility_id): - """Delete a disponibility block.""" - disponibility = PlayerDisponibility.query.get_or_404(disponibility_id) - if disponibility.player_id != current_user.id: - return jsonify({'error': 'Unauthorized'}), 403 - db.session.delete(disponibility) - db.session.commit() - return jsonify({'success': True}) - - -# --------------------------------------------------------------------------- -# Contracts -# --------------------------------------------------------------------------- - -def can_manage_player_contract(user, player_id): - """Check if a user can upload contracts for a specific player.""" - if isinstance(user, Admin): - return True - if isinstance(user, Manager): - return True - if isinstance(user, Coach): - org_team = OrgTeam.query.filter_by(coach_id=user.id).first() - if org_team: - tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=org_team.id).first() - if tp: - return True - return False - - -@users_bp.route('/contracts') -@login_required -def list_contracts(): - """View contracts for the current user or players they manage.""" - contracts = None - players = None - - if isinstance(current_user, Player): - contracts = Contract.query.filter_by( - player_id=current_user.id, - ).order_by(Contract.uploaded_at.desc()).all() - elif isinstance(current_user, (Admin, Manager, Coach)): - players = [] - if isinstance(current_user, Coach): - org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() - if org_team: - player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()] - players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else [] - else: - players = User.query.filter_by(role='player').all() - - if players: - player_ids = [p.id for p in players] - contracts = Contract.query.filter( - Contract.player_id.in_(player_ids), - ).order_by(Contract.uploaded_at.desc()).all() - - return render_template('pages/contracts.html', contracts=contracts, players=players - if isinstance(current_user, (Admin, Manager, Coach)) else None) - - -@users_bp.route('/contracts/upload', methods=['GET', 'POST']) -@login_required -def upload_contract(): - """Upload a contract for a player.""" - if not isinstance(current_user, (Admin, Manager, Coach)): - flash('Only presidents, managers, and coaches can upload contracts.', 'danger') - return redirect(url_for('users.list_contracts')) - - if isinstance(current_user, Coach): - org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() - if org_team: - player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()] - players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else [] - else: - players = [] - else: - players = User.query.filter_by(role='player').all() - - if request.method == 'POST': - contract_schema = UploadContractSchema() - try: - validated = contract_schema.load(request.form) - except ValidationError as err: - for field, messages in err.messages.items(): - for msg in messages: - flash(f'{field}: {msg}', 'danger') - return render_template('pages/upload_contract.html', players=players) - - player_id = validated['player_id'] - notes = validated.get('notes') - - if not can_manage_player_contract(current_user, player_id): - flash('You do not have permission to upload a contract for this player.', 'danger') - return redirect(url_for('users.upload_contract')) - - if 'contract_file' not in request.files: - flash('No file selected.', 'danger') - return redirect(url_for('users.upload_contract')) - - file = request.files['contract_file'] - if file.filename == '': - flash('No file selected.', 'danger') - return redirect(url_for('users.upload_contract')) - if not file.filename.lower().endswith('.pdf'): - flash('Only PDF files are allowed for contracts.', 'danger') - return redirect(url_for('users.upload_contract')) - - upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés') - os.makedirs(upload_dir, exist_ok=True) - - player = User.query.get_or_404(player_id) - player_teams = player.get_org_teams() - team = player_teams[0] if player_teams else None - - if team: - team_folder = os.path.join(upload_dir, secure_filename(team.name)) - os.makedirs(team_folder, exist_ok=True) - final_dir = team_folder - else: - final_dir = upload_dir - - original_filename = secure_filename(file.filename) - file_uuid = str(uuid.uuid4()) - stored_filename = f"{file_uuid}.pdf" - file_path = os.path.join(final_dir, stored_filename) - file.save(file_path) - - contract = Contract( - player_id=player_id, team_id=team.id if team else None, - uploaded_by_id=current_user.id, - original_filename=original_filename, - stored_filename=stored_filename, - file_path=file_path, - notes=notes if notes else None, - ) - db.session.add(contract) - db.session.commit() - flash(f'Contract uploaded successfully for {player.username}!', 'success') - return redirect(url_for('users.list_contracts')) - - return render_template('pages/upload_contract.html', players=players) - - -@users_bp.route('/contracts//upload_signed', methods=['POST']) -@login_required -def upload_signed_contract(contract_id): - """Upload a signed contract (player only).""" - contract = Contract.query.get_or_404(contract_id) - if not contract.can_upload_signed(current_user): - flash('Only the player can upload their signed contract.', 'danger') - return redirect(url_for('users.list_contracts')) - - if 'signed_file' not in request.files: - flash('No file selected.', 'danger') - return redirect(url_for('users.list_contracts')) - - file = request.files['signed_file'] - if file.filename == '': - flash('No file selected.', 'danger') - return redirect(url_for('users.list_contracts')) - - signed_filename = f"signed_{contract.stored_filename}" - file.save(contract.file_path.replace(contract.stored_filename, signed_filename)) - - contract.signed_filename = signed_filename - contract.signed_file_path = contract.file_path.replace(contract.stored_filename, signed_filename) - contract.status = 'signed' - contract.signed_at = utc_now_naive() - db.session.commit() - flash('Signed contract uploaded successfully!', 'success') - return redirect(url_for('users.list_contracts')) - - -@users_bp.route('/contracts//download') -@login_required -def download_contract(contract_id): - """Download a contract file.""" - contract = Contract.query.get_or_404(contract_id) - if not contract.can_view(current_user): - flash('You do not have permission to download this contract.', 'danger') - return redirect(url_for('users.list_contracts')) - return send_file(contract.file_path, as_attachment=True, download_name=contract.original_filename) - - -@users_bp.route('/contracts//download_signed') -@login_required -def download_signed_contract(contract_id): - """Download a signed contract file.""" - contract = Contract.query.get_or_404(contract_id) - if not contract.can_view(current_user): - flash('You do not have permission to download this contract.', 'danger') - return redirect(url_for('users.list_contracts')) - if not contract.signed_file_path: - flash('No signed contract available.', 'danger') - return redirect(url_for('users.list_contracts')) - return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename) - - -# --------------------------------------------------------------------------- -# One on One -# --------------------------------------------------------------------------- - -DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '') - - -def send_discord_notification(player_name, points, date_str, start_time_str, end_time_str, - team_name, coach_name, coach_discord, coach_discord_id, request_id=None): - """Send a Discord notification for a One on One request.""" - import logging - logger = logging.getLogger(__name__) - - if coach_discord_id: - try: - from app.discord_bot import send_one_on_one_dm - send_one_on_one_dm( - coach_name=coach_name, coach_discord_id=coach_discord_id, - player_name=player_name, team_name=team_name, - date_str=date_str, start_time=start_time_str, - end_time=end_time_str, points=points, request_id=request_id, - ) - except Exception as e: - logger.warning(f"Failed to send Discord DM: {e}") - - if DISCORD_WEBHOOK_URL: - try: - from app.discord_bot import send_one_on_one_dm - if DISCORD_WEBHOOK_URL.isdigit() and not coach_discord_id: - send_one_on_one_dm( - coach_name=coach_name, coach_discord_id=DISCORD_WEBHOOK_URL, - player_name=player_name, team_name=team_name, - date_str=date_str, start_time=start_time_str, - end_time=end_time_str, points=points, - ) - elif not DISCORD_WEBHOOK_URL.isdigit(): - embed = { - "embeds": [{ - "title": "One on One Request", "color": 3447003, - "fields": [ - {"name": "Player", "value": player_name, "inline": True}, - {"name": "Team", "value": team_name or "Unknown Team", "inline": True}, - {"name": "Date", "value": date_str, "inline": True}, - {"name": "Time", "value": f"{start_time_str} - {end_time_str}", "inline": True}, - {"name": "Discussion Points", "value": points or "No specific points provided", "inline": False}, - ], - "footer": { - "text": f"Coach: {coach_name}" - + (f" (Discord: {coach_discord})" if coach_discord else ""), - }, - }], - } - requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5) - except Exception as e: - logger.warning(f"Failed to send Discord notification: {e}") - - -def _get_player_coach_options(player): - """Build a deduplicated list of coaches (with their teams) for a player. - - Returns a list of dicts: - [{'coach_id': int, 'coach_name': str, 'teams': [{'team_id': int, 'team_name': str}]}] - """ - coach_map = {} - for org_team in player.get_org_teams(): - for coach in org_team.get_coaches(): - entry = coach_map.setdefault(coach.id, { - 'coach_id': coach.id, - 'coach_name': coach.full_name or coach.username, - 'teams': [], - }) - entry['teams'].append({ - 'team_id': org_team.id, - 'team_name': org_team.name, - }) - return list(coach_map.values()) - - -def _get_player_coach_ids(player): - """Return the set of coach ids the player is allowed to request.""" - return {c['coach_id'] for c in _get_player_coach_options(player)} - - -@users_bp.route('/one-on-one/coaches') -@login_required -def one_on_one_coaches(): - """API: list coaches (and their teams) available to the current player.""" - if not isinstance(current_user, Player): - return jsonify({'error': 'Only players can request One on One sessions.'}), 403 - - coaches = _get_player_coach_options(current_user) - return jsonify({'coaches': coaches}) - - -@users_bp.route('/one-on-one/coaches//availability') -@login_required -def one_on_one_coach_availability(coach_id): - """API: return a single coach's availability for the current player.""" - if not isinstance(current_user, Player): - return jsonify({'error': 'Only players can request One on One sessions.'}), 403 - - if coach_id not in _get_player_coach_ids(current_user): - return jsonify({'error': 'Coach is not assigned to any of your teams.'}), 403 - - coach = User.query.get_or_404(coach_id) - availabilities = CoachAvailability.query.filter_by(coach_id=coach_id).all() - availability = [ - { - 'day_of_week': av.day_of_week, - 'start_time': av.start_time.strftime('%H:%M'), - 'end_time': av.end_time.strftime('%H:%M'), - } - for av in availabilities - ] - return jsonify({ - 'coach_id': coach.id, - 'coach_name': coach.full_name or coach.username, - 'availability': availability, - }) - - -@users_bp.route('/one-on-one', methods=['GET', 'POST']) -@login_required -def one_on_one(): - """One on One request page for players.""" - if not isinstance(current_user, Player): - flash('Only players can request One on One sessions.', 'danger') - return redirect(url_for('main.dashboard')) - - coach_options = _get_player_coach_options(current_user) - - if request.method == 'POST': - coach_id = request.form.get('coach_id', type=int) - org_team_id = request.form.get('org_team_id', type=int) - date_str = request.form.get('date') - start_time_str = request.form.get('start_time') - end_time_str = request.form.get('end_time') - points = request.form.get('points', '').strip() - - if not coach_options: - flash('Cannot request One on One - no coach assigned.', 'danger') - return redirect(url_for('users.one_on_one')) - - if coach_id not in _get_player_coach_ids(current_user): - flash('Selected coach is not assigned to any of your teams.', 'danger') - return redirect(url_for('users.one_on_one')) - - coach = User.query.get_or_404(coach_id) - - # Validate org_team_id belongs to the player and includes this coach - org_team = None - if org_team_id: - org_team = OrgTeam.query.get(org_team_id) - if not org_team or org_team not in current_user.get_org_teams(): - flash('Invalid team selection.', 'danger') - return redirect(url_for('users.one_on_one')) - if coach not in org_team.get_coaches(): - flash('Selected coach is not part of the selected team.', 'danger') - return redirect(url_for('users.one_on_one')) - - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() - start_time = datetime.strptime(start_time_str, '%H:%M').time() - end_time = datetime.strptime(end_time_str, '%H:%M').time() - except (ValueError, TypeError): - flash('Invalid date or time format.', 'danger') - return redirect(url_for('users.one_on_one')) - - day_of_week = date_obj.weekday() - - availabilities = CoachAvailability.query.filter_by(coach_id=coach.id).all() - is_available = any( - av.day_of_week == day_of_week - and av.start_time <= start_time - and av.end_time >= end_time - for av in availabilities - ) - - if not is_available: - flash("The requested time is not within the coach's availability.", 'danger') - return redirect(url_for('users.one_on_one')) - - request_obj = OneOnOneRequest( - player_id=current_user.id, coach_id=coach.id, - org_team_id=org_team.id if org_team else None, - date=date_obj, start_time=start_time, end_time=end_time, - points=points if points else None, - ) - db.session.add(request_obj) - db.session.commit() - - send_discord_notification( - player_name=current_user.full_name, - points=points, date_str=date_str, - start_time_str=start_time_str, end_time_str=end_time_str, - team_name=org_team.name if org_team else 'Unknown Team', - coach_name=coach.full_name, - coach_discord=coach.discord_username or '', - coach_discord_id=coach.discord_user_id or '', - request_id=request_obj.id, - ) - - flash('Your One on One request has been submitted!', 'success') - return redirect(url_for('users.one_on_one')) - - # Player's own One on One request history - my_requests = OneOnOneRequest.query.filter_by( - player_id=current_user.id - ).order_by(OneOnOneRequest.created_at.desc()).all() - - return render_template('pages/one_on_one.html', - coach_options=coach_options, - my_requests=my_requests) - - -@users_bp.route('/one-on-one//accept', methods=['POST']) -@login_required -def accept_one_on_one(request_id): - """Coach accepts a One on One request.""" - if not isinstance(current_user, Coach): - flash('Only coaches can accept One on One requests.', 'danger') - return redirect(url_for('main.dashboard')) - - request_obj = OneOnOneRequest.query.get_or_404(request_id) - - if request_obj.coach_id != current_user.id: - flash('This request is not for you.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - if request_obj.status != 'pending': - flash('This request has already been processed.', 'info') - return redirect(url_for('users.notes_dashboard')) - - player = request_obj.player - request_obj.status = 'approved' - request_obj.responded_at = utc_now_naive() - db.session.commit() - - # Notify player via Discord (same message as if approved through Discord reactions) - if player and player.discord_user_id: - from app.discord_bot import send_one_on_one_response - send_one_on_one_response( - player_discord_id=player.discord_user_id, - player_full_name=player.full_name, - coach_full_name=current_user.full_name, - date_str=request_obj.date.strftime('%A, %B %d, %Y'), - start_time=request_obj.start_time.strftime('%I:%M %p') if request_obj.start_time else 'TBD', - end_time=request_obj.end_time.strftime('%I:%M %p') if request_obj.end_time else 'TBD', - points=request_obj.points or 'No specific points provided', - approved=True, - ) - - flash(f'One on One request from {player.username if player else "Unknown"} has been approved!', 'success') - return redirect(url_for('users.notes_dashboard')) - - -@users_bp.route('/one-on-one//reject', methods=['POST']) -@login_required -def reject_one_on_one(request_id): - """Coach rejects a One on One request.""" - if not isinstance(current_user, Coach): - flash('Only coaches can reject One on One requests.', 'danger') - return redirect(url_for('main.dashboard')) - - request_obj = OneOnOneRequest.query.get_or_404(request_id) - - if request_obj.coach_id != current_user.id: - flash('This request is not for you.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - if request_obj.status != 'pending': - flash('This request has already been processed.', 'info') - return redirect(url_for('users.notes_dashboard')) - - rejection_reason = request.form.get('rejection_reason', '').strip() - player = request_obj.player - - request_obj.status = 'rejected' - request_obj.responded_at = utc_now_naive() - if rejection_reason: - request_obj.coach_rejection_message = rejection_reason - db.session.commit() - - # Notify player via Discord (same message as if rejected through Discord reactions) - if player and player.discord_user_id: - from app.discord_bot import send_one_on_one_response - send_one_on_one_response( - player_discord_id=player.discord_user_id, - player_full_name=player.full_name, - coach_full_name=current_user.full_name, - date_str=request_obj.date.strftime('%A, %B %d, %Y'), - start_time=request_obj.start_time.strftime('%I:%M %p') if request_obj.start_time else 'TBD', - end_time=request_obj.end_time.strftime('%I:%M %p') if request_obj.end_time else 'TBD', - points=request_obj.points or 'No specific points provided', - approved=False, - refusal_note=rejection_reason or None, - ) - - flash(f'One on One request from {player.username if player else "Unknown"} has been rejected.', 'info') - return redirect(url_for('users.notes_dashboard')) - - -# --------------------------------------------------------------------------- -# My Notes (Player) -# --------------------------------------------------------------------------- - -@users_bp.route('/my-notes') -@login_required -def my_notes(): - """View personal and team notes for the current player.""" - if not isinstance(current_user, Player): - flash('This page is for players only.', 'info') - return redirect(url_for('main.dashboard')) - - org_teams = current_user.get_org_teams() - org_team = org_teams[0] if org_teams else None - - personal_notes = PersonalNote.query.filter_by( - player_id=current_user.id, - ).order_by(PersonalNote.created_at.desc()).all() - - team_notes = [] - if org_team: - team_notes = TeamNote.query.filter_by( - org_team_id=org_team.id, - ).order_by(TeamNote.created_at.desc()).all() - - return render_template('pages/player_personal_notes.html', - org_team=org_team, - personal_notes=personal_notes, - team_notes=team_notes) - - -# --------------------------------------------------------------------------- -# Coach Availability -# --------------------------------------------------------------------------- - -@users_bp.route('/coach-availability', methods=['GET', 'POST']) -@login_required -def manage_coach_availability(): - """Manage coach availability for One on One sessions.""" - if not isinstance(current_user, Coach): - flash('Only coaches can manage availability.', 'danger') - return redirect(url_for('main.dashboard')) - - if request.method == 'POST': - data = request.get_json() - slots = data.get('slots', []) if data else [] - - # Clear existing availability - CoachAvailability.query.filter_by(coach_id=current_user.id).delete() - - # Add new slots - for slot in slots: - day_of_week = slot.get('day_of_week') - start_time_str = slot.get('start_time') - if day_of_week is None or day_of_week < 0 or day_of_week > 6: - continue - try: - start_time = datetime.strptime(start_time_str, '%H:%M').time() - end_time = (datetime.combine(datetime.today(), start_time) + timedelta(minutes=30)).time() - except (ValueError, TypeError): - continue - - availability = CoachAvailability( - coach_id=current_user.id, - day_of_week=day_of_week, - start_time=start_time, - end_time=end_time, - ) - db.session.add(availability) - - db.session.commit() - return jsonify({'success': True}) - - # GET requests redirect to profile page where availability is now managed - return redirect(url_for('users.profile')) - - -@users_bp.route('/coach-availability/clear', methods=['POST']) -@login_required -def clear_coach_availability(): - """Clear all coach availability slots.""" - if not isinstance(current_user, Coach): - return jsonify({'error': 'Unauthorized'}), 403 - - CoachAvailability.query.filter_by(coach_id=current_user.id).delete() - db.session.commit() - return jsonify({'success': True}) - - -# --------------------------------------------------------------------------- -# Notes Dashboard (Coach) -# --------------------------------------------------------------------------- - -@users_bp.route('/notes-dashboard') -@login_required -def notes_dashboard(): - """Notes and One on One dashboard for coaches.""" - if not isinstance(current_user, Coach): - flash('Only coaches can access the notes dashboard.', 'danger') - return redirect(url_for('main.dashboard')) - - org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() - - players = [] - if org_team: - player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()] - players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else [] - - team_notes = [] - personal_notes = [] - latest_team_note = None - if org_team: - team_notes = TeamNote.query.filter_by( - org_team_id=org_team.id, - ).order_by(TeamNote.created_at.desc()).all() - latest_team_note = team_notes[0] if team_notes else None - - personal_notes = PersonalNote.query.filter_by( - coach_id=current_user.id, - ).order_by(PersonalNote.created_at.desc()).all() - - # One on One requests addressed to this coach from their team players - one_on_one_requests = [] - if org_team and players: - player_ids_list = [p.id for p in players] - one_on_one_requests = OneOnOneRequest.query.filter( - OneOnOneRequest.player_id.in_(player_ids_list), - OneOnOneRequest.coach_id == current_user.id, - ).order_by(OneOnOneRequest.created_at.desc()).all() - - # For context selectors in the form - from app.models import Team as MatchTeam - matches = Match.query.filter( - db.or_(Match.created_by == current_user.id, Match.status == 'scheduled'), - ).order_by(Match.date.desc()).limit(20).all() - tryouts = Tryout.query.filter_by( - created_by=current_user.id, - ).order_by(Tryout.date.desc()).limit(20).all() - teams = OrgTeam.query.order_by(OrgTeam.name).all() - - return render_template('pages/notes.html', - org_team=org_team, - players=players, - team_notes=team_notes, - latest_team_note=latest_team_note, - personal_notes=personal_notes, - one_on_one_requests=one_on_one_requests, - matches=matches, - tryouts=tryouts, - teams=teams) - - -# --------------------------------------------------------------------------- -# Manage Team Notes (POST) -# --------------------------------------------------------------------------- - -@users_bp.route('/team-notes/manage', methods=['POST']) -@login_required -def manage_team_notes(): - """Create or update team notes for the coach's org team.""" - if not isinstance(current_user, Coach): - flash('Only coaches can manage team notes.', 'danger') - return redirect(url_for('main.dashboard')) - - org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() - if not org_team: - flash('You are not assigned to a team.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - content = request.form.get('content', '').strip() - if content: - note = TeamNote( - org_team_id=org_team.id, - coach_id=current_user.id, - content=content, - ) - db.session.add(note) - db.session.commit() - flash('Team notes saved successfully!', 'success') - - return redirect(url_for('users.notes_dashboard')) - - -# --------------------------------------------------------------------------- -# Manage Personal Notes (POST, simple form) -# --------------------------------------------------------------------------- - -@users_bp.route('/personal-notes/manage', methods=['POST']) -@login_required -def manage_personal_notes(): - """Create a personal note for a player (coach only, simple form).""" - if not isinstance(current_user, Coach): - flash('Only coaches can manage personal notes.', 'danger') - return redirect(url_for('main.dashboard')) - - player_id = request.form.get('player_id', type=int) - content = request.form.get('content', '').strip() - - if not player_id or not content: - flash('Player and content are required.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - player = User.query.get_or_404(player_id) - if not isinstance(player, Player): - flash('Can only add notes for players.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - note = PersonalNote( - player_id=player_id, - coach_id=current_user.id, - content=content, - ) - db.session.add(note) - db.session.commit() - flash(f'Note added for {player.username}.', 'success') - return redirect(url_for('users.notes_dashboard')) - - -# --------------------------------------------------------------------------- -# Add Personal Note (POST, full form with context) -# --------------------------------------------------------------------------- - -@users_bp.route('/personal-notes/add', methods=['POST']) -@login_required -def add_personal_note(): - """Create a personal note for a player with optional context (coach only).""" - if not isinstance(current_user, Coach): - flash('Only coaches can add personal notes.', 'danger') - return redirect(url_for('main.dashboard')) - - player_id = request.form.get('player_id', type=int) - content = request.form.get('content', '').strip() - match_id = request.form.get('match_id', type=int) - tryout_id = request.form.get('tryout_id', type=int) - team_id_str = request.form.get('team_id') - - if not player_id or not content: - flash('Player and content are required.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - player = User.query.get_or_404(player_id) - if not isinstance(player, Player): - flash('Can only add notes for players.', 'danger') - return redirect(url_for('users.notes_dashboard')) - - note = PersonalNote( - player_id=player_id, - coach_id=current_user.id, - content=content, - match_id=match_id if match_id else None, - tryout_id=tryout_id if tryout_id else None, - team_id=int(team_id_str) if team_id_str and team_id_str.isdigit() else None, - ) - db.session.add(note) - db.session.commit() - flash(f'Note added for {player.username}.', 'success') - return redirect(url_for('users.notes_dashboard')) - - -# --------------------------------------------------------------------------- -# Add Note from Tryout context (GET + POST) -# --------------------------------------------------------------------------- - -@users_bp.route('/personal-notes/tryout/', methods=['GET', 'POST']) -@login_required -def add_note_from_tryout(tryout_id): - """Add a personal note for a player in the context of a tryout.""" - if not isinstance(current_user, Coach): - flash('Only coaches can add personal notes.', 'danger') - return redirect(url_for('main.dashboard')) - - tryout = Tryout.query.get_or_404(tryout_id) - preselected_player_id = request.args.get('player_id', type=int) - - # Get registrations as players for the select list - registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() - players = [r.player for r in registrations if r.player] - - if request.method == 'POST': - player_id = request.form.get('player_id', type=int) - content = request.form.get('content', '').strip() - - if not player_id or not content: - flash('Player and content are required.', 'danger') - return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) - - note = PersonalNote( - player_id=player_id, - coach_id=current_user.id, - content=content, - tryout_id=tryout_id, - ) - db.session.add(note) - db.session.commit() - flash('Note added successfully.', 'success') - return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) - - return render_template('pages/add_note.html', - context_type='tryout', - tryout=tryout, - players=players, - preselected_player_id=preselected_player_id, - team_notes=[]) - - -# --------------------------------------------------------------------------- -# Add Note from Match context (GET + POST) -# --------------------------------------------------------------------------- - -@users_bp.route('/personal-notes/match/', methods=['GET', 'POST']) -@login_required -def add_note_from_match(match_id): - """Add a personal note for a player in the context of a match.""" - if not isinstance(current_user, Coach): - flash('Only coaches can add personal notes.', 'danger') - return redirect(url_for('main.dashboard')) - - match_obj = Match.query.get_or_404(match_id) - - # Get participants as players for the select list - participants = MatchParticipant.query.filter_by(match_id=match_id).all() - players = [p.player for p in participants if p.player] - - preselected_player_id = request.args.get('player_id', type=int) - - if request.method == 'POST': - player_id = request.form.get('player_id', type=int) - content = request.form.get('content', '').strip() - - if not player_id or not content: - flash('Player and content are required.', 'danger') - return redirect(url_for('users.add_note_from_match', match_id=match_id)) - - note = PersonalNote( - player_id=player_id, - coach_id=current_user.id, - content=content, - match_id=match_id, - ) - db.session.add(note) - db.session.commit() - flash('Note added successfully.', 'success') - return redirect(url_for('tryouts.view_tryout', tryout_id=match_obj.tryout_id)) - - return render_template('pages/add_note.html', - context_type='match', - tryout=match_obj, - match=match_obj, - players=players, - preselected_player_id=preselected_player_id, - team_notes=[]) diff --git a/app/routes/users/_shared.py b/app/routes/users/_shared.py index 4acd465..88920c3 100644 --- a/app/routes/users/_shared.py +++ b/app/routes/users/_shared.py @@ -39,6 +39,10 @@ def pdf_upload_error(file, allowed_extensions): could put an arbitrary file on the server under a name the application later hands back for download (SEC-021). + The check now goes beyond the header: it parses the upload with pypdf + and requires at least one parseable page, so a file with ``%PDF-`` + prepended or a renamed non-PDF is rejected. + Args: file: The uploaded FileStorage, or None. allowed_extensions: Extensions to accept, lowercase and without dot. @@ -53,11 +57,27 @@ def pdf_upload_error(file, allowed_extensions): if not (stem and dot) or extension.lower() not in allowed_extensions: return _('Only PDF files are allowed for contracts.') + # Fast-fail: the PDF signature must appear at the start of the stream. head = file.stream.read(len(PDF_SIGNATURE)) file.stream.seek(0) if head != PDF_SIGNATURE: return _('That file is not a PDF, whatever its name says.') + # Full-content validation: parse with pypdf and require at least one + # page. A file with a forged header but no real PDF structure (e.g. a + # renamed executable with ``%PDF-`` prepended) will fail here. + try: + from pypdf import PdfReader + from pypdf.errors import PyPdfError + + reader = PdfReader(file.stream, strict=False) + if len(reader.pages) == 0: + return _('That file is not a PDF, whatever its name says.') + except (PyPdfError, ValueError, TypeError): + return _('That file is not a PDF, whatever its name says.') + finally: + file.stream.seek(0) + return None diff --git a/app/routes/users/contracts.py b/app/routes/users/contracts.py index 8505d94..fb43412 100644 --- a/app/routes/users/contracts.py +++ b/app/routes/users/contracts.py @@ -135,7 +135,9 @@ def upload_contract(): try: stored_path = store_uploaded_document(file, relative_path) except GoogleDriveStorageError: - flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger') + flash( + _('Contract storage is temporarily unavailable. Please try again later.'), 'danger' + ) return redirect(url_for('users.upload_contract')) contract = Contract( diff --git a/app/storage.py b/app/storage.py index 10c27ea..c2b1cea 100644 --- a/app/storage.py +++ b/app/storage.py @@ -34,8 +34,14 @@ import os from app.google_drive import ( GoogleDriveStorageError, +) +from app.google_drive import ( delete_file as delete_google_drive_file, +) +from app.google_drive import ( download_file as download_google_drive_file, +) +from app.google_drive import ( upload_file as upload_google_drive_file, ) @@ -112,7 +118,7 @@ def _rooted(env_name, default_name): def document_storage_backend(): """Return the configured backend for new document uploads.""" - backend = os.getenv(DOCUMENT_STORAGE_BACKEND_ENV, 'google_drive').strip().lower() + backend = os.getenv(DOCUMENT_STORAGE_BACKEND_ENV, 'local').strip().lower() if backend not in {'google_drive', 'local'}: raise ValueError( f'{DOCUMENT_STORAGE_BACKEND_ENV} must be "google_drive" or "local", not {backend!r}' diff --git a/app/supporting_scripts/backup.py b/app/supporting_scripts/backup.py index 2432031..d740bca 100644 --- a/app/supporting_scripts/backup.py +++ b/app/supporting_scripts/backup.py @@ -42,7 +42,11 @@ from urllib.parse import unquote, urlparse # to be — see DOCUMENTS_DIR below. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) -from app.storage import backups_root, documents_root # noqa: E402 — needs the path above +from app.storage import ( # noqa: E402 — needs the path above + backups_root, + document_storage_backend, + documents_root, +) # Configuration BACKUP_DIR = backups_root() @@ -292,6 +296,75 @@ def backup_documents(): return zip_path +def backup_drive_documents(): + """Download every file from the configured Google Drive folder and archive it. + + Called **in addition to** ``backup_documents()`` when the storage backend is + ``google_drive``. When the backend is ``local`` this function silently + skips — local backups already handled by ``backup_documents()``. + + Returns: + str | None: Path to the created archive, or None when skipped. + + Raises: + BackupError: If Drive is configured but listing or downloading fails. + """ + if document_storage_backend() != 'google_drive': + print('[SKIP] Google Drive is not configured; local documents only.') + return None + + try: + from app.google_drive import ( + GoogleDriveStorageError, + list_folder_files, + ) + from app.google_drive import ( + download_file as download_drive_file, + ) + except ImportError: + raise BackupError( + 'Google Drive dependencies are not installed. Install requirements.txt again.' + ) from None + + print('[INFO] Listing Google Drive contract folder...') + try: + files = list_folder_files() + except (GoogleDriveStorageError, Exception) as exc: # noqa: BLE001 — boundary + raise BackupError(f'Google Drive listing failed: {exc}') from exc + + if not files: + print('[INFO] Google Drive folder is empty; no Drive documents to back up.') + return None + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + archive_basename = os.path.join(BACKUP_DIR, f'documents_backup_drive_{timestamp}') + + download = {} + for file_id, name in files: + print(f' Downloading {name} ({file_id})...') + try: + download[name] = download_drive_file(file_id) + except (GoogleDriveStorageError, Exception) as exc: # noqa: BLE001 — boundary + raise BackupError(f'Google Drive download failed for {name}: {exc}') from exc + + try: + import zipfile + + zip_path = f'{archive_basename}.zip' + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: + for name, data in download.items(): + zf.writestr(name, data) + except Exception as exc: # noqa: BLE001 — boundary + raise BackupError(f'Drive document archive failed: {exc}') from exc + + if not os.path.exists(zip_path) or os.path.getsize(zip_path) == 0: + raise BackupError('Drive document archiver reported success but produced an empty file.') + + size_mb = os.path.getsize(zip_path) / (1024 * 1024) + print(f'[OK] Drive documents backed up to: {zip_path} ({size_mb:.1f} MB)') + return zip_path + + def cleanup_old_backups(): """Remove backup files older than BACKUP_RETENTION_DAYS.""" if not os.path.exists(BACKUP_DIR): @@ -365,10 +438,16 @@ def main(argv=None): except BackupError as exc: documents_ok = False print(f'[ERROR] {exc}') + drive_ok = True + try: + backup_drive_documents() + except BackupError as exc: + drive_ok = False + print(f'[ERROR] {exc}') cleanup_old_backups() print() - if verified and documents_ok: + if verified and documents_ok and drive_ok: print('=== Backup completed successfully ===') return 0 diff --git a/app/supporting_scripts/get_refresh_token.py b/app/supporting_scripts/get_refresh_token.py new file mode 100644 index 0000000..1601d7c --- /dev/null +++ b/app/supporting_scripts/get_refresh_token.py @@ -0,0 +1,121 @@ +"""Obtain a Google Drive refresh token for the application. + +Run this once from a development machine that has a browser. It opens a local +OAuth redirect server, asks you to log in, and prints the refresh token. + +Usage: + python app/supporting_scripts/get_refresh_token.py + +Requires: + pip install google-auth-oauthlib + +Before running: + 1. Create a Google Cloud project and enable the Drive API. + 2. Create a **Desktop** OAuth 2.0 client ID. + 3. Add your email as a test user in the OAuth consent screen. + 4. Run this script and paste the values when prompted. +""" + +import os +import sys + + +def _prompt(label, env_name, default=""): + """Ask the user for a value, accepting an env-var fallback or a default.""" + candidate = os.getenv(env_name) or default + hint = f" [{candidate}]" if candidate else "" + raw = input(f"{label}{hint}: ").strip() + if raw: + return raw + if candidate: + return candidate + return default + + +def _validate_client_id(client_id): + """Warn if the client ID doesn't look like a real OAuth client ID.""" + if client_id == "YOUR_CLIENT_ID.apps.googleusercontent.com": + print("[ERROR] You must provide a real client ID from the Google Cloud console.") + return False + if not client_id.endswith(".apps.googleusercontent.com"): + print( + f"[WARNING] Client ID should end with '.apps.googleusercontent.com'.\n" + f" Got: {client_id!r}\n" + f" If this is a placeholder, the OAuth request will fail.\n" + ) + return True + + +def _validate_client_secret(secret): + if secret == "YOUR_CLIENT_SECRET": + print("[ERROR] You must provide a real client secret from the Google Cloud console.") + return False + # The GOCSPX- prefix is now used by every client type, so it no longer + # tells us anything about whether the client was created as a Desktop app. + return True + + +def main(): + print("=" * 60) + print("Google Drive Refresh Token — Setup") + print("=" * 60) + print() + + # When both env vars are set, skip prompts entirely for headless runs. + env_id = os.getenv("GOOGLE_DRIVE_CLIENT_ID") + env_secret = os.getenv("GOOGLE_DRIVE_CLIENT_SECRET") + if env_id and env_secret: + client_id = env_id + client_secret = env_secret + print(f"Using Client ID from environment: {client_id[:30]}...") + else: + print("Enter your Google Cloud OAuth credentials.") + print("You can also set GOOGLE_DRIVE_CLIENT_ID and GOOGLE_DRIVE_CLIENT_SECRET") + print("environment variables and re-run to skip the prompts.") + print() + + client_id = _prompt("Client ID", "GOOGLE_DRIVE_CLIENT_ID") + client_secret = _prompt("Client Secret", "GOOGLE_DRIVE_CLIENT_SECRET") + + if not _validate_client_id(client_id) or not _validate_client_secret(client_secret): + sys.exit(1) + + print() + print("Opening browser to authenticate with Google...") + print("(If it doesn't open, copy the URL below into your browser.)") + print() + + from google_auth_oauthlib.flow import InstalledAppFlow + + SCOPE = "https://www.googleapis.com/auth/drive.file" + + client_config = { + "installed": { + "client_id": client_id, + "client_secret": client_secret, + "redirect_uris": ["http://localhost"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + } + } + + flow = InstalledAppFlow.from_client_config(client_config, scopes=[SCOPE]) + credentials = flow.run_local_server(host="localhost", port=8080, open_browser=True) + + print() + print("=" * 60) + print("SUCCESS — copy these values into your .env file:") + print("=" * 60) + print() + print("DOCUMENT_STORAGE_BACKEND=google_drive") + print(f"GOOGLE_DRIVE_CLIENT_ID={credentials.client_id}") + print(f"GOOGLE_DRIVE_CLIENT_SECRET={client_secret}") + print(f"GOOGLE_DRIVE_REFRESH_TOKEN={credentials.refresh_token}") + print("GOOGLE_DRIVE_FOLDER_ID=") + print() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/app/templates/pages/contracts.html b/app/templates/pages/contracts.html index d20e3f6..b828bfd 100644 --- a/app/templates/pages/contracts.html +++ b/app/templates/pages/contracts.html @@ -94,8 +94,8 @@
- - {{ _('Accepted formats: PDF, DOC, DOCX, JPG, PNG') }} + + {{ _('Accepted format: PDF') }}
diff --git a/app/templates/pages/upload_contract.html b/app/templates/pages/upload_contract.html index a415298..419b78c 100644 --- a/app/templates/pages/upload_contract.html +++ b/app/templates/pages/upload_contract.html @@ -24,8 +24,8 @@
- - {{ _('Accepted formats: PDF, DOC, DOCX, JPG, PNG') }} + + {{ _('Accepted format: PDF') }}
diff --git a/app/translations/en/LC_MESSAGES/messages.mo b/app/translations/en/LC_MESSAGES/messages.mo index bbd497e7953adeb3797bed5ed9e71e0bb5702d79..6fbd4fbef4accd23d064fa560f755d9c24e93418 100644 GIT binary patch delta 11004 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 Nc&%u9$p7cb{{Snz(0l*@ delta 10880 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 diff --git a/app/translations/en/LC_MESSAGES/messages.po b/app/translations/en/LC_MESSAGES/messages.po index 954f1f4..3365170 100644 --- a/app/translations/en/LC_MESSAGES/messages.po +++ b/app/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: team-tryouts VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-25 18:15-0400\n" +"POT-Creation-Date: 2026-08-25 19:38-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -18,17 +18,14 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" - #: app/app.py:564 msgid "Please log in to access this page." msgstr "Please log in to access this page." - #: app/forms.py:37 app/routes/auth.py:230 app/routes/auth.py:389 -#: app/routes/auth.py:403 app/routes/users/contracts.py:103 +#: app/routes/auth.py:403 app/routes/users/contracts.py:104 #, python-format msgid "%(field)s: %(msg)s" msgstr "%(field)s: %(msg)s" - #: app/validators.py:50 msgid "" "Password must be at least 8 characters with uppercase, lowercase, and a " @@ -36,241 +33,182 @@ msgid "" msgstr "" "Password must be at least 8 characters with uppercase, lowercase, and a " "number." - #: app/validators.py:68 msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgstr "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." - #: app/validators.py:87 msgid "Invalid Discord username format." msgstr "Invalid Discord username format." - #: app/validators.py:104 msgid "Discord User ID must be a 17-20 digit number." msgstr "Discord User ID must be a 17-20 digit number." - #: app/validators.py:122 msgid "Invalid phone number format." msgstr "Invalid phone number format." - #: app/validators.py:201 msgid "Username is required." msgstr "Username is required." - #: app/validators.py:205 msgid "Password is required." msgstr "Password is required." - #: app/validators.py:227 app/validators.py:324 msgid "Username must be 3-80 characters." msgstr "Username must be 3-80 characters." - #: app/validators.py:233 msgid "Email must be 120 characters or less." msgstr "Email must be 120 characters or less." - #: app/validators.py:246 app/validators.py:339 app/validators.py:370 #: app/validators.py:433 msgid "Full name is required." msgstr "Full name is required." - #: app/validators.py:276 msgid "Passwords do not match." msgstr "Passwords do not match." - #: app/validators.py:284 app/validators.py:924 msgid "Unknown game." msgstr "Unknown game." - #: app/validators.py:291 msgid "Gamertag must be between 1 and 120 characters." msgstr "Gamertag must be between 1 and 120 characters." - #: app/validators.py:297 msgid "Platform must be 30 characters or less." msgstr "Platform must be 30 characters or less." - #: app/validators.py:306 msgid "Unknown platform for this game." msgstr "Unknown platform for this game." - #: app/validators.py:343 app/validators.py:378 msgid "Invalid role selected." msgstr "Invalid role selected." - #: app/validators.py:473 app/validators.py:596 app/validators.py:629 msgid "Player must be selected." msgstr "Player must be selected." - #: app/validators.py:476 msgid "Notes must be 2000 characters or less." msgstr "Notes must be 2000 characters or less." - #: app/validators.py:513 app/validators.py:994 msgid "Invalid coach selection." msgstr "Invalid coach selection." - #: app/validators.py:519 app/validators.py:1000 msgid "Invalid manager selection." msgstr "Invalid manager selection." - #: app/validators.py:537 app/validators.py:595 app/validators.py:628 msgid "Invalid player selection." msgstr "Invalid player selection." - #: app/validators.py:546 msgid "Unknown roster status." msgstr "Unknown roster status." - #: app/validators.py:559 msgid "Unknown tryout status." msgstr "Unknown tryout status." - #: app/validators.py:570 msgid "Unknown registration status." msgstr "Unknown registration status." - #: app/validators.py:583 msgid "Team name must be between 1 and 100 characters." msgstr "Team name must be between 1 and 100 characters." - #: app/validators.py:603 msgid "Position must be 50 characters or less." msgstr "Position must be 50 characters or less." - #: app/validators.py:616 msgid "Note content must be between 1 and 5000 characters." msgstr "Note content must be between 1 and 5000 characters." - #: app/validators.py:642 msgid "Select at most one note context." msgstr "Select at most one note context." - #: app/validators.py:654 msgid "Rejection reason must be 2000 characters or less." msgstr "Rejection reason must be 2000 characters or less." - #: app/validators.py:678 msgid "Date must be in YYYY-MM-DD format." msgstr "Date must be in YYYY-MM-DD format." - #: app/validators.py:679 msgid "A date is required." msgstr "A date is required." - #: app/validators.py:685 app/validators.py:750 msgid "Start time must be in HH:MM format." msgstr "Start time must be in HH:MM format." - #: app/validators.py:686 app/validators.py:751 msgid "A start time is required." msgstr "A start time is required." - #: app/validators.py:692 msgid "End time must be in HH:MM format." msgstr "End time must be in HH:MM format." - #: app/validators.py:693 msgid "An end time is required." msgstr "An end time is required." - #: app/validators.py:697 msgid "Points must be 2000 characters or less." msgstr "Points must be 2000 characters or less." - #: app/validators.py:712 msgid "End time must be after start time." msgstr "End time must be after start time." - #: app/validators.py:741 app/validators.py:743 msgid "Day must be 0 (Monday) to 6 (Sunday)." msgstr "Day must be 0 (Monday) to 6 (Sunday)." - #: app/validators.py:744 msgid "A day is required." msgstr "A day is required." - #: app/validators.py:779 msgid "Player selection is malformed." msgstr "Player selection is malformed." - #: app/validators.py:805 app/validators.py:915 msgid "A title is required." msgstr "A title is required." - #: app/validators.py:814 msgid "Invalid date format." msgstr "Invalid date format." - #: app/validators.py:819 app/validators.py:826 msgid "Invalid time format." msgstr "Invalid time format." - #: app/validators.py:820 msgid "Start time is required. Please select a time slot." msgstr "Start time is required. Please select a time slot." - #: app/validators.py:834 msgid "Unknown match status." msgstr "Unknown match status." - #: app/validators.py:850 msgid "The end time must come after the start time." msgstr "The end time must come after the start time." - #: app/validators.py:864 msgid "Unknown match type." msgstr "Unknown match type." - #: app/validators.py:876 msgid "A team cannot play against itself." msgstr "A team cannot play against itself." - #: app/validators.py:929 msgid "Invalid start date format." msgstr "Invalid start date format." - #: app/validators.py:930 msgid "A start date is required." msgstr "A start date is required." - #: app/validators.py:936 msgid "Invalid end date format." msgstr "Invalid end date format." - #: app/validators.py:944 msgid "A tryout must allow at least one player." msgstr "A tryout must allow at least one player." - #: app/validators.py:947 msgid "The player limit must be a whole number." msgstr "The player limit must be a whole number." - #: app/validators.py:960 msgid "End date cannot be before start date." msgstr "End date cannot be before start date." - #: app/validators.py:987 app/validators.py:988 msgid "Team name is required." msgstr "Team name is required." - #: app/validators.py:1012 msgid "Scores run from 1 to 10." msgstr "Scores run from 1 to 10." - #: app/validators.py:1013 msgid "A score must be a whole number from 1 to 10." msgstr "A score must be a whole number from 1 to 10." - #: app/routes/auth.py:247 msgid "This account has been deactivated." msgstr "This account has been deactivated." - #: app/routes/auth.py:282 #, python-format msgid "Welcome back, %(username)s!" msgstr "Welcome back, %(username)s!" - #: app/routes/auth.py:312 msgid "" "Login unsuccessful. Please check your username and password, or ask a " @@ -278,32 +216,25 @@ msgid "" msgstr "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." - #: app/routes/auth.py:378 msgid "Your registration could not be processed. Please try again." msgstr "Your registration could not be processed. Please try again." - #: app/routes/auth.py:420 app/routes/users/accounts.py:343 msgid "Username already exists." msgstr "Username already exists." - #: app/routes/auth.py:424 app/routes/users/accounts.py:347 msgid "Email already registered." msgstr "Email already registered." - #: app/routes/auth.py:431 app/routes/auth.py:624 #: app/routes/users/accounts.py:121 msgid "This Discord account is already linked to another account." msgstr "This Discord account is already linked to another account." - #: app/routes/auth.py:473 msgid "Your account has been created! You can now log in." msgstr "Your account has been created! You can now log in." - #: app/routes/auth.py:498 msgid "Discord OAuth2 is not configured." msgstr "Discord OAuth2 is not configured." - #: app/routes/auth.py:547 msgid "" "Discord authorization could not be verified. Please start the connection " @@ -311,67 +242,51 @@ msgid "" msgstr "" "Discord authorization could not be verified. Please start the connection " "again from this page." - #: app/routes/auth.py:556 msgid "Discord authorization failed. No code received." msgstr "Discord authorization failed. No code received." - #: app/routes/auth.py:580 msgid "Failed to connect to Discord. Please try again." msgstr "Failed to connect to Discord. Please try again." - #: app/routes/auth.py:584 msgid "Failed to obtain Discord access token." msgstr "Failed to obtain Discord access token." - #: app/routes/auth.py:599 app/routes/auth.py:609 msgid "Failed to fetch Discord user profile." msgstr "Failed to fetch Discord user profile." - #: app/routes/auth.py:616 msgid "Please log in to connect your Discord account." msgstr "Please log in to connect your Discord account." - #: app/routes/auth.py:635 msgid "Discord account connected!" msgstr "Discord account connected!" - #: app/routes/auth.py:682 msgid "Discord account connected! Your profile has been pre-filled." msgstr "Discord account connected! Your profile has been pre-filled." - #: app/routes/auth.py:710 msgid "You have been logged out." msgstr "You have been logged out." - #: app/routes/evaluations.py:53 msgid "You do not have permission to view evaluations." msgstr "You do not have permission to view evaluations." - #: app/routes/evaluations.py:143 msgid "You do not have permission to evaluate players." msgstr "You do not have permission to evaluate players." - #: app/routes/evaluations.py:148 app/routes/evaluations.py:234 msgid "You do not have permission to evaluate players in this tryout." msgstr "You do not have permission to evaluate players in this tryout." - #: app/routes/evaluations.py:159 msgid "Player is not registered for this tryout." msgstr "Player is not registered for this tryout." - #: app/routes/evaluations.py:164 msgid "Can only evaluate players." msgstr "Can only evaluate players." - #: app/routes/evaluations.py:210 msgid "Evaluation submitted successfully!" msgstr "Evaluation submitted successfully!" - #: app/routes/evaluations.py:212 msgid "Evaluation updated!" msgstr "Evaluation updated!" - #: app/routes/evaluations.py:229 app/routes/teams.py:340 #: app/routes/teams.py:383 app/routes/teams.py:426 app/routes/teams.py:454 #: app/routes/teams.py:482 app/routes/teams.py:519 app/routes/tryouts.py:507 @@ -380,313 +295,242 @@ msgstr "Evaluation updated!" #: app/routes/tryouts.py:677 msgid "Permission denied." msgstr "Permission denied." - #: app/routes/main.py:55 msgid "That language is not available." msgstr "That language is not available." - #: app/routes/matches.py:383 msgid "You do not have permission to schedule matches for this tryout." msgstr "You do not have permission to schedule matches for this tryout." - #: app/routes/matches.py:387 app/routes/matches.py:463 msgid "This tryout has ended. Matches can no longer be created or modified." msgstr "This tryout has ended. Matches can no longer be created or modified." - #: app/routes/matches.py:445 msgid "Match scheduled successfully!" msgstr "Match scheduled successfully!" - #: app/routes/matches.py:459 app/routes/team_matches.py:235 msgid "You do not have permission to edit this match." msgstr "You do not have permission to edit this match." - #: app/routes/matches.py:552 app/routes/team_matches.py:265 msgid "Match updated successfully!" msgstr "Match updated successfully!" - #: app/routes/matches.py:588 app/routes/team_matches.py:284 msgid "You do not have permission to delete this match." msgstr "You do not have permission to delete this match." - #: app/routes/matches.py:591 msgid "This tryout has ended. Matches can no longer be deleted." msgstr "This tryout has ended. Matches can no longer be deleted." - #: app/routes/matches.py:604 app/routes/team_matches.py:289 msgid "Match deleted successfully." msgstr "Match deleted successfully." - #: app/routes/team_matches.py:128 msgid "You do not have permission to schedule matches for this team." msgstr "You do not have permission to schedule matches for this team." - #: app/routes/team_matches.py:220 #, python-format msgid "Team match \"%(title)s\" scheduled successfully!" msgstr "Team match \"%(title)s\" scheduled successfully!" - #: app/routes/teams.py:43 msgid "Use My Team(s) to view your teams." msgstr "Use My Team(s) to view your teams." - #: app/routes/teams.py:46 msgid "You do not have permission to view teams." msgstr "You do not have permission to view teams." - #: app/routes/teams.py:78 msgid "This page is for players." msgstr "This page is for players." - #: app/routes/teams.py:185 msgid "You do not have permission to create teams." msgstr "You do not have permission to create teams." - #: app/routes/teams.py:196 app/routes/teams.py:238 #, python-format msgid "Team \"%(name)s\" already exists." msgstr "Team \"%(name)s\" already exists." - #: app/routes/teams.py:217 #, python-format msgid "Team \"%(name)s\" created successfully!" msgstr "Team \"%(name)s\" created successfully!" - #: app/routes/teams.py:227 msgid "You do not have permission to edit this team." msgstr "You do not have permission to edit this team." - #: app/routes/teams.py:271 #, python-format msgid "Team \"%(name)s\" updated successfully!" msgstr "Team \"%(name)s\" updated successfully!" - #: app/routes/teams.py:299 msgid "You do not have permission to delete teams." msgstr "You do not have permission to delete teams." - #: app/routes/teams.py:330 #, python-format msgid "Team \"%(name)s\" deleted successfully." msgstr "Team \"%(name)s\" deleted successfully." - #: app/routes/teams.py:347 msgid "Please select a coach." msgstr "Please select a coach." - #: app/routes/teams.py:352 msgid "Only coaches can be assigned as coach." msgstr "Only coaches can be assigned as coach." - #: app/routes/teams.py:358 #, python-format msgid "%(username)s is already a coach of %(name)s." msgstr "%(username)s is already a coach of %(name)s." - #: app/routes/teams.py:371 #, python-format msgid "%(username)s added as coach of %(name)s." msgstr "%(username)s added as coach of %(name)s." - #: app/routes/teams.py:390 msgid "Please select a manager." msgstr "Please select a manager." - #: app/routes/teams.py:395 msgid "Only managers can be assigned as manager." msgstr "Only managers can be assigned as manager." - #: app/routes/teams.py:401 #, python-format msgid "%(username)s is already a manager of %(name)s." msgstr "%(username)s is already a manager of %(name)s." - #: app/routes/teams.py:414 #, python-format msgid "%(username)s added as manager of %(name)s." msgstr "%(username)s added as manager of %(name)s." - #: app/routes/teams.py:444 #, python-format msgid "Coach removed from %(name)s." msgstr "Coach removed from %(name)s." - #: app/routes/teams.py:472 #, python-format msgid "Manager removed from %(name)s." msgstr "Manager removed from %(name)s." - #: app/routes/teams.py:489 app/routes/tryouts.py:568 msgid "Please select a player." msgstr "Please select a player." - #: app/routes/teams.py:495 msgid "Can only assign players to teams." msgstr "Can only assign players to teams." - #: app/routes/teams.py:501 #, python-format msgid "%(username)s is already on %(name)s." msgstr "%(username)s is already on %(name)s." - #: app/routes/teams.py:509 #, python-format msgid "%(username)s added to %(name)s!" msgstr "%(username)s added to %(name)s!" - #: app/routes/teams.py:526 app/routes/teams.py:604 #, python-format msgid "%(username)s is not on %(name)s." msgstr "%(username)s is not on %(name)s." - #: app/routes/teams.py:534 #, python-format msgid "%(username)s removed from %(name)s." msgstr "%(username)s removed from %(name)s." - #: app/routes/teams.py:571 app/routes/teams.py:593 msgid "You do not have permission to add notes to this team." msgstr "You do not have permission to add notes to this team." - #: app/routes/teams.py:583 msgid "Team notes added successfully!" msgstr "Team notes added successfully!" - #: app/routes/teams.py:598 app/routes/users/notes.py:222 #: app/routes/users/notes.py:262 msgid "Can only add notes for players." msgstr "Can only add notes for players." - #: app/routes/teams.py:618 #, python-format msgid "Note added for %(username)s!" msgstr "Note added for %(username)s!" - #: app/routes/tryouts.py:147 msgid "You do not have permission to create tryouts." msgstr "You do not have permission to create tryouts." - #: app/routes/tryouts.py:194 msgid "Tryout created successfully!" msgstr "Tryout created successfully!" - #: app/routes/tryouts.py:211 msgid "You do not have permission to edit this tryout." msgstr "You do not have permission to edit this tryout." - #: app/routes/tryouts.py:215 msgid "This tryout has ended and can no longer be modified." msgstr "This tryout has ended and can no longer be modified." - #: app/routes/tryouts.py:261 msgid "Tryout updated successfully!" msgstr "Tryout updated successfully!" - #: app/routes/tryouts.py:301 msgid "You do not have permission to view this tryout." msgstr "You do not have permission to view this tryout." - #: app/routes/tryouts.py:469 msgid "Only players can register for tryouts." msgstr "Only players can register for tryouts." - #: app/routes/tryouts.py:474 msgid "This tryout is not accepting registrations." msgstr "This tryout is not accepting registrations." - #: app/routes/tryouts.py:481 msgid "You are already registered for this tryout." msgstr "You are already registered for this tryout." - #: app/routes/tryouts.py:487 app/routes/tryouts.py:590 msgid "This tryout is full." msgstr "This tryout is full." - #: app/routes/tryouts.py:493 msgid "Successfully registered for tryout!" msgstr "Successfully registered for tryout!" - #: app/routes/tryouts.py:517 #, python-format msgid "Tryout status updated to %(new_status)s." msgstr "Tryout status updated to %(new_status)s." - #: app/routes/tryouts.py:545 msgid "Registration status updated." msgstr "Registration status updated." - #: app/routes/tryouts.py:576 msgid "Can only register players." msgstr "Can only register players." - #: app/routes/tryouts.py:582 #, python-format msgid "%(username)s is already registered for this tryout." msgstr "%(username)s is already registered for this tryout." - #: app/routes/tryouts.py:596 #, python-format msgid "%(username)s registered for tryout!" msgstr "%(username)s registered for tryout!" - #: app/routes/tryouts.py:636 #, python-format msgid "%(username)s removed from tryout." msgstr "%(username)s removed from tryout." - #: app/routes/tryouts.py:662 #, python-format msgid "Team \"%(team_name)s\" created!" msgstr "Team \"%(team_name)s\" created!" - #: app/routes/tryouts.py:699 app/routes/users/notes.py:350 msgid "That player is not registered for this tryout." msgstr "That player is not registered for this tryout." - #: app/routes/tryouts.py:704 msgid "Player is already on this team." msgstr "Player is already on this team." - #: app/routes/tryouts.py:709 msgid "Player added to team!" msgstr "Player added to team!" - #: app/routes/tryouts.py:723 msgid "You do not have permission to delete this tryout." msgstr "You do not have permission to delete this tryout." - #: app/routes/tryouts.py:759 msgid "Tryout deleted successfully." msgstr "Tryout deleted successfully." - -#: app/routes/users/_shared.py:50 +#: app/routes/users/_shared.py:54 msgid "No file selected." msgstr "No file selected." - -#: app/routes/users/_shared.py:54 +#: app/routes/users/_shared.py:58 msgid "Only PDF files are allowed for contracts." msgstr "Only PDF files are allowed for contracts." - -#: app/routes/users/_shared.py:59 +#: app/routes/users/_shared.py:64 app/routes/users/_shared.py:75 +#: app/routes/users/_shared.py:77 msgid "That file is not a PDF, whatever its name says." msgstr "That file is not a PDF, whatever its name says." - #: app/routes/users/accounts.py:55 msgid "Only the president can manage users." msgstr "Only the president can manage users." - #: app/routes/users/accounts.py:70 msgid "Only the president can edit users." msgstr "Only the president can edit users." - #: app/routes/users/accounts.py:111 msgid "Email already in use by another account." msgstr "Email already in use by another account." - #: app/routes/users/accounts.py:138 msgid "You cannot change your own role. Ask another president to do it." msgstr "You cannot change your own role. Ask another president to do it." - #: app/routes/users/accounts.py:151 msgid "" "This is the last active president. Promote another account before " @@ -694,229 +538,175 @@ msgid "" msgstr "" "This is the last active president. Promote another account before " "changing this one." - #: app/routes/users/accounts.py:228 #, python-format msgid "User %(username)s updated successfully!" msgstr "User %(username)s updated successfully!" - #: app/routes/users/accounts.py:249 msgid "Only the president can delete users." msgstr "Only the president can delete users." - #: app/routes/users/accounts.py:253 msgid "You cannot delete your own account." msgstr "You cannot delete your own account." - #: app/routes/users/accounts.py:312 #, python-format msgid "User %(deleted_username)s has been removed." msgstr "User %(deleted_username)s has been removed." - #: app/routes/users/accounts.py:323 msgid "Only the president can create users." msgstr "Only the president can create users." - #: app/routes/users/accounts.py:371 #, python-format msgid "User %(full_name)s created as %(role)s!" msgstr "User %(full_name)s created as %(role)s!" - #: app/routes/users/availability.py:210 msgid "Only coaches can manage availability." msgstr "Only coaches can manage availability." - -#: app/routes/users/contracts.py:91 +#: app/routes/users/contracts.py:92 msgid "Only presidents, managers, and coaches can upload contracts." msgstr "Only presidents, managers, and coaches can upload contracts." - -#: app/routes/users/contracts.py:110 +#: app/routes/users/contracts.py:111 msgid "You do not have permission to upload a contract for this player." msgstr "You do not have permission to upload a contract for this player." - -#: app/routes/users/contracts.py:137 app/routes/users/contracts.py:180 -#: app/routes/users/contracts.py:203 app/routes/users/contracts.py:222 +#: app/routes/users/contracts.py:138 app/routes/users/contracts.py:188 +#: app/routes/users/contracts.py:211 app/routes/users/contracts.py:230 msgid "Contract storage is temporarily unavailable. Please try again later." -msgstr "" - -#: app/routes/users/contracts.py:152 +msgstr "Contract storage is temporarily unavailable. Please try again later." +#: app/routes/users/contracts.py:153 #, python-format msgid "Contract uploaded successfully for %(username)s!" msgstr "Contract uploaded successfully for %(username)s!" - -#: app/routes/users/contracts.py:166 +#: app/routes/users/contracts.py:167 msgid "Only the player can upload their signed contract." msgstr "Only the player can upload their signed contract." - -#: app/routes/users/contracts.py:188 +#: app/routes/users/contracts.py:196 msgid "Signed contract uploaded successfully!" msgstr "Signed contract uploaded successfully!" - -#: app/routes/users/contracts.py:198 app/routes/users/contracts.py:214 +#: app/routes/users/contracts.py:206 app/routes/users/contracts.py:222 msgid "You do not have permission to download this contract." msgstr "You do not have permission to download this contract." - -#: app/routes/users/contracts.py:217 +#: app/routes/users/contracts.py:225 msgid "No signed contract available." msgstr "No signed contract available." - #: app/routes/users/notes.py:43 msgid "This page is for players only." msgstr "This page is for players only." - #: app/routes/users/notes.py:80 msgid "Only coaches can access the notes dashboard." msgstr "Only coaches can access the notes dashboard." - #: app/routes/users/notes.py:172 msgid "Only coaches can manage team notes." msgstr "Only coaches can manage team notes." - #: app/routes/users/notes.py:178 msgid "You are not assigned to a team." msgstr "You are not assigned to a team." - #: app/routes/users/notes.py:195 msgid "Team notes saved successfully!" msgstr "Team notes saved successfully!" - #: app/routes/users/notes.py:210 msgid "Only coaches can manage personal notes." msgstr "Only coaches can manage personal notes." - #: app/routes/users/notes.py:226 app/routes/users/notes.py:266 #: app/routes/users/notes.py:346 app/routes/users/notes.py:411 msgid "You can only write notes about players you work with." msgstr "You can only write notes about players you work with." - #: app/routes/users/notes.py:236 app/routes/users/notes.py:306 #, python-format msgid "Note added for %(username)s." msgstr "Note added for %(username)s." - #: app/routes/users/notes.py:250 app/routes/users/notes.py:320 #: app/routes/users/notes.py:384 msgid "Only coaches can add personal notes." msgstr "Only coaches can add personal notes." - #: app/routes/users/notes.py:272 msgid "You cannot use that match as note context." msgstr "You cannot use that match as note context." - #: app/routes/users/notes.py:275 msgid "That player did not participate in the selected match." msgstr "That player did not participate in the selected match." - #: app/routes/users/notes.py:281 msgid "You cannot use that tryout as note context." msgstr "You cannot use that tryout as note context." - #: app/routes/users/notes.py:284 msgid "That player is not registered for the selected tryout." msgstr "That player is not registered for the selected tryout." - #: app/routes/users/notes.py:290 msgid "You cannot use that team as note context." msgstr "You cannot use that team as note context." - #: app/routes/users/notes.py:293 msgid "That player is not on the selected team." msgstr "That player is not on the selected team." - #: app/routes/users/notes.py:325 msgid "You do not have permission to add notes for this tryout." msgstr "You do not have permission to add notes for this tryout." - #: app/routes/users/notes.py:342 msgid "Invalid tryout context." msgstr "Invalid tryout context." - #: app/routes/users/notes.py:361 app/routes/users/notes.py:426 msgid "Note added successfully." msgstr "Note added successfully." - #: app/routes/users/notes.py:389 msgid "You do not have permission to add notes for this match." msgstr "You do not have permission to add notes for this match." - #: app/routes/users/notes.py:407 msgid "Invalid match context." msgstr "Invalid match context." - #: app/routes/users/notes.py:415 msgid "That player did not participate in this match." msgstr "That player did not participate in this match." - #: app/routes/users/one_on_one.py:22 msgid "Only players can request One on One sessions." msgstr "Only players can request One on One sessions." - #: app/routes/users/one_on_one.py:35 msgid "You do not have a coach assigned to your team." msgstr "You do not have a coach assigned to your team." - #: app/routes/users/one_on_one.py:66 msgid "Cannot request One on One - no coach assigned." msgstr "Cannot request One on One - no coach assigned." - #: app/routes/users/one_on_one.py:92 msgid "The requested time is not within the coach's availability." msgstr "The requested time is not within the coach's availability." - #: app/routes/users/one_on_one.py:120 msgid "Your One on One request has been submitted!" msgstr "Your One on One request has been submitted!" - #: app/routes/users/one_on_one.py:165 msgid "Only coaches can accept One on One requests." msgstr "Only coaches can accept One on One requests." - #: app/routes/users/one_on_one.py:171 app/routes/users/one_on_one.py:221 msgid "This request is not for you." msgstr "This request is not for you." - #: app/routes/users/one_on_one.py:175 app/routes/users/one_on_one.py:225 msgid "This request has already been processed." msgstr "This request has already been processed." - #: app/routes/users/one_on_one.py:202 #, python-format msgid "One on One request from %(player)s has been approved!" msgstr "One on One request from %(player)s has been approved!" - #: app/routes/users/one_on_one.py:215 msgid "Only coaches can reject One on One requests." msgstr "Only coaches can reject One on One requests." - #: app/routes/users/one_on_one.py:262 #, python-format msgid "One on One request from %(player)s has been rejected." msgstr "One on One request from %(player)s has been rejected." - #: app/routes/users/profile.py:82 msgid "Username already taken." msgstr "Username already taken." - #: app/routes/users/profile.py:92 msgid "Email already in use." msgstr "Email already in use." - #: app/routes/users/profile.py:131 msgid "Profile updated successfully!" msgstr "Profile updated successfully!" - #: app/templates/errors/400.html:2 msgid "400 Bad Request" msgstr "400 Bad Request" - #: app/templates/errors/400.html:3 msgid "Bad Request" msgstr "Bad Request" - #: app/templates/errors/400.html:9 msgid "400 — Bad Request" msgstr "400 — Bad Request" - #: app/templates/errors/400.html:10 msgid "" "The request could not be understood by the server. Please check your " @@ -924,24 +714,19 @@ msgid "" msgstr "" "The request could not be understood by the server. Please check your " "input and try again." - #: app/templates/errors/400.html:12 app/templates/errors/403.html:12 #: app/templates/errors/429.html:12 msgid "Go Back" msgstr "Go Back" - #: app/templates/errors/403.html:2 msgid "403 Forbidden" msgstr "403 Forbidden" - #: app/templates/errors/403.html:3 msgid "Access Denied" msgstr "Access Denied" - #: app/templates/errors/403.html:9 msgid "403 — Forbidden" msgstr "403 — Forbidden" - #: app/templates/errors/403.html:10 msgid "" "You do not have permission to access this resource. If you believe this " @@ -949,19 +734,15 @@ msgid "" msgstr "" "You do not have permission to access this resource. If you believe this " "is an error, please contact an administrator." - #: app/templates/errors/404.html:2 msgid "404 Not Found" msgstr "404 Not Found" - #: app/templates/errors/404.html:3 msgid "Page Not Found" msgstr "Page Not Found" - #: app/templates/errors/404.html:9 msgid "404 — Not Found" msgstr "404 — Not Found" - #: app/templates/errors/404.html:10 msgid "" "The page you are looking for does not exist. It may have been moved or " @@ -969,23 +750,18 @@ msgid "" msgstr "" "The page you are looking for does not exist. It may have been moved or " "deleted." - #: app/templates/errors/404.html:12 msgid "Return Home" msgstr "Return Home" - #: app/templates/errors/429.html:2 msgid "429 Too Many Requests" msgstr "429 Too Many Requests" - #: app/templates/errors/429.html:3 msgid "Rate Limit Exceeded" msgstr "Rate Limit Exceeded" - #: app/templates/errors/429.html:9 msgid "429 — Too Many Requests" msgstr "429 — Too Many Requests" - #: app/templates/errors/429.html:10 msgid "" "You have sent too many requests in a short period. Please wait a moment " @@ -993,19 +769,15 @@ msgid "" msgstr "" "You have sent too many requests in a short period. Please wait a moment " "and try again." - #: app/templates/errors/500.html:2 msgid "500 Server Error" msgstr "500 Server Error" - #: app/templates/errors/500.html:3 msgid "Internal Server Error" msgstr "Internal Server Error" - #: app/templates/errors/500.html:9 msgid "500 — Internal Server Error" msgstr "500 — Internal Server Error" - #: app/templates/errors/500.html:10 msgid "" "Something went wrong on our end. The error has been logged and will be " @@ -1013,124 +785,98 @@ msgid "" msgstr "" "Something went wrong on our end. The error has been logged and will be " "investigated. Please try again later." - #: app/templates/errors/500.html:15 msgid "Reference to quote if you report this:" msgstr "Reference to quote if you report this:" - #: app/templates/errors/500.html:18 msgid "Try Again" msgstr "Try Again" - #: app/templates/layouts/_language_switcher.html:10 msgid "Language" msgstr "Language" - #: app/templates/layouts/_pagination.html:19 msgid "Pagination" msgstr "Pagination" - #: app/templates/layouts/_pagination.html:22 #: app/templates/layouts/_pagination.html:24 msgid "Previous" msgstr "Previous" - #: app/templates/layouts/_pagination.html:28 #, python-format msgid "Page %(page)s of %(pages)s" msgstr "Page %(page)s of %(pages)s" - #: app/templates/layouts/_pagination.html:30 #, python-format msgid "%(total)s in total" msgstr "%(total)s in total" - #: app/templates/layouts/_pagination.html:35 #: app/templates/layouts/_pagination.html:37 msgid "Next" msgstr "Next" - #: app/templates/layouts/base.html:48 app/templates/layouts/base.html:154 #: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3 msgid "Dashboard" msgstr "Dashboard" - #: app/templates/layouts/base.html:54 app/templates/pages/tryouts.html:2 #: app/templates/pages/tryouts.html:3 msgid "Tryouts" msgstr "Tryouts" - #: app/templates/layouts/base.html:60 app/templates/pages/calendar.html:2 #: app/templates/pages/calendar.html:3 msgid "Calendar" msgstr "Calendar" - #: app/templates/layouts/base.html:67 app/templates/pages/dashboard.html:43 #: app/templates/pages/evaluations.html:3 #: app/templates/pages/evaluations.html:4 msgid "Evaluations" msgstr "Evaluations" - #: app/templates/layouts/base.html:75 app/templates/pages/my_teams.html:2 #: app/templates/pages/my_teams.html:3 msgid "My Team(s)" msgstr "My Team(s)" - #: app/templates/layouts/base.html:82 msgid "Manage Teams" msgstr "Manage Teams" - #: app/templates/layouts/base.html:90 app/templates/pages/users.html:3 #: app/templates/pages/users.html:4 msgid "Manage Users" msgstr "Manage Users" - #: app/templates/layouts/base.html:104 #: app/templates/pages/player_personal_notes.html:2 #: app/templates/pages/player_personal_notes.html:3 msgid "My Notes" msgstr "My Notes" - #: app/templates/layouts/base.html:112 msgid "Notes & One on One" msgstr "Notes & One on One" - #: app/templates/layouts/base.html:119 app/templates/pages/contracts.html:2 #: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9 msgid "Contracts" msgstr "Contracts" - #: app/templates/layouts/base.html:126 app/templates/pages/profile.html:2 #: app/templates/pages/profile.html:3 msgid "My Profile" msgstr "My Profile" - #: app/templates/layouts/base.html:137 msgid "Logout" msgstr "Logout" - #: app/templates/layouts/base.html:158 msgid "Toggle dark mode" msgstr "Toggle dark mode" - #: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189 msgid "Dismiss" msgstr "Dismiss" - #: app/templates/layouts/base.html:199 msgid "UdeS team manager" msgstr "UdeS team manager" - #: app/templates/layouts/macros.html:116 msgid "Close" msgstr "Close" - #: app/templates/pages/add_note.html:2 app/templates/pages/add_note.html:3 #: app/templates/pages/add_note.html:22 app/templates/pages/notes.html:38 #: app/templates/pages/personal_notes.html:9 msgid "Add Personal Note" msgstr "Add Personal Note" - #: app/templates/pages/add_note.html:47 app/templates/pages/contracts.html:25 #: app/templates/pages/dashboard.html:249 #: app/templates/pages/dashboard.html:363 app/templates/pages/my_teams.html:52 @@ -1140,50 +886,39 @@ msgstr "Add Personal Note" #: app/templates/pages/view_tryout.html:498 msgid "Player" msgstr "Player" - #: app/templates/pages/add_note.html:49 app/templates/pages/teams.html:216 #: app/templates/pages/upload_contract.html:18 #: app/templates/pages/view_tryout.html:130 msgid "-- Select a player --" msgstr "-- Select a player --" - #: app/templates/pages/add_note.html:59 app/templates/pages/notes.html:58 #: app/templates/pages/personal_notes.html:29 msgid "Note Content" msgstr "Note Content" - #: app/templates/pages/add_note.html:60 msgid "Enter your coaching notes..." msgstr "Enter your coaching notes..." - #: app/templates/pages/add_note.html:65 msgid "Link to Tryout (Optional)" msgstr "Link to Tryout (Optional)" - #: app/templates/pages/add_note.html:67 msgid "-- No tryout --" msgstr "-- No tryout --" - #: app/templates/pages/add_note.html:75 msgid "Link to Match (Optional)" msgstr "Link to Match (Optional)" - #: app/templates/pages/add_note.html:77 msgid "-- No match --" msgstr "-- No match --" - #: app/templates/pages/add_note.html:85 msgid "Link to Team (Optional)" msgstr "Link to Team (Optional)" - #: app/templates/pages/add_note.html:87 msgid "-- No team --" msgstr "-- No team --" - #: app/templates/pages/add_note.html:97 msgid "Team Notes (Reference)" msgstr "Team Notes (Reference)" - #: app/templates/pages/add_note.html:120 #: app/templates/pages/evaluate_player.html:131 #: app/templates/pages/notes.html:100 @@ -1191,144 +926,111 @@ msgstr "Team Notes (Reference)" #: app/templates/pages/view_tryout.html:309 msgid "Add Note" msgstr "Add Note" - #: app/templates/pages/calendar.html:9 app/templates/pages/view_tryout.html:302 msgid "Schedule" msgstr "Schedule" - #: app/templates/pages/calendar.html:13 msgid "Month" msgstr "Month" - #: app/templates/pages/calendar.html:16 msgid "Week" msgstr "Week" - #: app/templates/pages/calendar.html:19 msgid "Day" msgstr "Day" - #: app/templates/pages/calendar.html:22 msgid "List" msgstr "List" - #: app/templates/pages/calendar.html:37 msgid "Create New Event" msgstr "Create New Event" - #: app/templates/pages/calendar.html:41 msgid "Date:" msgstr "Date:" - #: app/templates/pages/calendar.html:46 msgid "Schedule Tryout Match" msgstr "Schedule Tryout Match" - #: app/templates/pages/calendar.html:47 msgid "Add a scrim/match inside an existing tryout" msgstr "Add a scrim/match inside an existing tryout" - #: app/templates/pages/calendar.html:50 msgid "-- Select a tryout --" msgstr "-- Select a tryout --" - #: app/templates/pages/calendar.html:53 app/templates/pages/calendar.html:68 msgid "Go" msgstr "Go" - #: app/templates/pages/calendar.html:61 app/templates/pages/teams.html:131 msgid "Schedule Team Match" msgstr "Schedule Team Match" - #: app/templates/pages/calendar.html:62 msgid "Regular season match for an org team" msgstr "Regular season match for an org team" - #: app/templates/pages/calendar.html:65 msgid "-- Select a team --" msgstr "-- Select a team --" - #: app/templates/pages/calendar.html:76 msgid "Create New Tryout" msgstr "Create New Tryout" - #: app/templates/pages/calendar.html:77 msgid "Create a brand new tryout event" msgstr "Create a brand new tryout event" - #: app/templates/pages/calendar.html:79 msgid "Create Tryout" msgstr "Create Tryout" - #: app/templates/pages/calendar.html:92 msgid "Event Details" msgstr "Event Details" - #: app/templates/pages/calendar.html:98 msgid "Confirm" msgstr "Confirm" - #: app/templates/pages/calendar.html:102 #: app/templates/pages/team_matches.html:103 msgid "Delete Match" msgstr "Delete Match" - #: app/templates/pages/calendar.html:105 #: app/templates/pages/team_matches.html:98 msgid "Edit Match" msgstr "Edit Match" - #: app/templates/pages/calendar.html:108 msgid "View Tryout" msgstr "View Tryout" - #: app/templates/pages/coach_availability.html:2 #: app/templates/pages/coach_availability.html:3 msgid "Manage Availability" msgstr "Manage Availability" - #: app/templates/pages/coach_availability.html:9 msgid "Set Your Weekly Availability" msgstr "Set Your Weekly Availability" - #: app/templates/pages/coach_availability.html:10 msgid "Select time slots when you're available for One on One sessions" msgstr "Select time slots when you're available for One on One sessions" - #: app/templates/pages/coach_availability.html:14 #: app/templates/pages/profile.html:256 msgid "Loading availability grid..." msgstr "Loading availability grid..." - #: app/templates/pages/coach_availability.html:19 msgid "Save Availability" msgstr "Save Availability" - #: app/templates/pages/coach_availability.html:22 #: app/templates/pages/profile.html:240 app/templates/pages/profile.html:260 msgid "Clear All" msgstr "Clear All" - #: app/templates/pages/contracts.html:9 #: app/templates/pages/upload_contract.html:2 #: app/templates/pages/upload_contract.html:3 #: app/templates/pages/upload_contract.html:39 msgid "Upload Contract" msgstr "Upload Contract" - #: app/templates/pages/contracts.html:17 msgid "Contract Dropbox" msgstr "Contract Dropbox" - #: app/templates/pages/contracts.html:26 app/templates/pages/notes.html:87 #: app/templates/pages/team_matches.html:27 msgid "Team" msgstr "Team" - #: app/templates/pages/contracts.html:27 msgid "Contract" msgstr "Contract" - #: app/templates/pages/contracts.html:28 app/templates/pages/dashboard.html:95 #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:175 @@ -1342,11 +1044,9 @@ msgstr "Contract" #: app/templates/pages/view_tryout.html:327 msgid "Status" msgstr "Status" - #: app/templates/pages/contracts.html:29 msgid "Uploaded" msgstr "Uploaded" - #: app/templates/pages/contracts.html:30 app/templates/pages/dashboard.html:175 #: app/templates/pages/notes.html:127 app/templates/pages/team_matches.html:34 #: app/templates/pages/teams.html:151 app/templates/pages/users.html:26 @@ -1354,32 +1054,25 @@ msgstr "Uploaded" #: app/templates/pages/view_tryout.html:329 msgid "Actions" msgstr "Actions" - #: app/templates/pages/contracts.html:49 app/templates/pages/contracts.html:50 msgid "Download" msgstr "Download" - #: app/templates/pages/contracts.html:53 msgid "Download Signed" msgstr "Download Signed" - #: app/templates/pages/contracts.html:54 msgid "Signed" msgstr "Signed" - #: app/templates/pages/contracts.html:58 app/templates/pages/contracts.html:89 #: app/templates/pages/contracts.html:102 msgid "Upload Signed Contract" msgstr "Upload Signed Contract" - #: app/templates/pages/contracts.html:59 msgid "Return Signed" msgstr "Return Signed" - #: app/templates/pages/contracts.html:72 msgid "No contracts found" msgstr "No contracts found" - #: app/templates/pages/contracts.html:74 msgid "" "No contracts have been uploaded for you yet. Contact your coach or " @@ -1387,7 +1080,6 @@ msgid "" msgstr "" "No contracts have been uploaded for you yet. Contact your coach or " "manager." - #: app/templates/pages/contracts.html:76 msgid "" "No contracts have been uploaded yet. Upload a contract using the button " @@ -1395,126 +1087,100 @@ msgid "" msgstr "" "No contracts have been uploaded yet. Upload a contract using the button " "above." - #: app/templates/pages/contracts.html:96 msgid "Select Signed Contract File" msgstr "Select Signed Contract File" - #: app/templates/pages/contracts.html:98 #: app/templates/pages/upload_contract.html:28 -msgid "Accepted formats: PDF, DOC, DOCX, JPG, PNG" +msgid "Accepted format: PDF" msgstr "Accepted formats: PDF, DOC, DOCX, JPG, PNG" - #: app/templates/pages/contracts.html:101 #: app/templates/pages/match_form.html:265 app/templates/pages/notes.html:189 #: app/templates/pages/teams.html:48 app/templates/pages/teams.html:295 #: app/templates/pages/view_tryout.html:245 msgid "Cancel" msgstr "Cancel" - #: app/templates/pages/create_user.html:2 #: app/templates/pages/create_user.html:3 #: app/templates/pages/create_user.html:47 msgid "Create User" msgstr "Create User" - #: app/templates/pages/create_user.html:13 #: app/templates/pages/edit_profile.html:17 #: app/templates/pages/edit_user.html:13 app/templates/pages/register.html:10 msgid "Full Name" msgstr "Full Name" - #: app/templates/pages/create_user.html:14 msgid "Enter full name" msgstr "Enter full name" - #: app/templates/pages/create_user.html:17 #: app/templates/pages/edit_profile.html:13 app/templates/pages/login.html:7 #: app/templates/pages/register.html:15 app/templates/pages/users.html:21 msgid "Username" msgstr "Username" - #: app/templates/pages/create_user.html:18 msgid "Choose username" msgstr "Choose username" - #: app/templates/pages/create_user.html:23 #: app/templates/pages/edit_profile.html:23 #: app/templates/pages/edit_user.html:17 app/templates/pages/register.html:20 #: app/templates/pages/teams.html:148 app/templates/pages/users.html:22 msgid "Email" msgstr "Email" - #: app/templates/pages/create_user.html:24 msgid "Enter email" msgstr "Enter email" - #: app/templates/pages/create_user.html:27 #: app/templates/pages/edit_profile.html:27 #: app/templates/pages/edit_user.html:23 app/templates/pages/teams.html:149 msgid "Phone" msgstr "Phone" - #: app/templates/pages/create_user.html:28 msgid "Phone number" msgstr "Phone number" - #: app/templates/pages/create_user.html:31 #: app/templates/pages/dashboard.html:74 app/templates/pages/edit_user.html:27 #: app/templates/pages/users.html:23 msgid "Role" msgstr "Role" - #: app/templates/pages/create_user.html:41 app/templates/pages/login.html:11 #: app/templates/pages/register.html:137 msgid "Password" msgstr "Password" - #: app/templates/pages/create_user.html:42 msgid "Set password" msgstr "Set password" - #: app/templates/pages/dashboard.html:16 msgid "Total Users" msgstr "Total Users" - #: app/templates/pages/dashboard.html:25 app/templates/pages/dashboard.html:175 msgid "Players" msgstr "Players" - #: app/templates/pages/dashboard.html:34 msgid "Total Tryouts" msgstr "Total Tryouts" - #: app/templates/pages/dashboard.html:52 msgid "Active Tryouts" msgstr "Active Tryouts" - #: app/templates/pages/dashboard.html:61 #: app/templates/pages/view_tryout.html:21 msgid "Upcoming" msgstr "Upcoming" - #: app/templates/pages/dashboard.html:69 msgid "Recent Users" msgstr "Recent Users" - #: app/templates/pages/dashboard.html:74 app/templates/pages/users.html:20 msgid "Name" msgstr "Name" - #: app/templates/pages/dashboard.html:74 app/templates/pages/users.html:25 msgid "Joined" msgstr "Joined" - #: app/templates/pages/dashboard.html:90 msgid "Recent Tryouts" msgstr "Recent Tryouts" - #: app/templates/pages/dashboard.html:95 app/templates/pages/dashboard.html:175 msgid "Title" msgstr "Title" - #: app/templates/pages/dashboard.html:95 app/templates/pages/dashboard.html:175 #: app/templates/pages/dashboard.html:249 #: app/templates/pages/dashboard.html:321 @@ -1525,12 +1191,10 @@ msgstr "Title" #: app/templates/pages/view_tryout.html:323 msgid "Date" msgstr "Date" - #: app/templates/pages/dashboard.html:114 #: app/templates/pages/dashboard.html:198 app/templates/pages/my_teams.html:84 msgid "Upcoming Matches" msgstr "Upcoming Matches" - #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:90 @@ -1538,7 +1202,6 @@ msgstr "Upcoming Matches" #: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:321 msgid "Match" msgstr "Match" - #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:249 @@ -1546,112 +1209,88 @@ msgstr "Match" #: app/templates/pages/dashboard.html:321 app/templates/pages/notes.html:78 msgid "Tryout" msgstr "Tryout" - #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:285 msgid "Date & Time" msgstr "Date & Time" - #: app/templates/pages/dashboard.html:145 #: app/templates/pages/dashboard.html:169 #: app/templates/pages/dashboard.html:273 msgid "My Tryouts" msgstr "My Tryouts" - #: app/templates/pages/dashboard.html:154 msgid "Active" msgstr "Active" - #: app/templates/pages/dashboard.html:163 msgid "My Evaluations" msgstr "My Evaluations" - #: app/templates/pages/dashboard.html:229 msgid "Evaluations Done" msgstr "Evaluations Done" - #: app/templates/pages/dashboard.html:238 #: app/templates/pages/one_on_one.html:85 msgid "Pending" msgstr "Pending" - #: app/templates/pages/dashboard.html:244 msgid "Recent Evaluations" msgstr "Recent Evaluations" - #: app/templates/pages/dashboard.html:249 msgid "Score" msgstr "Score" - #: app/templates/pages/dashboard.html:280 msgid "My Next Matches" msgstr "My Next Matches" - #: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:91 #: app/templates/pages/team_matches.html:28 msgid "Opponent" msgstr "Opponent" - #: app/templates/pages/dashboard.html:311 msgid "No upcoming matches yet." msgstr "No upcoming matches yet." - #: app/templates/pages/dashboard.html:317 msgid "My Registrations" msgstr "My Registrations" - #: app/templates/pages/dashboard.html:344 msgid "Total Players" msgstr "Total Players" - #: app/templates/pages/dashboard.html:353 msgid "Total Evaluations" msgstr "Total Evaluations" - #: app/templates/pages/dashboard.html:359 msgid "Top Rated Players" msgstr "Top Rated Players" - #: app/templates/pages/dashboard.html:363 msgid "Avg Score" msgstr "Avg Score" - #: app/templates/pages/dashboard.html:373 #: app/templates/pages/view_tryout.html:532 msgid "No evaluations yet." msgstr "No evaluations yet." - #: app/templates/pages/edit_profile.html:2 #: app/templates/pages/edit_profile.html:3 app/templates/pages/profile.html:12 msgid "Edit Profile" msgstr "Edit Profile" - #: app/templates/pages/edit_profile.html:33 #: app/templates/pages/edit_user.html:43 app/templates/pages/profile.html:63 #: app/templates/pages/register.html:81 msgid "E-Sports Profile" msgstr "E-Sports Profile" - #: app/templates/pages/edit_profile.html:34 msgid "Update your competitive gaming profile for tryouts." msgstr "Update your competitive gaming profile for tryouts." - #: app/templates/pages/edit_profile.html:37 #: app/templates/pages/register.html:85 msgid "Games You Play" msgstr "Games You Play" - #: app/templates/pages/edit_profile.html:47 #: app/templates/pages/register.html:99 msgid "Select all games you're signing in for." msgstr "Select all games you're signing in for." - #: app/templates/pages/edit_profile.html:53 #: app/templates/pages/edit_user.html:61 msgid "Gamertags for TRN" msgstr "Gamertags for TRN" - #: app/templates/pages/edit_profile.html:54 msgid "" "Enter your gamertag for each selected game to link to your Tracker " @@ -1659,110 +1298,86 @@ msgid "" msgstr "" "Enter your gamertag for each selected game to link to your Tracker " "Network profile." - #: app/templates/pages/edit_profile.html:62 #: app/templates/pages/edit_user.html:70 app/templates/pages/edit_user.html:71 #: app/templates/pages/view_user.html:58 msgid "Gamertag" msgstr "Gamertag" - #: app/templates/pages/edit_profile.html:67 #: app/templates/pages/edit_user.html:75 app/templates/pages/view_user.html:59 msgid "Platform" msgstr "Platform" - #: app/templates/pages/edit_profile.html:69 #: app/templates/pages/edit_user.html:77 msgid "Select Platform" msgstr "Select Platform" - #: app/templates/pages/edit_profile.html:83 #: app/templates/pages/edit_user.html:91 msgid "Discord Username" msgstr "Discord Username" - #: app/templates/pages/edit_profile.html:84 #: app/templates/pages/edit_user.html:92 msgid "e.g. Name#1234" msgstr "e.g. Name#1234" - #: app/templates/pages/edit_profile.html:87 #: app/templates/pages/register.html:57 msgid "Reconnect" msgstr "Reconnect" - #: app/templates/pages/edit_profile.html:87 #: app/templates/pages/register.html:67 msgid "Connect Discord Account" msgstr "Connect Discord Account" - #: app/templates/pages/edit_profile.html:93 #: app/templates/pages/edit_user.html:100 msgid "League OS Connection" msgstr "League OS Connection" - #: app/templates/pages/edit_profile.html:94 #: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:129 msgid "League OS profile link or ID" msgstr "League OS profile link or ID" - #: app/templates/pages/edit_profile.html:99 msgid "Change Password" msgstr "Change Password" - #: app/templates/pages/edit_profile.html:100 msgid "Leave blank to keep your current password." msgstr "Leave blank to keep your current password." - #: app/templates/pages/edit_profile.html:102 msgid "New Password" msgstr "New Password" - #: app/templates/pages/edit_profile.html:103 #: app/templates/pages/edit_user.html:107 msgid "Enter new password" msgstr "Enter new password" - #: app/templates/pages/edit_profile.html:108 app/templates/pages/teams.html:296 msgid "Save Changes" msgstr "Save Changes" - #: app/templates/pages/edit_user.html:3 msgid "Edit User" msgstr "Edit User" - #: app/templates/pages/edit_user.html:46 msgid "Games" msgstr "Games" - #: app/templates/pages/edit_user.html:62 msgid "Enter gamertag for each selected game to link to Tracker Network." msgstr "Enter gamertag for each selected game to link to Tracker Network." - #: app/templates/pages/edit_user.html:95 msgid "(for DMs)" msgstr "(for DMs)" - #: app/templates/pages/edit_user.html:96 msgid "Numeric ID (e.g. 123456789012345678)" msgstr "Numeric ID (e.g. 123456789012345678)" - #: app/templates/pages/edit_user.html:97 msgid "Enable Developer Mode in Discord → Right-click profile → Copy ID" msgstr "Enable Developer Mode in Discord → Right-click profile → Copy ID" - #: app/templates/pages/edit_user.html:106 msgid "(leave blank to keep current)" msgstr "(leave blank to keep current)" - #: app/templates/pages/edit_user.html:111 msgid "Update User" msgstr "Update User" - #: app/templates/pages/evaluate_player.html:10 msgid "Player Evaluation" msgstr "Player Evaluation" - #: app/templates/pages/evaluate_player.html:24 msgid "" "You have already evaluated this player. Your previous scores are shown " @@ -1770,213 +1385,166 @@ msgid "" msgstr "" "You have already evaluated this player. Your previous scores are shown " "below." - #: app/templates/pages/evaluate_player.html:33 msgid "Mecanics (1-10)" msgstr "Mecanics (1-10)" - #: app/templates/pages/evaluate_player.html:40 msgid "Cohesion (1-10)" msgstr "Cohesion (1-10)" - #: app/templates/pages/evaluate_player.html:47 msgid "Communication (1-10)" msgstr "Communication (1-10)" - #: app/templates/pages/evaluate_player.html:57 msgid "Gamesense (1-10)" msgstr "Gamesense (1-10)" - #: app/templates/pages/evaluate_player.html:64 msgid "Versatility (1-10)" msgstr "Versatility (1-10)" - #: app/templates/pages/evaluate_player.html:71 msgid "Discipline (1-10)" msgstr "Discipline (1-10)" - #: app/templates/pages/evaluate_player.html:81 msgid "Analysis (1-10)" msgstr "Analysis (1-10)" - #: app/templates/pages/evaluate_player.html:88 msgid "Sport Ethics (1-10)" msgstr "Sport Ethics (1-10)" - #: app/templates/pages/evaluate_player.html:95 msgid "Mental (1-10)" msgstr "Mental (1-10)" - #: app/templates/pages/evaluate_player.html:106 msgid "Recommended Position" msgstr "Recommended Position" - #: app/templates/pages/evaluate_player.html:109 #: app/templates/pages/view_tryout.html:280 msgid "-- Select Position --" msgstr "-- Select Position --" - #: app/templates/pages/evaluate_player.html:115 msgid "Enter position (optional)" msgstr "Enter position (optional)" - #: app/templates/pages/evaluate_player.html:121 msgid "Comments" msgstr "Comments" - #: app/templates/pages/evaluate_player.html:122 msgid "Enter your evaluation notes..." msgstr "Enter your evaluation notes..." - #: app/templates/pages/evaluate_player.html:130 msgid "Add note for this player" msgstr "Add note for this player" - #: app/templates/pages/evaluate_player.html:147 #: app/templates/pages/view_tryout.html:499 msgid "Evaluator" msgstr "Evaluator" - #: app/templates/pages/evaluate_player.html:148 #: app/templates/pages/view_tryout.html:500 msgid "Mecanics" msgstr "Mecanics" - #: app/templates/pages/evaluate_player.html:149 #: app/templates/pages/view_tryout.html:501 msgid "Cohesion" msgstr "Cohesion" - #: app/templates/pages/evaluate_player.html:150 #: app/templates/pages/view_tryout.html:502 msgid "Communication" msgstr "Communication" - #: app/templates/pages/evaluate_player.html:151 #: app/templates/pages/view_tryout.html:503 msgid "Gamesense" msgstr "Gamesense" - #: app/templates/pages/evaluate_player.html:152 #: app/templates/pages/view_tryout.html:504 msgid "Versatility" msgstr "Versatility" - #: app/templates/pages/evaluate_player.html:153 #: app/templates/pages/view_tryout.html:505 msgid "Discipline" msgstr "Discipline" - #: app/templates/pages/evaluate_player.html:154 #: app/templates/pages/view_tryout.html:506 msgid "Analysis" msgstr "Analysis" - #: app/templates/pages/evaluate_player.html:155 #: app/templates/pages/view_tryout.html:507 msgid "Sport Ethics" msgstr "Sport Ethics" - #: app/templates/pages/evaluate_player.html:156 #: app/templates/pages/view_tryout.html:508 msgid "Mental" msgstr "Mental" - #: app/templates/pages/evaluate_player.html:157 #: app/templates/pages/view_tryout.html:509 msgid "Overall" msgstr "Overall" - #: app/templates/pages/evaluate_player.html:158 #: app/templates/pages/my_teams.html:54 #: app/templates/pages/view_tryout.html:286 msgid "Position" msgstr "Position" - #: app/templates/pages/evaluations.html:97 msgid "No evaluations yet" msgstr "No evaluations yet" - #: app/templates/pages/login.html:2 msgid "Login" msgstr "Login" - #: app/templates/pages/login.html:8 msgid "Enter your username" msgstr "Enter your username" - #: app/templates/pages/login.html:12 msgid "Enter your password" msgstr "Enter your password" - #: app/templates/pages/login.html:14 msgid "Sign In" msgstr "Sign In" - #: app/templates/pages/login.html:15 msgid "Don't have an account?" msgstr "Don't have an account?" - #: app/templates/pages/login.html:15 msgid "Register here" msgstr "Register here" - #: app/templates/pages/match_form.html:34 msgid "Match Type" msgstr "Match Type" - #: app/templates/pages/match_form.html:36 msgid "Team vs Team" msgstr "Team vs Team" - #: app/templates/pages/match_form.html:37 msgid "Player vs Player" msgstr "Player vs Player" - #: app/templates/pages/match_form.html:38 msgid "Player Scrim" msgstr "Player Scrim" - #: app/templates/pages/match_form.html:48 #: app/templates/pages/team_match_form.html:17 msgid "Match Title" msgstr "Match Title" - #: app/templates/pages/match_form.html:49 msgid "e.g., Alpha vs Bravo Scrimmage" msgstr "e.g., Alpha vs Bravo Scrimmage" - #: app/templates/pages/match_form.html:62 #: app/templates/pages/team_match_form.html:63 msgid "Scheduled" msgstr "Scheduled" - #: app/templates/pages/match_form.html:63 #: app/templates/pages/team_match_form.html:64 #: app/templates/pages/view_tryout.html:23 msgid "Completed" msgstr "Completed" - #: app/templates/pages/match_form.html:64 #: app/templates/pages/team_match_form.html:65 msgid "Cancelled" msgstr "Cancelled" - #: app/templates/pages/match_form.html:69 app/templates/pages/my_teams.html:94 #: app/templates/pages/team_match_form.html:54 #: app/templates/pages/team_matches.html:31 #: app/templates/pages/tryout_form.html:49 msgid "Location" msgstr "Location" - #: app/templates/pages/match_form.html:70 msgid "Match location" msgstr "Match location" - #: app/templates/pages/match_form.html:77 msgid "Select Match Time" msgstr "Select Match Time" - #: app/templates/pages/match_form.html:79 msgid "" "Click time slots consecutively to set match duration. Players available " @@ -1984,89 +1552,70 @@ msgid "" msgstr "" "Click time slots consecutively to set match duration. Players available " "in all selected time blocks are shown below." - #: app/templates/pages/match_form.html:80 msgid "Reset time selection" msgstr "Reset time selection" - #: app/templates/pages/match_form.html:81 msgid "Reset Time" msgstr "Reset Time" - #: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:236 msgid "Loading..." msgstr "Loading..." - #: app/templates/pages/match_form.html:103 #: app/templates/pages/one_on_one.html:140 #: app/templates/pages/team_match_form.html:40 msgid "Start Time" msgstr "Start Time" - #: app/templates/pages/match_form.html:107 #: app/templates/pages/one_on_one.html:146 #: app/templates/pages/team_match_form.html:45 msgid "End Time" msgstr "End Time" - #: app/templates/pages/match_form.html:114 #: app/templates/pages/tryout_form.html:110 msgid "Description" msgstr "Description" - #: app/templates/pages/match_form.html:115 msgid "Optional notes about this match" msgstr "Optional notes about this match" - #: app/templates/pages/match_form.html:121 msgid "Select Teams" msgstr "Select Teams" - #: app/templates/pages/match_form.html:125 #: app/templates/pages/match_form.html:219 msgid "Team 1" msgstr "Team 1" - #: app/templates/pages/match_form.html:127 msgid "-- Select Team 1 --" msgstr "-- Select Team 1 --" - #: app/templates/pages/match_form.html:134 #: app/templates/pages/match_form.html:231 msgid "Team 2" msgstr "Team 2" - #: app/templates/pages/match_form.html:136 msgid "-- Select Team 2 --" msgstr "-- Select Team 2 --" - #: app/templates/pages/match_form.html:158 #: app/templates/pages/match_form.html:184 msgid "Click to toggle presence" msgstr "Click to toggle presence" - #: app/templates/pages/match_form.html:164 #: app/templates/pages/match_form.html:190 app/templates/pages/teams.html:197 msgid "No players assigned" msgstr "No players assigned" - #: app/templates/pages/match_form.html:201 #: app/templates/pages/match_form.html:243 msgid "Select Players" msgstr "Select Players" - #: app/templates/pages/match_form.html:206 msgid "Randomize Teams:" msgstr "Randomize Teams:" - #: app/templates/pages/match_form.html:211 msgid "Randomize" msgstr "Randomize" - #: app/templates/pages/match_form.html:214 msgid "Select players then click Randomize to split them into teams." msgstr "Select players then click Randomize to split them into teams." - #: app/templates/pages/match_form.html:226 msgid "" "All registered players are shown. Click time slots to filter available " @@ -2074,123 +1623,96 @@ msgid "" msgstr "" "All registered players are shown. Click time slots to filter available " "players." - #: app/templates/pages/match_form.html:246 msgid "Green indicators show player availability for the match date/time" msgstr "Green indicators show player availability for the match date/time" - #: app/templates/pages/match_form.html:632 msgid "Click time slots consecutively to set match duration" msgstr "Click time slots consecutively to set match duration" - #: app/templates/pages/match_form.html:899 msgid "No players registered" msgstr "No players registered" - #: app/templates/pages/match_form.html:932 msgid "T1" msgstr "T1" - #: app/templates/pages/match_form.html:933 msgid "T2" msgstr "T2" - #: app/templates/pages/match_form.html:938 msgid "No players available" msgstr "No players available" - #: app/templates/pages/my_teams.html:18 app/templates/pages/teams.html:84 #: app/templates/pages/teams.html:273 msgid "Coaches" msgstr "Coaches" - #: app/templates/pages/my_teams.html:30 app/templates/pages/teams.html:106 #: app/templates/pages/teams.html:284 msgid "Managers" msgstr "Managers" - #: app/templates/pages/my_teams.html:45 msgid "Team Roster" msgstr "Team Roster" - #: app/templates/pages/my_teams.html:79 msgid "No players on this team." msgstr "No players on this team." - #: app/templates/pages/my_teams.html:93 app/templates/pages/notes.html:124 #: app/templates/pages/one_on_one.html:71 #: app/templates/pages/team_matches.html:30 #: app/templates/pages/view_tryout.html:325 msgid "Time" msgstr "Time" - #: app/templates/pages/my_teams.html:95 #: app/templates/pages/team_match_form.html:115 #: app/templates/pages/team_matches.html:32 #: app/templates/pages/view_tryout.html:326 msgid "Presence" msgstr "Presence" - #: app/templates/pages/my_teams.html:96 msgid "My Status" msgstr "My Status" - #: app/templates/pages/my_teams.html:154 msgid "No upcoming matches scheduled." msgstr "No upcoming matches scheduled." - #: app/templates/pages/my_teams.html:165 msgid "No Teams" msgstr "No Teams" - #: app/templates/pages/my_teams.html:166 msgid "You are not currently assigned to any team." msgstr "You are not currently assigned to any team." - #: app/templates/pages/notes.html:2 app/templates/pages/notes.html:3 #: app/templates/pages/teams.html:75 msgid "Notes" msgstr "Notes" - #: app/templates/pages/notes.html:12 app/templates/pages/one_on_one.html:11 #: app/templates/pages/player_personal_notes.html:55 #: app/templates/pages/team_notes.html:2 app/templates/pages/team_notes.html:3 msgid "Team Notes" msgstr "Team Notes" - #: app/templates/pages/notes.html:20 app/templates/pages/team_notes.html:19 msgid "Team Notes Content" msgstr "Team Notes Content" - #: app/templates/pages/notes.html:21 app/templates/pages/team_notes.html:20 msgid "Enter improvement suggestions and notes for your team..." msgstr "Enter improvement suggestions and notes for your team..." - #: app/templates/pages/notes.html:22 app/templates/pages/team_notes.html:21 msgid "These notes will be visible to all players on your team." msgstr "These notes will be visible to all players on your team." - #: app/templates/pages/notes.html:48 app/templates/pages/personal_notes.html:19 #: app/templates/pages/upload_contract.html:16 msgid "Select Player" msgstr "Select Player" - #: app/templates/pages/notes.html:50 app/templates/pages/personal_notes.html:21 msgid "-- Select a Player --" msgstr "-- Select a Player --" - #: app/templates/pages/notes.html:59 app/templates/pages/personal_notes.html:30 msgid "Enter personal feedback or coaching tips for this player..." msgstr "Enter personal feedback or coaching tips for this player..." - #: app/templates/pages/notes.html:60 app/templates/pages/personal_notes.html:31 msgid "These notes will only be visible to the selected player." msgstr "These notes will only be visible to the selected player." - #: app/templates/pages/notes.html:64 msgid "Context (Optional)" msgstr "Context (Optional)" - #: app/templates/pages/notes.html:65 msgid "" "Link this note to a specific match, tryout, or team for better " @@ -2198,90 +1720,69 @@ msgid "" msgstr "" "Link this note to a specific match, tryout, or team for better " "organization." - #: app/templates/pages/notes.html:71 msgid "-- Select Match --" msgstr "-- Select Match --" - #: app/templates/pages/notes.html:80 msgid "-- Select Tryout --" msgstr "-- Select Tryout --" - #: app/templates/pages/notes.html:89 msgid "-- Select Team --" msgstr "-- Select Team --" - #: app/templates/pages/notes.html:111 msgid "One on One Requests" msgstr "One on One Requests" - #: app/templates/pages/notes.html:125 app/templates/pages/one_on_one.html:72 msgid "Discussion Points" msgstr "Discussion Points" - #: app/templates/pages/notes.html:150 app/templates/pages/notes.html:151 msgid "Accept" msgstr "Accept" - #: app/templates/pages/notes.html:154 app/templates/pages/notes.html:155 msgid "Refuse" msgstr "Refuse" - #: app/templates/pages/notes.html:167 msgid "No One on One requests from your players yet." msgstr "No One on One requests from your players yet." - #: app/templates/pages/notes.html:177 msgid "Reject One on One Request" msgstr "Reject One on One Request" - #: app/templates/pages/notes.html:184 msgid "Reason for rejection (optional):" msgstr "Reason for rejection (optional):" - #: app/templates/pages/notes.html:185 msgid "Let the player know why this time doesn't work..." msgstr "Let the player know why this time doesn't work..." - #: app/templates/pages/notes.html:190 msgid "Reject Request" msgstr "Reject Request" - #: app/templates/pages/notes.html:201 msgid "Team Notes History" msgstr "Team Notes History" - #: app/templates/pages/notes.html:207 app/templates/pages/team_notes.html:42 msgid "Last Updated" msgstr "Last Updated" - #: app/templates/pages/notes.html:208 app/templates/pages/team_notes.html:43 msgid "Content Preview" msgstr "Content Preview" - #: app/templates/pages/notes.html:228 msgid "Recent Personal Notes" msgstr "Recent Personal Notes" - #: app/templates/pages/notes.html:243 #: app/templates/pages/player_personal_notes.html:27 msgid "From match" msgstr "From match" - #: app/templates/pages/notes.html:246 #: app/templates/pages/player_personal_notes.html:32 msgid "From team" msgstr "From team" - #: app/templates/pages/notes.html:249 #: app/templates/pages/player_personal_notes.html:37 msgid "From tryout" msgstr "From tryout" - #: app/templates/pages/one_on_one.html:2 app/templates/pages/one_on_one.html:3 msgid "One on One" msgstr "One on One" - #: app/templates/pages/one_on_one.html:28 #: app/templates/pages/player_personal_notes.html:72 msgid "" @@ -2290,14 +1791,12 @@ msgid "" msgstr "" "No team notes have been added yet. Your coach will post improvement " "suggestions here." - #: app/templates/pages/one_on_one.html:36 #: app/templates/pages/personal_notes.html:2 #: app/templates/pages/personal_notes.html:3 #: app/templates/pages/player_personal_notes.html:11 msgid "Personal Notes" msgstr "Personal Notes" - #: app/templates/pages/one_on_one.html:53 #: app/templates/pages/player_personal_notes.html:47 msgid "" @@ -2306,53 +1805,41 @@ msgid "" msgstr "" "No personal notes have been added yet. Your coach may provide individual " "feedback here." - #: app/templates/pages/one_on_one.html:62 msgid "My One on One Requests" msgstr "My One on One Requests" - #: app/templates/pages/one_on_one.html:74 msgid "Coach Response" msgstr "Coach Response" - #: app/templates/pages/one_on_one.html:87 msgid "Approved" msgstr "Approved" - #: app/templates/pages/one_on_one.html:89 msgid "Rejected" msgstr "Rejected" - #: app/templates/pages/one_on_one.html:111 msgid "You haven't made any One on One requests yet." msgstr "You haven't made any One on One requests yet." - #: app/templates/pages/one_on_one.html:120 #: app/templates/pages/player_personal_notes.html:81 msgid "Request One on One Session" msgstr "Request One on One Session" - #: app/templates/pages/one_on_one.html:132 msgid "Select Date" msgstr "Select Date" - #: app/templates/pages/one_on_one.html:142 msgid "-- Select Date First --" msgstr "-- Select Date First --" - #: app/templates/pages/one_on_one.html:148 #: app/templates/pages/one_on_one.html:271 msgid "-- Select Start Time First --" msgstr "-- Select Start Time First --" - #: app/templates/pages/one_on_one.html:155 msgid "Enter topics you'd like to cover in your One on One session..." msgstr "Enter topics you'd like to cover in your One on One session..." - #: app/templates/pages/one_on_one.html:160 msgid "Send Request" msgstr "Send Request" - #: app/templates/pages/one_on_one.html:165 msgid "" "You need to be assigned to a team with a coach to request a One on One " @@ -2360,76 +1847,58 @@ msgid "" msgstr "" "You need to be assigned to a team with a coach to request a One on One " "session." - #: app/templates/pages/one_on_one.html:246 msgid "-- Select Start Time --" msgstr "-- Select Start Time --" - #: app/templates/pages/one_on_one.html:294 msgid "-- Select End Time --" msgstr "-- Select End Time --" - #: app/templates/pages/personal_notes.html:46 msgid "Recent Notes" msgstr "Recent Notes" - #: app/templates/pages/players_to_evaluate.html:2 #: app/templates/pages/players_to_evaluate.html:3 msgid "Players to Evaluate" msgstr "Players to Evaluate" - #: app/templates/pages/profile.html:21 msgid "Account Information" msgstr "Account Information" - #: app/templates/pages/profile.html:68 app/templates/pages/view_user.html:52 msgid "Gamertags" msgstr "Gamertags" - #: app/templates/pages/profile.html:94 msgid "Discord" msgstr "Discord" - #: app/templates/pages/profile.html:104 app/templates/pages/view_user.html:39 msgid "League OS" msgstr "League OS" - #: app/templates/pages/profile.html:121 msgid "My Statistics" msgstr "My Statistics" - #: app/templates/pages/profile.html:128 msgid "Tryouts Registered" msgstr "Tryouts Registered" - #: app/templates/pages/profile.html:132 msgid "Team Assignments" msgstr "Team Assignments" - #: app/templates/pages/profile.html:139 msgid "Evaluations Given" msgstr "Evaluations Given" - #: app/templates/pages/profile.html:143 msgid "No statistics available for this role." msgstr "No statistics available for this role." - #: app/templates/pages/profile.html:195 msgid "My Contracts" msgstr "My Contracts" - #: app/templates/pages/profile.html:217 msgid "View All Contracts" msgstr "View All Contracts" - #: app/templates/pages/profile.html:221 msgid "No contracts have been uploaded for you yet." msgstr "No contracts have been uploaded for you yet." - #: app/templates/pages/profile.html:231 msgid "My Disponibilities" msgstr "My Disponibilities" - #: app/templates/pages/profile.html:234 msgid "" "Select your available time blocks for matches (5pm to 12am). Green = " @@ -2437,11 +1906,9 @@ msgid "" msgstr "" "Select your available time blocks for matches (5pm to 12am). Green = " "selected, Gray = available to select." - #: app/templates/pages/profile.html:251 msgid "My Coaching Availability" msgstr "My Coaching Availability" - #: app/templates/pages/profile.html:252 msgid "" "Select time slots when you're available for One on One sessions (8am to " @@ -2449,43 +1916,33 @@ msgid "" msgstr "" "Select time slots when you're available for One on One sessions (8am to " "10pm)." - #: app/templates/pages/profile.html:500 msgid "Click or click-and-drag to select your available hours" msgstr "Click or click-and-drag to select your available hours" - #: app/templates/pages/register.html:2 msgid "Register" msgstr "Register" - #: app/templates/pages/register.html:8 msgid "Personal Information" msgstr "Personal Information" - #: app/templates/pages/register.html:11 msgid "Enter your full name" msgstr "Enter your full name" - #: app/templates/pages/register.html:16 msgid "Choose a username" msgstr "Choose a username" - #: app/templates/pages/register.html:21 msgid "Enter your email" msgstr "Enter your email" - #: app/templates/pages/register.html:25 msgid "Phone (Optional)" msgstr "Phone (Optional)" - #: app/templates/pages/register.html:26 msgid "Enter your phone number" msgstr "Enter your phone number" - #: app/templates/pages/register.html:33 msgid "Discord Connection" msgstr "Discord Connection" - #: app/templates/pages/register.html:34 msgid "" "Connect your Discord account to automatically fill your gamertags from " @@ -2493,11 +1950,9 @@ msgid "" msgstr "" "Connect your Discord account to automatically fill your gamertags from " "your connected game accounts." - #: app/templates/pages/register.html:52 msgid "Connected" msgstr "Connected" - #: app/templates/pages/register.html:61 msgid "" "Discord connected. Game connections have been used to pre-fill your " @@ -2505,407 +1960,309 @@ msgid "" msgstr "" "Discord connected. Game connections have been used to pre-fill your " "profile below." - #: app/templates/pages/register.html:69 msgid "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc." msgstr "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc." - #: app/templates/pages/register.html:72 msgid "Discord Username (Manual)" msgstr "Discord Username (Manual)" - #: app/templates/pages/register.html:73 msgid "e.g. YourName" msgstr "e.g. YourName" - #: app/templates/pages/register.html:82 msgid "Set up your competitive gaming profile for tryouts." msgstr "Set up your competitive gaming profile for tryouts." - #: app/templates/pages/register.html:127 msgid "League OS Connection (Optional)" msgstr "League OS Connection (Optional)" - #: app/templates/pages/register.html:131 msgid "Connect your League OS profile for organized play." msgstr "Connect your League OS profile for organized play." - #: app/templates/pages/register.html:135 msgid "Security" msgstr "Security" - #: app/templates/pages/register.html:138 msgid "Create a password" msgstr "Create a password" - #: app/templates/pages/register.html:142 msgid "Confirm Password" msgstr "Confirm Password" - #: app/templates/pages/register.html:143 msgid "Confirm your password" msgstr "Confirm your password" - #: app/templates/pages/register.html:157 msgid "Create Account" msgstr "Create Account" - #: app/templates/pages/team_match_form.html:20 msgid "e.g., Regular Season vs Opponent" msgstr "e.g., Regular Season vs Opponent" - #: app/templates/pages/team_match_form.html:24 msgid "Opponent (optional)" msgstr "Opponent (optional)" - #: app/templates/pages/team_match_form.html:27 msgid "e.g., University of Toronto" msgstr "e.g., University of Toronto" - #: app/templates/pages/team_match_form.html:48 msgid "Auto-calculated if left empty (start + 30 min)" msgstr "Auto-calculated if left empty (start + 30 min)" - #: app/templates/pages/team_match_form.html:57 msgid "e.g., Online, Gym A" msgstr "e.g., Online, Gym A" - #: app/templates/pages/team_match_form.html:72 msgid "Description (optional)" msgstr "Description (optional)" - #: app/templates/pages/team_match_form.html:74 msgid "Additional details about the match..." msgstr "Additional details about the match..." - #: app/templates/pages/team_match_form.html:81 msgid "Team Roster (auto-included)" msgstr "Team Roster (auto-included)" - #: app/templates/pages/team_match_form.html:98 msgid "No players on this team. Add players in the Teams page first." msgstr "No players on this team. Add players in the Teams page first." - #: app/templates/pages/team_match_form.html:107 #: app/templates/pages/view_tryout.html:324 msgid "Participants" msgstr "Participants" - #: app/templates/pages/team_match_form.html:134 msgid "No participants recorded." msgstr "No participants recorded." - #: app/templates/pages/team_matches.html:3 #: app/templates/pages/team_matches.html:4 msgid "Team Matches" msgstr "Team Matches" - #: app/templates/pages/team_matches.html:11 msgid "+ Schedule Match" msgstr "+ Schedule Match" - #: app/templates/pages/team_matches.html:101 msgid "Delete this match?" msgstr "Delete this match?" - #: app/templates/pages/team_matches.html:133 msgid "No Team Matches" msgstr "No Team Matches" - #: app/templates/pages/team_matches.html:134 msgid "Regular season matches have not been scheduled yet." msgstr "Regular season matches have not been scheduled yet." - #: app/templates/pages/team_matches.html:138 msgid "-- Schedule a Match --" msgstr "-- Schedule a Match --" - #: app/templates/pages/team_notes.html:9 msgid "Team Improvement Notes" msgstr "Team Improvement Notes" - #: app/templates/pages/team_notes.html:26 msgid "Add Team Notes" msgstr "Add Team Notes" - #: app/templates/pages/team_notes.html:36 msgid "Note History" msgstr "Note History" - #: app/templates/pages/teams.html:2 app/templates/pages/teams.html:3 #: app/templates/pages/view_tryout.html:231 msgid "Teams" msgstr "Teams" - #: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:234 msgid "New Team" msgstr "New Team" - #: app/templates/pages/teams.html:18 msgid "Create New Team" msgstr "Create New Team" - #: app/templates/pages/teams.html:25 app/templates/pages/teams.html:266 msgid "Team Name" msgstr "Team Name" - #: app/templates/pages/teams.html:26 msgid "e.g., Varsity, JV, U14" msgstr "e.g., Varsity, JV, U14" - #: app/templates/pages/teams.html:29 msgid "Assigned Coach" msgstr "Assigned Coach" - #: app/templates/pages/teams.html:31 msgid "-- No coach assigned --" msgstr "-- No coach assigned --" - #: app/templates/pages/teams.html:38 msgid "Assigned Manager" msgstr "Assigned Manager" - #: app/templates/pages/teams.html:40 msgid "-- No manager assigned --" msgstr "-- No manager assigned --" - #: app/templates/pages/teams.html:49 app/templates/pages/teams.html:243 msgid "Create Team" msgstr "Create Team" - #: app/templates/pages/teams.html:64 app/templates/pages/users.html:51 #: app/templates/pages/view_tryout.html:29 #: app/templates/pages/view_tryout.html:459 msgid "Edit" msgstr "Edit" - #: app/templates/pages/teams.html:66 #, python-format msgid "Delete team %(name)s? It will be unassigned from any linked tryouts." msgstr "Delete team %(name)s? It will be unassigned from any linked tryouts." - #: app/templates/pages/teams.html:69 msgid "Delete" msgstr "Delete" - #: app/templates/pages/teams.html:74 msgid "Add Notes" msgstr "Add Notes" - #: app/templates/pages/teams.html:92 #, python-format msgid "Remove coach %(coach)s from %(team)s?" msgstr "Remove coach %(coach)s from %(team)s?" - #: app/templates/pages/teams.html:95 msgid "Remove Coach" msgstr "Remove Coach" - #: app/templates/pages/teams.html:114 #, python-format msgid "Remove manager %(manager)s from %(team)s?" msgstr "Remove manager %(manager)s from %(team)s?" - #: app/templates/pages/teams.html:117 msgid "Remove Manager" msgstr "Remove Manager" - #: app/templates/pages/teams.html:127 msgid "View Team Matches" msgstr "View Team Matches" - #: app/templates/pages/teams.html:128 msgid "Matches" msgstr "Matches" - #: app/templates/pages/teams.html:134 msgid "Schedule Practice" msgstr "Schedule Practice" - #: app/templates/pages/teams.html:135 msgid "Practice" msgstr "Practice" - #: app/templates/pages/teams.html:147 msgid "Position / Role" msgstr "Position / Role" - #: app/templates/pages/teams.html:183 #, python-format msgid "Remove %(player)s from %(team)s?" msgstr "Remove %(player)s from %(team)s?" - #: app/templates/pages/teams.html:186 app/templates/pages/view_tryout.html:211 msgid "Remove" msgstr "Remove" - #: app/templates/pages/teams.html:199 msgid "Add players to this team using the form below." msgstr "Add players to this team using the form below." - #: app/templates/pages/teams.html:224 msgid "Starter" msgstr "Starter" - #: app/templates/pages/teams.html:225 msgid "Substitute" msgstr "Substitute" - #: app/templates/pages/teams.html:228 msgid "Add to Team" msgstr "Add to Team" - #: app/templates/pages/teams.html:240 msgid "No teams yet" msgstr "No teams yet" - #: app/templates/pages/teams.html:242 msgid "Create organization teams and assign coaches to manage tryouts." msgstr "Create organization teams and assign coaches to manage tryouts." - #: app/templates/pages/teams.html:245 msgid "There are no teams to display." msgstr "There are no teams to display." - #: app/templates/pages/teams.html:257 msgid "Edit Team" msgstr "Edit Team" - #: app/templates/pages/teams.html:280 msgid "Hold Ctrl/Cmd to select multiple. Only unassigned coaches shown." msgstr "Hold Ctrl/Cmd to select multiple. Only unassigned coaches shown." - #: app/templates/pages/teams.html:291 msgid "Hold Ctrl/Cmd to select multiple. Only unassigned managers shown." msgstr "Hold Ctrl/Cmd to select multiple. Only unassigned managers shown." - #: app/templates/pages/tryout_form.html:23 msgid "Tryout Title" msgstr "Tryout Title" - #: app/templates/pages/tryout_form.html:24 msgid "e.g., Spring Season Tryouts" msgstr "e.g., Spring Season Tryouts" - #: app/templates/pages/tryout_form.html:29 #: app/templates/pages/view_user.html:57 msgid "Game" msgstr "Game" - #: app/templates/pages/tryout_form.html:31 msgid "-- Select a game --" msgstr "-- Select a game --" - #: app/templates/pages/tryout_form.html:38 msgid "Start Date" msgstr "Start Date" - #: app/templates/pages/tryout_form.html:42 msgid "End Date" msgstr "End Date" - #: app/templates/pages/tryout_form.html:44 msgid "Optional. Leave blank for single-day tryout." msgstr "Optional. Leave blank for single-day tryout." - #: app/templates/pages/tryout_form.html:50 msgid "e.g., Online" msgstr "e.g., Online" - #: app/templates/pages/tryout_form.html:53 msgid "Max Players" msgstr "Max Players" - #: app/templates/pages/tryout_form.html:54 msgid "Leave blank for unlimited" msgstr "Leave blank for unlimited" - #: app/templates/pages/tryout_form.html:59 msgid "Target Team" msgstr "Target Team" - #: app/templates/pages/tryout_form.html:61 msgid "-- No target team --" msgstr "-- No target team --" - #: app/templates/pages/tryout_form.html:70 msgid "Assigned Managers" msgstr "Assigned Managers" - #: app/templates/pages/tryout_form.html:85 msgid "Select one or more managers for this tryout." msgstr "Select one or more managers for this tryout." - #: app/templates/pages/tryout_form.html:90 msgid "Assigned Coaches" msgstr "Assigned Coaches" - #: app/templates/pages/tryout_form.html:106 msgid "Select one or more coaches for this tryout." msgstr "Select one or more coaches for this tryout." - #: app/templates/pages/tryout_form.html:111 msgid "Enter any details about the tryout..." msgstr "Enter any details about the tryout..." - #: app/templates/pages/tryouts.html:9 msgid "New Tryout" msgstr "New Tryout" - #: app/templates/pages/tryouts.html:80 msgid "View Details" msgstr "View Details" - #: app/templates/pages/tryouts.html:84 msgid "Evaluate" msgstr "Evaluate" - #: app/templates/pages/tryouts.html:94 msgid "No tryouts found" msgstr "No tryouts found" - #: app/templates/pages/tryouts.html:96 msgid "Get started by creating a new tryout." msgstr "Get started by creating a new tryout." - #: app/templates/pages/upload_contract.html:9 msgid "Upload Contract for Player" msgstr "Upload Contract for Player" - #: app/templates/pages/upload_contract.html:26 msgid "Contract File" msgstr "Contract File" - #: app/templates/pages/upload_contract.html:32 msgid "Notes (Optional)" msgstr "Notes (Optional)" - #: app/templates/pages/upload_contract.html:33 msgid "Add any notes about this contract..." msgstr "Add any notes about this contract..." - #: app/templates/pages/users.html:9 msgid "Add User" msgstr "Add User" - #: app/templates/pages/users.html:54 #, python-format msgid "Delete %(name)s? This cannot be undone." msgstr "Delete %(name)s? This cannot be undone." - #: app/templates/pages/view_tryout.html:10 msgid "Tryout Details" msgstr "Tryout Details" - #: app/templates/pages/view_tryout.html:14 #: app/templates/pages/view_tryout.html:306 msgid "Schedule Match" msgstr "Schedule Match" - #: app/templates/pages/view_tryout.html:22 msgid "In Progress" msgstr "In Progress" - #: app/templates/pages/view_tryout.html:33 msgid "" "Delete this entire tryout? This removes all its matches, teams, " @@ -2913,43 +2270,33 @@ msgid "" msgstr "" "Delete this entire tryout? This removes all its matches, teams, " "registrations and evaluations." - #: app/templates/pages/view_tryout.html:36 msgid "Delete Tryout" msgstr "Delete Tryout" - #: app/templates/pages/view_tryout.html:114 msgid "Register for this Tryout" msgstr "Register for this Tryout" - #: app/templates/pages/view_tryout.html:124 msgid "Add Player to Tryout" msgstr "Add Player to Tryout" - #: app/templates/pages/view_tryout.html:137 msgid "Register Player" msgstr "Register Player" - #: app/templates/pages/view_tryout.html:157 msgid "Evaluation" msgstr "Evaluation" - #: app/templates/pages/view_tryout.html:179 msgid "Registered" msgstr "Registered" - #: app/templates/pages/view_tryout.html:180 msgid "Attended" msgstr "Attended" - #: app/templates/pages/view_tryout.html:181 msgid "No Show" msgstr "No Show" - #: app/templates/pages/view_tryout.html:200 msgid "Evaluate player" msgstr "Evaluate player" - #: app/templates/pages/view_tryout.html:208 #, python-format msgid "" @@ -2958,81 +2305,61 @@ msgid "" msgstr "" "Remove %(name)s from this tryout? They will also be removed from every " "team and match within it." - #: app/templates/pages/view_tryout.html:210 msgid "Remove player from tryout" msgstr "Remove player from tryout" - #: app/templates/pages/view_tryout.html:220 msgid "No players registered yet." msgstr "No players registered yet." - #: app/templates/pages/view_tryout.html:243 msgid "Team name" msgstr "Team name" - #: app/templates/pages/view_tryout.html:244 msgid "Create" msgstr "Create" - #: app/templates/pages/view_tryout.html:263 msgid "No players assigned yet." msgstr "No players assigned yet." - #: app/templates/pages/view_tryout.html:271 msgid "Select player..." msgstr "Select player..." - #: app/templates/pages/view_tryout.html:288 msgid "Add" msgstr "Add" - #: app/templates/pages/view_tryout.html:293 msgid "No teams created yet." msgstr "No teams created yet." - #: app/templates/pages/view_tryout.html:322 msgid "Type" msgstr "Type" - #: app/templates/pages/view_tryout.html:387 msgid "Toggle your attendance" msgstr "Toggle your attendance" - #: app/templates/pages/view_tryout.html:462 msgid "Add Note for this Match" msgstr "Add Note for this Match" - #: app/templates/pages/view_tryout.html:463 msgid "Note" msgstr "Note" - #: app/templates/pages/view_tryout.html:481 msgid "No matches scheduled yet. Check back later!" msgstr "No matches scheduled yet. Check back later!" - #: app/templates/pages/view_tryout.html:491 msgid "Evaluation Summary" msgstr "Evaluation Summary" - #: app/templates/pages/view_tryout.html:510 msgid "Recommendation" msgstr "Recommendation" - #: app/templates/pages/view_user.html:60 msgid "Profile" msgstr "Profile" - #: app/templates/pages/view_user.html:72 msgid "View Profile" msgstr "View Profile" - #~ msgid "×" #~ msgstr "×" - #~ msgid "One on One request from %(value)s has been approved!" #~ msgstr "" - #~ msgid "" #~ "Account is locked due to too many" #~ " failed attempts. Please try again in" @@ -3041,7 +2368,6 @@ msgstr "View Profile" #~ "Account is locked due to too many" #~ " failed attempts. Please try again in" #~ " %(remaining)s minute(s)." - #~ msgid "" #~ "Account locked after %(attempts)s failed " #~ "attempts. Please try again in " @@ -3050,43 +2376,29 @@ msgstr "View Profile" #~ "Account locked after %(attempts)s failed " #~ "attempts. Please try again in " #~ "%(minutes)s minutes." - #~ msgid "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout." #~ msgstr "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout." - #~ msgid "Incorrect CAPTCHA answer. Please try again." #~ msgstr "Incorrect CAPTCHA answer. Please try again." - #~ msgid "Answer" #~ msgstr "Answer" - #~ msgid "Invalid start time format." #~ msgstr "Invalid start time format." - #~ msgid "Invalid end time format." #~ msgstr "Invalid end time format." - #~ msgid "Invalid date or time format." #~ msgstr "Invalid date or time format." - #~ msgid "Availability" #~ msgstr "Availability" - #~ msgid "Team Tryout Management System" #~ msgstr "Team Tryout Management System" - #~ msgid "Player and content are required." #~ msgstr "Player and content are required." - #~ msgid "Save Disponibilities" #~ msgstr "Save Disponibilities" - #~ msgid "Contact" #~ msgstr "Contact" - #~ msgid "Attendance" #~ msgstr "Attendance" - #~ msgid "No players registered for this tryout." -#~ msgstr "No players registered for this tryout." - +#~ msgstr "No players registered for this tryout." \ No newline at end of file diff --git a/app/translations/fr/LC_MESSAGES/messages.mo b/app/translations/fr/LC_MESSAGES/messages.mo index 5f69323221bfeccb23f33add1d6f2b88248c1f4c..1182cd7fa81e91cf381e7ca298601c82cf0f6a5e 100644 GIT binary patch delta 11097 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 Z#kteHMV`q8QwcV=C@*RMn7J!^{ud-x8<7A2 delta 10955 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\n" "Language: fr\n" @@ -18,17 +18,14 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" - #: app/app.py:564 msgid "Please log in to access this page." msgstr "Veuillez vous connecter pour accéder à cette page." - #: app/forms.py:37 app/routes/auth.py:230 app/routes/auth.py:389 -#: app/routes/auth.py:403 app/routes/users/contracts.py:103 +#: app/routes/auth.py:403 app/routes/users/contracts.py:104 #, python-format msgid "%(field)s: %(msg)s" msgstr "%(field)s : %(msg)s" - #: app/validators.py:50 msgid "" "Password must be at least 8 characters with uppercase, lowercase, and a " @@ -36,243 +33,184 @@ msgid "" msgstr "" "Le mot de passe doit compter au moins 8 caractères, dont une majuscule, " "une minuscule et un chiffre." - #: app/validators.py:68 msgid "Username must be 3-30 characters (letters, numbers, underscore, hyphen)." msgstr "" "Le nom d’utilisateur doit compter de 3 à 30 caractères (lettres, " "chiffres, tiret bas, trait d’union)." - #: app/validators.py:87 msgid "Invalid Discord username format." msgstr "Format de nom d’utilisateur Discord invalide." - #: app/validators.py:104 msgid "Discord User ID must be a 17-20 digit number." msgstr "L’identifiant Discord doit être un nombre de 17 à 20 chiffres." - #: app/validators.py:122 msgid "Invalid phone number format." msgstr "Format de numéro de téléphone invalide." - #: app/validators.py:201 msgid "Username is required." msgstr "Le nom d’utilisateur est obligatoire." - #: app/validators.py:205 msgid "Password is required." msgstr "Le mot de passe est obligatoire." - #: app/validators.py:227 app/validators.py:324 msgid "Username must be 3-80 characters." msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères." - #: app/validators.py:233 msgid "Email must be 120 characters or less." msgstr "L’adresse courriel ne doit pas dépasser 120 caractères." - #: app/validators.py:246 app/validators.py:339 app/validators.py:370 #: app/validators.py:433 msgid "Full name is required." msgstr "Le nom complet est obligatoire." - #: app/validators.py:276 msgid "Passwords do not match." msgstr "Les mots de passe ne concordent pas." - #: app/validators.py:284 app/validators.py:924 msgid "Unknown game." msgstr "Jeu inconnu." - #: app/validators.py:291 msgid "Gamertag must be between 1 and 120 characters." msgstr "Le gamertag doit compter de 1 à 120 caractères." - #: app/validators.py:297 msgid "Platform must be 30 characters or less." msgstr "La plateforme ne doit pas dépasser 30 caractères." - #: app/validators.py:306 msgid "Unknown platform for this game." msgstr "Plateforme inconnue pour ce jeu." - #: app/validators.py:343 app/validators.py:378 msgid "Invalid role selected." msgstr "Rôle sélectionné invalide." - #: app/validators.py:473 app/validators.py:596 app/validators.py:629 msgid "Player must be selected." msgstr "Vous devez choisir un joueur." - #: app/validators.py:476 msgid "Notes must be 2000 characters or less." msgstr "Les notes ne doivent pas dépasser 2000 caractères." - #: app/validators.py:513 app/validators.py:994 msgid "Invalid coach selection." msgstr "Sélection de coach invalide." - #: app/validators.py:519 app/validators.py:1000 msgid "Invalid manager selection." msgstr "Sélection de gérant invalide." - #: app/validators.py:537 app/validators.py:595 app/validators.py:628 msgid "Invalid player selection." msgstr "Sélection de joueur invalide." - #: app/validators.py:546 msgid "Unknown roster status." msgstr "Statut d'effectif inconnu." - #: app/validators.py:559 msgid "Unknown tryout status." msgstr "Statut de sélection inconnu." - #: app/validators.py:570 msgid "Unknown registration status." msgstr "Statut d’inscription inconnu." - #: app/validators.py:583 msgid "Team name must be between 1 and 100 characters." msgstr "Le nom de l’équipe doit compter de 1 à 100 caractères." - #: app/validators.py:603 msgid "Position must be 50 characters or less." msgstr "La position ne doit pas dépasser 50 caractères." - #: app/validators.py:616 msgid "Note content must be between 1 and 5000 characters." msgstr "La note doit compter de 1 à 5000 caractères." - #: app/validators.py:642 msgid "Select at most one note context." msgstr "Sélectionnez au plus un contexte pour la note." - #: app/validators.py:654 msgid "Rejection reason must be 2000 characters or less." msgstr "Le motif de refus ne doit pas dépasser 2000 caractères." - #: app/validators.py:678 msgid "Date must be in YYYY-MM-DD format." msgstr "La date doit être au format AAAA-MM-JJ." - #: app/validators.py:679 msgid "A date is required." msgstr "Une date est requise." - #: app/validators.py:685 app/validators.py:750 msgid "Start time must be in HH:MM format." msgstr "L’heure de début doit être au format HH:MM." - #: app/validators.py:686 app/validators.py:751 msgid "A start time is required." msgstr "Une heure de début est requise." - #: app/validators.py:692 msgid "End time must be in HH:MM format." msgstr "L’heure de fin doit être au format HH:MM." - #: app/validators.py:693 msgid "An end time is required." msgstr "Une heure de fin est requise." - #: app/validators.py:697 msgid "Points must be 2000 characters or less." msgstr "Les points ne doivent pas dépasser 2000 caractères." - #: app/validators.py:712 msgid "End time must be after start time." msgstr "L'heure de fin doit être postérieure à l'heure de début." - #: app/validators.py:741 app/validators.py:743 msgid "Day must be 0 (Monday) to 6 (Sunday)." msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)." - #: app/validators.py:744 msgid "A day is required." msgstr "Un jour est requis." - #: app/validators.py:779 msgid "Player selection is malformed." msgstr "La sélection de joueurs est mal formée." - #: app/validators.py:805 app/validators.py:915 msgid "A title is required." msgstr "Un titre est requis." - #: app/validators.py:814 msgid "Invalid date format." msgstr "Format de date invalide." - #: app/validators.py:819 app/validators.py:826 msgid "Invalid time format." msgstr "Format d’heure invalide." - #: app/validators.py:820 msgid "Start time is required. Please select a time slot." msgstr "L’heure de début est obligatoire. Choisissez une plage horaire." - #: app/validators.py:834 msgid "Unknown match status." msgstr "Statut de match inconnu." - #: app/validators.py:850 msgid "The end time must come after the start time." msgstr "L'heure de fin doit être postérieure à l'heure de début." - #: app/validators.py:864 msgid "Unknown match type." msgstr "Type de match inconnu." - #: app/validators.py:876 msgid "A team cannot play against itself." msgstr "Une équipe ne peut pas jouer contre elle-même." - #: app/validators.py:929 msgid "Invalid start date format." msgstr "Format de date de début invalide." - #: app/validators.py:930 msgid "A start date is required." msgstr "Une date de début est requise." - #: app/validators.py:936 msgid "Invalid end date format." msgstr "Format de date de fin invalide." - #: app/validators.py:944 msgid "A tryout must allow at least one player." msgstr "Une sélection doit accepter au moins un joueur." - #: app/validators.py:947 msgid "The player limit must be a whole number." msgstr "La limite de joueurs doit être un nombre entier." - #: app/validators.py:960 msgid "End date cannot be before start date." msgstr "La date de fin ne peut pas précéder la date de début." - #: app/validators.py:987 app/validators.py:988 msgid "Team name is required." msgstr "Le nom de l’équipe est obligatoire." - #: app/validators.py:1012 msgid "Scores run from 1 to 10." msgstr "Les notes vont de 1 à 10." - #: app/validators.py:1013 msgid "A score must be a whole number from 1 to 10." msgstr "Une note doit être un nombre entier de 1 à 10." - #: app/routes/auth.py:247 msgid "This account has been deactivated." msgstr "Ce compte a été désactivé." - #: app/routes/auth.py:282 #, python-format msgid "Welcome back, %(username)s!" msgstr "Bon retour, %(username)s !" - #: app/routes/auth.py:312 msgid "" "Login unsuccessful. Please check your username and password, or ask a " @@ -280,32 +218,25 @@ msgid "" msgstr "" "Échec de la connexion. Vérifiez le nom d’utilisateur et le mot de passe, " "ou demandez de l’aide à un président." - #: app/routes/auth.py:378 msgid "Your registration could not be processed. Please try again." msgstr "Votre inscription n'a pas pu être traitée. Veuillez réessayer." - #: app/routes/auth.py:420 app/routes/users/accounts.py:343 msgid "Username already exists." msgstr "Ce nom d’utilisateur est déjà pris." - #: app/routes/auth.py:424 app/routes/users/accounts.py:347 msgid "Email already registered." msgstr "Cette adresse courriel est déjà enregistrée." - #: app/routes/auth.py:431 app/routes/auth.py:624 #: app/routes/users/accounts.py:121 msgid "This Discord account is already linked to another account." msgstr "Ce compte Discord est déjà lié à un autre compte." - #: app/routes/auth.py:473 msgid "Your account has been created! You can now log in." msgstr "Votre compte a été créé. Vous pouvez maintenant vous connecter." - #: app/routes/auth.py:498 msgid "Discord OAuth2 is not configured." msgstr "La connexion Discord n’est pas configurée." - #: app/routes/auth.py:547 msgid "" "Discord authorization could not be verified. Please start the connection " @@ -313,67 +244,51 @@ msgid "" msgstr "" "L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion " "depuis cette page." - #: app/routes/auth.py:556 msgid "Discord authorization failed. No code received." msgstr "L’autorisation Discord a échoué : aucun code reçu." - #: app/routes/auth.py:580 msgid "Failed to connect to Discord. Please try again." msgstr "Impossible de joindre Discord. Veuillez réessayer." - #: app/routes/auth.py:584 msgid "Failed to obtain Discord access token." msgstr "Impossible d’obtenir le jeton d’accès Discord." - #: app/routes/auth.py:599 app/routes/auth.py:609 msgid "Failed to fetch Discord user profile." msgstr "Impossible de récupérer le profil Discord." - #: app/routes/auth.py:616 msgid "Please log in to connect your Discord account." msgstr "Veuillez vous connecter pour lier votre compte Discord." - #: app/routes/auth.py:635 msgid "Discord account connected!" msgstr "Compte Discord connecté !" - #: app/routes/auth.py:682 msgid "Discord account connected! Your profile has been pre-filled." msgstr "Compte Discord connecté. Votre profil a été pré-rempli." - #: app/routes/auth.py:710 msgid "You have been logged out." msgstr "Vous avez été déconnecté." - #: app/routes/evaluations.py:53 msgid "You do not have permission to view evaluations." msgstr "Vous n’avez pas les droits pour consulter les évaluations." - #: app/routes/evaluations.py:143 msgid "You do not have permission to evaluate players." msgstr "Vous n’avez pas les droits pour évaluer des joueurs." - #: app/routes/evaluations.py:148 app/routes/evaluations.py:234 msgid "You do not have permission to evaluate players in this tryout." msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection." - #: app/routes/evaluations.py:159 msgid "Player is not registered for this tryout." msgstr "Ce joueur n’est pas inscrit à cette sélection." - #: app/routes/evaluations.py:164 msgid "Can only evaluate players." msgstr "Seuls des joueurs peuvent être évalués." - #: app/routes/evaluations.py:210 msgid "Evaluation submitted successfully!" msgstr "Évaluation enregistrée." - #: app/routes/evaluations.py:212 msgid "Evaluation updated!" msgstr "Évaluation mise à jour." - #: app/routes/evaluations.py:229 app/routes/teams.py:340 #: app/routes/teams.py:383 app/routes/teams.py:426 app/routes/teams.py:454 #: app/routes/teams.py:482 app/routes/teams.py:519 app/routes/tryouts.py:507 @@ -382,317 +297,246 @@ msgstr "Évaluation mise à jour." #: app/routes/tryouts.py:677 msgid "Permission denied." msgstr "Accès refusé." - #: app/routes/main.py:55 msgid "That language is not available." msgstr "Cette langue n’est pas disponible." - #: app/routes/matches.py:383 msgid "You do not have permission to schedule matches for this tryout." msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection." - #: app/routes/matches.py:387 app/routes/matches.py:463 msgid "This tryout has ended. Matches can no longer be created or modified." msgstr "" "Cette sélection est terminée. Les matchs ne peuvent plus être créés ni " "modifiés." - #: app/routes/matches.py:445 msgid "Match scheduled successfully!" msgstr "Match planifié." - #: app/routes/matches.py:459 app/routes/team_matches.py:235 msgid "You do not have permission to edit this match." msgstr "Vous n’avez pas les droits pour modifier ce match." - #: app/routes/matches.py:552 app/routes/team_matches.py:265 msgid "Match updated successfully!" msgstr "Match mis à jour." - #: app/routes/matches.py:588 app/routes/team_matches.py:284 msgid "You do not have permission to delete this match." msgstr "Vous n’avez pas les droits pour supprimer ce match." - #: app/routes/matches.py:591 msgid "This tryout has ended. Matches can no longer be deleted." msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés." - #: app/routes/matches.py:604 app/routes/team_matches.py:289 msgid "Match deleted successfully." msgstr "Match supprimé." - #: app/routes/team_matches.py:128 msgid "You do not have permission to schedule matches for this team." msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe." - #: app/routes/team_matches.py:220 #, python-format msgid "Team match \"%(title)s\" scheduled successfully!" msgstr "Match d’équipe « %(title)s » planifié." - #: app/routes/teams.py:43 msgid "Use My Team(s) to view your teams." msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes." - #: app/routes/teams.py:46 msgid "You do not have permission to view teams." msgstr "Vous n’avez pas les droits pour consulter les équipes." - #: app/routes/teams.py:78 msgid "This page is for players." msgstr "Cette page est réservée aux joueurs." - #: app/routes/teams.py:185 msgid "You do not have permission to create teams." msgstr "Vous n’avez pas les droits pour créer une équipe." - #: app/routes/teams.py:196 app/routes/teams.py:238 #, python-format msgid "Team \"%(name)s\" already exists." msgstr "L’équipe « %(name)s » existe déjà." - #: app/routes/teams.py:217 #, python-format msgid "Team \"%(name)s\" created successfully!" msgstr "Équipe « %(name)s » créée." - #: app/routes/teams.py:227 msgid "You do not have permission to edit this team." msgstr "Vous n’avez pas les droits pour modifier cette équipe." - #: app/routes/teams.py:271 #, python-format msgid "Team \"%(name)s\" updated successfully!" msgstr "Équipe « %(name)s » mise à jour." - #: app/routes/teams.py:299 msgid "You do not have permission to delete teams." msgstr "Vous n’avez pas les droits pour supprimer une équipe." - #: app/routes/teams.py:330 #, python-format msgid "Team \"%(name)s\" deleted successfully." msgstr "Équipe « %(name)s » supprimée." - #: app/routes/teams.py:347 msgid "Please select a coach." msgstr "Veuillez choisir un coach." - #: app/routes/teams.py:352 msgid "Only coaches can be assigned as coach." msgstr "Seuls les coachs peuvent être assignés comme coach." - #: app/routes/teams.py:358 #, python-format msgid "%(username)s is already a coach of %(name)s." msgstr "%(username)s est déjà coach de %(name)s." - #: app/routes/teams.py:371 #, python-format msgid "%(username)s added as coach of %(name)s." msgstr "%(username)s a été ajouté comme coach de %(name)s." - #: app/routes/teams.py:390 msgid "Please select a manager." msgstr "Veuillez choisir un gérant." - #: app/routes/teams.py:395 msgid "Only managers can be assigned as manager." msgstr "Seuls les gérants peuvent être assignés comme gérant." - #: app/routes/teams.py:401 #, python-format msgid "%(username)s is already a manager of %(name)s." msgstr "%(username)s est déjà gérant de %(name)s." - #: app/routes/teams.py:414 #, python-format msgid "%(username)s added as manager of %(name)s." msgstr "%(username)s a été ajouté comme gérant de %(name)s." - #: app/routes/teams.py:444 #, python-format msgid "Coach removed from %(name)s." msgstr "Coach retiré de %(name)s." - #: app/routes/teams.py:472 #, python-format msgid "Manager removed from %(name)s." msgstr "Gérant retiré de %(name)s." - #: app/routes/teams.py:489 app/routes/tryouts.py:568 msgid "Please select a player." msgstr "Veuillez choisir un joueur." - #: app/routes/teams.py:495 msgid "Can only assign players to teams." msgstr "Seuls des joueurs peuvent être assignés à une équipe." - #: app/routes/teams.py:501 #, python-format msgid "%(username)s is already on %(name)s." msgstr "%(username)s fait déjà partie de %(name)s." - #: app/routes/teams.py:509 #, python-format msgid "%(username)s added to %(name)s!" msgstr "%(username)s a été ajouté à %(name)s." - #: app/routes/teams.py:526 app/routes/teams.py:604 #, python-format msgid "%(username)s is not on %(name)s." msgstr "%(username)s ne fait pas partie de %(name)s." - #: app/routes/teams.py:534 #, python-format msgid "%(username)s removed from %(name)s." msgstr "%(username)s a été retiré de %(name)s." - #: app/routes/teams.py:571 app/routes/teams.py:593 msgid "You do not have permission to add notes to this team." msgstr "Vous n’avez pas les droits pour ajouter des notes à cette équipe." - #: app/routes/teams.py:583 msgid "Team notes added successfully!" msgstr "Notes d’équipe ajoutées." - #: app/routes/teams.py:598 app/routes/users/notes.py:222 #: app/routes/users/notes.py:262 msgid "Can only add notes for players." msgstr "Il n’est possible d’ajouter des notes que pour des joueurs." - #: app/routes/teams.py:618 #, python-format msgid "Note added for %(username)s!" msgstr "Note ajoutée pour %(username)s." - #: app/routes/tryouts.py:147 msgid "You do not have permission to create tryouts." msgstr "Vous n’avez pas les droits pour créer une sélection." - #: app/routes/tryouts.py:194 msgid "Tryout created successfully!" msgstr "Sélection créée." - #: app/routes/tryouts.py:211 msgid "You do not have permission to edit this tryout." msgstr "Vous n’avez pas les droits pour modifier cette sélection." - #: app/routes/tryouts.py:215 msgid "This tryout has ended and can no longer be modified." msgstr "Cette sélection est terminée et ne peut plus être modifiée." - #: app/routes/tryouts.py:261 msgid "Tryout updated successfully!" msgstr "Sélection mise à jour." - #: app/routes/tryouts.py:301 msgid "You do not have permission to view this tryout." msgstr "Vous n’avez pas les droits pour consulter cette sélection." - #: app/routes/tryouts.py:469 msgid "Only players can register for tryouts." msgstr "Seuls les joueurs peuvent s’inscrire à une sélection." - #: app/routes/tryouts.py:474 msgid "This tryout is not accepting registrations." msgstr "Cette sélection n’accepte pas d’inscriptions." - #: app/routes/tryouts.py:481 msgid "You are already registered for this tryout." msgstr "Vous êtes déjà inscrit à cette sélection." - #: app/routes/tryouts.py:487 app/routes/tryouts.py:590 msgid "This tryout is full." msgstr "Cette sélection est complète." - #: app/routes/tryouts.py:493 msgid "Successfully registered for tryout!" msgstr "Inscription à la sélection réussie." - #: app/routes/tryouts.py:517 #, python-format msgid "Tryout status updated to %(new_status)s." msgstr "Statut de la sélection mis à jour : %(new_status)s." - #: app/routes/tryouts.py:545 msgid "Registration status updated." msgstr "Statut d’inscription mis à jour." - #: app/routes/tryouts.py:576 msgid "Can only register players." msgstr "Seuls des joueurs peuvent être inscrits." - #: app/routes/tryouts.py:582 #, python-format msgid "%(username)s is already registered for this tryout." msgstr "%(username)s est déjà inscrit à cette sélection." - #: app/routes/tryouts.py:596 #, python-format msgid "%(username)s registered for tryout!" msgstr "%(username)s est inscrit à la sélection." - #: app/routes/tryouts.py:636 #, python-format msgid "%(username)s removed from tryout." msgstr "%(username)s a été retiré de la sélection." - #: app/routes/tryouts.py:662 #, python-format msgid "Team \"%(team_name)s\" created!" msgstr "Équipe « %(team_name)s » créée." - #: app/routes/tryouts.py:699 app/routes/users/notes.py:350 msgid "That player is not registered for this tryout." msgstr "Ce joueur n’est pas inscrit à cette sélection." - #: app/routes/tryouts.py:704 msgid "Player is already on this team." msgstr "Ce joueur est déjà dans cette équipe." - #: app/routes/tryouts.py:709 msgid "Player added to team!" msgstr "Joueur ajouté à l’équipe." - #: app/routes/tryouts.py:723 msgid "You do not have permission to delete this tryout." msgstr "Vous n’avez pas les droits pour supprimer cette sélection." - #: app/routes/tryouts.py:759 msgid "Tryout deleted successfully." msgstr "Sélection supprimée." - -#: app/routes/users/_shared.py:50 +#: app/routes/users/_shared.py:54 msgid "No file selected." msgstr "Aucun fichier sélectionné." - -#: app/routes/users/_shared.py:54 +#: app/routes/users/_shared.py:58 msgid "Only PDF files are allowed for contracts." msgstr "Seuls les fichiers PDF sont acceptés pour les contrats." - -#: app/routes/users/_shared.py:59 +#: app/routes/users/_shared.py:64 app/routes/users/_shared.py:75 +#: app/routes/users/_shared.py:77 msgid "That file is not a PDF, whatever its name says." msgstr "Ce fichier n’est pas un PDF, quel que soit son nom." - #: app/routes/users/accounts.py:55 msgid "Only the president can manage users." msgstr "Seul le président peut gérer les utilisateurs." - #: app/routes/users/accounts.py:70 msgid "Only the president can edit users." msgstr "Seul le président peut modifier des utilisateurs." - #: app/routes/users/accounts.py:111 msgid "Email already in use by another account." msgstr "Cette adresse courriel est déjà utilisée par un autre compte." - #: app/routes/users/accounts.py:138 msgid "You cannot change your own role. Ask another president to do it." msgstr "" "Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre " "président de le faire." - #: app/routes/users/accounts.py:151 msgid "" "This is the last active president. Promote another account before " @@ -700,231 +544,177 @@ msgid "" msgstr "" "C’est le dernier président actif. Promouvez un autre compte avant de " "modifier celui-ci." - #: app/routes/users/accounts.py:228 #, python-format msgid "User %(username)s updated successfully!" msgstr "Utilisateur %(username)s mis à jour." - #: app/routes/users/accounts.py:249 msgid "Only the president can delete users." msgstr "Seul le président peut supprimer des utilisateurs." - #: app/routes/users/accounts.py:253 msgid "You cannot delete your own account." msgstr "Vous ne pouvez pas supprimer votre propre compte." - #: app/routes/users/accounts.py:312 #, python-format msgid "User %(deleted_username)s has been removed." msgstr "L’utilisateur %(deleted_username)s a été supprimé." - #: app/routes/users/accounts.py:323 msgid "Only the president can create users." msgstr "Seul le président peut créer des utilisateurs." - #: app/routes/users/accounts.py:371 #, python-format msgid "User %(full_name)s created as %(role)s!" msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s." - #: app/routes/users/availability.py:210 msgid "Only coaches can manage availability." msgstr "Seuls les coachs peuvent gérer leurs disponibilités." - -#: app/routes/users/contracts.py:91 +#: app/routes/users/contracts.py:92 msgid "Only presidents, managers, and coaches can upload contracts." msgstr "Seuls les présidents, gérants et coachs peuvent téléverser un contrat." - -#: app/routes/users/contracts.py:110 +#: app/routes/users/contracts.py:111 msgid "You do not have permission to upload a contract for this player." msgstr "Vous n’avez pas les droits pour téléverser un contrat pour ce joueur." - -#: app/routes/users/contracts.py:137 app/routes/users/contracts.py:180 -#: app/routes/users/contracts.py:203 app/routes/users/contracts.py:222 +#: app/routes/users/contracts.py:138 app/routes/users/contracts.py:188 +#: app/routes/users/contracts.py:211 app/routes/users/contracts.py:230 msgid "Contract storage is temporarily unavailable. Please try again later." -msgstr "" - -#: app/routes/users/contracts.py:152 +msgstr "Le stockage des contrats est temporairement indisponible. Veuillez r\xc3\xa9essayer plus tard." +#: app/routes/users/contracts.py:153 #, python-format msgid "Contract uploaded successfully for %(username)s!" msgstr "Contrat téléversé pour %(username)s." - -#: app/routes/users/contracts.py:166 +#: app/routes/users/contracts.py:167 msgid "Only the player can upload their signed contract." msgstr "Seul le joueur peut téléverser son contrat signé." - -#: app/routes/users/contracts.py:188 +#: app/routes/users/contracts.py:196 msgid "Signed contract uploaded successfully!" msgstr "Contrat signé téléversé." - -#: app/routes/users/contracts.py:198 app/routes/users/contracts.py:214 +#: app/routes/users/contracts.py:206 app/routes/users/contracts.py:222 msgid "You do not have permission to download this contract." msgstr "Vous n’avez pas les droits pour télécharger ce contrat." - -#: app/routes/users/contracts.py:217 +#: app/routes/users/contracts.py:225 msgid "No signed contract available." msgstr "Aucun contrat signé disponible." - #: app/routes/users/notes.py:43 msgid "This page is for players only." msgstr "Cette page est réservée aux joueurs." - #: app/routes/users/notes.py:80 msgid "Only coaches can access the notes dashboard." msgstr "Seuls les coachs ont accès au tableau des notes." - #: app/routes/users/notes.py:172 msgid "Only coaches can manage team notes." msgstr "Seuls les coachs peuvent gérer les notes d’équipe." - #: app/routes/users/notes.py:178 msgid "You are not assigned to a team." msgstr "Vous n’êtes assigné à aucune équipe." - #: app/routes/users/notes.py:195 msgid "Team notes saved successfully!" msgstr "Notes d’équipe enregistrées." - #: app/routes/users/notes.py:210 msgid "Only coaches can manage personal notes." msgstr "Seuls les coachs peuvent gérer les notes personnelles." - #: app/routes/users/notes.py:226 app/routes/users/notes.py:266 #: app/routes/users/notes.py:346 app/routes/users/notes.py:411 msgid "You can only write notes about players you work with." msgstr "" "Vous ne pouvez écrire des notes que sur les joueurs avec qui vous " "travaillez." - #: app/routes/users/notes.py:236 app/routes/users/notes.py:306 #, python-format msgid "Note added for %(username)s." msgstr "Note ajoutée pour %(username)s." - #: app/routes/users/notes.py:250 app/routes/users/notes.py:320 #: app/routes/users/notes.py:384 msgid "Only coaches can add personal notes." msgstr "Seuls les coachs peuvent ajouter des notes personnelles." - #: app/routes/users/notes.py:272 msgid "You cannot use that match as note context." msgstr "Vous ne pouvez pas utiliser ce match comme contexte de note." - #: app/routes/users/notes.py:275 msgid "That player did not participate in the selected match." msgstr "Ce joueur n’a pas participé au match sélectionné." - #: app/routes/users/notes.py:281 msgid "You cannot use that tryout as note context." msgstr "Vous ne pouvez pas utiliser cette sélection comme contexte de note." - #: app/routes/users/notes.py:284 msgid "That player is not registered for the selected tryout." msgstr "Ce joueur n’est pas inscrit à la sélection choisie." - #: app/routes/users/notes.py:290 msgid "You cannot use that team as note context." msgstr "Vous ne pouvez pas utiliser cette équipe comme contexte de note." - #: app/routes/users/notes.py:293 msgid "That player is not on the selected team." msgstr "Ce joueur ne fait pas partie de l’équipe sélectionnée." - #: app/routes/users/notes.py:325 msgid "You do not have permission to add notes for this tryout." msgstr "Vous n’avez pas les droits pour ajouter des notes à cette sélection." - #: app/routes/users/notes.py:342 msgid "Invalid tryout context." msgstr "Contexte de sélection invalide." - #: app/routes/users/notes.py:361 app/routes/users/notes.py:426 msgid "Note added successfully." msgstr "Note ajoutée." - #: app/routes/users/notes.py:389 msgid "You do not have permission to add notes for this match." msgstr "Vous n’avez pas les droits pour ajouter des notes à ce match." - #: app/routes/users/notes.py:407 msgid "Invalid match context." msgstr "Contexte de match invalide." - #: app/routes/users/notes.py:415 msgid "That player did not participate in this match." msgstr "Ce joueur n’a pas participé à ce match." - #: app/routes/users/one_on_one.py:22 msgid "Only players can request One on One sessions." msgstr "Seuls les joueurs peuvent demander une rencontre individuelle." - #: app/routes/users/one_on_one.py:35 msgid "You do not have a coach assigned to your team." msgstr "Aucun coach n’est assigné à votre équipe." - #: app/routes/users/one_on_one.py:66 msgid "Cannot request One on One - no coach assigned." msgstr "Impossible de demander une rencontre : aucun coach assigné." - #: app/routes/users/one_on_one.py:92 msgid "The requested time is not within the coach's availability." msgstr "L’horaire demandé ne correspond à aucune disponibilité du coach." - #: app/routes/users/one_on_one.py:120 msgid "Your One on One request has been submitted!" msgstr "Votre demande de rencontre a été envoyée." - #: app/routes/users/one_on_one.py:165 msgid "Only coaches can accept One on One requests." msgstr "Seuls les coachs peuvent accepter une demande de rencontre." - #: app/routes/users/one_on_one.py:171 app/routes/users/one_on_one.py:221 msgid "This request is not for you." msgstr "Cette demande ne vous est pas destinée." - #: app/routes/users/one_on_one.py:175 app/routes/users/one_on_one.py:225 msgid "This request has already been processed." msgstr "Cette demande a déjà été traitée." - #: app/routes/users/one_on_one.py:202 #, python-format msgid "One on One request from %(player)s has been approved!" msgstr "La demande de rencontre de %(player)s a été approuvée." - #: app/routes/users/one_on_one.py:215 msgid "Only coaches can reject One on One requests." msgstr "Seuls les coachs peuvent refuser une demande de rencontre." - #: app/routes/users/one_on_one.py:262 #, python-format msgid "One on One request from %(player)s has been rejected." msgstr "La demande de rencontre de %(player)s a été refusée." - #: app/routes/users/profile.py:82 msgid "Username already taken." msgstr "Ce nom d’utilisateur est déjà pris." - #: app/routes/users/profile.py:92 msgid "Email already in use." msgstr "Cette adresse courriel est déjà utilisée." - #: app/routes/users/profile.py:131 msgid "Profile updated successfully!" msgstr "Profil mis à jour." - #: app/templates/errors/400.html:2 msgid "400 Bad Request" msgstr "400 Requête incorrecte" - #: app/templates/errors/400.html:3 msgid "Bad Request" msgstr "Requête incorrecte" - #: app/templates/errors/400.html:9 msgid "400 — Bad Request" msgstr "400 — Requête incorrecte" - #: app/templates/errors/400.html:10 msgid "" "The request could not be understood by the server. Please check your " @@ -932,24 +722,19 @@ msgid "" msgstr "" "Le serveur n’a pas pu interpréter la requête. Vérifiez votre saisie et " "réessayez." - #: app/templates/errors/400.html:12 app/templates/errors/403.html:12 #: app/templates/errors/429.html:12 msgid "Go Back" msgstr "Retour" - #: app/templates/errors/403.html:2 msgid "403 Forbidden" msgstr "403 Accès refusé" - #: app/templates/errors/403.html:3 msgid "Access Denied" msgstr "Accès refusé" - #: app/templates/errors/403.html:9 msgid "403 — Forbidden" msgstr "403 — Accès refusé" - #: app/templates/errors/403.html:10 msgid "" "You do not have permission to access this resource. If you believe this " @@ -957,41 +742,32 @@ msgid "" msgstr "" "Vous n’avez pas les droits d’accès à cette ressource. Si vous pensez " "qu’il s’agit d’une erreur, contactez un administrateur." - #: app/templates/errors/404.html:2 msgid "404 Not Found" msgstr "404 Page introuvable" - #: app/templates/errors/404.html:3 msgid "Page Not Found" msgstr "Page introuvable" - #: app/templates/errors/404.html:9 msgid "404 — Not Found" msgstr "404 — Page introuvable" - #: app/templates/errors/404.html:10 msgid "" "The page you are looking for does not exist. It may have been moved or " "deleted." msgstr "La page demandée n’existe pas. Elle a peut-être été déplacée ou supprimée." - #: app/templates/errors/404.html:12 msgid "Return Home" msgstr "Retour à l’accueil" - #: app/templates/errors/429.html:2 msgid "429 Too Many Requests" msgstr "429 Trop de requêtes" - #: app/templates/errors/429.html:3 msgid "Rate Limit Exceeded" msgstr "Limite de requêtes atteinte" - #: app/templates/errors/429.html:9 msgid "429 — Too Many Requests" msgstr "429 — Trop de requêtes" - #: app/templates/errors/429.html:10 msgid "" "You have sent too many requests in a short period. Please wait a moment " @@ -999,19 +775,15 @@ msgid "" msgstr "" "Vous avez envoyé trop de requêtes en peu de temps. Patientez un instant " "puis réessayez." - #: app/templates/errors/500.html:2 msgid "500 Server Error" msgstr "500 Erreur du serveur" - #: app/templates/errors/500.html:3 msgid "Internal Server Error" msgstr "Erreur interne du serveur" - #: app/templates/errors/500.html:9 msgid "500 — Internal Server Error" msgstr "500 — Erreur interne du serveur" - #: app/templates/errors/500.html:10 msgid "" "Something went wrong on our end. The error has been logged and will be " @@ -1019,124 +791,98 @@ msgid "" msgstr "" "Une erreur est survenue de notre côté. Elle a été journalisée et sera " "examinée. Veuillez réessayer dans un instant." - #: app/templates/errors/500.html:15 msgid "Reference to quote if you report this:" msgstr "Référence à indiquer si vous signalez ce problème :" - #: app/templates/errors/500.html:18 msgid "Try Again" msgstr "Réessayer" - #: app/templates/layouts/_language_switcher.html:10 msgid "Language" msgstr "Langue" - #: app/templates/layouts/_pagination.html:19 msgid "Pagination" msgstr "Pagination" - #: app/templates/layouts/_pagination.html:22 #: app/templates/layouts/_pagination.html:24 msgid "Previous" msgstr "Précédent" - #: app/templates/layouts/_pagination.html:28 #, python-format msgid "Page %(page)s of %(pages)s" msgstr "Page %(page)s sur %(pages)s" - #: app/templates/layouts/_pagination.html:30 #, python-format msgid "%(total)s in total" msgstr "%(total)s au total" - #: app/templates/layouts/_pagination.html:35 #: app/templates/layouts/_pagination.html:37 msgid "Next" msgstr "Suivant" - #: app/templates/layouts/base.html:48 app/templates/layouts/base.html:154 #: app/templates/pages/dashboard.html:2 app/templates/pages/dashboard.html:3 msgid "Dashboard" msgstr "Tableau de bord" - #: app/templates/layouts/base.html:54 app/templates/pages/tryouts.html:2 #: app/templates/pages/tryouts.html:3 msgid "Tryouts" msgstr "Sélections" - #: app/templates/layouts/base.html:60 app/templates/pages/calendar.html:2 #: app/templates/pages/calendar.html:3 msgid "Calendar" msgstr "Calendrier" - #: app/templates/layouts/base.html:67 app/templates/pages/dashboard.html:43 #: app/templates/pages/evaluations.html:3 #: app/templates/pages/evaluations.html:4 msgid "Evaluations" msgstr "Évaluations" - #: app/templates/layouts/base.html:75 app/templates/pages/my_teams.html:2 #: app/templates/pages/my_teams.html:3 msgid "My Team(s)" msgstr "Mon ou mes équipes" - #: app/templates/layouts/base.html:82 msgid "Manage Teams" msgstr "Gestion des équipes" - #: app/templates/layouts/base.html:90 app/templates/pages/users.html:3 #: app/templates/pages/users.html:4 msgid "Manage Users" msgstr "Gestion des utilisateurs" - #: app/templates/layouts/base.html:104 #: app/templates/pages/player_personal_notes.html:2 #: app/templates/pages/player_personal_notes.html:3 msgid "My Notes" msgstr "Mes notes" - #: app/templates/layouts/base.html:112 msgid "Notes & One on One" msgstr "Notes et rencontres individuelles" - #: app/templates/layouts/base.html:119 app/templates/pages/contracts.html:2 #: app/templates/pages/contracts.html:3 app/templates/pages/profile.html:9 msgid "Contracts" msgstr "Contrats" - #: app/templates/layouts/base.html:126 app/templates/pages/profile.html:2 #: app/templates/pages/profile.html:3 msgid "My Profile" msgstr "Mon profil" - #: app/templates/layouts/base.html:137 msgid "Logout" msgstr "Déconnexion" - #: app/templates/layouts/base.html:158 msgid "Toggle dark mode" msgstr "Basculer le mode sombre" - #: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189 msgid "Dismiss" msgstr "Fermer" - #: app/templates/layouts/base.html:199 msgid "UdeS team manager" msgstr "UdeS team manager" - #: app/templates/layouts/macros.html:116 msgid "Close" msgstr "Fermer" - #: app/templates/pages/add_note.html:2 app/templates/pages/add_note.html:3 #: app/templates/pages/add_note.html:22 app/templates/pages/notes.html:38 #: app/templates/pages/personal_notes.html:9 msgid "Add Personal Note" msgstr "Ajouter une note personnelle" - #: app/templates/pages/add_note.html:47 app/templates/pages/contracts.html:25 #: app/templates/pages/dashboard.html:249 #: app/templates/pages/dashboard.html:363 app/templates/pages/my_teams.html:52 @@ -1146,50 +892,39 @@ msgstr "Ajouter une note personnelle" #: app/templates/pages/view_tryout.html:498 msgid "Player" msgstr "Joueur" - #: app/templates/pages/add_note.html:49 app/templates/pages/teams.html:216 #: app/templates/pages/upload_contract.html:18 #: app/templates/pages/view_tryout.html:130 msgid "-- Select a player --" msgstr "-- Choisir un joueur --" - #: app/templates/pages/add_note.html:59 app/templates/pages/notes.html:58 #: app/templates/pages/personal_notes.html:29 msgid "Note Content" msgstr "Contenu de la note" - #: app/templates/pages/add_note.html:60 msgid "Enter your coaching notes..." msgstr "Vos notes de coaching..." - #: app/templates/pages/add_note.html:65 msgid "Link to Tryout (Optional)" msgstr "Lier à une sélection (facultatif)" - #: app/templates/pages/add_note.html:67 msgid "-- No tryout --" msgstr "-- Aucune sélection --" - #: app/templates/pages/add_note.html:75 msgid "Link to Match (Optional)" msgstr "Lier à un match (facultatif)" - #: app/templates/pages/add_note.html:77 msgid "-- No match --" msgstr "-- Aucun match --" - #: app/templates/pages/add_note.html:85 msgid "Link to Team (Optional)" msgstr "Lier à une équipe (facultatif)" - #: app/templates/pages/add_note.html:87 msgid "-- No team --" msgstr "-- Aucune équipe --" - #: app/templates/pages/add_note.html:97 msgid "Team Notes (Reference)" msgstr "Notes d’équipe (référence)" - #: app/templates/pages/add_note.html:120 #: app/templates/pages/evaluate_player.html:131 #: app/templates/pages/notes.html:100 @@ -1197,146 +932,113 @@ msgstr "Notes d’équipe (référence)" #: app/templates/pages/view_tryout.html:309 msgid "Add Note" msgstr "Ajouter une note" - #: app/templates/pages/calendar.html:9 app/templates/pages/view_tryout.html:302 msgid "Schedule" msgstr "Planifier" - #: app/templates/pages/calendar.html:13 msgid "Month" msgstr "Mois" - #: app/templates/pages/calendar.html:16 msgid "Week" msgstr "Semaine" - #: app/templates/pages/calendar.html:19 msgid "Day" msgstr "Jour" - #: app/templates/pages/calendar.html:22 msgid "List" msgstr "Liste" - #: app/templates/pages/calendar.html:37 msgid "Create New Event" msgstr "Créer un événement" - #: app/templates/pages/calendar.html:41 msgid "Date:" msgstr "Date :" - #: app/templates/pages/calendar.html:46 msgid "Schedule Tryout Match" msgstr "Planifier un match de sélection" - #: app/templates/pages/calendar.html:47 msgid "Add a scrim/match inside an existing tryout" msgstr "Ajouter un scrim ou un match dans une sélection existante" - #: app/templates/pages/calendar.html:50 msgid "-- Select a tryout --" msgstr "-- Choisir une sélection --" - #: app/templates/pages/calendar.html:53 app/templates/pages/calendar.html:68 msgid "Go" msgstr "Aller" - #: app/templates/pages/calendar.html:61 app/templates/pages/teams.html:131 msgid "Schedule Team Match" msgstr "Planifier un match d’équipe" - #: app/templates/pages/calendar.html:62 msgid "Regular season match for an org team" msgstr "Match de saison régulière pour une équipe du club" - #: app/templates/pages/calendar.html:65 msgid "-- Select a team --" msgstr "-- Choisir une équipe --" - #: app/templates/pages/calendar.html:76 msgid "Create New Tryout" msgstr "Créer une sélection" - #: app/templates/pages/calendar.html:77 msgid "Create a brand new tryout event" msgstr "Créer une nouvelle sélection" - #: app/templates/pages/calendar.html:79 msgid "Create Tryout" msgstr "Créer la sélection" - #: app/templates/pages/calendar.html:92 msgid "Event Details" msgstr "Détail de l’événement" - #: app/templates/pages/calendar.html:98 msgid "Confirm" msgstr "Confirmer" - #: app/templates/pages/calendar.html:102 #: app/templates/pages/team_matches.html:103 msgid "Delete Match" msgstr "Supprimer le match" - #: app/templates/pages/calendar.html:105 #: app/templates/pages/team_matches.html:98 msgid "Edit Match" msgstr "Modifier le match" - #: app/templates/pages/calendar.html:108 msgid "View Tryout" msgstr "Voir la sélection" - #: app/templates/pages/coach_availability.html:2 #: app/templates/pages/coach_availability.html:3 msgid "Manage Availability" msgstr "Gérer mes disponibilités" - #: app/templates/pages/coach_availability.html:9 msgid "Set Your Weekly Availability" msgstr "Définir vos disponibilités hebdomadaires" - #: app/templates/pages/coach_availability.html:10 msgid "Select time slots when you're available for One on One sessions" msgstr "" "Choisissez les plages où vous êtes disponible pour des rencontres " "individuelles" - #: app/templates/pages/coach_availability.html:14 #: app/templates/pages/profile.html:256 msgid "Loading availability grid..." msgstr "Chargement de la grille de disponibilités..." - #: app/templates/pages/coach_availability.html:19 msgid "Save Availability" msgstr "Enregistrer les disponibilités" - #: app/templates/pages/coach_availability.html:22 #: app/templates/pages/profile.html:240 app/templates/pages/profile.html:260 msgid "Clear All" msgstr "Tout effacer" - #: app/templates/pages/contracts.html:9 #: app/templates/pages/upload_contract.html:2 #: app/templates/pages/upload_contract.html:3 #: app/templates/pages/upload_contract.html:39 msgid "Upload Contract" msgstr "Téléverser un contrat" - #: app/templates/pages/contracts.html:17 msgid "Contract Dropbox" msgstr "Dépôt de contrats" - #: app/templates/pages/contracts.html:26 app/templates/pages/notes.html:87 #: app/templates/pages/team_matches.html:27 msgid "Team" msgstr "Équipe" - #: app/templates/pages/contracts.html:27 msgid "Contract" msgstr "Contrat" - #: app/templates/pages/contracts.html:28 app/templates/pages/dashboard.html:95 #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:175 @@ -1350,11 +1052,9 @@ msgstr "Contrat" #: app/templates/pages/view_tryout.html:327 msgid "Status" msgstr "Statut" - #: app/templates/pages/contracts.html:29 msgid "Uploaded" msgstr "Téléversé le" - #: app/templates/pages/contracts.html:30 app/templates/pages/dashboard.html:175 #: app/templates/pages/notes.html:127 app/templates/pages/team_matches.html:34 #: app/templates/pages/teams.html:151 app/templates/pages/users.html:26 @@ -1362,32 +1062,25 @@ msgstr "Téléversé le" #: app/templates/pages/view_tryout.html:329 msgid "Actions" msgstr "Actions" - #: app/templates/pages/contracts.html:49 app/templates/pages/contracts.html:50 msgid "Download" msgstr "Télécharger" - #: app/templates/pages/contracts.html:53 msgid "Download Signed" msgstr "Télécharger la version signée" - #: app/templates/pages/contracts.html:54 msgid "Signed" msgstr "Signé" - #: app/templates/pages/contracts.html:58 app/templates/pages/contracts.html:89 #: app/templates/pages/contracts.html:102 msgid "Upload Signed Contract" msgstr "Téléverser le contrat signé" - #: app/templates/pages/contracts.html:59 msgid "Return Signed" msgstr "Retourner signé" - #: app/templates/pages/contracts.html:72 msgid "No contracts found" msgstr "Aucun contrat" - #: app/templates/pages/contracts.html:74 msgid "" "No contracts have been uploaded for you yet. Contact your coach or " @@ -1395,132 +1088,105 @@ msgid "" msgstr "" "Aucun contrat n’a encore été téléversé pour vous. Contactez votre coach " "ou votre gérant." - #: app/templates/pages/contracts.html:76 msgid "" "No contracts have been uploaded yet. Upload a contract using the button " "above." msgstr "Aucun contrat n’a encore été téléversé. Utilisez le bouton ci-dessus." - #: app/templates/pages/contracts.html:96 msgid "Select Signed Contract File" msgstr "Choisir le fichier du contrat signé" - #: app/templates/pages/contracts.html:98 #: app/templates/pages/upload_contract.html:28 -msgid "Accepted formats: PDF, DOC, DOCX, JPG, PNG" -msgstr "Formats acceptés : PDF, DOC, DOCX, JPG, PNG" - +msgid "Accepted format: PDF" +msgstr "Format accepté : PDF" #: app/templates/pages/contracts.html:101 #: app/templates/pages/match_form.html:265 app/templates/pages/notes.html:189 #: app/templates/pages/teams.html:48 app/templates/pages/teams.html:295 #: app/templates/pages/view_tryout.html:245 msgid "Cancel" msgstr "Annuler" - #: app/templates/pages/create_user.html:2 #: app/templates/pages/create_user.html:3 #: app/templates/pages/create_user.html:47 msgid "Create User" msgstr "Créer l’utilisateur" - #: app/templates/pages/create_user.html:13 #: app/templates/pages/edit_profile.html:17 #: app/templates/pages/edit_user.html:13 app/templates/pages/register.html:10 msgid "Full Name" msgstr "Nom complet" - #: app/templates/pages/create_user.html:14 msgid "Enter full name" msgstr "Saisir le nom complet" - #: app/templates/pages/create_user.html:17 #: app/templates/pages/edit_profile.html:13 app/templates/pages/login.html:7 #: app/templates/pages/register.html:15 app/templates/pages/users.html:21 msgid "Username" msgstr "Nom d’utilisateur" - #: app/templates/pages/create_user.html:18 msgid "Choose username" msgstr "Choisir un nom d’utilisateur" - #: app/templates/pages/create_user.html:23 #: app/templates/pages/edit_profile.html:23 #: app/templates/pages/edit_user.html:17 app/templates/pages/register.html:20 #: app/templates/pages/teams.html:148 app/templates/pages/users.html:22 msgid "Email" msgstr "Courriel" - #: app/templates/pages/create_user.html:24 msgid "Enter email" msgstr "Saisir le courriel" - #: app/templates/pages/create_user.html:27 #: app/templates/pages/edit_profile.html:27 #: app/templates/pages/edit_user.html:23 app/templates/pages/teams.html:149 msgid "Phone" msgstr "Téléphone" - #: app/templates/pages/create_user.html:28 msgid "Phone number" msgstr "Numéro de téléphone" - #: app/templates/pages/create_user.html:31 #: app/templates/pages/dashboard.html:74 app/templates/pages/edit_user.html:27 #: app/templates/pages/users.html:23 msgid "Role" msgstr "Rôle" - #: app/templates/pages/create_user.html:41 app/templates/pages/login.html:11 #: app/templates/pages/register.html:137 msgid "Password" msgstr "Mot de passe" - #: app/templates/pages/create_user.html:42 msgid "Set password" msgstr "Définir le mot de passe" - #: app/templates/pages/dashboard.html:16 msgid "Total Users" msgstr "Total des utilisateurs" - #: app/templates/pages/dashboard.html:25 app/templates/pages/dashboard.html:175 msgid "Players" msgstr "Joueurs" - #: app/templates/pages/dashboard.html:34 msgid "Total Tryouts" msgstr "Total des sélections" - #: app/templates/pages/dashboard.html:52 msgid "Active Tryouts" msgstr "Sélections en cours" - #: app/templates/pages/dashboard.html:61 #: app/templates/pages/view_tryout.html:21 msgid "Upcoming" msgstr "À venir" - #: app/templates/pages/dashboard.html:69 msgid "Recent Users" msgstr "Utilisateurs récents" - #: app/templates/pages/dashboard.html:74 app/templates/pages/users.html:20 msgid "Name" msgstr "Nom" - #: app/templates/pages/dashboard.html:74 app/templates/pages/users.html:25 msgid "Joined" msgstr "Inscrit le" - #: app/templates/pages/dashboard.html:90 msgid "Recent Tryouts" msgstr "Sélections récentes" - #: app/templates/pages/dashboard.html:95 app/templates/pages/dashboard.html:175 msgid "Title" msgstr "Titre" - #: app/templates/pages/dashboard.html:95 app/templates/pages/dashboard.html:175 #: app/templates/pages/dashboard.html:249 #: app/templates/pages/dashboard.html:321 @@ -1531,12 +1197,10 @@ msgstr "Titre" #: app/templates/pages/view_tryout.html:323 msgid "Date" msgstr "Date" - #: app/templates/pages/dashboard.html:114 #: app/templates/pages/dashboard.html:198 app/templates/pages/my_teams.html:84 msgid "Upcoming Matches" msgstr "Matchs à venir" - #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:90 @@ -1544,7 +1208,6 @@ msgstr "Matchs à venir" #: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:321 msgid "Match" msgstr "Match" - #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:249 @@ -1552,112 +1215,88 @@ msgstr "Match" #: app/templates/pages/dashboard.html:321 app/templates/pages/notes.html:78 msgid "Tryout" msgstr "Sélection" - #: app/templates/pages/dashboard.html:118 #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:285 msgid "Date & Time" msgstr "Date et heure" - #: app/templates/pages/dashboard.html:145 #: app/templates/pages/dashboard.html:169 #: app/templates/pages/dashboard.html:273 msgid "My Tryouts" msgstr "Mes sélections" - #: app/templates/pages/dashboard.html:154 msgid "Active" msgstr "Actif" - #: app/templates/pages/dashboard.html:163 msgid "My Evaluations" msgstr "Mes évaluations" - #: app/templates/pages/dashboard.html:229 msgid "Evaluations Done" msgstr "Évaluations réalisées" - #: app/templates/pages/dashboard.html:238 #: app/templates/pages/one_on_one.html:85 msgid "Pending" msgstr "En attente" - #: app/templates/pages/dashboard.html:244 msgid "Recent Evaluations" msgstr "Évaluations récentes" - #: app/templates/pages/dashboard.html:249 msgid "Score" msgstr "Note" - #: app/templates/pages/dashboard.html:280 msgid "My Next Matches" msgstr "Mes prochains matchs" - #: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:91 #: app/templates/pages/team_matches.html:28 msgid "Opponent" msgstr "Adversaire" - #: app/templates/pages/dashboard.html:311 msgid "No upcoming matches yet." msgstr "Aucun match à venir pour l’instant." - #: app/templates/pages/dashboard.html:317 msgid "My Registrations" msgstr "Mes inscriptions" - #: app/templates/pages/dashboard.html:344 msgid "Total Players" msgstr "Total des joueurs" - #: app/templates/pages/dashboard.html:353 msgid "Total Evaluations" msgstr "Total des évaluations" - #: app/templates/pages/dashboard.html:359 msgid "Top Rated Players" msgstr "Joueurs les mieux notés" - #: app/templates/pages/dashboard.html:363 msgid "Avg Score" msgstr "Note moyenne" - #: app/templates/pages/dashboard.html:373 #: app/templates/pages/view_tryout.html:532 msgid "No evaluations yet." msgstr "Aucune évaluation pour l’instant." - #: app/templates/pages/edit_profile.html:2 #: app/templates/pages/edit_profile.html:3 app/templates/pages/profile.html:12 msgid "Edit Profile" msgstr "Modifier le profil" - #: app/templates/pages/edit_profile.html:33 #: app/templates/pages/edit_user.html:43 app/templates/pages/profile.html:63 #: app/templates/pages/register.html:81 msgid "E-Sports Profile" msgstr "Profil e-sport" - #: app/templates/pages/edit_profile.html:34 msgid "Update your competitive gaming profile for tryouts." msgstr "Mettez à jour votre profil de joueur compétitif pour les sélections." - #: app/templates/pages/edit_profile.html:37 #: app/templates/pages/register.html:85 msgid "Games You Play" msgstr "Jeux auxquels vous jouez" - #: app/templates/pages/edit_profile.html:47 #: app/templates/pages/register.html:99 msgid "Select all games you're signing in for." msgstr "Sélectionnez tous les jeux pour lesquels vous vous inscrivez." - #: app/templates/pages/edit_profile.html:53 #: app/templates/pages/edit_user.html:61 msgid "Gamertags for TRN" msgstr "Pseudos pour Tracker Network" - #: app/templates/pages/edit_profile.html:54 msgid "" "Enter your gamertag for each selected game to link to your Tracker " @@ -1665,112 +1304,88 @@ msgid "" msgstr "" "Saisissez votre pseudo pour chaque jeu sélectionné afin de le lier à " "votre profil Tracker Network." - #: app/templates/pages/edit_profile.html:62 #: app/templates/pages/edit_user.html:70 app/templates/pages/edit_user.html:71 #: app/templates/pages/view_user.html:58 msgid "Gamertag" msgstr "Pseudo de jeu" - #: app/templates/pages/edit_profile.html:67 #: app/templates/pages/edit_user.html:75 app/templates/pages/view_user.html:59 msgid "Platform" msgstr "Plateforme" - #: app/templates/pages/edit_profile.html:69 #: app/templates/pages/edit_user.html:77 msgid "Select Platform" msgstr "Choisir une plateforme" - #: app/templates/pages/edit_profile.html:83 #: app/templates/pages/edit_user.html:91 msgid "Discord Username" msgstr "Nom d’utilisateur Discord" - #: app/templates/pages/edit_profile.html:84 #: app/templates/pages/edit_user.html:92 msgid "e.g. Name#1234" msgstr "ex. : Nom#1234" - #: app/templates/pages/edit_profile.html:87 #: app/templates/pages/register.html:57 msgid "Reconnect" msgstr "Reconnecter" - #: app/templates/pages/edit_profile.html:87 #: app/templates/pages/register.html:67 msgid "Connect Discord Account" msgstr "Connecter un compte Discord" - #: app/templates/pages/edit_profile.html:93 #: app/templates/pages/edit_user.html:100 msgid "League OS Connection" msgstr "Connexion League OS" - #: app/templates/pages/edit_profile.html:94 #: app/templates/pages/edit_user.html:101 app/templates/pages/register.html:129 msgid "League OS profile link or ID" msgstr "Lien ou identifiant du profil League OS" - #: app/templates/pages/edit_profile.html:99 msgid "Change Password" msgstr "Changer le mot de passe" - #: app/templates/pages/edit_profile.html:100 msgid "Leave blank to keep your current password." msgstr "Laissez vide pour conserver votre mot de passe actuel." - #: app/templates/pages/edit_profile.html:102 msgid "New Password" msgstr "Nouveau mot de passe" - #: app/templates/pages/edit_profile.html:103 #: app/templates/pages/edit_user.html:107 msgid "Enter new password" msgstr "Saisir le nouveau mot de passe" - #: app/templates/pages/edit_profile.html:108 app/templates/pages/teams.html:296 msgid "Save Changes" msgstr "Enregistrer les modifications" - #: app/templates/pages/edit_user.html:3 msgid "Edit User" msgstr "Modifier l’utilisateur" - #: app/templates/pages/edit_user.html:46 msgid "Games" msgstr "Jeux" - #: app/templates/pages/edit_user.html:62 msgid "Enter gamertag for each selected game to link to Tracker Network." msgstr "Saisissez un pseudo par jeu sélectionné pour le lier à Tracker Network." - #: app/templates/pages/edit_user.html:95 msgid "(for DMs)" msgstr "(pour les messages privés)" - #: app/templates/pages/edit_user.html:96 msgid "Numeric ID (e.g. 123456789012345678)" msgstr "Identifiant numérique (ex. : 123456789012345678)" - #: app/templates/pages/edit_user.html:97 msgid "Enable Developer Mode in Discord → Right-click profile → Copy ID" msgstr "" "Activez le mode développeur dans Discord → clic droit sur le profil → " "Copier l’identifiant" - #: app/templates/pages/edit_user.html:106 msgid "(leave blank to keep current)" msgstr "(laisser vide pour conserver l’actuel)" - #: app/templates/pages/edit_user.html:111 msgid "Update User" msgstr "Mettre à jour l’utilisateur" - #: app/templates/pages/evaluate_player.html:10 msgid "Player Evaluation" msgstr "Évaluation du joueur" - #: app/templates/pages/evaluate_player.html:24 msgid "" "You have already evaluated this player. Your previous scores are shown " @@ -1778,213 +1393,166 @@ msgid "" msgstr "" "Vous avez déjà évalué ce joueur. Vos notes précédentes sont affichées ci-" "dessous." - #: app/templates/pages/evaluate_player.html:33 msgid "Mecanics (1-10)" msgstr "Mécaniques (1-10)" - #: app/templates/pages/evaluate_player.html:40 msgid "Cohesion (1-10)" msgstr "Cohésion (1-10)" - #: app/templates/pages/evaluate_player.html:47 msgid "Communication (1-10)" msgstr "Communication (1-10)" - #: app/templates/pages/evaluate_player.html:57 msgid "Gamesense (1-10)" msgstr "Sens du jeu (1-10)" - #: app/templates/pages/evaluate_player.html:64 msgid "Versatility (1-10)" msgstr "Polyvalence (1-10)" - #: app/templates/pages/evaluate_player.html:71 msgid "Discipline (1-10)" msgstr "Discipline (1-10)" - #: app/templates/pages/evaluate_player.html:81 msgid "Analysis (1-10)" msgstr "Analyse (1-10)" - #: app/templates/pages/evaluate_player.html:88 msgid "Sport Ethics (1-10)" msgstr "Éthique sportive (1-10)" - #: app/templates/pages/evaluate_player.html:95 msgid "Mental (1-10)" msgstr "Mental (1-10)" - #: app/templates/pages/evaluate_player.html:106 msgid "Recommended Position" msgstr "Poste recommandé" - #: app/templates/pages/evaluate_player.html:109 #: app/templates/pages/view_tryout.html:280 msgid "-- Select Position --" msgstr "-- Choisir un poste --" - #: app/templates/pages/evaluate_player.html:115 msgid "Enter position (optional)" msgstr "Saisir un poste (facultatif)" - #: app/templates/pages/evaluate_player.html:121 msgid "Comments" msgstr "Commentaires" - #: app/templates/pages/evaluate_player.html:122 msgid "Enter your evaluation notes..." msgstr "Vos notes d’évaluation..." - #: app/templates/pages/evaluate_player.html:130 msgid "Add note for this player" msgstr "Ajouter une note pour ce joueur" - #: app/templates/pages/evaluate_player.html:147 #: app/templates/pages/view_tryout.html:499 msgid "Evaluator" msgstr "Évaluateur" - #: app/templates/pages/evaluate_player.html:148 #: app/templates/pages/view_tryout.html:500 msgid "Mecanics" msgstr "Mécaniques" - #: app/templates/pages/evaluate_player.html:149 #: app/templates/pages/view_tryout.html:501 msgid "Cohesion" msgstr "Cohésion" - #: app/templates/pages/evaluate_player.html:150 #: app/templates/pages/view_tryout.html:502 msgid "Communication" msgstr "Communication" - #: app/templates/pages/evaluate_player.html:151 #: app/templates/pages/view_tryout.html:503 msgid "Gamesense" msgstr "Sens du jeu" - #: app/templates/pages/evaluate_player.html:152 #: app/templates/pages/view_tryout.html:504 msgid "Versatility" msgstr "Polyvalence" - #: app/templates/pages/evaluate_player.html:153 #: app/templates/pages/view_tryout.html:505 msgid "Discipline" msgstr "Discipline" - #: app/templates/pages/evaluate_player.html:154 #: app/templates/pages/view_tryout.html:506 msgid "Analysis" msgstr "Analyse" - #: app/templates/pages/evaluate_player.html:155 #: app/templates/pages/view_tryout.html:507 msgid "Sport Ethics" msgstr "Éthique sportive" - #: app/templates/pages/evaluate_player.html:156 #: app/templates/pages/view_tryout.html:508 msgid "Mental" msgstr "Mental" - #: app/templates/pages/evaluate_player.html:157 #: app/templates/pages/view_tryout.html:509 msgid "Overall" msgstr "Global" - #: app/templates/pages/evaluate_player.html:158 #: app/templates/pages/my_teams.html:54 #: app/templates/pages/view_tryout.html:286 msgid "Position" msgstr "Poste" - #: app/templates/pages/evaluations.html:97 msgid "No evaluations yet" msgstr "Aucune évaluation" - #: app/templates/pages/login.html:2 msgid "Login" msgstr "Connexion" - #: app/templates/pages/login.html:8 msgid "Enter your username" msgstr "Saisissez votre nom d’utilisateur" - #: app/templates/pages/login.html:12 msgid "Enter your password" msgstr "Saisissez votre mot de passe" - #: app/templates/pages/login.html:14 msgid "Sign In" msgstr "Se connecter" - #: app/templates/pages/login.html:15 msgid "Don't have an account?" msgstr "Vous n’avez pas de compte ?" - #: app/templates/pages/login.html:15 msgid "Register here" msgstr "Inscrivez-vous ici" - #: app/templates/pages/match_form.html:34 msgid "Match Type" msgstr "Type de match" - #: app/templates/pages/match_form.html:36 msgid "Team vs Team" msgstr "Équipe contre équipe" - #: app/templates/pages/match_form.html:37 msgid "Player vs Player" msgstr "Joueur contre joueur" - #: app/templates/pages/match_form.html:38 msgid "Player Scrim" msgstr "Scrim entre joueurs" - #: app/templates/pages/match_form.html:48 #: app/templates/pages/team_match_form.html:17 msgid "Match Title" msgstr "Titre du match" - #: app/templates/pages/match_form.html:49 msgid "e.g., Alpha vs Bravo Scrimmage" msgstr "ex. : Scrim Alpha contre Bravo" - #: app/templates/pages/match_form.html:62 #: app/templates/pages/team_match_form.html:63 msgid "Scheduled" msgstr "Planifié" - #: app/templates/pages/match_form.html:63 #: app/templates/pages/team_match_form.html:64 #: app/templates/pages/view_tryout.html:23 msgid "Completed" msgstr "Terminé" - #: app/templates/pages/match_form.html:64 #: app/templates/pages/team_match_form.html:65 msgid "Cancelled" msgstr "Annulé" - #: app/templates/pages/match_form.html:69 app/templates/pages/my_teams.html:94 #: app/templates/pages/team_match_form.html:54 #: app/templates/pages/team_matches.html:31 #: app/templates/pages/tryout_form.html:49 msgid "Location" msgstr "Lieu" - #: app/templates/pages/match_form.html:70 msgid "Match location" msgstr "Lieu du match" - #: app/templates/pages/match_form.html:77 msgid "Select Match Time" msgstr "Choisir l’heure du match" - #: app/templates/pages/match_form.html:79 msgid "" "Click time slots consecutively to set match duration. Players available " @@ -1993,91 +1561,72 @@ msgstr "" "Cliquez des plages consécutives pour définir la durée du match. Les " "joueurs disponibles sur toutes les plages choisies apparaissent ci-" "dessous." - #: app/templates/pages/match_form.html:80 msgid "Reset time selection" msgstr "Réinitialiser la plage horaire" - #: app/templates/pages/match_form.html:81 msgid "Reset Time" msgstr "Réinitialiser l’heure" - #: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:236 msgid "Loading..." msgstr "Chargement..." - #: app/templates/pages/match_form.html:103 #: app/templates/pages/one_on_one.html:140 #: app/templates/pages/team_match_form.html:40 msgid "Start Time" msgstr "Heure de début" - #: app/templates/pages/match_form.html:107 #: app/templates/pages/one_on_one.html:146 #: app/templates/pages/team_match_form.html:45 msgid "End Time" msgstr "Heure de fin" - #: app/templates/pages/match_form.html:114 #: app/templates/pages/tryout_form.html:110 msgid "Description" msgstr "Description" - #: app/templates/pages/match_form.html:115 msgid "Optional notes about this match" msgstr "Notes facultatives sur ce match" - #: app/templates/pages/match_form.html:121 msgid "Select Teams" msgstr "Choisir les équipes" - #: app/templates/pages/match_form.html:125 #: app/templates/pages/match_form.html:219 msgid "Team 1" msgstr "Équipe 1" - #: app/templates/pages/match_form.html:127 msgid "-- Select Team 1 --" msgstr "-- Choisir l’équipe 1 --" - #: app/templates/pages/match_form.html:134 #: app/templates/pages/match_form.html:231 msgid "Team 2" msgstr "Équipe 2" - #: app/templates/pages/match_form.html:136 msgid "-- Select Team 2 --" msgstr "-- Choisir l’équipe 2 --" - #: app/templates/pages/match_form.html:158 #: app/templates/pages/match_form.html:184 msgid "Click to toggle presence" msgstr "Cliquer pour basculer la présence" - #: app/templates/pages/match_form.html:164 #: app/templates/pages/match_form.html:190 app/templates/pages/teams.html:197 msgid "No players assigned" msgstr "Aucun joueur assigné" - #: app/templates/pages/match_form.html:201 #: app/templates/pages/match_form.html:243 msgid "Select Players" msgstr "Choisir des joueurs" - #: app/templates/pages/match_form.html:206 msgid "Randomize Teams:" msgstr "Répartition aléatoire :" - #: app/templates/pages/match_form.html:211 msgid "Randomize" msgstr "Répartir au hasard" - #: app/templates/pages/match_form.html:214 msgid "Select players then click Randomize to split them into teams." msgstr "" "Choisissez des joueurs puis cliquez sur « Répartir au hasard » pour " "former les équipes." - #: app/templates/pages/match_form.html:226 msgid "" "All registered players are shown. Click time slots to filter available " @@ -2085,125 +1634,98 @@ msgid "" msgstr "" "Tous les joueurs inscrits sont affichés. Cliquez des plages horaires pour" " filtrer ceux qui sont disponibles." - #: app/templates/pages/match_form.html:246 msgid "Green indicators show player availability for the match date/time" msgstr "" "Les indicateurs verts signalent les joueurs disponibles à la date et à " "l’heure du match" - #: app/templates/pages/match_form.html:632 msgid "Click time slots consecutively to set match duration" msgstr "Cliquez des plages consécutives pour définir la durée du match" - #: app/templates/pages/match_form.html:899 msgid "No players registered" msgstr "Aucun joueur inscrit" - #: app/templates/pages/match_form.html:932 msgid "T1" msgstr "É1" - #: app/templates/pages/match_form.html:933 msgid "T2" msgstr "É2" - #: app/templates/pages/match_form.html:938 msgid "No players available" msgstr "Aucun joueur disponible" - #: app/templates/pages/my_teams.html:18 app/templates/pages/teams.html:84 #: app/templates/pages/teams.html:273 msgid "Coaches" msgstr "Coachs" - #: app/templates/pages/my_teams.html:30 app/templates/pages/teams.html:106 #: app/templates/pages/teams.html:284 msgid "Managers" msgstr "Gérants" - #: app/templates/pages/my_teams.html:45 msgid "Team Roster" msgstr "Alignement" - #: app/templates/pages/my_teams.html:79 msgid "No players on this team." msgstr "Aucun joueur dans cette équipe." - #: app/templates/pages/my_teams.html:93 app/templates/pages/notes.html:124 #: app/templates/pages/one_on_one.html:71 #: app/templates/pages/team_matches.html:30 #: app/templates/pages/view_tryout.html:325 msgid "Time" msgstr "Heure" - #: app/templates/pages/my_teams.html:95 #: app/templates/pages/team_match_form.html:115 #: app/templates/pages/team_matches.html:32 #: app/templates/pages/view_tryout.html:326 msgid "Presence" msgstr "Présence" - #: app/templates/pages/my_teams.html:96 msgid "My Status" msgstr "Mon statut" - #: app/templates/pages/my_teams.html:154 msgid "No upcoming matches scheduled." msgstr "Aucun match à venir." - #: app/templates/pages/my_teams.html:165 msgid "No Teams" msgstr "Aucune équipe" - #: app/templates/pages/my_teams.html:166 msgid "You are not currently assigned to any team." msgstr "Vous n’êtes assigné à aucune équipe pour le moment." - #: app/templates/pages/notes.html:2 app/templates/pages/notes.html:3 #: app/templates/pages/teams.html:75 msgid "Notes" msgstr "Notes" - #: app/templates/pages/notes.html:12 app/templates/pages/one_on_one.html:11 #: app/templates/pages/player_personal_notes.html:55 #: app/templates/pages/team_notes.html:2 app/templates/pages/team_notes.html:3 msgid "Team Notes" msgstr "Notes d’équipe" - #: app/templates/pages/notes.html:20 app/templates/pages/team_notes.html:19 msgid "Team Notes Content" msgstr "Contenu des notes d’équipe" - #: app/templates/pages/notes.html:21 app/templates/pages/team_notes.html:20 msgid "Enter improvement suggestions and notes for your team..." msgstr "Suggestions d’amélioration et notes pour votre équipe..." - #: app/templates/pages/notes.html:22 app/templates/pages/team_notes.html:21 msgid "These notes will be visible to all players on your team." msgstr "Ces notes seront visibles par tous les joueurs de votre équipe." - #: app/templates/pages/notes.html:48 app/templates/pages/personal_notes.html:19 #: app/templates/pages/upload_contract.html:16 msgid "Select Player" msgstr "Choisir un joueur" - #: app/templates/pages/notes.html:50 app/templates/pages/personal_notes.html:21 msgid "-- Select a Player --" msgstr "-- Choisir un joueur --" - #: app/templates/pages/notes.html:59 app/templates/pages/personal_notes.html:30 msgid "Enter personal feedback or coaching tips for this player..." msgstr "Commentaires ou conseils personnalisés pour ce joueur..." - #: app/templates/pages/notes.html:60 app/templates/pages/personal_notes.html:31 msgid "These notes will only be visible to the selected player." msgstr "Ces notes ne seront visibles que par le joueur sélectionné." - #: app/templates/pages/notes.html:64 msgid "Context (Optional)" msgstr "Contexte (facultatif)" - #: app/templates/pages/notes.html:65 msgid "" "Link this note to a specific match, tryout, or team for better " @@ -2211,90 +1733,69 @@ msgid "" msgstr "" "Liez cette note à un match, une sélection ou une équipe pour mieux vous y" " retrouver." - #: app/templates/pages/notes.html:71 msgid "-- Select Match --" msgstr "-- Choisir un match --" - #: app/templates/pages/notes.html:80 msgid "-- Select Tryout --" msgstr "-- Choisir une sélection --" - #: app/templates/pages/notes.html:89 msgid "-- Select Team --" msgstr "-- Choisir une équipe --" - #: app/templates/pages/notes.html:111 msgid "One on One Requests" msgstr "Demandes de rencontre individuelle" - #: app/templates/pages/notes.html:125 app/templates/pages/one_on_one.html:72 msgid "Discussion Points" msgstr "Points à aborder" - #: app/templates/pages/notes.html:150 app/templates/pages/notes.html:151 msgid "Accept" msgstr "Accepter" - #: app/templates/pages/notes.html:154 app/templates/pages/notes.html:155 msgid "Refuse" msgstr "Refuser" - #: app/templates/pages/notes.html:167 msgid "No One on One requests from your players yet." msgstr "Aucune demande de rencontre de vos joueurs pour l’instant." - #: app/templates/pages/notes.html:177 msgid "Reject One on One Request" msgstr "Refuser la demande de rencontre" - #: app/templates/pages/notes.html:184 msgid "Reason for rejection (optional):" msgstr "Motif du refus (facultatif) :" - #: app/templates/pages/notes.html:185 msgid "Let the player know why this time doesn't work..." msgstr "Expliquez au joueur pourquoi ce créneau ne convient pas..." - #: app/templates/pages/notes.html:190 msgid "Reject Request" msgstr "Refuser la demande" - #: app/templates/pages/notes.html:201 msgid "Team Notes History" msgstr "Historique des notes d’équipe" - #: app/templates/pages/notes.html:207 app/templates/pages/team_notes.html:42 msgid "Last Updated" msgstr "Dernière mise à jour" - #: app/templates/pages/notes.html:208 app/templates/pages/team_notes.html:43 msgid "Content Preview" msgstr "Aperçu du contenu" - #: app/templates/pages/notes.html:228 msgid "Recent Personal Notes" msgstr "Notes personnelles récentes" - #: app/templates/pages/notes.html:243 #: app/templates/pages/player_personal_notes.html:27 msgid "From match" msgstr "Depuis le match" - #: app/templates/pages/notes.html:246 #: app/templates/pages/player_personal_notes.html:32 msgid "From team" msgstr "Depuis l’équipe" - #: app/templates/pages/notes.html:249 #: app/templates/pages/player_personal_notes.html:37 msgid "From tryout" msgstr "Depuis la sélection" - #: app/templates/pages/one_on_one.html:2 app/templates/pages/one_on_one.html:3 msgid "One on One" msgstr "Rencontre individuelle" - #: app/templates/pages/one_on_one.html:28 #: app/templates/pages/player_personal_notes.html:72 msgid "" @@ -2303,14 +1804,12 @@ msgid "" msgstr "" "Aucune note d’équipe pour l’instant. Votre coach y publiera ses " "suggestions d’amélioration." - #: app/templates/pages/one_on_one.html:36 #: app/templates/pages/personal_notes.html:2 #: app/templates/pages/personal_notes.html:3 #: app/templates/pages/player_personal_notes.html:11 msgid "Personal Notes" msgstr "Notes personnelles" - #: app/templates/pages/one_on_one.html:53 #: app/templates/pages/player_personal_notes.html:47 msgid "" @@ -2319,53 +1818,41 @@ msgid "" msgstr "" "Aucune note personnelle pour l’instant. Votre coach peut vous laisser des" " commentaires ici." - #: app/templates/pages/one_on_one.html:62 msgid "My One on One Requests" msgstr "Mes demandes de rencontre" - #: app/templates/pages/one_on_one.html:74 msgid "Coach Response" msgstr "Réponse du coach" - #: app/templates/pages/one_on_one.html:87 msgid "Approved" msgstr "Approuvée" - #: app/templates/pages/one_on_one.html:89 msgid "Rejected" msgstr "Refusée" - #: app/templates/pages/one_on_one.html:111 msgid "You haven't made any One on One requests yet." msgstr "Vous n’avez encore fait aucune demande de rencontre individuelle." - #: app/templates/pages/one_on_one.html:120 #: app/templates/pages/player_personal_notes.html:81 msgid "Request One on One Session" msgstr "Demander une rencontre individuelle" - #: app/templates/pages/one_on_one.html:132 msgid "Select Date" msgstr "Choisir une date" - #: app/templates/pages/one_on_one.html:142 msgid "-- Select Date First --" msgstr "-- Choisissez d’abord une date --" - #: app/templates/pages/one_on_one.html:148 #: app/templates/pages/one_on_one.html:271 msgid "-- Select Start Time First --" msgstr "-- Choisissez d’abord l’heure de début --" - #: app/templates/pages/one_on_one.html:155 msgid "Enter topics you'd like to cover in your One on One session..." msgstr "Sujets que vous aimeriez aborder pendant la rencontre..." - #: app/templates/pages/one_on_one.html:160 msgid "Send Request" msgstr "Envoyer la demande" - #: app/templates/pages/one_on_one.html:165 msgid "" "You need to be assigned to a team with a coach to request a One on One " @@ -2373,76 +1860,58 @@ msgid "" msgstr "" "Vous devez être assigné à une équipe ayant un coach pour demander une " "rencontre individuelle." - #: app/templates/pages/one_on_one.html:246 msgid "-- Select Start Time --" msgstr "-- Heure de début --" - #: app/templates/pages/one_on_one.html:294 msgid "-- Select End Time --" msgstr "-- Heure de fin --" - #: app/templates/pages/personal_notes.html:46 msgid "Recent Notes" msgstr "Notes récentes" - #: app/templates/pages/players_to_evaluate.html:2 #: app/templates/pages/players_to_evaluate.html:3 msgid "Players to Evaluate" msgstr "Joueurs à évaluer" - #: app/templates/pages/profile.html:21 msgid "Account Information" msgstr "Informations du compte" - #: app/templates/pages/profile.html:68 app/templates/pages/view_user.html:52 msgid "Gamertags" msgstr "Pseudos de jeu" - #: app/templates/pages/profile.html:94 msgid "Discord" msgstr "Discord" - #: app/templates/pages/profile.html:104 app/templates/pages/view_user.html:39 msgid "League OS" msgstr "League OS" - #: app/templates/pages/profile.html:121 msgid "My Statistics" msgstr "Mes statistiques" - #: app/templates/pages/profile.html:128 msgid "Tryouts Registered" msgstr "Sélections inscrites" - #: app/templates/pages/profile.html:132 msgid "Team Assignments" msgstr "Répartition des équipes" - #: app/templates/pages/profile.html:139 msgid "Evaluations Given" msgstr "Évaluations données" - #: app/templates/pages/profile.html:143 msgid "No statistics available for this role." msgstr "Aucune statistique pour ce rôle." - #: app/templates/pages/profile.html:195 msgid "My Contracts" msgstr "Mes contrats" - #: app/templates/pages/profile.html:217 msgid "View All Contracts" msgstr "Voir tous les contrats" - #: app/templates/pages/profile.html:221 msgid "No contracts have been uploaded for you yet." msgstr "Aucun contrat n’a encore été téléversé pour vous." - #: app/templates/pages/profile.html:231 msgid "My Disponibilities" msgstr "Mes disponibilités" - #: app/templates/pages/profile.html:234 msgid "" "Select your available time blocks for matches (5pm to 12am). Green = " @@ -2450,11 +1919,9 @@ msgid "" msgstr "" "Choisissez vos plages disponibles pour les matchs (17 h à minuit). Vert =" " sélectionné, gris = disponible." - #: app/templates/pages/profile.html:251 msgid "My Coaching Availability" msgstr "Mes disponibilités de coaching" - #: app/templates/pages/profile.html:252 msgid "" "Select time slots when you're available for One on One sessions (8am to " @@ -2462,43 +1929,33 @@ msgid "" msgstr "" "Choisissez les plages où vous êtes disponible pour des rencontres " "individuelles (8 h à 22 h)." - #: app/templates/pages/profile.html:500 msgid "Click or click-and-drag to select your available hours" msgstr "Cliquez ou faites glisser pour choisir vos heures de disponibilité" - #: app/templates/pages/register.html:2 msgid "Register" msgstr "S’inscrire" - #: app/templates/pages/register.html:8 msgid "Personal Information" msgstr "Renseignements personnels" - #: app/templates/pages/register.html:11 msgid "Enter your full name" msgstr "Saisissez votre nom complet" - #: app/templates/pages/register.html:16 msgid "Choose a username" msgstr "Choisissez un nom d’utilisateur" - #: app/templates/pages/register.html:21 msgid "Enter your email" msgstr "Saisissez votre courriel" - #: app/templates/pages/register.html:25 msgid "Phone (Optional)" msgstr "Téléphone (facultatif)" - #: app/templates/pages/register.html:26 msgid "Enter your phone number" msgstr "Saisissez votre numéro de téléphone" - #: app/templates/pages/register.html:33 msgid "Discord Connection" msgstr "Connexion Discord" - #: app/templates/pages/register.html:34 msgid "" "Connect your Discord account to automatically fill your gamertags from " @@ -2506,11 +1963,9 @@ msgid "" msgstr "" "Connectez votre compte Discord pour remplir automatiquement vos pseudos à" " partir de vos comptes de jeu liés." - #: app/templates/pages/register.html:52 msgid "Connected" msgstr "Connecté" - #: app/templates/pages/register.html:61 msgid "" "Discord connected. Game connections have been used to pre-fill your " @@ -2518,415 +1973,317 @@ msgid "" msgstr "" "Discord connecté. Vos comptes de jeu ont servi à pré-remplir le profil " "ci-dessous." - #: app/templates/pages/register.html:69 msgid "Connect to pre-fill your gamertags from Steam, Battle.net, Xbox, etc." msgstr "" "Connectez-vous pour pré-remplir vos pseudos depuis Steam, Battle.net, " "Xbox, etc." - #: app/templates/pages/register.html:72 msgid "Discord Username (Manual)" msgstr "Nom d’utilisateur Discord (saisie manuelle)" - #: app/templates/pages/register.html:73 msgid "e.g. YourName" msgstr "ex. : VotrePseudo" - #: app/templates/pages/register.html:82 msgid "Set up your competitive gaming profile for tryouts." msgstr "Configurez votre profil de joueur compétitif pour les sélections." - #: app/templates/pages/register.html:127 msgid "League OS Connection (Optional)" msgstr "Connexion League OS (facultative)" - #: app/templates/pages/register.html:131 msgid "Connect your League OS profile for organized play." msgstr "Liez votre profil League OS pour le jeu organisé." - #: app/templates/pages/register.html:135 msgid "Security" msgstr "Sécurité" - #: app/templates/pages/register.html:138 msgid "Create a password" msgstr "Créez un mot de passe" - #: app/templates/pages/register.html:142 msgid "Confirm Password" msgstr "Confirmer le mot de passe" - #: app/templates/pages/register.html:143 msgid "Confirm your password" msgstr "Confirmez votre mot de passe" - #: app/templates/pages/register.html:157 msgid "Create Account" msgstr "Créer le compte" - #: app/templates/pages/team_match_form.html:20 msgid "e.g., Regular Season vs Opponent" msgstr "ex. : Saison régulière contre adversaire" - #: app/templates/pages/team_match_form.html:24 msgid "Opponent (optional)" msgstr "Adversaire (facultatif)" - #: app/templates/pages/team_match_form.html:27 msgid "e.g., University of Toronto" msgstr "ex. : Université de Toronto" - #: app/templates/pages/team_match_form.html:48 msgid "Auto-calculated if left empty (start + 30 min)" msgstr "Calculée automatiquement si laissée vide (début + 30 min)" - #: app/templates/pages/team_match_form.html:57 msgid "e.g., Online, Gym A" msgstr "ex. : En ligne, Gymnase A" - #: app/templates/pages/team_match_form.html:72 msgid "Description (optional)" msgstr "Description (facultative)" - #: app/templates/pages/team_match_form.html:74 msgid "Additional details about the match..." msgstr "Précisions sur le match..." - #: app/templates/pages/team_match_form.html:81 msgid "Team Roster (auto-included)" msgstr "Alignement (inclus automatiquement)" - #: app/templates/pages/team_match_form.html:98 msgid "No players on this team. Add players in the Teams page first." msgstr "Aucun joueur dans cette équipe. Ajoutez-en d’abord depuis la page Équipes." - #: app/templates/pages/team_match_form.html:107 #: app/templates/pages/view_tryout.html:324 msgid "Participants" msgstr "Participants" - #: app/templates/pages/team_match_form.html:134 msgid "No participants recorded." msgstr "Aucun participant enregistré." - #: app/templates/pages/team_matches.html:3 #: app/templates/pages/team_matches.html:4 msgid "Team Matches" msgstr "Matchs d’équipe" - #: app/templates/pages/team_matches.html:11 msgid "+ Schedule Match" msgstr "+ Planifier un match" - #: app/templates/pages/team_matches.html:101 msgid "Delete this match?" msgstr "Supprimer ce match ?" - #: app/templates/pages/team_matches.html:133 msgid "No Team Matches" msgstr "Aucun match d’équipe" - #: app/templates/pages/team_matches.html:134 msgid "Regular season matches have not been scheduled yet." msgstr "Aucun match de saison régulière n’a encore été planifié." - #: app/templates/pages/team_matches.html:138 msgid "-- Schedule a Match --" msgstr "-- Planifier un match --" - #: app/templates/pages/team_notes.html:9 msgid "Team Improvement Notes" msgstr "Notes d’amélioration de l’équipe" - #: app/templates/pages/team_notes.html:26 msgid "Add Team Notes" msgstr "Ajouter des notes d’équipe" - #: app/templates/pages/team_notes.html:36 msgid "Note History" msgstr "Historique des notes" - #: app/templates/pages/teams.html:2 app/templates/pages/teams.html:3 #: app/templates/pages/view_tryout.html:231 msgid "Teams" msgstr "Équipes" - #: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:234 msgid "New Team" msgstr "Nouvelle équipe" - #: app/templates/pages/teams.html:18 msgid "Create New Team" msgstr "Créer une équipe" - #: app/templates/pages/teams.html:25 app/templates/pages/teams.html:266 msgid "Team Name" msgstr "Nom de l’équipe" - #: app/templates/pages/teams.html:26 msgid "e.g., Varsity, JV, U14" msgstr "ex. : Varsity, JV, U14" - #: app/templates/pages/teams.html:29 msgid "Assigned Coach" msgstr "Coach assigné" - #: app/templates/pages/teams.html:31 msgid "-- No coach assigned --" msgstr "-- Aucun coach assigné --" - #: app/templates/pages/teams.html:38 msgid "Assigned Manager" msgstr "Gérant assigné" - #: app/templates/pages/teams.html:40 msgid "-- No manager assigned --" msgstr "-- Aucun gérant assigné --" - #: app/templates/pages/teams.html:49 app/templates/pages/teams.html:243 msgid "Create Team" msgstr "Créer l’équipe" - #: app/templates/pages/teams.html:64 app/templates/pages/users.html:51 #: app/templates/pages/view_tryout.html:29 #: app/templates/pages/view_tryout.html:459 msgid "Edit" msgstr "Modifier" - #: app/templates/pages/teams.html:66 #, python-format msgid "Delete team %(name)s? It will be unassigned from any linked tryouts." msgstr "" "Supprimer l’équipe %(name)s ? Elle sera détachée de toutes les sélections" " liées." - #: app/templates/pages/teams.html:69 msgid "Delete" msgstr "Supprimer" - #: app/templates/pages/teams.html:74 msgid "Add Notes" msgstr "Ajouter des notes" - #: app/templates/pages/teams.html:92 #, python-format msgid "Remove coach %(coach)s from %(team)s?" msgstr "Retirer le coach %(coach)s de %(team)s ?" - #: app/templates/pages/teams.html:95 msgid "Remove Coach" msgstr "Retirer le coach" - #: app/templates/pages/teams.html:114 #, python-format msgid "Remove manager %(manager)s from %(team)s?" msgstr "Retirer le gérant %(manager)s de %(team)s ?" - #: app/templates/pages/teams.html:117 msgid "Remove Manager" msgstr "Retirer le gérant" - #: app/templates/pages/teams.html:127 msgid "View Team Matches" msgstr "Voir les matchs d’équipe" - #: app/templates/pages/teams.html:128 msgid "Matches" msgstr "Matchs" - #: app/templates/pages/teams.html:134 msgid "Schedule Practice" msgstr "Planifier un entraînement" - #: app/templates/pages/teams.html:135 msgid "Practice" msgstr "Entraînement" - #: app/templates/pages/teams.html:147 msgid "Position / Role" msgstr "Poste ou rôle" - #: app/templates/pages/teams.html:183 #, python-format msgid "Remove %(player)s from %(team)s?" msgstr "Retirer %(player)s de %(team)s ?" - #: app/templates/pages/teams.html:186 app/templates/pages/view_tryout.html:211 msgid "Remove" msgstr "Retirer" - #: app/templates/pages/teams.html:199 msgid "Add players to this team using the form below." msgstr "Ajoutez des joueurs à cette équipe avec le formulaire ci-dessous." - #: app/templates/pages/teams.html:224 msgid "Starter" msgstr "Partant" - #: app/templates/pages/teams.html:225 msgid "Substitute" msgstr "Remplaçant" - #: app/templates/pages/teams.html:228 msgid "Add to Team" msgstr "Ajouter à l’équipe" - #: app/templates/pages/teams.html:240 msgid "No teams yet" msgstr "Aucune équipe" - #: app/templates/pages/teams.html:242 msgid "Create organization teams and assign coaches to manage tryouts." msgstr "Créez des équipes et assignez-leur des coachs pour gérer les sélections." - #: app/templates/pages/teams.html:245 msgid "There are no teams to display." msgstr "Aucune équipe à afficher." - #: app/templates/pages/teams.html:257 msgid "Edit Team" msgstr "Modifier l’équipe" - #: app/templates/pages/teams.html:280 msgid "Hold Ctrl/Cmd to select multiple. Only unassigned coaches shown." msgstr "" "Maintenez Ctrl ou Cmd pour en choisir plusieurs. Seuls les coachs non " "assignés sont proposés." - #: app/templates/pages/teams.html:291 msgid "Hold Ctrl/Cmd to select multiple. Only unassigned managers shown." msgstr "" "Maintenez Ctrl ou Cmd pour en choisir plusieurs. Seuls les gérants non " "assignés sont proposés." - #: app/templates/pages/tryout_form.html:23 msgid "Tryout Title" msgstr "Titre de la sélection" - #: app/templates/pages/tryout_form.html:24 msgid "e.g., Spring Season Tryouts" msgstr "ex. : Sélections de la saison printanière" - #: app/templates/pages/tryout_form.html:29 #: app/templates/pages/view_user.html:57 msgid "Game" msgstr "Jeu" - #: app/templates/pages/tryout_form.html:31 msgid "-- Select a game --" msgstr "-- Choisir un jeu --" - #: app/templates/pages/tryout_form.html:38 msgid "Start Date" msgstr "Date de début" - #: app/templates/pages/tryout_form.html:42 msgid "End Date" msgstr "Date de fin" - #: app/templates/pages/tryout_form.html:44 msgid "Optional. Leave blank for single-day tryout." msgstr "Facultatif. Laissez vide pour une sélection d’une seule journée." - #: app/templates/pages/tryout_form.html:50 msgid "e.g., Online" msgstr "ex. : En ligne" - #: app/templates/pages/tryout_form.html:53 msgid "Max Players" msgstr "Nombre maximal de joueurs" - #: app/templates/pages/tryout_form.html:54 msgid "Leave blank for unlimited" msgstr "Laisser vide pour un nombre illimité" - #: app/templates/pages/tryout_form.html:59 msgid "Target Team" msgstr "Équipe visée" - #: app/templates/pages/tryout_form.html:61 msgid "-- No target team --" msgstr "-- Aucune équipe visée --" - #: app/templates/pages/tryout_form.html:70 msgid "Assigned Managers" msgstr "Gérants assignés" - #: app/templates/pages/tryout_form.html:85 msgid "Select one or more managers for this tryout." msgstr "Sélectionnez un ou plusieurs gérants pour cette sélection." - #: app/templates/pages/tryout_form.html:90 msgid "Assigned Coaches" msgstr "Coachs assignés" - #: app/templates/pages/tryout_form.html:106 msgid "Select one or more coaches for this tryout." msgstr "Choisissez un ou plusieurs coachs pour cette sélection." - #: app/templates/pages/tryout_form.html:111 msgid "Enter any details about the tryout..." msgstr "Précisions sur la sélection..." - #: app/templates/pages/tryouts.html:9 msgid "New Tryout" msgstr "Nouvelle sélection" - #: app/templates/pages/tryouts.html:80 msgid "View Details" msgstr "Voir le détail" - #: app/templates/pages/tryouts.html:84 msgid "Evaluate" msgstr "Évaluer" - #: app/templates/pages/tryouts.html:94 msgid "No tryouts found" msgstr "Aucune sélection" - #: app/templates/pages/tryouts.html:96 msgid "Get started by creating a new tryout." msgstr "Commencez par créer une sélection." - #: app/templates/pages/upload_contract.html:9 msgid "Upload Contract for Player" msgstr "Téléverser un contrat pour un joueur" - #: app/templates/pages/upload_contract.html:26 msgid "Contract File" msgstr "Fichier du contrat" - #: app/templates/pages/upload_contract.html:32 msgid "Notes (Optional)" msgstr "Notes (facultatives)" - #: app/templates/pages/upload_contract.html:33 msgid "Add any notes about this contract..." msgstr "Notes éventuelles sur ce contrat..." - #: app/templates/pages/users.html:9 msgid "Add User" msgstr "Ajouter un utilisateur" - #: app/templates/pages/users.html:54 #, python-format msgid "Delete %(name)s? This cannot be undone." msgstr "Supprimer %(name)s ? Cette action est irréversible." - #: app/templates/pages/view_tryout.html:10 msgid "Tryout Details" msgstr "Détail de la sélection" - #: app/templates/pages/view_tryout.html:14 #: app/templates/pages/view_tryout.html:306 msgid "Schedule Match" msgstr "Planifier un match" - #: app/templates/pages/view_tryout.html:22 msgid "In Progress" msgstr "En cours" - #: app/templates/pages/view_tryout.html:33 msgid "" "Delete this entire tryout? This removes all its matches, teams, " @@ -2934,43 +2291,33 @@ msgid "" msgstr "" "Supprimer entièrement cette sélection ? Cela supprime tous ses matchs, " "équipes, inscriptions et évaluations." - #: app/templates/pages/view_tryout.html:36 msgid "Delete Tryout" msgstr "Supprimer la sélection" - #: app/templates/pages/view_tryout.html:114 msgid "Register for this Tryout" msgstr "S’inscrire à cette sélection" - #: app/templates/pages/view_tryout.html:124 msgid "Add Player to Tryout" msgstr "Ajouter un joueur à la sélection" - #: app/templates/pages/view_tryout.html:137 msgid "Register Player" msgstr "Inscrire un joueur" - #: app/templates/pages/view_tryout.html:157 msgid "Evaluation" msgstr "Évaluation" - #: app/templates/pages/view_tryout.html:179 msgid "Registered" msgstr "Inscrit" - #: app/templates/pages/view_tryout.html:180 msgid "Attended" msgstr "Présent" - #: app/templates/pages/view_tryout.html:181 msgid "No Show" msgstr "Absent" - #: app/templates/pages/view_tryout.html:200 msgid "Evaluate player" msgstr "Évaluer le joueur" - #: app/templates/pages/view_tryout.html:208 #, python-format msgid "" @@ -2979,81 +2326,61 @@ msgid "" msgstr "" "Retirer %(name)s de cette sélection ? Il sera aussi retiré de toutes ses " "équipes et de tous ses matchs." - #: app/templates/pages/view_tryout.html:210 msgid "Remove player from tryout" msgstr "Retirer le joueur de la sélection" - #: app/templates/pages/view_tryout.html:220 msgid "No players registered yet." msgstr "Aucun joueur inscrit pour l’instant." - #: app/templates/pages/view_tryout.html:243 msgid "Team name" msgstr "Nom de l’équipe" - #: app/templates/pages/view_tryout.html:244 msgid "Create" msgstr "Créer" - #: app/templates/pages/view_tryout.html:263 msgid "No players assigned yet." msgstr "Aucun joueur assigné pour l’instant." - #: app/templates/pages/view_tryout.html:271 msgid "Select player..." msgstr "Choisir un joueur..." - #: app/templates/pages/view_tryout.html:288 msgid "Add" msgstr "Ajouter" - #: app/templates/pages/view_tryout.html:293 msgid "No teams created yet." msgstr "Aucune équipe créée." - #: app/templates/pages/view_tryout.html:322 msgid "Type" msgstr "Type" - #: app/templates/pages/view_tryout.html:387 msgid "Toggle your attendance" msgstr "Basculer votre présence" - #: app/templates/pages/view_tryout.html:462 msgid "Add Note for this Match" msgstr "Ajouter une note pour ce match" - #: app/templates/pages/view_tryout.html:463 msgid "Note" msgstr "Note" - #: app/templates/pages/view_tryout.html:481 msgid "No matches scheduled yet. Check back later!" msgstr "Aucun match planifié pour l’instant. Revenez plus tard." - #: app/templates/pages/view_tryout.html:491 msgid "Evaluation Summary" msgstr "Synthèse de l’évaluation" - #: app/templates/pages/view_tryout.html:510 msgid "Recommendation" msgstr "Recommandation" - #: app/templates/pages/view_user.html:60 msgid "Profile" msgstr "Profil" - #: app/templates/pages/view_user.html:72 msgid "View Profile" msgstr "Voir le profil" - #~ msgid "×" #~ msgstr "×" - #~ msgid "One on One request from %(value)s has been approved!" #~ msgstr "" - #~ msgid "" #~ "Account is locked due to too many" #~ " failed attempts. Please try again in" @@ -3062,7 +2389,6 @@ msgstr "Voir le profil" #~ "Compte verrouillé après trop de " #~ "tentatives échouées. Réessayez dans " #~ "%(remaining)s minute(s)." - #~ msgid "" #~ "Account locked after %(attempts)s failed " #~ "attempts. Please try again in " @@ -3071,46 +2397,32 @@ msgstr "Voir le profil" #~ "Compte verrouillé après %(attempts)s " #~ "tentatives échouées. Réessayez dans " #~ "%(minutes)s minutes." - #~ msgid "Login unsuccessful. %(remaining)s attempt(s) remaining before lockout." #~ msgstr "" #~ "Échec de la connexion. Il reste " #~ "%(remaining)s tentative(s) avant le " #~ "verrouillage." - #~ msgid "Incorrect CAPTCHA answer. Please try again." #~ msgstr "Réponse au CAPTCHA incorrecte. Veuillez réessayer." - #~ msgid "Answer" #~ msgstr "Réponse" - #~ msgid "Invalid start time format." #~ msgstr "Format d’heure de début invalide." - #~ msgid "Invalid end time format." #~ msgstr "Format d’heure de fin invalide." - #~ msgid "Invalid date or time format." #~ msgstr "Format de date ou d’heure invalide." - #~ msgid "Availability" #~ msgstr "Disponibilités" - #~ msgid "Team Tryout Management System" #~ msgstr "Système de gestion des sélections d’équipe" - #~ msgid "Player and content are required." #~ msgstr "Le joueur et le contenu sont obligatoires." - #~ msgid "Save Disponibilities" #~ msgstr "Enregistrer mes disponibilités" - #~ msgid "Contact" #~ msgstr "Coordonnées" - #~ msgid "Attendance" #~ msgstr "Présence" - #~ msgid "No players registered for this tryout." -#~ msgstr "Aucun joueur inscrit à cette sélection." - +#~ msgstr "Aucun joueur inscrit à cette sélection." \ No newline at end of file diff --git a/docs/database-restore.md b/docs/database-restore.md index 5e32690..89435eb 100644 --- a/docs/database-restore.md +++ b/docs/database-restore.md @@ -15,6 +15,7 @@ incident. |---|---|---| | `db_backup_.dump` | Full PostgreSQL dump, custom format | Every account, tryout, evaluation, note and contract record | | `documents_backup_.zip` | `documents/` directory | The contract **files** themselves — they exist only on disk, the database stores paths | +| `documents_backup_drive_.zip` | Google Drive contract folder | Drive-stored contracts downloaded and archived when `DOCUMENT_STORAGE_BACKEND=google_drive` | Losing either one alone loses data. A database restore without the document archive leaves contract rows pointing at files that no longer exist. diff --git a/docs/security-checklist.md b/docs/security-checklist.md index fbccf8d..b37aab5 100644 --- a/docs/security-checklist.md +++ b/docs/security-checklist.md @@ -82,8 +82,9 @@ Run through this checklist before deploying to production. - [ ] Upload size limited to 16MB (`MAX_CONTENT_LENGTH`) - [ ] File extensions restricted to PDF only +- [ ] File content validated by parsing with pypdf (`PdfReader`) — header (`%PDF-`) + at least one parseable page - [ ] Files stored with UUID filenames (not original names) -- [ ] Upload directory outside web root +- [ ] Upload directory outside web root (or Google Drive in production) ## Phase 13 — Logging & Monitoring diff --git a/requirements.txt b/requirements.txt index 36b574c..d7a8034 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ frozenlist==1.8.0 greenlet==3.5.4 google-api-python-client==2.198.0 google-auth==2.56.3 +pypdf==6.16.2 idna==3.18 itsdangerous==2.2.0 Jinja2==3.1.6 diff --git a/tests/test_contract_uploads.py b/tests/test_contract_uploads.py index a7d6ac8..ccffb79 100644 --- a/tests/test_contract_uploads.py +++ b/tests/test_contract_uploads.py @@ -14,13 +14,25 @@ import io import os import pytest +from pypdf import PdfWriter as _Pw from app.extensions import db from app.models import Contract -PDF_BYTES = b'%PDF-1.7\n1 0 obj\n<<>>\nendobj\ntrailer\n%%EOF\n' + +def _valid_pdf_bytes(): + w = _Pw() + w.add_blank_page(612, 792) + buf = io.BytesIO() + w.write(buf) + return buf.getvalue() + + +_PDF_BYTES = _valid_pdf_bytes() NOT_PDF_BYTES = b'MZ\x90\x00\x03\x00\x00\x00' # a Windows executable header +PDF_HEADER_ONLY_BYTES = b'%PDF-1.7\n' + b'MZ' * 100 # PDF header over non-PDF content + @pytest.fixture def contract_for(app, tmp_path): @@ -29,7 +41,7 @@ def contract_for(app, tmp_path): def _make(player_id, uploader_id): stored = 'deadbeef.pdf' path = tmp_path / stored - path.write_bytes(PDF_BYTES) + path.write_bytes(_PDF_BYTES) with app.app_context(): contract = Contract( player_id=player_id, @@ -89,7 +101,7 @@ class TestSignedUpload: client.post( f'/users/contracts/{contract_id}/upload_signed', - data={'signed_file': (io.BytesIO(PDF_BYTES), 'signed.pdf')}, + data={'signed_file': (io.BytesIO(_PDF_BYTES), 'signed.pdf')}, content_type='multipart/form-data', follow_redirects=True, ) @@ -110,7 +122,7 @@ class TestSignedUpload: client.post( f'/users/contracts/{contract_id}/upload_signed', - data={'signed_file': (io.BytesIO(PDF_BYTES), 'signed.pdf')}, + data={'signed_file': (io.BytesIO(_PDF_BYTES), 'signed.pdf')}, content_type='multipart/form-data', follow_redirects=True, ) @@ -118,6 +130,31 @@ class TestSignedUpload: with app.app_context(): assert db.session.get(Contract, contract_id).status != 'signed' + def test_signed_upload_with_google_drive( + self, app, client, as_role, make_user, contract_for, monkeypatch + ): + """Signing with Drive backend stores gdrive:// in signed_file_path.""" + monkeypatch.setenv('DOCUMENT_STORAGE_BACKEND', 'google_drive') + monkeypatch.setattr( + 'app.storage.upload_google_drive_file', + lambda **_kwargs: 'drive-signed-456', + ) + player_id = as_role('player') + admin_id = make_user('admin') + contract_id, directory = contract_for(player_id, admin_id) + + client.post( + f'/users/contracts/{contract_id}/upload_signed', + data={'signed_file': (io.BytesIO(_PDF_BYTES), 'signed.pdf')}, + content_type='multipart/form-data', + follow_redirects=True, + ) + + with app.app_context(): + contract = db.session.get(Contract, contract_id) + assert contract.status == 'signed' + assert contract.signed_file_path == 'gdrive://drive-signed-456' + class TestTheHelper: def test_it_reports_a_missing_file(self, app): @@ -133,16 +170,27 @@ class TestTheHelper: from app.routes.users import ALLOWED_CONTRACT_EXTENSIONS, pdf_upload_error - upload = FileStorage(stream=io.BytesIO(PDF_BYTES), filename='c.pdf') + upload = FileStorage(stream=io.BytesIO(_PDF_BYTES), filename='c.pdf') with app.test_request_context('/'): assert pdf_upload_error(upload, ALLOWED_CONTRACT_EXTENSIONS) is None - assert upload.stream.read() == PDF_BYTES + assert upload.stream.read() == _PDF_BYTES def test_a_name_without_a_dot_is_refused(self, app): from werkzeug.datastructures import FileStorage from app.routes.users import ALLOWED_CONTRACT_EXTENSIONS, pdf_upload_error - upload = FileStorage(stream=io.BytesIO(PDF_BYTES), filename='pdf') + upload = FileStorage(stream=io.BytesIO(_PDF_BYTES), filename='pdf') + with app.test_request_context('/'): + assert pdf_upload_error(upload, ALLOWED_CONTRACT_EXTENSIONS) + + def test_a_header_only_file_is_refused(self, app): + """A stream starting with %PDF- but holding non-PDF data is + rejected (the header check is not enough).""" + from werkzeug.datastructures import FileStorage + + from app.routes.users import ALLOWED_CONTRACT_EXTENSIONS, pdf_upload_error + + upload = FileStorage(stream=io.BytesIO(PDF_HEADER_ONLY_BYTES), filename='clever.pdf') with app.test_request_context('/'): assert pdf_upload_error(upload, ALLOWED_CONTRACT_EXTENSIONS) diff --git a/tests/test_storage.py b/tests/test_storage.py index 3b43f44..5725dd2 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -28,6 +28,20 @@ from app.storage import ( ) +def _valid_pdf(): + """A real one-page PDF that passes pypdf content validation.""" + import io + + from pypdf import PdfWriter + + w = PdfWriter() + w.add_blank_page(612, 792) + buf = io.BytesIO() + w.write(buf) + buf.seek(0) + return buf + + class TestDocumentsRoot: def test_the_environment_wins(self, tmp_path, monkeypatch): monkeypatch.setenv('DOCUMENTS_ROOT', str(tmp_path / 'elsewhere')) @@ -165,10 +179,8 @@ class TestThroughTheUploadRoute: def _pdf(): - """The smallest thing pdf_upload_error accepts.""" - import io - - return io.BytesIO(b'%PDF-1.4\n%%EOF\n') + """The smallest valid one-page PDF that passes pypdf content validation.""" + return _valid_pdf() class TestDeletingAnAccountTakesItsDocuments: