diff --git a/app/.env.example b/app/.env.example index 2329195..d2f2cac 100644 --- a/app/.env.example +++ b/app/.env.example @@ -88,17 +88,28 @@ DISCORD_CLIENT_SECRET= DISCORD_REDIRECT_URI=https://your-domain/auth/discord/callback # ============================================================================= -# Optional — storage +# Contract storage — Google Drive # ============================================================================= -# Where uploaded contracts live. Empty means `documents/` beside the -# application. Set it to a path OUTSIDE the deployment directory if you move -# to a release-directory layout, or a deployment will take the documents with -# it (OPS-011, app/storage.py). -# -# IMPORTANT: the backup script reads this same variable. Before wave J it -# did not, and archived `./documents` regardless — so setting this here and -# nowhere else produced empty contract backups that still exited 0. +# New contracts are uploaded to the owner's Google Drive. The OAuth client, +# refresh token, and folder ID are secrets/configuration: do not commit them. +# Use `local` only for isolated development and legacy-file maintenance. +DOCUMENT_STORAGE_BACKEND=google_drive + +# Target folder in the owner's personal Drive. The application keeps every +# contract directly in this folder and stores only each Drive file ID in the +# database. +GOOGLE_DRIVE_FOLDER_ID= + +# OAuth 2.0 credentials for the owner's Google account. Create a Google Cloud +# Desktop OAuth client, authorize the Drive scope once, and place the resulting +# refresh token here. See docs/deployment.md before enabling this in production. +GOOGLE_DRIVE_CLIENT_ID= +GOOGLE_DRIVE_CLIENT_SECRET= +GOOGLE_DRIVE_REFRESH_TOKEN= + +# Local storage is retained only for historical rows and local test runs. +# It is not used for new contracts while DOCUMENT_STORAGE_BACKEND=google_drive. DOCUMENTS_ROOT= # Where the log files go. Empty means `logs/` beside the application. Both diff --git a/app/google_drive.py b/app/google_drive.py new file mode 100644 index 0000000..3463fb3 --- /dev/null +++ b/app/google_drive.py @@ -0,0 +1,105 @@ +"""Google Drive storage for contract PDFs. + +The application uses an OAuth refresh token for the owner's personal Google +account. Tokens and client secrets come only from environment variables; the +database stores opaque file IDs, never a credential or a shareable Drive URL. +""" + +import io +import os + +GOOGLE_DRIVE_FOLDER_ID_ENV = 'GOOGLE_DRIVE_FOLDER_ID' +GOOGLE_DRIVE_CLIENT_ID_ENV = 'GOOGLE_DRIVE_CLIENT_ID' +GOOGLE_DRIVE_CLIENT_SECRET_ENV = 'GOOGLE_DRIVE_CLIENT_SECRET' +GOOGLE_DRIVE_REFRESH_TOKEN_ENV = 'GOOGLE_DRIVE_REFRESH_TOKEN' +GOOGLE_DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.file' + + +class GoogleDriveStorageError(RuntimeError): + """A configuration or API failure while storing a contract in Drive.""" + + +def _setting(name): + value = os.getenv(name) + if not value: + raise GoogleDriveStorageError(f'{name} must be configured for Google Drive document storage.') + return value + + +def _drive_service(): + """Build an authorized Drive client from the owner's refresh token.""" + try: + from google.oauth2.credentials import Credentials + from googleapiclient.discovery import build + except ImportError as exc: + raise GoogleDriveStorageError( + 'Google Drive dependencies are not installed. Install requirements.txt again.' + ) from exc + + credentials = Credentials( + token=None, + refresh_token=_setting(GOOGLE_DRIVE_REFRESH_TOKEN_ENV), + token_uri='https://oauth2.googleapis.com/token', + client_id=_setting(GOOGLE_DRIVE_CLIENT_ID_ENV), + client_secret=_setting(GOOGLE_DRIVE_CLIENT_SECRET_ENV), + scopes=[GOOGLE_DRIVE_SCOPE], + ) + return build('drive', 'v3', credentials=credentials, cache_discovery=False) + + +def upload_file(*, stream, filename, mimetype): + """Upload a PDF to the configured owner folder and return its Drive ID.""" + try: + from googleapiclient.http import MediaIoBaseUpload + + stream.seek(0) + media = MediaIoBaseUpload(stream, mimetype=mimetype, resumable=True) + response = ( + _drive_service() + .files() + .create( + body={'name': filename, 'parents': [_setting(GOOGLE_DRIVE_FOLDER_ID_ENV)]}, + media_body=media, + fields='id', + ) + .execute() + ) + except GoogleDriveStorageError: + raise + except Exception as exc: # Google client exceptions share no stable base class. + raise GoogleDriveStorageError('Google Drive rejected the contract upload.') from exc + + file_id = response.get('id') + if not file_id: + raise GoogleDriveStorageError('Google Drive did not return an uploaded file identifier.') + return file_id + + +def download_file(file_id): + """Download a Drive file into memory for Flask's authenticated response.""" + try: + from googleapiclient.http import MediaIoBaseDownload + + destination = io.BytesIO() + downloader = MediaIoBaseDownload( + destination, + _drive_service().files().get_media(fileId=file_id), + ) + complete = False + while not complete: + _status, complete = downloader.next_chunk() + return destination.getvalue() + except GoogleDriveStorageError: + raise + except Exception as exc: # Google client exceptions share no stable base class. + raise GoogleDriveStorageError('Google Drive could not download this contract.') from exc + + +def delete_file(file_id): + """Permanently delete a Drive document when its contract record is deleted.""" + try: + _drive_service().files().delete(fileId=file_id).execute() + except GoogleDriveStorageError: + raise + except Exception as exc: # Google client exceptions share no stable base class. + raise GoogleDriveStorageError('Google Drive could not delete this contract.') from exc diff --git a/app/models/audit_log.py b/app/models/audit_log.py index 611ad57..72b2be7 100644 --- a/app/models/audit_log.py +++ b/app/models/audit_log.py @@ -1,7 +1,7 @@ """Audit log for tracking sensitive administrative actions.""" from app.extensions import db -from datetime import datetime +from app.time_utils import utc_now_naive class AuditLog(db.Model): @@ -14,7 +14,7 @@ class AuditLog(db.Model): action = db.Column(db.String(100), nullable=False) details = db.Column(db.Text, nullable=True) ip_address = db.Column(db.String(64), nullable=True) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=utc_now_naive) user = db.relationship('User', foreign_keys=[user_id], backref='audit_logs') @@ -29,4 +29,4 @@ class AuditLog(db.Model): ) db.session.add(entry) db.session.commit() - return entry \ No newline at end of file + return entry diff --git a/app/models/backup_record.py b/app/models/backup_record.py index 248d9cf..50c395f 100644 --- a/app/models/backup_record.py +++ b/app/models/backup_record.py @@ -1,7 +1,7 @@ """Records of database backups created through the admin panel.""" from app.extensions import db -from datetime import datetime +from app.time_utils import utc_now_naive class BackupRecord(db.Model): @@ -16,6 +16,6 @@ class BackupRecord(db.Model): backup_type = db.Column(db.String(20), default='manual') # 'manual' or 'auto' notes = db.Column(db.Text, nullable=True) created_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=utc_now_naive) - creator = db.relationship('User', foreign_keys=[created_by_id], backref='backups') \ No newline at end of file + creator = db.relationship('User', foreign_keys=[created_by_id], backref='backups') diff --git a/app/routes/users.py b/app/routes/users.py index 946d563..72ca599 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -8,6 +8,7 @@ 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, @@ -625,7 +626,7 @@ def upload_signed_contract(contract_id): contract.signed_filename = signed_filename contract.signed_file_path = contract.file_path.replace(contract.stored_filename, signed_filename) contract.status = 'signed' - contract.signed_at = datetime.utcnow() + contract.signed_at = utc_now_naive() db.session.commit() flash('Signed contract uploaded successfully!', 'success') return redirect(url_for('users.list_contracts')) @@ -891,7 +892,7 @@ def accept_one_on_one(request_id): player = request_obj.player request_obj.status = 'approved' - request_obj.responded_at = datetime.utcnow() + request_obj.responded_at = utc_now_naive() db.session.commit() # Notify player via Discord (same message as if approved through Discord reactions) @@ -934,7 +935,7 @@ def reject_one_on_one(request_id): player = request_obj.player request_obj.status = 'rejected' - request_obj.responded_at = datetime.utcnow() + request_obj.responded_at = utc_now_naive() if rejection_reason: request_obj.coach_rejection_message = rejection_reason db.session.commit() diff --git a/app/routes/users/contracts.py b/app/routes/users/contracts.py index 62e227d..8505d94 100644 --- a/app/routes/users/contracts.py +++ b/app/routes/users/contracts.py @@ -18,7 +18,13 @@ from app.routes.users._shared import ( pdf_upload_error, ) from app.routes.users.blueprint import users_bp -from app.storage import CONTRACTS_DIR, document_path +from app.storage import ( + CONTRACTS_DIR, + GoogleDriveStorageError, + is_google_drive_path, + open_document, + store_uploaded_document, +) from app.time_utils import utc_now_naive from app.validators import UploadContractSchema @@ -126,9 +132,11 @@ def upload_contract(): if team: relative_path = os.path.join(CONTRACTS_DIR, secure_filename(team.name), stored_filename) - absolute_path = document_path(relative_path) - os.makedirs(os.path.dirname(absolute_path), exist_ok=True) - file.save(absolute_path) + try: + stored_path = store_uploaded_document(file, relative_path) + except GoogleDriveStorageError: + flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger') + return redirect(url_for('users.upload_contract')) contract = Contract( player_id=player_id, @@ -136,7 +144,7 @@ def upload_contract(): uploaded_by_id=current_user.id, original_filename=original_filename, stored_filename=stored_filename, - file_path=relative_path, + file_path=stored_path, notes=notes if notes else None, ) db.session.add(contract) @@ -166,11 +174,22 @@ def upload_signed_contract(contract_id): return redirect(url_for('users.list_contracts')) signed_filename = f"signed_{contract.stored_filename}" - signed_path = contract.file_path.replace(contract.stored_filename, signed_filename) - file.save(document_path(signed_path)) + # Legacy local records keep their signed copy beside the original. Drive + # identifiers are opaque rather than filenames, so new Drive records use + # a fresh logical path and receive a second Drive ID. + signed_path = ( + os.path.join(CONTRACTS_DIR, signed_filename) + if is_google_drive_path(contract.file_path) + else contract.file_path.replace(contract.stored_filename, signed_filename) + ) + try: + stored_signed_path = store_uploaded_document(file, signed_path) + except GoogleDriveStorageError: + flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger') + return redirect(url_for('users.list_contracts')) contract.signed_filename = signed_filename - contract.signed_file_path = signed_path + contract.signed_file_path = stored_signed_path contract.status = 'signed' contract.signed_at = utc_now_naive() db.session.commit() @@ -186,11 +205,12 @@ def download_contract(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( - document_path(contract.file_path), - as_attachment=True, - download_name=contract.original_filename, - ) + try: + document = open_document(contract.file_path) + except GoogleDriveStorageError: + flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger') + return redirect(url_for('users.list_contracts')) + return send_file(document, as_attachment=True, download_name=contract.original_filename) @users_bp.route('/contracts//download_signed') @@ -204,8 +224,9 @@ def download_signed_contract(contract_id): if not contract.signed_file_path: flash(_('No signed contract available.'), 'danger') return redirect(url_for('users.list_contracts')) - return send_file( - document_path(contract.signed_file_path), - as_attachment=True, - download_name=contract.signed_filename, - ) + try: + document = open_document(contract.signed_file_path) + except GoogleDriveStorageError: + flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger') + return redirect(url_for('users.list_contracts')) + return send_file(document, as_attachment=True, download_name=contract.signed_filename) diff --git a/app/static/js/main.js b/app/static/js/main.js index 0ba36be..7ddacd1 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -530,13 +530,31 @@ document.addEventListener('change', function (event) { * Confirmation before a destructive submit. * *
+ * * * Replaces onsubmit="return confirm(...)", and keeps the wording in the * markup where it can be translated. */ document.addEventListener('submit', function (event) { const form = event.target.closest('[data-confirm]'); - if (form && !window.confirm(form.getAttribute('data-confirm'))) { + if (!form) { + return; + } + + const inputName = form.getAttribute('data-confirm-input'); + if (inputName) { + const input = form.elements.namedItem(inputName); + const expectedValue = form.getAttribute('data-confirm-value') || ''; + if (!input || input.value.trim() !== expectedValue) { + window.alert(form.getAttribute('data-confirm-input-message') || 'Confirmation is required.'); + event.preventDefault(); + return; + } + } + + if (!window.confirm(form.getAttribute('data-confirm'))) { event.preventDefault(); } }); diff --git a/app/storage.py b/app/storage.py index 40d54c9..10c27ea 100644 --- a/app/storage.py +++ b/app/storage.py @@ -28,8 +28,17 @@ absolute path and are returned untouched, so this change needs no data migration and can ship before Alembic does (DB-002). """ +import io +import logging import os +from app.google_drive import ( + GoogleDriveStorageError, + delete_file as delete_google_drive_file, + download_file as download_google_drive_file, + upload_file as upload_google_drive_file, +) + #: Environment override for the document root. What a release-directory #: deployment sets, to a path outside the releases — alongside them, not #: inside whichever one is current. @@ -45,6 +54,16 @@ BACKUP_DIR_ENV = 'BACKUP_DIR' #: Sub-directory holding uploaded contracts, under the document root. CONTRACTS_DIR = 'contrats signés' +#: Database marker for documents stored remotely. Existing rows continue to +#: hold relative or absolute filesystem paths, so switching storage does not +#: invalidate contracts already uploaded before the Google Drive move. +GOOGLE_DRIVE_PATH_PREFIX = 'gdrive://' + +#: Storage backends deliberately stay explicit. Google Drive is the +#: production default; local disk exists only for legacy rows and isolated +#: test/development environments. +DOCUMENT_STORAGE_BACKEND_ENV = 'DOCUMENT_STORAGE_BACKEND' + def project_root(): """Absolute path of the project, derived from this file's location. @@ -91,6 +110,56 @@ def _rooted(env_name, default_name): return os.path.join(project_root(), 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() + if backend not in {'google_drive', 'local'}: + raise ValueError( + f'{DOCUMENT_STORAGE_BACKEND_ENV} must be "google_drive" or "local", not {backend!r}' + ) + return backend + + +def is_google_drive_path(stored_path): + """Whether a database path references a Google Drive file.""" + return bool(stored_path and stored_path.startswith(GOOGLE_DRIVE_PATH_PREFIX)) + + +def _google_drive_id(stored_path): + file_id = stored_path.removeprefix(GOOGLE_DRIVE_PATH_PREFIX) + if not file_id: + raise GoogleDriveStorageError('The stored Google Drive file identifier is empty.') + return file_id + + +def store_uploaded_document(file_storage, relative_path): + """Store an uploaded document and return its durable database reference. + + Local storage retains the relative-path format used by existing rows. A + Google Drive upload returns an opaque Drive file identifier prefixed with + ``gdrive://`` so it cannot be mistaken for a filesystem path. + """ + if document_storage_backend() == 'local': + absolute_path = document_path(relative_path) + os.makedirs(os.path.dirname(absolute_path), exist_ok=True) + file_storage.save(absolute_path) + return relative_path + + file_id = upload_google_drive_file( + stream=file_storage.stream, + filename=os.path.basename(relative_path), + mimetype=file_storage.mimetype or 'application/pdf', + ) + return f'{GOOGLE_DRIVE_PATH_PREFIX}{file_id}' + + +def open_document(stored_path): + """Return a filesystem path or in-memory stream suitable for ``send_file``.""" + if is_google_drive_path(stored_path): + return io.BytesIO(download_google_drive_file(_google_drive_id(stored_path))) + return document_path(stored_path) + + def discard_documents(stored_paths): """Remove these documents from disk. Returns how many went (DATA-012). @@ -107,27 +176,27 @@ def discard_documents(stored_paths): A path that cannot be removed is logged and skipped. Nothing here should be able to abort the deletion of an account. """ - import logging - logger = logging.getLogger(__name__) removed = 0 for stored_path in stored_paths: if not stored_path: continue - target = document_path(stored_path) try: - os.remove(target) + if is_google_drive_path(stored_path): + delete_google_drive_file(_google_drive_id(stored_path)) + else: + os.remove(document_path(stored_path)) removed += 1 except FileNotFoundError: # Already gone. Two contracts sharing a stem, or a previous # attempt: not a problem, and not worth an error line. - logger.info('Document already absent: %s', target) - except OSError as exc: + logger.info('Document already absent: %s', stored_path) + except (GoogleDriveStorageError, OSError) as exc: logger.error( 'Could not remove %s (%s). It is now an orphan: no database row ' 'refers to it, so nothing in the application will ever offer to ' 'delete it again.', - target, + stored_path, exc, ) return removed diff --git a/app/templates/pages/admin.html b/app/templates/pages/admin.html index 73bb47a..818abd4 100644 --- a/app/templates/pages/admin.html +++ b/app/templates/pages/admin.html @@ -135,7 +135,10 @@ This removes all players from organization teams and deletes all regular-season matches. Team structures, coaches, and managers are preserved. A safety backup is created automatically.

- +
@@ -197,14 +200,14 @@ + data-confirm="Restore backup {{ b.filename }}? This will overwrite all current data. A safety backup will be made first.">
+ data-confirm="Delete backup {{ b.filename }}?">
{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/app/templates/pages/profile.html b/app/templates/pages/profile.html index 8a45c19..04a40e8 100644 --- a/app/templates/pages/profile.html +++ b/app/templates/pages/profile.html @@ -157,10 +157,10 @@

Loading...

- -
@@ -180,7 +180,7 @@

Loading availability grid...

-
@@ -650,6 +650,7 @@ document.addEventListener('DOMContentLoaded', function() { // dispatched by the delegated listener in main.js. This replaces inline // onclick attributes, which no CSP nonce is able to authorise. registerActions({ + 'save-disponibilities': saveDisponibilities, 'clear-disponibilities': clearDisponibilities, 'clear-availability': clearAllAvailability, }); diff --git a/app/translations/en/LC_MESSAGES/messages.mo b/app/translations/en/LC_MESSAGES/messages.mo index 278780b..bbd497e 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 6ce50a8..954f1f4 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-17 14:18-0400\n" +"POT-Creation-Date: 2026-08-25 18:15-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -19,12 +19,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: app/app.py:562 +#: 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:229 app/routes/auth.py:388 -#: app/routes/auth.py:402 app/routes/users/contracts.py:98 +#: 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 #, python-format msgid "%(field)s: %(msg)s" msgstr "%(field)s: %(msg)s" @@ -106,11 +106,11 @@ msgstr "Player must be selected." msgid "Notes must be 2000 characters or less." msgstr "Notes must be 2000 characters or less." -#: app/validators.py:513 app/validators.py:993 +#: app/validators.py:513 app/validators.py:994 msgid "Invalid coach selection." msgstr "Invalid coach selection." -#: app/validators.py:519 app/validators.py:999 +#: app/validators.py:519 app/validators.py:1000 msgid "Invalid manager selection." msgstr "Invalid manager selection." @@ -246,32 +246,32 @@ msgstr "A tryout must allow at least one player." msgid "The player limit must be a whole number." msgstr "The player limit must be a whole number." -#: app/validators.py:959 +#: app/validators.py:960 msgid "End date cannot be before start date." msgstr "End date cannot be before start date." -#: app/validators.py:986 app/validators.py:987 +#: app/validators.py:987 app/validators.py:988 msgid "Team name is required." msgstr "Team name is required." -#: app/validators.py:1011 +#: app/validators.py:1012 msgid "Scores run from 1 to 10." msgstr "Scores run from 1 to 10." -#: app/validators.py:1012 +#: 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:246 +#: app/routes/auth.py:247 msgid "This account has been deactivated." msgstr "This account has been deactivated." -#: app/routes/auth.py:281 +#: app/routes/auth.py:282 #, python-format msgid "Welcome back, %(username)s!" msgstr "Welcome back, %(username)s!" -#: app/routes/auth.py:311 +#: app/routes/auth.py:312 msgid "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." @@ -279,32 +279,32 @@ msgstr "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." -#: app/routes/auth.py:377 +#: 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:419 app/routes/users/accounts.py:343 +#: app/routes/auth.py:420 app/routes/users/accounts.py:343 msgid "Username already exists." msgstr "Username already exists." -#: app/routes/auth.py:423 app/routes/users/accounts.py:347 +#: app/routes/auth.py:424 app/routes/users/accounts.py:347 msgid "Email already registered." msgstr "Email already registered." -#: app/routes/auth.py:430 app/routes/auth.py:623 +#: 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:472 +#: 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:497 +#: app/routes/auth.py:498 msgid "Discord OAuth2 is not configured." msgstr "Discord OAuth2 is not configured." -#: app/routes/auth.py:546 +#: app/routes/auth.py:547 msgid "" "Discord authorization could not be verified. Please start the connection " "again from this page." @@ -312,72 +312,72 @@ msgstr "" "Discord authorization could not be verified. Please start the connection " "again from this page." -#: app/routes/auth.py:555 +#: app/routes/auth.py:556 msgid "Discord authorization failed. No code received." msgstr "Discord authorization failed. No code received." -#: app/routes/auth.py:579 +#: 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:583 +#: app/routes/auth.py:584 msgid "Failed to obtain Discord access token." msgstr "Failed to obtain Discord access token." -#: app/routes/auth.py:598 app/routes/auth.py:608 +#: 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:615 +#: 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:634 +#: app/routes/auth.py:635 msgid "Discord account connected!" msgstr "Discord account connected!" -#: app/routes/auth.py:681 +#: 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:709 +#: app/routes/auth.py:710 msgid "You have been logged out." msgstr "You have been logged out." -#: app/routes/evaluations.py:37 +#: 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:126 +#: 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:131 app/routes/evaluations.py:215 +#: 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:142 +#: 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:147 +#: app/routes/evaluations.py:164 msgid "Can only evaluate players." msgstr "Can only evaluate players." -#: app/routes/evaluations.py:191 +#: app/routes/evaluations.py:210 msgid "Evaluation submitted successfully!" msgstr "Evaluation submitted successfully!" -#: app/routes/evaluations.py:193 +#: app/routes/evaluations.py:212 msgid "Evaluation updated!" msgstr "Evaluation updated!" -#: app/routes/evaluations.py:210 app/routes/teams.py:341 -#: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455 -#: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:471 -#: app/routes/tryouts.py:491 app/routes/tryouts.py:515 -#: app/routes/tryouts.py:562 app/routes/tryouts.py:598 -#: app/routes/tryouts.py:621 +#: 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 +#: app/routes/tryouts.py:531 app/routes/tryouts.py:559 +#: app/routes/tryouts.py:610 app/routes/tryouts.py:650 +#: app/routes/tryouts.py:677 msgid "Permission denied." msgstr "Permission denied." @@ -397,15 +397,15 @@ msgstr "This tryout has ended. Matches can no longer be created or modified." msgid "Match scheduled successfully!" msgstr "Match scheduled successfully!" -#: app/routes/matches.py:459 app/routes/team_matches.py:220 +#: 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:250 +#: 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:265 +#: 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." @@ -413,249 +413,249 @@ msgstr "You do not have permission to delete this match." 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:269 +#: app/routes/matches.py:604 app/routes/team_matches.py:289 msgid "Match deleted successfully." msgstr "Match deleted successfully." -#: app/routes/team_matches.py:113 +#: 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:205 +#: 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:44 +#: 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:47 +#: 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:79 +#: app/routes/teams.py:78 msgid "This page is for players." msgstr "This page is for players." -#: app/routes/teams.py:186 +#: 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:197 app/routes/teams.py:239 +#: 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:218 +#: app/routes/teams.py:217 #, python-format msgid "Team \"%(name)s\" created successfully!" msgstr "Team \"%(name)s\" created successfully!" -#: app/routes/teams.py:228 +#: 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:272 +#: app/routes/teams.py:271 #, python-format msgid "Team \"%(name)s\" updated successfully!" msgstr "Team \"%(name)s\" updated successfully!" -#: app/routes/teams.py:300 +#: 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:331 +#: app/routes/teams.py:330 #, python-format msgid "Team \"%(name)s\" deleted successfully." msgstr "Team \"%(name)s\" deleted successfully." -#: app/routes/teams.py:348 +#: app/routes/teams.py:347 msgid "Please select a coach." msgstr "Please select a coach." -#: app/routes/teams.py:353 +#: 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:359 +#: 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:372 +#: 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:391 +#: app/routes/teams.py:390 msgid "Please select a manager." msgstr "Please select a manager." -#: app/routes/teams.py:396 +#: 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:402 +#: 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:415 +#: 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:445 +#: app/routes/teams.py:444 #, python-format msgid "Coach removed from %(name)s." msgstr "Coach removed from %(name)s." -#: app/routes/teams.py:473 +#: app/routes/teams.py:472 #, python-format msgid "Manager removed from %(name)s." msgstr "Manager removed from %(name)s." -#: app/routes/teams.py:490 app/routes/tryouts.py:524 +#: app/routes/teams.py:489 app/routes/tryouts.py:568 msgid "Please select a player." msgstr "Please select a player." -#: app/routes/teams.py:496 +#: app/routes/teams.py:495 msgid "Can only assign players to teams." msgstr "Can only assign players to teams." -#: app/routes/teams.py:502 +#: 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:510 +#: 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:527 app/routes/teams.py:605 +#: 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:535 +#: 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:572 app/routes/teams.py:594 +#: 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:584 +#: app/routes/teams.py:583 msgid "Team notes added successfully!" msgstr "Team notes added successfully!" -#: app/routes/teams.py:599 app/routes/users/notes.py:222 +#: 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:619 +#: app/routes/teams.py:618 #, python-format msgid "Note added for %(username)s!" msgstr "Note added for %(username)s!" -#: app/routes/tryouts.py:125 +#: 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:172 +#: app/routes/tryouts.py:194 msgid "Tryout created successfully!" msgstr "Tryout created successfully!" -#: app/routes/tryouts.py:185 +#: 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:189 +#: 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:229 +#: app/routes/tryouts.py:261 msgid "Tryout updated successfully!" msgstr "Tryout updated successfully!" -#: app/routes/tryouts.py:269 +#: 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:437 +#: app/routes/tryouts.py:469 msgid "Only players can register for tryouts." msgstr "Only players can register for tryouts." -#: app/routes/tryouts.py:442 +#: app/routes/tryouts.py:474 msgid "This tryout is not accepting registrations." msgstr "This tryout is not accepting registrations." -#: app/routes/tryouts.py:449 +#: 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:455 app/routes/tryouts.py:546 +#: app/routes/tryouts.py:487 app/routes/tryouts.py:590 msgid "This tryout is full." msgstr "This tryout is full." -#: app/routes/tryouts.py:461 +#: app/routes/tryouts.py:493 msgid "Successfully registered for tryout!" msgstr "Successfully registered for tryout!" -#: app/routes/tryouts.py:481 +#: 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:505 +#: app/routes/tryouts.py:545 msgid "Registration status updated." msgstr "Registration status updated." -#: app/routes/tryouts.py:532 +#: app/routes/tryouts.py:576 msgid "Can only register players." msgstr "Can only register players." -#: app/routes/tryouts.py:538 +#: 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:552 +#: app/routes/tryouts.py:596 #, python-format msgid "%(username)s registered for tryout!" msgstr "%(username)s registered for tryout!" -#: app/routes/tryouts.py:588 +#: app/routes/tryouts.py:636 #, python-format msgid "%(username)s removed from tryout." msgstr "%(username)s removed from tryout." -#: app/routes/tryouts.py:610 +#: app/routes/tryouts.py:662 #, python-format msgid "Team \"%(team_name)s\" created!" msgstr "Team \"%(team_name)s\" created!" -#: app/routes/tryouts.py:643 app/routes/users/notes.py:350 +#: 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:648 +#: app/routes/tryouts.py:704 msgid "Player is already on this team." msgstr "Player is already on this team." -#: app/routes/tryouts.py:653 +#: app/routes/tryouts.py:709 msgid "Player added to team!" msgstr "Player added to team!" -#: app/routes/tryouts.py:663 +#: 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:699 +#: app/routes/tryouts.py:759 msgid "Tryout deleted successfully." msgstr "Tryout deleted successfully." @@ -726,32 +726,37 @@ msgstr "User %(full_name)s created as %(role)s!" msgid "Only coaches can manage availability." msgstr "Only coaches can manage availability." -#: app/routes/users/contracts.py:86 +#: app/routes/users/contracts.py:91 msgid "Only presidents, managers, and coaches can upload contracts." msgstr "Only presidents, managers, and coaches can upload contracts." -#: app/routes/users/contracts.py:105 +#: app/routes/users/contracts.py:110 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:145 +#: app/routes/users/contracts.py:137 app/routes/users/contracts.py:180 +#: app/routes/users/contracts.py:203 app/routes/users/contracts.py:222 +msgid "Contract storage is temporarily unavailable. Please try again later." +msgstr "" + +#: app/routes/users/contracts.py:152 #, python-format msgid "Contract uploaded successfully for %(username)s!" msgstr "Contract uploaded successfully for %(username)s!" -#: app/routes/users/contracts.py:159 +#: app/routes/users/contracts.py:166 msgid "Only the player can upload their signed contract." msgstr "Only the player can upload their signed contract." -#: app/routes/users/contracts.py:177 +#: app/routes/users/contracts.py:188 msgid "Signed contract uploaded successfully!" msgstr "Signed contract uploaded successfully!" -#: app/routes/users/contracts.py:187 app/routes/users/contracts.py:202 +#: app/routes/users/contracts.py:198 app/routes/users/contracts.py:214 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:205 +#: app/routes/users/contracts.py:217 msgid "No signed contract available." msgstr "No signed contract available." @@ -842,48 +847,48 @@ msgstr "Invalid match context." 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:23 +#: 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:36 +#: 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:67 +#: 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:93 +#: 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:121 +#: 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:166 +#: 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:172 app/routes/users/one_on_one.py:222 +#: 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:176 app/routes/users/one_on_one.py:226 +#: 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:203 +#: 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:216 +#: 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:263 +#: 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." @@ -1045,7 +1050,7 @@ msgstr "%(total)s in total" msgid "Next" msgstr "Next" -#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:148 +#: 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" @@ -1080,39 +1085,39 @@ msgstr "Manage Teams" msgid "Manage Users" msgstr "Manage Users" -#: app/templates/layouts/base.html:98 +#: 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:106 +#: app/templates/layouts/base.html:112 msgid "Notes & One on One" msgstr "Notes & One on One" -#: app/templates/layouts/base.html:113 app/templates/pages/contracts.html:2 +#: 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:120 app/templates/pages/profile.html:2 +#: 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:131 +#: app/templates/layouts/base.html:137 msgid "Logout" msgstr "Logout" -#: app/templates/layouts/base.html:152 +#: app/templates/layouts/base.html:158 msgid "Toggle dark mode" msgstr "Toggle dark mode" -#: app/templates/layouts/base.html:164 app/templates/layouts/base.html:183 +#: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189 msgid "Dismiss" msgstr "Dismiss" -#: app/templates/layouts/base.html:193 +#: app/templates/layouts/base.html:199 msgid "UdeS team manager" msgstr "UdeS team manager" @@ -1130,16 +1135,15 @@ msgstr "Add Personal Note" #: app/templates/pages/dashboard.html:249 #: app/templates/pages/dashboard.html:363 app/templates/pages/my_teams.html:52 #: app/templates/pages/notes.html:122 -#: app/templates/pages/players_to_evaluate.html:16 #: app/templates/pages/team_match_form.html:114 -#: app/templates/pages/teams.html:145 app/templates/pages/view_tryout.html:148 -#: app/templates/pages/view_tryout.html:492 +#: app/templates/pages/teams.html:145 app/templates/pages/view_tryout.html:154 +#: 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:124 +#: app/templates/pages/view_tryout.html:130 msgid "-- Select a player --" msgstr "-- Select a player --" @@ -1184,11 +1188,11 @@ msgstr "Team Notes (Reference)" #: app/templates/pages/evaluate_player.html:131 #: app/templates/pages/notes.html:100 #: app/templates/pages/personal_notes.html:36 -#: app/templates/pages/view_tryout.html:303 +#: 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:296 +#: app/templates/pages/calendar.html:9 app/templates/pages/view_tryout.html:302 msgid "Schedule" msgstr "Schedule" @@ -1292,7 +1296,7 @@ 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:213 +#: app/templates/pages/profile.html:256 msgid "Loading availability grid..." msgstr "Loading availability grid..." @@ -1301,7 +1305,7 @@ msgid "Save Availability" msgstr "Save Availability" #: app/templates/pages/coach_availability.html:22 -#: app/templates/pages/profile.html:197 app/templates/pages/profile.html:217 +#: app/templates/pages/profile.html:240 app/templates/pages/profile.html:260 msgid "Clear All" msgstr "Clear All" @@ -1332,11 +1336,10 @@ msgstr "Contract" #: app/templates/pages/dashboard.html:321 #: app/templates/pages/match_form.html:60 app/templates/pages/my_teams.html:53 #: app/templates/pages/notes.html:126 app/templates/pages/one_on_one.html:73 -#: app/templates/pages/players_to_evaluate.html:19 #: app/templates/pages/team_match_form.html:61 #: app/templates/pages/team_matches.html:33 app/templates/pages/teams.html:146 -#: app/templates/pages/users.html:24 app/templates/pages/view_tryout.html:149 -#: app/templates/pages/view_tryout.html:321 +#: app/templates/pages/users.html:24 app/templates/pages/view_tryout.html:155 +#: app/templates/pages/view_tryout.html:327 msgid "Status" msgstr "Status" @@ -1345,11 +1348,10 @@ 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/players_to_evaluate.html:20 -#: app/templates/pages/team_matches.html:34 app/templates/pages/teams.html:151 -#: app/templates/pages/users.html:26 app/templates/pages/view_tryout.html:154 -#: app/templates/pages/view_tryout.html:323 +#: 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 +#: app/templates/pages/view_tryout.html:160 +#: app/templates/pages/view_tryout.html:329 msgid "Actions" msgstr "Actions" @@ -1406,7 +1408,7 @@ 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:239 +#: app/templates/pages/view_tryout.html:245 msgid "Cancel" msgstr "Cancel" @@ -1520,7 +1522,7 @@ msgstr "Title" #: app/templates/pages/notes.html:123 app/templates/pages/one_on_one.html:70 #: app/templates/pages/team_match_form.html:34 #: app/templates/pages/team_matches.html:29 -#: app/templates/pages/view_tryout.html:317 +#: app/templates/pages/view_tryout.html:323 msgid "Date" msgstr "Date" @@ -1533,7 +1535,7 @@ msgstr "Upcoming Matches" #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:90 #: app/templates/pages/notes.html:69 app/templates/pages/team_matches.html:26 -#: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:315 +#: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:321 msgid "Match" msgstr "Match" @@ -1616,7 +1618,7 @@ msgid "Avg Score" msgstr "Avg Score" #: app/templates/pages/dashboard.html:373 -#: app/templates/pages/view_tryout.html:526 +#: app/templates/pages/view_tryout.html:532 msgid "No evaluations yet." msgstr "No evaluations yet." @@ -1810,7 +1812,7 @@ msgid "Recommended Position" msgstr "Recommended Position" #: app/templates/pages/evaluate_player.html:109 -#: app/templates/pages/view_tryout.html:274 +#: app/templates/pages/view_tryout.html:280 msgid "-- Select Position --" msgstr "-- Select Position --" @@ -1831,63 +1833,63 @@ 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:493 +#: app/templates/pages/view_tryout.html:499 msgid "Evaluator" msgstr "Evaluator" #: app/templates/pages/evaluate_player.html:148 -#: app/templates/pages/view_tryout.html:494 +#: app/templates/pages/view_tryout.html:500 msgid "Mecanics" msgstr "Mecanics" #: app/templates/pages/evaluate_player.html:149 -#: app/templates/pages/view_tryout.html:495 +#: app/templates/pages/view_tryout.html:501 msgid "Cohesion" msgstr "Cohesion" #: app/templates/pages/evaluate_player.html:150 -#: app/templates/pages/view_tryout.html:496 +#: app/templates/pages/view_tryout.html:502 msgid "Communication" msgstr "Communication" #: app/templates/pages/evaluate_player.html:151 -#: app/templates/pages/view_tryout.html:497 +#: app/templates/pages/view_tryout.html:503 msgid "Gamesense" msgstr "Gamesense" #: app/templates/pages/evaluate_player.html:152 -#: app/templates/pages/view_tryout.html:498 +#: app/templates/pages/view_tryout.html:504 msgid "Versatility" msgstr "Versatility" #: app/templates/pages/evaluate_player.html:153 -#: app/templates/pages/view_tryout.html:499 +#: app/templates/pages/view_tryout.html:505 msgid "Discipline" msgstr "Discipline" #: app/templates/pages/evaluate_player.html:154 -#: app/templates/pages/view_tryout.html:500 +#: app/templates/pages/view_tryout.html:506 msgid "Analysis" msgstr "Analysis" #: app/templates/pages/evaluate_player.html:155 -#: app/templates/pages/view_tryout.html:501 +#: 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:502 +#: app/templates/pages/view_tryout.html:508 msgid "Mental" msgstr "Mental" #: app/templates/pages/evaluate_player.html:157 -#: app/templates/pages/view_tryout.html:503 +#: 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:280 +#: app/templates/pages/view_tryout.html:286 msgid "Position" msgstr "Position" @@ -1991,7 +1993,7 @@ msgstr "Reset time selection" msgid "Reset Time" msgstr "Reset Time" -#: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:193 +#: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:236 msgid "Loading..." msgstr "Loading..." @@ -2008,7 +2010,7 @@ msgid "End Time" msgstr "End Time" #: app/templates/pages/match_form.html:114 -#: app/templates/pages/tryout_form.html:103 +#: app/templates/pages/tryout_form.html:110 msgid "Description" msgstr "Description" @@ -2118,14 +2120,14 @@ 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:319 +#: 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:320 +#: app/templates/pages/view_tryout.html:326 msgid "Presence" msgstr "Presence" @@ -2376,18 +2378,6 @@ msgstr "Recent Notes" msgid "Players to Evaluate" msgstr "Players to Evaluate" -#: app/templates/pages/players_to_evaluate.html:17 -msgid "Contact" -msgstr "Contact" - -#: app/templates/pages/players_to_evaluate.html:18 -msgid "Attendance" -msgstr "Attendance" - -#: app/templates/pages/players_to_evaluate.html:51 -msgid "No players registered for this tryout." -msgstr "No players registered for this tryout." - #: app/templates/pages/profile.html:21 msgid "Account Information" msgstr "Account Information" @@ -2424,23 +2414,23 @@ msgstr "Evaluations Given" msgid "No statistics available for this role." msgstr "No statistics available for this role." -#: app/templates/pages/profile.html:152 +#: app/templates/pages/profile.html:195 msgid "My Contracts" msgstr "My Contracts" -#: app/templates/pages/profile.html:174 +#: app/templates/pages/profile.html:217 msgid "View All Contracts" msgstr "View All Contracts" -#: app/templates/pages/profile.html:178 +#: 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:188 +#: app/templates/pages/profile.html:231 msgid "My Disponibilities" msgstr "My Disponibilities" -#: app/templates/pages/profile.html:191 +#: app/templates/pages/profile.html:234 msgid "" "Select your available time blocks for matches (5pm to 12am). Green = " "selected, Gray = available to select." @@ -2448,11 +2438,11 @@ msgstr "" "Select your available time blocks for matches (5pm to 12am). Green = " "selected, Gray = available to select." -#: app/templates/pages/profile.html:208 +#: app/templates/pages/profile.html:251 msgid "My Coaching Availability" msgstr "My Coaching Availability" -#: app/templates/pages/profile.html:209 +#: app/templates/pages/profile.html:252 msgid "" "Select time slots when you're available for One on One sessions (8am to " "10pm)." @@ -2460,7 +2450,7 @@ msgstr "" "Select time slots when you're available for One on One sessions (8am to " "10pm)." -#: app/templates/pages/profile.html:457 +#: 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" @@ -2597,7 +2587,7 @@ 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:318 +#: app/templates/pages/view_tryout.html:324 msgid "Participants" msgstr "Participants" @@ -2643,11 +2633,11 @@ msgid "Note History" msgstr "Note History" #: app/templates/pages/teams.html:2 app/templates/pages/teams.html:3 -#: app/templates/pages/view_tryout.html:225 +#: app/templates/pages/view_tryout.html:231 msgid "Teams" msgstr "Teams" -#: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:228 +#: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:234 msgid "New Team" msgstr "New Team" @@ -2671,11 +2661,11 @@ msgstr "Assigned Coach" msgid "-- No coach assigned --" msgstr "-- No coach assigned --" -#: app/templates/pages/teams.html:38 app/templates/pages/tryout_form.html:70 +#: app/templates/pages/teams.html:38 msgid "Assigned Manager" msgstr "Assigned Manager" -#: app/templates/pages/teams.html:40 app/templates/pages/tryout_form.html:72 +#: app/templates/pages/teams.html:40 msgid "-- No manager assigned --" msgstr "-- No manager assigned --" @@ -2685,7 +2675,7 @@ 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:453 +#: app/templates/pages/view_tryout.html:459 msgid "Edit" msgstr "Edit" @@ -2745,7 +2735,7 @@ msgstr "Position / Role" 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:205 +#: app/templates/pages/teams.html:186 app/templates/pages/view_tryout.html:211 msgid "Remove" msgstr "Remove" @@ -2838,15 +2828,23 @@ msgstr "Target Team" msgid "-- No target team --" msgstr "-- No target team --" -#: app/templates/pages/tryout_form.html:83 +#: 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:99 +#: 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:104 +#: app/templates/pages/tryout_form.html:111 msgid "Enter any details about the tryout..." msgstr "Enter any details about the tryout..." @@ -2900,7 +2898,7 @@ msgid "Tryout Details" msgstr "Tryout Details" #: app/templates/pages/view_tryout.html:14 -#: app/templates/pages/view_tryout.html:300 +#: app/templates/pages/view_tryout.html:306 msgid "Schedule Match" msgstr "Schedule Match" @@ -2920,39 +2918,39 @@ msgstr "" msgid "Delete Tryout" msgstr "Delete Tryout" -#: app/templates/pages/view_tryout.html:108 +#: app/templates/pages/view_tryout.html:114 msgid "Register for this Tryout" msgstr "Register for this Tryout" -#: app/templates/pages/view_tryout.html:118 +#: app/templates/pages/view_tryout.html:124 msgid "Add Player to Tryout" msgstr "Add Player to Tryout" -#: app/templates/pages/view_tryout.html:131 +#: app/templates/pages/view_tryout.html:137 msgid "Register Player" msgstr "Register Player" -#: app/templates/pages/view_tryout.html:151 +#: app/templates/pages/view_tryout.html:157 msgid "Evaluation" msgstr "Evaluation" -#: app/templates/pages/view_tryout.html:173 +#: app/templates/pages/view_tryout.html:179 msgid "Registered" msgstr "Registered" -#: app/templates/pages/view_tryout.html:174 +#: app/templates/pages/view_tryout.html:180 msgid "Attended" msgstr "Attended" -#: app/templates/pages/view_tryout.html:175 +#: app/templates/pages/view_tryout.html:181 msgid "No Show" msgstr "No Show" -#: app/templates/pages/view_tryout.html:194 +#: app/templates/pages/view_tryout.html:200 msgid "Evaluate player" msgstr "Evaluate player" -#: app/templates/pages/view_tryout.html:202 +#: app/templates/pages/view_tryout.html:208 #, python-format msgid "" "Remove %(name)s from this tryout? They will also be removed from every " @@ -2961,63 +2959,63 @@ 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:204 +#: app/templates/pages/view_tryout.html:210 msgid "Remove player from tryout" msgstr "Remove player from tryout" -#: app/templates/pages/view_tryout.html:214 +#: app/templates/pages/view_tryout.html:220 msgid "No players registered yet." msgstr "No players registered yet." -#: app/templates/pages/view_tryout.html:237 +#: app/templates/pages/view_tryout.html:243 msgid "Team name" msgstr "Team name" -#: app/templates/pages/view_tryout.html:238 +#: app/templates/pages/view_tryout.html:244 msgid "Create" msgstr "Create" -#: app/templates/pages/view_tryout.html:257 +#: app/templates/pages/view_tryout.html:263 msgid "No players assigned yet." msgstr "No players assigned yet." -#: app/templates/pages/view_tryout.html:265 +#: app/templates/pages/view_tryout.html:271 msgid "Select player..." msgstr "Select player..." -#: app/templates/pages/view_tryout.html:282 +#: app/templates/pages/view_tryout.html:288 msgid "Add" msgstr "Add" -#: app/templates/pages/view_tryout.html:287 +#: app/templates/pages/view_tryout.html:293 msgid "No teams created yet." msgstr "No teams created yet." -#: app/templates/pages/view_tryout.html:316 +#: app/templates/pages/view_tryout.html:322 msgid "Type" msgstr "Type" -#: app/templates/pages/view_tryout.html:381 +#: app/templates/pages/view_tryout.html:387 msgid "Toggle your attendance" msgstr "Toggle your attendance" -#: app/templates/pages/view_tryout.html:456 +#: 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:457 +#: app/templates/pages/view_tryout.html:463 msgid "Note" msgstr "Note" -#: app/templates/pages/view_tryout.html:475 +#: 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:485 +#: app/templates/pages/view_tryout.html:491 msgid "Evaluation Summary" msgstr "Evaluation Summary" -#: app/templates/pages/view_tryout.html:504 +#: app/templates/pages/view_tryout.html:510 msgid "Recommendation" msgstr "Recommendation" @@ -3082,3 +3080,13 @@ msgstr "View Profile" #~ 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." + diff --git a/app/translations/fr/LC_MESSAGES/messages.mo b/app/translations/fr/LC_MESSAGES/messages.mo index 156d19d..5f69323 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 6bbdabb..1afe5ce 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-17 14:18-0400\n" +"POT-Creation-Date: 2026-08-25 18:15-0400\n" "PO-Revision-Date: 2026-08-07 20:22-0400\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -19,12 +19,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" -#: app/app.py:562 +#: 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:229 app/routes/auth.py:388 -#: app/routes/auth.py:402 app/routes/users/contracts.py:98 +#: 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 #, python-format msgid "%(field)s: %(msg)s" msgstr "%(field)s : %(msg)s" @@ -108,11 +108,11 @@ msgstr "Vous devez choisir un joueur." 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:993 +#: 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:999 +#: app/validators.py:519 app/validators.py:1000 msgid "Invalid manager selection." msgstr "Sélection de gérant invalide." @@ -248,32 +248,32 @@ msgstr "Une sélection doit accepter au moins un joueur." msgid "The player limit must be a whole number." msgstr "La limite de joueurs doit être un nombre entier." -#: app/validators.py:959 +#: 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:986 app/validators.py:987 +#: app/validators.py:987 app/validators.py:988 msgid "Team name is required." msgstr "Le nom de l’équipe est obligatoire." -#: app/validators.py:1011 +#: app/validators.py:1012 msgid "Scores run from 1 to 10." msgstr "Les notes vont de 1 à 10." -#: app/validators.py:1012 +#: 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:246 +#: app/routes/auth.py:247 msgid "This account has been deactivated." msgstr "Ce compte a été désactivé." -#: app/routes/auth.py:281 +#: app/routes/auth.py:282 #, python-format msgid "Welcome back, %(username)s!" msgstr "Bon retour, %(username)s !" -#: app/routes/auth.py:311 +#: app/routes/auth.py:312 msgid "" "Login unsuccessful. Please check your username and password, or ask a " "president for help." @@ -281,32 +281,32 @@ 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:377 +#: 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:419 app/routes/users/accounts.py:343 +#: 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:423 app/routes/users/accounts.py:347 +#: 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:430 app/routes/auth.py:623 +#: 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:472 +#: 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:497 +#: app/routes/auth.py:498 msgid "Discord OAuth2 is not configured." msgstr "La connexion Discord n’est pas configurée." -#: app/routes/auth.py:546 +#: app/routes/auth.py:547 msgid "" "Discord authorization could not be verified. Please start the connection " "again from this page." @@ -314,72 +314,72 @@ msgstr "" "L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion " "depuis cette page." -#: app/routes/auth.py:555 +#: 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:579 +#: 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:583 +#: 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:598 app/routes/auth.py:608 +#: 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:615 +#: 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:634 +#: app/routes/auth.py:635 msgid "Discord account connected!" msgstr "Compte Discord connecté !" -#: app/routes/auth.py:681 +#: 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:709 +#: app/routes/auth.py:710 msgid "You have been logged out." msgstr "Vous avez été déconnecté." -#: app/routes/evaluations.py:37 +#: 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:126 +#: 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:131 app/routes/evaluations.py:215 +#: 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:142 +#: 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:147 +#: app/routes/evaluations.py:164 msgid "Can only evaluate players." msgstr "Seuls des joueurs peuvent être évalués." -#: app/routes/evaluations.py:191 +#: app/routes/evaluations.py:210 msgid "Evaluation submitted successfully!" msgstr "Évaluation enregistrée." -#: app/routes/evaluations.py:193 +#: app/routes/evaluations.py:212 msgid "Evaluation updated!" msgstr "Évaluation mise à jour." -#: app/routes/evaluations.py:210 app/routes/teams.py:341 -#: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455 -#: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:471 -#: app/routes/tryouts.py:491 app/routes/tryouts.py:515 -#: app/routes/tryouts.py:562 app/routes/tryouts.py:598 -#: app/routes/tryouts.py:621 +#: 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 +#: app/routes/tryouts.py:531 app/routes/tryouts.py:559 +#: app/routes/tryouts.py:610 app/routes/tryouts.py:650 +#: app/routes/tryouts.py:677 msgid "Permission denied." msgstr "Accès refusé." @@ -401,15 +401,15 @@ msgstr "" msgid "Match scheduled successfully!" msgstr "Match planifié." -#: app/routes/matches.py:459 app/routes/team_matches.py:220 +#: 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:250 +#: 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:265 +#: 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." @@ -417,249 +417,249 @@ msgstr "Vous n’avez pas les droits pour supprimer ce match." 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:269 +#: app/routes/matches.py:604 app/routes/team_matches.py:289 msgid "Match deleted successfully." msgstr "Match supprimé." -#: app/routes/team_matches.py:113 +#: 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:205 +#: 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:44 +#: 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:47 +#: 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:79 +#: app/routes/teams.py:78 msgid "This page is for players." msgstr "Cette page est réservée aux joueurs." -#: app/routes/teams.py:186 +#: 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:197 app/routes/teams.py:239 +#: 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:218 +#: app/routes/teams.py:217 #, python-format msgid "Team \"%(name)s\" created successfully!" msgstr "Équipe « %(name)s » créée." -#: app/routes/teams.py:228 +#: 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:272 +#: app/routes/teams.py:271 #, python-format msgid "Team \"%(name)s\" updated successfully!" msgstr "Équipe « %(name)s » mise à jour." -#: app/routes/teams.py:300 +#: 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:331 +#: app/routes/teams.py:330 #, python-format msgid "Team \"%(name)s\" deleted successfully." msgstr "Équipe « %(name)s » supprimée." -#: app/routes/teams.py:348 +#: app/routes/teams.py:347 msgid "Please select a coach." msgstr "Veuillez choisir un coach." -#: app/routes/teams.py:353 +#: 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:359 +#: 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:372 +#: 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:391 +#: app/routes/teams.py:390 msgid "Please select a manager." msgstr "Veuillez choisir un gérant." -#: app/routes/teams.py:396 +#: 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:402 +#: 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:415 +#: 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:445 +#: app/routes/teams.py:444 #, python-format msgid "Coach removed from %(name)s." msgstr "Coach retiré de %(name)s." -#: app/routes/teams.py:473 +#: app/routes/teams.py:472 #, python-format msgid "Manager removed from %(name)s." msgstr "Gérant retiré de %(name)s." -#: app/routes/teams.py:490 app/routes/tryouts.py:524 +#: app/routes/teams.py:489 app/routes/tryouts.py:568 msgid "Please select a player." msgstr "Veuillez choisir un joueur." -#: app/routes/teams.py:496 +#: 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:502 +#: 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:510 +#: 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:527 app/routes/teams.py:605 +#: 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:535 +#: 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:572 app/routes/teams.py:594 +#: 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:584 +#: app/routes/teams.py:583 msgid "Team notes added successfully!" msgstr "Notes d’équipe ajoutées." -#: app/routes/teams.py:599 app/routes/users/notes.py:222 +#: 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:619 +#: app/routes/teams.py:618 #, python-format msgid "Note added for %(username)s!" msgstr "Note ajoutée pour %(username)s." -#: app/routes/tryouts.py:125 +#: 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:172 +#: app/routes/tryouts.py:194 msgid "Tryout created successfully!" msgstr "Sélection créée." -#: app/routes/tryouts.py:185 +#: 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:189 +#: 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:229 +#: app/routes/tryouts.py:261 msgid "Tryout updated successfully!" msgstr "Sélection mise à jour." -#: app/routes/tryouts.py:269 +#: 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:437 +#: 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:442 +#: 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:449 +#: 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:455 app/routes/tryouts.py:546 +#: 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:461 +#: app/routes/tryouts.py:493 msgid "Successfully registered for tryout!" msgstr "Inscription à la sélection réussie." -#: app/routes/tryouts.py:481 +#: 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:505 +#: app/routes/tryouts.py:545 msgid "Registration status updated." msgstr "Statut d’inscription mis à jour." -#: app/routes/tryouts.py:532 +#: app/routes/tryouts.py:576 msgid "Can only register players." msgstr "Seuls des joueurs peuvent être inscrits." -#: app/routes/tryouts.py:538 +#: 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:552 +#: 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:588 +#: 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:610 +#: app/routes/tryouts.py:662 #, python-format msgid "Team \"%(team_name)s\" created!" msgstr "Équipe « %(team_name)s » créée." -#: app/routes/tryouts.py:643 app/routes/users/notes.py:350 +#: 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:648 +#: 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:653 +#: app/routes/tryouts.py:709 msgid "Player added to team!" msgstr "Joueur ajouté à l’équipe." -#: app/routes/tryouts.py:663 +#: 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:699 +#: app/routes/tryouts.py:759 msgid "Tryout deleted successfully." msgstr "Sélection supprimée." @@ -732,32 +732,37 @@ msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s." msgid "Only coaches can manage availability." msgstr "Seuls les coachs peuvent gérer leurs disponibilités." -#: app/routes/users/contracts.py:86 +#: app/routes/users/contracts.py:91 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:105 +#: app/routes/users/contracts.py:110 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:145 +#: app/routes/users/contracts.py:137 app/routes/users/contracts.py:180 +#: app/routes/users/contracts.py:203 app/routes/users/contracts.py:222 +msgid "Contract storage is temporarily unavailable. Please try again later." +msgstr "" + +#: app/routes/users/contracts.py:152 #, python-format msgid "Contract uploaded successfully for %(username)s!" msgstr "Contrat téléversé pour %(username)s." -#: app/routes/users/contracts.py:159 +#: app/routes/users/contracts.py:166 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:177 +#: app/routes/users/contracts.py:188 msgid "Signed contract uploaded successfully!" msgstr "Contrat signé téléversé." -#: app/routes/users/contracts.py:187 app/routes/users/contracts.py:202 +#: app/routes/users/contracts.py:198 app/routes/users/contracts.py:214 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:205 +#: app/routes/users/contracts.py:217 msgid "No signed contract available." msgstr "Aucun contrat signé disponible." @@ -850,48 +855,48 @@ msgstr "Contexte de match invalide." 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:23 +#: 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:36 +#: 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:67 +#: 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:93 +#: 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:121 +#: 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:166 +#: 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:172 app/routes/users/one_on_one.py:222 +#: 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:176 app/routes/users/one_on_one.py:226 +#: 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:203 +#: 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:216 +#: 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:263 +#: 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." @@ -1051,7 +1056,7 @@ msgstr "%(total)s au total" msgid "Next" msgstr "Suivant" -#: app/templates/layouts/base.html:48 app/templates/layouts/base.html:148 +#: 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" @@ -1086,39 +1091,39 @@ msgstr "Gestion des équipes" msgid "Manage Users" msgstr "Gestion des utilisateurs" -#: app/templates/layouts/base.html:98 +#: 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:106 +#: app/templates/layouts/base.html:112 msgid "Notes & One on One" msgstr "Notes et rencontres individuelles" -#: app/templates/layouts/base.html:113 app/templates/pages/contracts.html:2 +#: 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:120 app/templates/pages/profile.html:2 +#: 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:131 +#: app/templates/layouts/base.html:137 msgid "Logout" msgstr "Déconnexion" -#: app/templates/layouts/base.html:152 +#: app/templates/layouts/base.html:158 msgid "Toggle dark mode" msgstr "Basculer le mode sombre" -#: app/templates/layouts/base.html:164 app/templates/layouts/base.html:183 +#: app/templates/layouts/base.html:170 app/templates/layouts/base.html:189 msgid "Dismiss" msgstr "Fermer" -#: app/templates/layouts/base.html:193 +#: app/templates/layouts/base.html:199 msgid "UdeS team manager" msgstr "UdeS team manager" @@ -1136,16 +1141,15 @@ msgstr "Ajouter une note personnelle" #: app/templates/pages/dashboard.html:249 #: app/templates/pages/dashboard.html:363 app/templates/pages/my_teams.html:52 #: app/templates/pages/notes.html:122 -#: app/templates/pages/players_to_evaluate.html:16 #: app/templates/pages/team_match_form.html:114 -#: app/templates/pages/teams.html:145 app/templates/pages/view_tryout.html:148 -#: app/templates/pages/view_tryout.html:492 +#: app/templates/pages/teams.html:145 app/templates/pages/view_tryout.html:154 +#: 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:124 +#: app/templates/pages/view_tryout.html:130 msgid "-- Select a player --" msgstr "-- Choisir un joueur --" @@ -1190,11 +1194,11 @@ msgstr "Notes d’équipe (référence)" #: app/templates/pages/evaluate_player.html:131 #: app/templates/pages/notes.html:100 #: app/templates/pages/personal_notes.html:36 -#: app/templates/pages/view_tryout.html:303 +#: 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:296 +#: app/templates/pages/calendar.html:9 app/templates/pages/view_tryout.html:302 msgid "Schedule" msgstr "Planifier" @@ -1300,7 +1304,7 @@ msgstr "" "individuelles" #: app/templates/pages/coach_availability.html:14 -#: app/templates/pages/profile.html:213 +#: app/templates/pages/profile.html:256 msgid "Loading availability grid..." msgstr "Chargement de la grille de disponibilités..." @@ -1309,7 +1313,7 @@ msgid "Save Availability" msgstr "Enregistrer les disponibilités" #: app/templates/pages/coach_availability.html:22 -#: app/templates/pages/profile.html:197 app/templates/pages/profile.html:217 +#: app/templates/pages/profile.html:240 app/templates/pages/profile.html:260 msgid "Clear All" msgstr "Tout effacer" @@ -1340,11 +1344,10 @@ msgstr "Contrat" #: app/templates/pages/dashboard.html:321 #: app/templates/pages/match_form.html:60 app/templates/pages/my_teams.html:53 #: app/templates/pages/notes.html:126 app/templates/pages/one_on_one.html:73 -#: app/templates/pages/players_to_evaluate.html:19 #: app/templates/pages/team_match_form.html:61 #: app/templates/pages/team_matches.html:33 app/templates/pages/teams.html:146 -#: app/templates/pages/users.html:24 app/templates/pages/view_tryout.html:149 -#: app/templates/pages/view_tryout.html:321 +#: app/templates/pages/users.html:24 app/templates/pages/view_tryout.html:155 +#: app/templates/pages/view_tryout.html:327 msgid "Status" msgstr "Statut" @@ -1353,11 +1356,10 @@ 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/players_to_evaluate.html:20 -#: app/templates/pages/team_matches.html:34 app/templates/pages/teams.html:151 -#: app/templates/pages/users.html:26 app/templates/pages/view_tryout.html:154 -#: app/templates/pages/view_tryout.html:323 +#: 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 +#: app/templates/pages/view_tryout.html:160 +#: app/templates/pages/view_tryout.html:329 msgid "Actions" msgstr "Actions" @@ -1412,7 +1414,7 @@ msgstr "Formats acceptés : 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:239 +#: app/templates/pages/view_tryout.html:245 msgid "Cancel" msgstr "Annuler" @@ -1526,7 +1528,7 @@ msgstr "Titre" #: app/templates/pages/notes.html:123 app/templates/pages/one_on_one.html:70 #: app/templates/pages/team_match_form.html:34 #: app/templates/pages/team_matches.html:29 -#: app/templates/pages/view_tryout.html:317 +#: app/templates/pages/view_tryout.html:323 msgid "Date" msgstr "Date" @@ -1539,7 +1541,7 @@ msgstr "Matchs à venir" #: app/templates/pages/dashboard.html:202 #: app/templates/pages/dashboard.html:285 app/templates/pages/my_teams.html:90 #: app/templates/pages/notes.html:69 app/templates/pages/team_matches.html:26 -#: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:315 +#: app/templates/pages/teams.html:132 app/templates/pages/view_tryout.html:321 msgid "Match" msgstr "Match" @@ -1622,7 +1624,7 @@ msgid "Avg Score" msgstr "Note moyenne" #: app/templates/pages/dashboard.html:373 -#: app/templates/pages/view_tryout.html:526 +#: app/templates/pages/view_tryout.html:532 msgid "No evaluations yet." msgstr "Aucune évaluation pour l’instant." @@ -1818,7 +1820,7 @@ msgid "Recommended Position" msgstr "Poste recommandé" #: app/templates/pages/evaluate_player.html:109 -#: app/templates/pages/view_tryout.html:274 +#: app/templates/pages/view_tryout.html:280 msgid "-- Select Position --" msgstr "-- Choisir un poste --" @@ -1839,63 +1841,63 @@ 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:493 +#: app/templates/pages/view_tryout.html:499 msgid "Evaluator" msgstr "Évaluateur" #: app/templates/pages/evaluate_player.html:148 -#: app/templates/pages/view_tryout.html:494 +#: 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:495 +#: 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:496 +#: app/templates/pages/view_tryout.html:502 msgid "Communication" msgstr "Communication" #: app/templates/pages/evaluate_player.html:151 -#: app/templates/pages/view_tryout.html:497 +#: 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:498 +#: app/templates/pages/view_tryout.html:504 msgid "Versatility" msgstr "Polyvalence" #: app/templates/pages/evaluate_player.html:153 -#: app/templates/pages/view_tryout.html:499 +#: app/templates/pages/view_tryout.html:505 msgid "Discipline" msgstr "Discipline" #: app/templates/pages/evaluate_player.html:154 -#: app/templates/pages/view_tryout.html:500 +#: app/templates/pages/view_tryout.html:506 msgid "Analysis" msgstr "Analyse" #: app/templates/pages/evaluate_player.html:155 -#: app/templates/pages/view_tryout.html:501 +#: 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:502 +#: app/templates/pages/view_tryout.html:508 msgid "Mental" msgstr "Mental" #: app/templates/pages/evaluate_player.html:157 -#: app/templates/pages/view_tryout.html:503 +#: 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:280 +#: app/templates/pages/view_tryout.html:286 msgid "Position" msgstr "Poste" @@ -2000,7 +2002,7 @@ msgstr "Réinitialiser la plage horaire" msgid "Reset Time" msgstr "Réinitialiser l’heure" -#: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:193 +#: app/templates/pages/match_form.html:86 app/templates/pages/profile.html:236 msgid "Loading..." msgstr "Chargement..." @@ -2017,7 +2019,7 @@ msgid "End Time" msgstr "Heure de fin" #: app/templates/pages/match_form.html:114 -#: app/templates/pages/tryout_form.html:103 +#: app/templates/pages/tryout_form.html:110 msgid "Description" msgstr "Description" @@ -2131,14 +2133,14 @@ 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:319 +#: 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:320 +#: app/templates/pages/view_tryout.html:326 msgid "Presence" msgstr "Présence" @@ -2389,18 +2391,6 @@ msgstr "Notes récentes" msgid "Players to Evaluate" msgstr "Joueurs à évaluer" -#: app/templates/pages/players_to_evaluate.html:17 -msgid "Contact" -msgstr "Coordonnées" - -#: app/templates/pages/players_to_evaluate.html:18 -msgid "Attendance" -msgstr "Présence" - -#: app/templates/pages/players_to_evaluate.html:51 -msgid "No players registered for this tryout." -msgstr "Aucun joueur inscrit à cette sélection." - #: app/templates/pages/profile.html:21 msgid "Account Information" msgstr "Informations du compte" @@ -2437,23 +2427,23 @@ msgstr "Évaluations données" msgid "No statistics available for this role." msgstr "Aucune statistique pour ce rôle." -#: app/templates/pages/profile.html:152 +#: app/templates/pages/profile.html:195 msgid "My Contracts" msgstr "Mes contrats" -#: app/templates/pages/profile.html:174 +#: app/templates/pages/profile.html:217 msgid "View All Contracts" msgstr "Voir tous les contrats" -#: app/templates/pages/profile.html:178 +#: 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:188 +#: app/templates/pages/profile.html:231 msgid "My Disponibilities" msgstr "Mes disponibilités" -#: app/templates/pages/profile.html:191 +#: app/templates/pages/profile.html:234 msgid "" "Select your available time blocks for matches (5pm to 12am). Green = " "selected, Gray = available to select." @@ -2461,11 +2451,11 @@ msgstr "" "Choisissez vos plages disponibles pour les matchs (17 h à minuit). Vert =" " sélectionné, gris = disponible." -#: app/templates/pages/profile.html:208 +#: app/templates/pages/profile.html:251 msgid "My Coaching Availability" msgstr "Mes disponibilités de coaching" -#: app/templates/pages/profile.html:209 +#: app/templates/pages/profile.html:252 msgid "" "Select time slots when you're available for One on One sessions (8am to " "10pm)." @@ -2473,7 +2463,7 @@ msgstr "" "Choisissez les plages où vous êtes disponible pour des rencontres " "individuelles (8 h à 22 h)." -#: app/templates/pages/profile.html:457 +#: 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é" @@ -2612,7 +2602,7 @@ 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:318 +#: app/templates/pages/view_tryout.html:324 msgid "Participants" msgstr "Participants" @@ -2658,11 +2648,11 @@ 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:225 +#: app/templates/pages/view_tryout.html:231 msgid "Teams" msgstr "Équipes" -#: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:228 +#: app/templates/pages/teams.html:9 app/templates/pages/view_tryout.html:234 msgid "New Team" msgstr "Nouvelle équipe" @@ -2686,11 +2676,11 @@ msgstr "Coach assigné" msgid "-- No coach assigned --" msgstr "-- Aucun coach assigné --" -#: app/templates/pages/teams.html:38 app/templates/pages/tryout_form.html:70 +#: app/templates/pages/teams.html:38 msgid "Assigned Manager" msgstr "Gérant assigné" -#: app/templates/pages/teams.html:40 app/templates/pages/tryout_form.html:72 +#: app/templates/pages/teams.html:40 msgid "-- No manager assigned --" msgstr "-- Aucun gérant assigné --" @@ -2700,7 +2690,7 @@ 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:453 +#: app/templates/pages/view_tryout.html:459 msgid "Edit" msgstr "Modifier" @@ -2762,7 +2752,7 @@ msgstr "Poste ou rôle" 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:205 +#: app/templates/pages/teams.html:186 app/templates/pages/view_tryout.html:211 msgid "Remove" msgstr "Retirer" @@ -2859,15 +2849,23 @@ msgstr "Équipe visée" msgid "-- No target team --" msgstr "-- Aucune équipe visée --" -#: app/templates/pages/tryout_form.html:83 +#: 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:99 +#: 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:104 +#: app/templates/pages/tryout_form.html:111 msgid "Enter any details about the tryout..." msgstr "Précisions sur la sélection..." @@ -2921,7 +2919,7 @@ msgid "Tryout Details" msgstr "Détail de la sélection" #: app/templates/pages/view_tryout.html:14 -#: app/templates/pages/view_tryout.html:300 +#: app/templates/pages/view_tryout.html:306 msgid "Schedule Match" msgstr "Planifier un match" @@ -2941,39 +2939,39 @@ msgstr "" msgid "Delete Tryout" msgstr "Supprimer la sélection" -#: app/templates/pages/view_tryout.html:108 +#: app/templates/pages/view_tryout.html:114 msgid "Register for this Tryout" msgstr "S’inscrire à cette sélection" -#: app/templates/pages/view_tryout.html:118 +#: 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:131 +#: app/templates/pages/view_tryout.html:137 msgid "Register Player" msgstr "Inscrire un joueur" -#: app/templates/pages/view_tryout.html:151 +#: app/templates/pages/view_tryout.html:157 msgid "Evaluation" msgstr "Évaluation" -#: app/templates/pages/view_tryout.html:173 +#: app/templates/pages/view_tryout.html:179 msgid "Registered" msgstr "Inscrit" -#: app/templates/pages/view_tryout.html:174 +#: app/templates/pages/view_tryout.html:180 msgid "Attended" msgstr "Présent" -#: app/templates/pages/view_tryout.html:175 +#: app/templates/pages/view_tryout.html:181 msgid "No Show" msgstr "Absent" -#: app/templates/pages/view_tryout.html:194 +#: app/templates/pages/view_tryout.html:200 msgid "Evaluate player" msgstr "Évaluer le joueur" -#: app/templates/pages/view_tryout.html:202 +#: app/templates/pages/view_tryout.html:208 #, python-format msgid "" "Remove %(name)s from this tryout? They will also be removed from every " @@ -2982,63 +2980,63 @@ 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:204 +#: 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:214 +#: 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:237 +#: app/templates/pages/view_tryout.html:243 msgid "Team name" msgstr "Nom de l’équipe" -#: app/templates/pages/view_tryout.html:238 +#: app/templates/pages/view_tryout.html:244 msgid "Create" msgstr "Créer" -#: app/templates/pages/view_tryout.html:257 +#: 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:265 +#: app/templates/pages/view_tryout.html:271 msgid "Select player..." msgstr "Choisir un joueur..." -#: app/templates/pages/view_tryout.html:282 +#: app/templates/pages/view_tryout.html:288 msgid "Add" msgstr "Ajouter" -#: app/templates/pages/view_tryout.html:287 +#: app/templates/pages/view_tryout.html:293 msgid "No teams created yet." msgstr "Aucune équipe créée." -#: app/templates/pages/view_tryout.html:316 +#: app/templates/pages/view_tryout.html:322 msgid "Type" msgstr "Type" -#: app/templates/pages/view_tryout.html:381 +#: app/templates/pages/view_tryout.html:387 msgid "Toggle your attendance" msgstr "Basculer votre présence" -#: app/templates/pages/view_tryout.html:456 +#: 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:457 +#: app/templates/pages/view_tryout.html:463 msgid "Note" msgstr "Note" -#: app/templates/pages/view_tryout.html:475 +#: 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:485 +#: app/templates/pages/view_tryout.html:491 msgid "Evaluation Summary" msgstr "Synthèse de l’évaluation" -#: app/templates/pages/view_tryout.html:504 +#: app/templates/pages/view_tryout.html:510 msgid "Recommendation" msgstr "Recommandation" @@ -3106,3 +3104,13 @@ msgstr "Voir le profil" #~ 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." + diff --git a/docs/deployment.md b/docs/deployment.md index a8e37f4..532cad2 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -67,6 +67,28 @@ documents every variable. Copying it verbatim gives a configuration that refuses to start until `SECRET_KEY` and `DATABASE_URL` are filled in, rather than one that starts and is wide open (OPS-003). +### Google Drive contract storage + +New contracts are stored in the owner's personal Google Drive when +`DOCUMENT_STORAGE_BACKEND=google_drive`. Enable the Google Drive API in a +Google Cloud project, create an OAuth client for the owner, and authorize it +once with the `drive.file` scope. Configure the resulting values outside the +repository: + +```text +DOCUMENT_STORAGE_BACKEND=google_drive +GOOGLE_DRIVE_FOLDER_ID= +GOOGLE_DRIVE_CLIENT_ID= +GOOGLE_DRIVE_CLIENT_SECRET= +GOOGLE_DRIVE_REFRESH_TOKEN= +``` + +The refresh token can create, download, and delete contracts created by this +application. Keep all four values out of source control. The database stores +only `gdrive://` references, so existing local-file rows continue to +work after the change. Use `DOCUMENT_STORAGE_BACKEND=local` only for tests, +local development, or while migrating legacy files. + ### Binding and proxy trust — read this before going live (OPS-002) Two variables decide whether the rate limiter, the account lockout and the diff --git a/pyproject.toml b/pyproject.toml index 91a98b1..b910ea5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ show_missing = true [tool.ruff] line-length = 100 -target-version = "py312" +target-version = "py313" exclude = [".venv", "venv", "migrations", "docs"] [tool.ruff.lint] diff --git a/requirements.txt b/requirements.txt index 610279c..36b574c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,8 @@ Flask-SQLAlchemy==3.1.1 Flask-WTF==1.3.0 frozenlist==1.8.0 greenlet==3.5.4 +google-api-python-client==2.198.0 +google-auth==2.56.3 idna==3.18 itsdangerous==2.2.0 Jinja2==3.1.6 diff --git a/tests/conftest.py b/tests/conftest.py index ccefa72..e043943 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,7 @@ def documents_in_a_throwaway_directory(tmp_path, monkeypatch): cannot leave a PDF in someone's working tree. """ monkeypatch.setenv('DOCUMENTS_ROOT', str(tmp_path / 'documents')) + monkeypatch.setenv('DOCUMENT_STORAGE_BACKEND', 'local') @pytest.fixture diff --git a/tests/test_database_url.py b/tests/test_database_url.py index 6ff9233..46b55c4 100644 --- a/tests/test_database_url.py +++ b/tests/test_database_url.py @@ -1,17 +1,17 @@ """Which PostgreSQL driver the application actually asks for — QUA-001. `postgresql://…` is not "whichever driver is installed". SQLAlchemy reads it -as psycopg2 and imports that module when the engine is created. -requirements.txt pins psycopg 3 and no psycopg2, so a clean install against -the URL Render hands out — the same form docs/database-restore.md documents -— fails before the first request: +as psycopg2 unless the URL names a driver. requirements.txt declares psycopg +3, so create_app() must explicitly select it. - ModuleNotFoundError: No module named 'psycopg2' - -create_app() now names the driver. These tests pin that down, and the last -one proves the failure is real rather than theoretical. +The test suite must not assume that psycopg2 is absent: a developer's virtual +environment can contain optional packages in addition to the declared +dependencies. These tests instead pin the application's selected driver and +the dependency contract that production installs use. """ +from pathlib import Path + import pytest from sqlalchemy import create_engine @@ -80,16 +80,13 @@ class TestTheFactory: assert app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite:///') -class TestTheFailureIsReal: - """Not a hypothetical: this is what the deployed configuration did.""" +class TestDriverContract: + def test_production_dependencies_select_psycopg_3(self): + requirements = Path(__file__).resolve().parents[1] / 'requirements.txt' + declared = requirements.read_text(encoding='utf-8') - def test_psycopg2_is_not_installed(self): - with pytest.raises(ImportError): - import psycopg2 # noqa: F401 - - def test_a_driverless_url_cannot_build_an_engine(self): - with pytest.raises(ModuleNotFoundError): - create_engine('postgresql://u:p@host/db') + assert 'psycopg[binary]==' in declared + assert 'psycopg2' not in declared def test_the_normalised_url_can(self): engine = create_engine(normalise_database_url('postgresql://u:p@host/db')) diff --git a/tests/test_storage.py b/tests/test_storage.py index e96197d..3b43f44 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -19,7 +19,13 @@ import os import pytest from app.extensions import db -from app.storage import CONTRACTS_DIR, document_path, documents_root +from app.storage import ( + CONTRACTS_DIR, + GOOGLE_DRIVE_PATH_PREFIX, + document_path, + documents_root, + store_uploaded_document, +) class TestDocumentsRoot: @@ -87,6 +93,22 @@ class TestDocumentPath: assert first_old == second_old, 'and must not move the ones already filed' +class TestGoogleDriveStorage: + def test_new_uploads_store_an_opaque_drive_reference(self, monkeypatch): + from werkzeug.datastructures import FileStorage + + monkeypatch.setenv('DOCUMENT_STORAGE_BACKEND', 'google_drive') + monkeypatch.setattr( + 'app.storage.upload_google_drive_file', + lambda **_kwargs: 'drive-file-123', + ) + upload = FileStorage(stream=_pdf(), filename='contract.pdf', content_type='application/pdf') + + stored_path = store_uploaded_document(upload, os.path.join(CONTRACTS_DIR, 'new.pdf')) + + assert stored_path == f'{GOOGLE_DRIVE_PATH_PREFIX}drive-file-123' + + class TestThroughTheUploadRoute: @pytest.fixture def uploaded(self, app, client, as_role, make_user):