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 bbd497e..6fbd4fb 100644 Binary files a/app/translations/en/LC_MESSAGES/messages.mo and b/app/translations/en/LC_MESSAGES/messages.mo differ 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 5f69323..1182cd7 100644 Binary files a/app/translations/fr/LC_MESSAGES/messages.mo and b/app/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/app/translations/fr/LC_MESSAGES/messages.po b/app/translations/fr/LC_MESSAGES/messages.po index 1afe5ce..0033a19 100644 --- a/app/translations/fr/LC_MESSAGES/messages.po +++ b/app/translations/fr/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: 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: