fix(audit): fermer les frontieres restantes
This commit is contained in:
@@ -61,3 +61,33 @@ def form_payload(*, checkboxes=(), list_fields=('games',), optional_blank=('pass
|
||||
if not payload.get(name):
|
||||
payload.pop(name, None)
|
||||
return payload
|
||||
|
||||
|
||||
def form_gamertags(selected_games):
|
||||
"""Validate the dynamic gamertag fields for the selected games.
|
||||
|
||||
These fields cannot be declared statically on the account schemas: their
|
||||
names contain the game label. They are still untrusted form data, so
|
||||
every caller uses this shared boundary before adding or changing rows.
|
||||
"""
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from app.models import GAME_PLATFORMS
|
||||
from app.validators import GamertagSchema
|
||||
|
||||
validated = {}
|
||||
for game in selected_games:
|
||||
raw_gamertag = request.form.get(f'gamertag_{game}', '')
|
||||
raw_platform = (
|
||||
request.form.get(f'platform_{game}', '') if GAME_PLATFORMS.get(game) else None
|
||||
)
|
||||
if not raw_gamertag.strip():
|
||||
continue
|
||||
try:
|
||||
validated[game] = GamertagSchema().load(
|
||||
{'game': game, 'gamertag': raw_gamertag, 'platform': raw_platform}
|
||||
)
|
||||
except ValidationError as err:
|
||||
messages = [message for values in err.messages.values() for message in values]
|
||||
raise ValidationError({f'gamertag_{game}': messages}) from err
|
||||
return validated
|
||||
|
||||
+16
-10
@@ -18,6 +18,7 @@ from flask_login import current_user, login_required, login_user, logout_user
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from app.extensions import check_password, db, hash_password, limiter
|
||||
from app.forms import form_gamertags
|
||||
from app.i18n import LOCALE_SESSION_KEY
|
||||
from app.logging_config import log_auth_event
|
||||
from app.models import ESPORT_GAMES, Player, User
|
||||
@@ -393,6 +394,13 @@ def register():
|
||||
full_name = validated['full_name']
|
||||
phone = validated.get('phone')
|
||||
selected_games = validated.get('games', [])
|
||||
try:
|
||||
submitted_gamertags = form_gamertags(selected_games)
|
||||
except ValidationError as err:
|
||||
for field, messages in err.messages.items():
|
||||
for msg in messages:
|
||||
flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger')
|
||||
return _rerender_registration(form_data)
|
||||
# The OAuth identity is server-side state. It used to be copied into
|
||||
# hidden inputs and read back from request.form, which let anyone
|
||||
# replace the verified Discord account before submitting (SEC-AUTH-005).
|
||||
@@ -445,16 +453,14 @@ def register():
|
||||
# Create UserGamertag records for each selected game
|
||||
from app.models import UserGamertag
|
||||
|
||||
for game in selected_games:
|
||||
field_name = f'gamertag_{game}'
|
||||
gamertag_value = request.form.get(field_name, '').strip()
|
||||
if gamertag_value:
|
||||
gamertag = UserGamertag(
|
||||
user_id=user.id,
|
||||
game=game,
|
||||
gamertag=gamertag_value,
|
||||
)
|
||||
db.session.add(gamertag)
|
||||
for game, gamertag_data in submitted_gamertags.items():
|
||||
gamertag = UserGamertag(
|
||||
user_id=user.id,
|
||||
game=game,
|
||||
gamertag=gamertag_data['gamertag'],
|
||||
platform=gamertag_data['platform'],
|
||||
)
|
||||
db.session.add(gamertag)
|
||||
db.session.commit()
|
||||
|
||||
# Clear Discord OAuth data from session after successful registration
|
||||
|
||||
+21
-8
@@ -46,6 +46,25 @@ def match_form_payload():
|
||||
return form_payload(list_fields=('player_ids',), optional_blank=())
|
||||
|
||||
|
||||
def registered_players(tryout_id):
|
||||
"""Players registered for one tryout, loaded in a single query.
|
||||
|
||||
The create form previously called ``User.query.get`` twice per
|
||||
registration (once in the filter and once in the result expression),
|
||||
and the edit form called it once per row. Besides scaling linearly, both
|
||||
paths could return duplicates while DB-006 is still pending. The join is
|
||||
bounded and ``distinct`` preserves the form's intended one-option-per-
|
||||
player contract until the database constraint lands.
|
||||
"""
|
||||
return (
|
||||
User.query.join(TryoutRegistration, TryoutRegistration.player_id == User.id)
|
||||
.filter(TryoutRegistration.tryout_id == tryout_id)
|
||||
.order_by(User.username)
|
||||
.distinct()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
#: How long a match lasts when the form gives a start and no end.
|
||||
DEFAULT_MATCH_MINUTES = 30
|
||||
|
||||
@@ -369,11 +388,7 @@ def create_match(tryout_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
||||
all_players = [
|
||||
User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)
|
||||
]
|
||||
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
|
||||
all_players = registered_players(tryout_id)
|
||||
prefill_date = request.args.get('date', '')
|
||||
|
||||
def rerender():
|
||||
@@ -449,9 +464,7 @@ def edit_match(match_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout.id).all()
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all()
|
||||
all_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
|
||||
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
|
||||
all_players = registered_players(tryout.id)
|
||||
current_player_ids = [p.player_id for p in match.participants.all()]
|
||||
team1_player_ids = [p.player_id for p in match.participants.filter_by(team_side=1).all()]
|
||||
team2_player_ids = [p.player_id for p in match.participants.filter_by(team_side=2).all()]
|
||||
|
||||
+22
-14
@@ -5,7 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, flash, jsonify, redirect, render_template, url_for
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
from marshmallow import ValidationError
|
||||
@@ -29,7 +29,7 @@ from app.models import (
|
||||
User,
|
||||
)
|
||||
from app.permissions import visible_org_teams
|
||||
from app.validators import OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
|
||||
from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
|
||||
|
||||
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
|
||||
|
||||
@@ -572,12 +572,16 @@ def add_team_note(team_id):
|
||||
flash(_('You do not have permission to add notes to this team.'), 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
content = request.form.get('content', '').strip()
|
||||
if content:
|
||||
note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=content)
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Team notes added successfully!'), 'success')
|
||||
try:
|
||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=data['content'])
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Team notes added successfully!'), 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@@ -603,10 +607,14 @@ def add_player_note(team_id, player_id):
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
content = request.form.get('content', '').strip()
|
||||
if content:
|
||||
note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=content)
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Note added for %(username)s!', username=player.username), 'success')
|
||||
try:
|
||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=data['content'])
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Note added for %(username)s!', username=player.username), 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
+64
-24
@@ -10,6 +10,7 @@ from flask import Blueprint, abort, flash, redirect, render_template, request, u
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
from marshmallow import ValidationError
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.extensions import db
|
||||
from app.forms import flash_validation_errors, form_payload
|
||||
@@ -32,7 +33,14 @@ from app.models import (
|
||||
TryoutRegistration,
|
||||
User,
|
||||
)
|
||||
from app.validators import PlayerSelectionSchema, TryoutSchema
|
||||
from app.validators import (
|
||||
PlayerSelectionSchema,
|
||||
TryoutRegistrationStatusSchema,
|
||||
TryoutSchema,
|
||||
TryoutStatusSchema,
|
||||
TryoutTeamMemberSchema,
|
||||
TryoutTeamSchema,
|
||||
)
|
||||
|
||||
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
||||
|
||||
@@ -79,6 +87,25 @@ def _users_by_id(user_ids):
|
||||
return {user.id: user for user in User.query.filter(User.id.in_(wanted)).all()}
|
||||
|
||||
|
||||
def registration_lock_statement(tryout_id):
|
||||
"""The PostgreSQL row lock used by both registration entry points."""
|
||||
return select(Tryout).where(Tryout.id == tryout_id).with_for_update()
|
||||
|
||||
|
||||
def locked_tryout_or_404(tryout_id):
|
||||
"""Load and row-lock a tryout while a registration slot is decided.
|
||||
|
||||
PostgreSQL serializes concurrent registration attempts on this row. The
|
||||
duplicate check, capacity count and insert that follow therefore form
|
||||
one decision instead of three independently racing statements. SQLite
|
||||
ignores ``FOR UPDATE`` in tests, but production does not.
|
||||
"""
|
||||
tryout = db.session.execute(registration_lock_statement(tryout_id)).scalar_one_or_none()
|
||||
if tryout is None:
|
||||
abort(404)
|
||||
return tryout
|
||||
|
||||
|
||||
@tryouts_bp.route('')
|
||||
@login_required
|
||||
def list_tryouts():
|
||||
@@ -406,10 +433,10 @@ def view_tryout(tryout_id):
|
||||
@login_required
|
||||
def register_for_tryout(tryout_id):
|
||||
"""Register a player for a tryout. Only Players can self-register."""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not isinstance(current_user, Player):
|
||||
flash(_('Only players can register for tryouts.'), 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
tryout = locked_tryout_or_404(tryout_id)
|
||||
|
||||
if tryout.status not in ['upcoming', 'in_progress']:
|
||||
flash(_('This tryout is not accepting registrations.'), 'danger')
|
||||
@@ -443,11 +470,15 @@ def update_status(tryout_id):
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash(_('Permission denied.'), 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
new_status = request.form.get('status')
|
||||
if new_status in ['upcoming', 'in_progress', 'completed']:
|
||||
tryout.status = new_status
|
||||
db.session.commit()
|
||||
flash(_('Tryout status updated to %(new_status)s.', new_status=new_status), 'success')
|
||||
try:
|
||||
data = TryoutStatusSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
tryout.status = data['status']
|
||||
db.session.commit()
|
||||
flash(_('Tryout status updated to %(new_status)s.', new_status=data['status']), 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@@ -463,11 +494,15 @@ def update_registration_status(tryout_id, player_id):
|
||||
registration = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id
|
||||
).first_or_404()
|
||||
new_status = request.form.get('status')
|
||||
if new_status in ['registered', 'attended', 'no_show']:
|
||||
registration.status = new_status
|
||||
db.session.commit()
|
||||
flash(_('Registration status updated.'), 'success')
|
||||
try:
|
||||
data = TryoutRegistrationStatusSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
registration.status = data['status']
|
||||
db.session.commit()
|
||||
flash(_('Registration status updated.'), 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@@ -475,7 +510,7 @@ def update_registration_status(tryout_id, player_id):
|
||||
@login_required
|
||||
def register_player(tryout_id):
|
||||
"""Manually register a player for a tryout (by managers/coaches)."""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
tryout = locked_tryout_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash(_('Permission denied.'), 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
@@ -563,12 +598,16 @@ def create_team(tryout_id):
|
||||
flash(_('Permission denied.'), 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
team_name = request.form.get('team_name')
|
||||
if team_name:
|
||||
team = Team(tryout_id=tryout_id, name=team_name, created_by=current_user.id)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
flash(_('Team "%(team_name)s" created!', team_name=team_name), 'success')
|
||||
try:
|
||||
data = TryoutTeamSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
team = Team(tryout_id=tryout_id, name=data['team_name'], created_by=current_user.id)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
flash(_('Team "%(team_name)s" created!', team_name=data['team_name']), 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@@ -588,10 +627,12 @@ def add_to_team(tryout_id, team_id):
|
||||
if team.tryout_id != tryout_id:
|
||||
abort(404)
|
||||
|
||||
player_id = request.form.get('player_id', type=int)
|
||||
if not player_id:
|
||||
flash(_('Please select a player.'), 'danger')
|
||||
try:
|
||||
data = TryoutTeamMemberSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
player_id = data['player_id']
|
||||
|
||||
# Only players registered for this tryout may be placed on its teams.
|
||||
is_registered = (
|
||||
@@ -602,12 +643,11 @@ def add_to_team(tryout_id, team_id):
|
||||
flash(_('That player is not registered for this tryout.'), 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
position = request.form.get('position', '')
|
||||
existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first()
|
||||
if existing:
|
||||
flash(_('Player is already on this team.'), 'info')
|
||||
else:
|
||||
member = TeamMember(team_id=team_id, player_id=player_id, position=position)
|
||||
member = TeamMember(team_id=team_id, player_id=player_id, position=data['position'])
|
||||
db.session.add(member)
|
||||
db.session.commit()
|
||||
flash(_('Player added to team!'), 'success')
|
||||
|
||||
+15
-11
@@ -4,7 +4,6 @@ Nothing here touches the blueprint: these are plain functions, so a test
|
||||
can call them with a request context and nothing else.
|
||||
"""
|
||||
|
||||
from flask import request
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from app.extensions import db
|
||||
@@ -13,7 +12,7 @@ from app.extensions import db
|
||||
# routes needed them as well (ARCH-005). Importing them from here still
|
||||
# works, so the thirty call sites in this package did not have to move.
|
||||
from app.forms import flash_validation_errors, form_payload # noqa: F401
|
||||
from app.models import GAME_PLATFORMS, Admin, Coach, Manager, Player, Scout, UserGamertag
|
||||
from app.models import Admin, Coach, Manager, Player, Scout, UserGamertag
|
||||
|
||||
ALLOWED_CONTRACT_EXTENSIONS = {'pdf'}
|
||||
ALLOWED_SIGNED_EXTENSIONS = {'pdf'}
|
||||
@@ -63,20 +62,25 @@ def pdf_upload_error(file, allowed_extensions):
|
||||
|
||||
|
||||
def update_user_gamertags(user, selected_games):
|
||||
"""Update gamertags for a user based on form input."""
|
||||
"""Update gamertags for a user from validated dynamic form fields."""
|
||||
from app.forms import form_gamertags
|
||||
|
||||
submitted = form_gamertags(selected_games)
|
||||
existing_gamertags = {gt.game: gt for gt in user.gamertags}
|
||||
for game in selected_games:
|
||||
gamertag = request.form.get(f'gamertag_{game}', '').strip()
|
||||
platform = (
|
||||
request.form.get(f'platform_{game}', '').strip() if GAME_PLATFORMS.get(game) else None
|
||||
)
|
||||
payload = submitted.get(game)
|
||||
existing = existing_gamertags.get(game)
|
||||
if gamertag:
|
||||
if payload:
|
||||
if existing:
|
||||
existing.gamertag = gamertag
|
||||
existing.platform = platform
|
||||
existing.gamertag = payload['gamertag']
|
||||
existing.platform = payload['platform']
|
||||
else:
|
||||
gt = UserGamertag(user_id=user.id, game=game, gamertag=gamertag, platform=platform)
|
||||
gt = UserGamertag(
|
||||
user_id=user.id,
|
||||
game=game,
|
||||
gamertag=payload['gamertag'],
|
||||
platform=payload['platform'],
|
||||
)
|
||||
db.session.add(gt)
|
||||
elif existing:
|
||||
db.session.delete(existing)
|
||||
|
||||
@@ -121,6 +121,12 @@ def edit_user(user_id):
|
||||
flash(_('This Discord account is already linked to another account.'), 'danger')
|
||||
return _rerender()
|
||||
|
||||
try:
|
||||
update_user_gamertags(user, selected_games)
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return _rerender()
|
||||
|
||||
role_changed = user.role != role
|
||||
previous_role = user.role
|
||||
|
||||
@@ -183,8 +189,6 @@ def edit_user(user_id):
|
||||
user.discord_user_id = discord_user_id or None
|
||||
user.league_os_profile = league_os_profile or None
|
||||
|
||||
update_user_gamertags(user, selected_games)
|
||||
|
||||
# Blank means "keep the current password"; anything else has already
|
||||
# been checked against the policy by the schema.
|
||||
password = validated.get('password')
|
||||
|
||||
+112
-51
@@ -7,23 +7,32 @@ it reads exactly what the coach routes write.
|
||||
from flask import flash, redirect, render_template, request, url_for
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from app.extensions import db
|
||||
from app.forms import flash_validation_errors, form_payload
|
||||
from app.models import (
|
||||
Coach,
|
||||
Match,
|
||||
MatchParticipant,
|
||||
OneOnOneRequest,
|
||||
OrgTeam,
|
||||
PersonalNote,
|
||||
Player,
|
||||
Team,
|
||||
TeamMember,
|
||||
TeamNote,
|
||||
Tryout,
|
||||
TryoutRegistration,
|
||||
User,
|
||||
)
|
||||
from app.permissions import coach_can_access_player, coach_org_teams, coach_player_ids
|
||||
from app.permissions import (
|
||||
coach_can_access_player,
|
||||
coach_org_teams,
|
||||
coach_player_ids,
|
||||
coach_tryouts,
|
||||
)
|
||||
from app.routes.users.blueprint import users_bp
|
||||
from app.validators import NoteContentSchema, PersonalNoteSchema
|
||||
|
||||
|
||||
@users_bp.route('/my-notes')
|
||||
@@ -116,23 +125,25 @@ def notes_dashboard():
|
||||
)
|
||||
|
||||
# For context selectors in the form
|
||||
# PersonalNote.team_id references a tryout-local Team, not OrgTeam. The
|
||||
# previous selector mixed the two namespaces and could either attach the
|
||||
# note to an unrelated team with the same integer id or fail its FK.
|
||||
# Every context list now comes from the tryouts this coach may manage.
|
||||
tryouts = list(reversed(coach_tryouts(current_user)))[:20]
|
||||
tryout_ids = [tryout.id for tryout in tryouts]
|
||||
matches = (
|
||||
Match.query.filter(
|
||||
db.or_(Match.created_by == current_user.id, Match.status == 'scheduled'),
|
||||
)
|
||||
Match.query.filter(Match.tryout_id.in_(tryout_ids))
|
||||
.order_by(Match.date.desc())
|
||||
.limit(20)
|
||||
.all()
|
||||
if tryout_ids
|
||||
else []
|
||||
)
|
||||
tryouts = (
|
||||
Tryout.query.filter_by(
|
||||
created_by=current_user.id,
|
||||
)
|
||||
.order_by(Tryout.date.desc())
|
||||
.limit(20)
|
||||
.all()
|
||||
teams = (
|
||||
Team.query.filter(Team.tryout_id.in_(tryout_ids)).order_by(Team.name).all()
|
||||
if tryout_ids
|
||||
else []
|
||||
)
|
||||
teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
|
||||
return render_template(
|
||||
'pages/notes.html',
|
||||
@@ -168,16 +179,20 @@ def manage_team_notes():
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
org_team = org_teams[0]
|
||||
|
||||
content = request.form.get('content', '').strip()
|
||||
if content:
|
||||
note = TeamNote(
|
||||
org_team_id=org_team.id,
|
||||
coach_id=current_user.id,
|
||||
content=content,
|
||||
)
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Team notes saved successfully!'), 'success')
|
||||
try:
|
||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
note = TeamNote(
|
||||
org_team_id=org_team.id,
|
||||
coach_id=current_user.id,
|
||||
content=data['content'],
|
||||
)
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Team notes saved successfully!'), 'success')
|
||||
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
@@ -195,12 +210,12 @@ def manage_personal_notes():
|
||||
flash(_('Only coaches can manage personal notes.'), 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
player_id = request.form.get('player_id', type=int)
|
||||
content = request.form.get('content', '').strip()
|
||||
|
||||
if not player_id or not content:
|
||||
flash(_('Player and content are required.'), 'danger')
|
||||
try:
|
||||
data = PersonalNoteSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
player_id = data['player_id']
|
||||
|
||||
player = User.query.get_or_404(player_id)
|
||||
if not isinstance(player, Player):
|
||||
@@ -214,7 +229,7 @@ def manage_personal_notes():
|
||||
note = PersonalNote(
|
||||
player_id=player_id,
|
||||
coach_id=current_user.id,
|
||||
content=content,
|
||||
content=data['content'],
|
||||
)
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
@@ -235,15 +250,12 @@ def add_personal_note():
|
||||
flash(_('Only coaches can add personal notes.'), 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
player_id = request.form.get('player_id', type=int)
|
||||
content = request.form.get('content', '').strip()
|
||||
match_id = request.form.get('match_id', type=int)
|
||||
tryout_id = request.form.get('tryout_id', type=int)
|
||||
team_id_str = request.form.get('team_id')
|
||||
|
||||
if not player_id or not content:
|
||||
flash(_('Player and content are required.'), 'danger')
|
||||
try:
|
||||
data = PersonalNoteSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
player_id = data['player_id']
|
||||
|
||||
player = User.query.get_or_404(player_id)
|
||||
if not isinstance(player, Player):
|
||||
@@ -254,13 +266,40 @@ def add_personal_note():
|
||||
flash(_('You can only write notes about players you work with.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
if data['match_id']:
|
||||
match = Match.query.get_or_404(data['match_id'])
|
||||
if not current_user.can_manage_this_tryout(match.tryout):
|
||||
flash(_('You cannot use that match as note context.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
if not MatchParticipant.query.filter_by(match_id=match.id, player_id=player_id).first():
|
||||
flash(_('That player did not participate in the selected match.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
if data['tryout_id']:
|
||||
tryout = Tryout.query.get_or_404(data['tryout_id'])
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash(_('You cannot use that tryout as note context.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
if not TryoutRegistration.query.filter_by(tryout_id=tryout.id, player_id=player_id).first():
|
||||
flash(_('That player is not registered for the selected tryout.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
if data['team_id']:
|
||||
team = Team.query.get_or_404(data['team_id'])
|
||||
if not current_user.can_manage_this_tryout(team.tryout):
|
||||
flash(_('You cannot use that team as note context.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
if not TeamMember.query.filter_by(team_id=team.id, player_id=player_id).first():
|
||||
flash(_('That player is not on the selected team.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
note = PersonalNote(
|
||||
player_id=player_id,
|
||||
coach_id=current_user.id,
|
||||
content=content,
|
||||
match_id=match_id if match_id else None,
|
||||
tryout_id=tryout_id if tryout_id else None,
|
||||
team_id=int(team_id_str) if team_id_str and team_id_str.isdigit() else None,
|
||||
content=data['content'],
|
||||
match_id=data['match_id'],
|
||||
tryout_id=data['tryout_id'],
|
||||
team_id=data['team_id'],
|
||||
)
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
@@ -282,6 +321,9 @@ def add_note_from_tryout(tryout_id):
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash(_('You do not have permission to add notes for this tryout.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
preselected_player_id = request.args.get('player_id', type=int)
|
||||
|
||||
# Get registrations as players for the select list
|
||||
@@ -289,21 +331,29 @@ def add_note_from_tryout(tryout_id):
|
||||
players = [r.player for r in registrations if r.player]
|
||||
|
||||
if request.method == 'POST':
|
||||
player_id = request.form.get('player_id', type=int)
|
||||
content = request.form.get('content', '').strip()
|
||||
try:
|
||||
data = PersonalNoteSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id))
|
||||
player_id = data['player_id']
|
||||
|
||||
if not player_id or not content:
|
||||
flash(_('Player and content are required.'), 'danger')
|
||||
if data['tryout_id'] not in (None, tryout_id):
|
||||
flash(_('Invalid tryout context.'), 'danger')
|
||||
return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id))
|
||||
|
||||
if not coach_can_access_player(current_user, player_id):
|
||||
flash(_('You can only write notes about players you work with.'), 'danger')
|
||||
return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id))
|
||||
|
||||
if not TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player_id).first():
|
||||
flash(_('That player is not registered for this tryout.'), 'danger')
|
||||
return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id))
|
||||
|
||||
note = PersonalNote(
|
||||
player_id=player_id,
|
||||
coach_id=current_user.id,
|
||||
content=content,
|
||||
content=data['content'],
|
||||
tryout_id=tryout_id,
|
||||
)
|
||||
db.session.add(note)
|
||||
@@ -335,6 +385,9 @@ def add_note_from_match(match_id):
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
match_obj = Match.query.get_or_404(match_id)
|
||||
if not current_user.can_manage_this_tryout(match_obj.tryout):
|
||||
flash(_('You do not have permission to add notes for this match.'), 'danger')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
# Get participants as players for the select list
|
||||
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
|
||||
@@ -343,21 +396,29 @@ def add_note_from_match(match_id):
|
||||
preselected_player_id = request.args.get('player_id', type=int)
|
||||
|
||||
if request.method == 'POST':
|
||||
player_id = request.form.get('player_id', type=int)
|
||||
content = request.form.get('content', '').strip()
|
||||
try:
|
||||
data = PersonalNoteSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('users.add_note_from_match', match_id=match_id))
|
||||
player_id = data['player_id']
|
||||
|
||||
if not player_id or not content:
|
||||
flash(_('Player and content are required.'), 'danger')
|
||||
if data['match_id'] not in (None, match_id):
|
||||
flash(_('Invalid match context.'), 'danger')
|
||||
return redirect(url_for('users.add_note_from_match', match_id=match_id))
|
||||
|
||||
if not coach_can_access_player(current_user, player_id):
|
||||
flash(_('You can only write notes about players you work with.'), 'danger')
|
||||
return redirect(url_for('users.add_note_from_match', match_id=match_id))
|
||||
|
||||
if not MatchParticipant.query.filter_by(match_id=match_id, player_id=player_id).first():
|
||||
flash(_('That player did not participate in this match.'), 'danger')
|
||||
return redirect(url_for('users.add_note_from_match', match_id=match_id))
|
||||
|
||||
note = PersonalNote(
|
||||
player_id=player_id,
|
||||
coach_id=current_user.id,
|
||||
content=content,
|
||||
content=data['content'],
|
||||
match_id=match_id,
|
||||
)
|
||||
db.session.add(note)
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.forms import flash_validation_errors, form_payload
|
||||
from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote
|
||||
from app.routes.users.blueprint import users_bp
|
||||
from app.services.notifications import send_discord_notification
|
||||
from app.validators import OneOnOneRequestSchema
|
||||
from app.validators import OneOnOneRejectionSchema, OneOnOneRequestSchema
|
||||
|
||||
|
||||
@users_bp.route('/one-on-one', methods=['GET', 'POST'])
|
||||
@@ -226,7 +226,12 @@ def reject_one_on_one(request_id):
|
||||
flash(_('This request has already been processed.'), 'info')
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
|
||||
rejection_reason = request.form.get('rejection_reason', '').strip()
|
||||
try:
|
||||
data = OneOnOneRejectionSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('users.notes_dashboard'))
|
||||
rejection_reason = data['rejection_reason']
|
||||
player = request_obj.player
|
||||
|
||||
request_obj.status = 'rejected'
|
||||
|
||||
@@ -98,6 +98,18 @@ def edit_profile():
|
||||
user_gamertags=current_user.get_gamertags(),
|
||||
)
|
||||
|
||||
try:
|
||||
update_user_gamertags(current_user, selected_games)
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return render_template(
|
||||
'pages/edit_profile.html',
|
||||
user=current_user,
|
||||
esport_games=ESPORT_GAMES,
|
||||
game_platforms=GAME_PLATFORMS,
|
||||
user_gamertags=current_user.get_gamertags(),
|
||||
)
|
||||
|
||||
current_user.username = username
|
||||
current_user.full_name = full_name
|
||||
current_user.email = email
|
||||
@@ -106,8 +118,6 @@ def edit_profile():
|
||||
current_user.discord_username = discord_username or None
|
||||
current_user.league_os_profile = league_os_profile or None
|
||||
|
||||
update_user_gamertags(current_user, selected_games)
|
||||
|
||||
# Blank means "keep the current password"; anything else has already
|
||||
# been checked against the policy by the schema.
|
||||
password = validated.get('password')
|
||||
|
||||
@@ -55,8 +55,8 @@ PG_RESTORE = os.getenv('PG_RESTORE', 'pg_restore')
|
||||
# wave G introduced DOCUMENTS_ROOT so a release-directory deployment could
|
||||
# keep uploads outside the releases, and docs/deployment.md now tells the
|
||||
# operator to set it — at which point this script archived a directory the
|
||||
# application had never written to. It does not fail on a missing directory
|
||||
# either; it prints "No documents directory found", skips, and exits 0.
|
||||
# application had never written to. A missing or unreadable document store
|
||||
# is now a failed full-backup run rather than a database-only green result.
|
||||
#
|
||||
# So the more correctly an operator followed the deployment documentation,
|
||||
# the more certainly their contract backups were empty (OBS-006).
|
||||
@@ -262,33 +262,31 @@ def backup_documents():
|
||||
module happened to be imported with.
|
||||
|
||||
Returns:
|
||||
str: Path to the created archive, or None if there is nothing to
|
||||
archive. Signed contracts live only on disk, so losing this
|
||||
directory loses the documents themselves.
|
||||
str: Path to the created archive.
|
||||
|
||||
Raises:
|
||||
BackupError: If the configured store is absent or cannot be archived.
|
||||
Signed contracts live only on disk, so a database-only run must
|
||||
never be reported as a complete backup.
|
||||
"""
|
||||
documents_dir = documents_root()
|
||||
|
||||
if not os.path.exists(documents_dir):
|
||||
# Says where it looked. The previous message named no path, so an
|
||||
# operator who had moved the documents read it as "there are no
|
||||
# documents" rather than "I am looking in the wrong place".
|
||||
print(f'[INFO] No documents directory at {documents_dir}. Skipping document backup.')
|
||||
return None
|
||||
raise BackupError(f'Documents directory does not exist: {documents_dir}')
|
||||
if not os.path.isdir(documents_dir):
|
||||
raise BackupError(f'Documents path is not a directory: {documents_dir}')
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
archive_basename = os.path.join(BACKUP_DIR, f'documents_backup_{timestamp}')
|
||||
|
||||
try:
|
||||
shutil.make_archive(archive_basename, 'zip', documents_dir)
|
||||
except Exception as exc: # noqa: BLE001 — a failed document archive must not lose the dump
|
||||
# This runs after the database dump has already succeeded. Letting
|
||||
# anything through here would abort the script with a traceback and
|
||||
# take the one part that worked down with it. Reported to stdout, in
|
||||
# the format the rest of this script uses; it has no logger.
|
||||
print(f'[ERROR] Document backup failed: {exc}')
|
||||
return None
|
||||
except Exception as exc: # noqa: BLE001 — normalize the shutil boundary
|
||||
raise BackupError(f'Document backup failed: {exc}') from exc
|
||||
|
||||
zip_path = f'{archive_basename}.zip'
|
||||
if not os.path.exists(zip_path) or os.path.getsize(zip_path) == 0:
|
||||
raise BackupError('Document archiver reported success but produced an empty file.')
|
||||
size_mb = os.path.getsize(zip_path) / (1024 * 1024)
|
||||
print(f'[OK] Documents backed up to: {zip_path} ({size_mb:.1f} MB)')
|
||||
return zip_path
|
||||
@@ -361,15 +359,20 @@ def main(argv=None):
|
||||
return 1
|
||||
|
||||
verified = verify_backup(backup_path)
|
||||
backup_documents()
|
||||
documents_ok = True
|
||||
try:
|
||||
backup_documents()
|
||||
except BackupError as exc:
|
||||
documents_ok = False
|
||||
print(f'[ERROR] {exc}')
|
||||
cleanup_old_backups()
|
||||
|
||||
print()
|
||||
if verified:
|
||||
if verified and documents_ok:
|
||||
print('=== Backup completed successfully ===')
|
||||
return 0
|
||||
|
||||
print('=== Backup FAILED verification — do not rely on this archive ===')
|
||||
print('=== Backup INCOMPLETE — do not treat this run as a full recovery point ===')
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
@@ -216,7 +216,14 @@ function flash(message, type) {
|
||||
const flashContainer = document.querySelector('.flash-messages');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-' + type + ' alert-dismissible';
|
||||
alert.innerHTML = '<span>' + message + '</span><button type="button" class="alert-close" data-action="remove-element">×</button>';
|
||||
const text = document.createElement('span');
|
||||
text.textContent = message;
|
||||
const close = document.createElement('button');
|
||||
close.type = 'button';
|
||||
close.className = 'alert-close';
|
||||
close.dataset.action = 'remove-element';
|
||||
close.textContent = '×';
|
||||
alert.append(text, close);
|
||||
flashContainer.appendChild(alert);
|
||||
}
|
||||
|
||||
|
||||
@@ -450,11 +450,18 @@
|
||||
var playerDataById = {
|
||||
player_data: {
|
||||
{%- for p in all_players %}
|
||||
{{ p.id }}: "{{ p.username | escape }}",
|
||||
{{ p.id }}: {{ p.username | tojson }},
|
||||
{%- endfor %}
|
||||
}
|
||||
};
|
||||
|
||||
var HTML_ESCAPES = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''};
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, function (character) {
|
||||
return HTML_ESCAPES[character];
|
||||
});
|
||||
}
|
||||
|
||||
// All registered player IDs
|
||||
var allRegisteredPlayers = [
|
||||
{%- for p in all_players %}
|
||||
@@ -555,7 +562,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var playerName = playerDataById.player_data[pid];
|
||||
if (playerName) {
|
||||
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
||||
html += playerName;
|
||||
html += escapeHtml(playerName);
|
||||
html += '<span class="remove-btn">↺</span>';
|
||||
html += '</div>';
|
||||
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||
@@ -568,7 +575,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var playerName = playerDataById.player_data[pid];
|
||||
if (playerName) {
|
||||
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
||||
html += playerName;
|
||||
html += escapeHtml(playerName);
|
||||
html += '<span class="remove-btn">↺</span>';
|
||||
html += '</div>';
|
||||
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||
@@ -920,7 +927,7 @@ function updatePlayerPool() {
|
||||
var availabilityClass = isAvailable ? 'available' : 'unavailable';
|
||||
|
||||
html += '<div class="player-item ' + availabilityClass + '" data-player-id="' + pid + '">';
|
||||
html += '<span class="player-name">' + playerName + '</span>';
|
||||
html += '<span class="player-name">' + escapeHtml(playerName) + '</span>';
|
||||
html += '<div class="player-actions">';
|
||||
html += '<button type="button" class="btn btn-sm btn-primary" data-action="assign-team" data-team-side="1">{{ _('T1') }}</button>';
|
||||
html += '<button type="button" class="btn btn-sm btn-secondary" data-action="assign-team" data-team-side="2">{{ _('T2') }}</button>';
|
||||
@@ -943,7 +950,7 @@ function assignToTeam(playerId, teamSide) {
|
||||
if (!playerName) return;
|
||||
|
||||
var html = '<div class="player-item" data-player-id="' + playerId + '" data-action="return-to-pool">';
|
||||
html += playerName;
|
||||
html += escapeHtml(playerName);
|
||||
html += '<span class="remove-btn">↺</span>';
|
||||
html += '</div>';
|
||||
|
||||
@@ -1062,7 +1069,7 @@ function randomizeTeams() {
|
||||
var playerName = playerDataById.player_data[pid];
|
||||
if (playerName) {
|
||||
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
||||
html += playerName;
|
||||
html += escapeHtml(playerName);
|
||||
html += '<span class="remove-btn">↺</span>';
|
||||
html += '</div>';
|
||||
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||
@@ -1074,7 +1081,7 @@ function randomizeTeams() {
|
||||
var playerName = playerDataById.player_data[pid];
|
||||
if (playerName) {
|
||||
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
||||
html += playerName;
|
||||
html += escapeHtml(playerName);
|
||||
html += '<span class="remove-btn">↺</span>';
|
||||
html += '</div>';
|
||||
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
Binary file not shown.
@@ -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-16 23:22-0400\n"
|
||||
"POT-Creation-Date: 2026-08-17 14:18-0400\n"
|
||||
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
@@ -23,8 +23,8 @@ msgstr ""
|
||||
msgid "Please log in to access this page."
|
||||
msgstr "Please log in to access this page."
|
||||
|
||||
#: app/forms.py:37 app/routes/auth.py:228 app/routes/auth.py:387
|
||||
#: app/routes/users/contracts.py:98
|
||||
#: 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
|
||||
#, python-format
|
||||
msgid "%(field)s: %(msg)s"
|
||||
msgstr "%(field)s: %(msg)s"
|
||||
@@ -61,7 +61,7 @@ msgstr "Username is required."
|
||||
msgid "Password is required."
|
||||
msgstr "Password is required."
|
||||
|
||||
#: app/validators.py:227 app/validators.py:294
|
||||
#: app/validators.py:227 app/validators.py:324
|
||||
msgid "Username must be 3-80 characters."
|
||||
msgstr "Username must be 3-80 characters."
|
||||
|
||||
@@ -69,8 +69,8 @@ msgstr "Username must be 3-80 characters."
|
||||
msgid "Email must be 120 characters or less."
|
||||
msgstr "Email must be 120 characters or less."
|
||||
|
||||
#: app/validators.py:246 app/validators.py:309 app/validators.py:340
|
||||
#: app/validators.py:403
|
||||
#: app/validators.py:246 app/validators.py:339 app/validators.py:370
|
||||
#: app/validators.py:433
|
||||
msgid "Full name is required."
|
||||
msgstr "Full name is required."
|
||||
|
||||
@@ -78,160 +78,200 @@ msgstr "Full name is required."
|
||||
msgid "Passwords do not match."
|
||||
msgstr "Passwords do not match."
|
||||
|
||||
#: app/validators.py:313 app/validators.py:348
|
||||
msgid "Invalid role selected."
|
||||
msgstr "Invalid role selected."
|
||||
|
||||
#: app/validators.py:443
|
||||
msgid "Player must be selected."
|
||||
msgstr "Player must be selected."
|
||||
|
||||
#: app/validators.py:446
|
||||
msgid "Notes must be 2000 characters or less."
|
||||
msgstr "Notes must be 2000 characters or less."
|
||||
|
||||
#: app/validators.py:483 app/validators.py:854
|
||||
msgid "Invalid coach selection."
|
||||
msgstr "Invalid coach selection."
|
||||
|
||||
#: app/validators.py:489 app/validators.py:860
|
||||
msgid "Invalid manager selection."
|
||||
msgstr "Invalid manager selection."
|
||||
|
||||
#: app/validators.py:507
|
||||
msgid "Invalid player selection."
|
||||
msgstr "Invalid player selection."
|
||||
|
||||
#: app/validators.py:516
|
||||
msgid "Unknown roster status."
|
||||
msgstr "Unknown roster status."
|
||||
|
||||
#: app/validators.py:539
|
||||
msgid "Date must be in YYYY-MM-DD format."
|
||||
msgstr "Date must be in YYYY-MM-DD format."
|
||||
|
||||
#: app/validators.py:540
|
||||
msgid "A date is required."
|
||||
msgstr "A date is required."
|
||||
|
||||
#: app/validators.py:546 app/validators.py:611
|
||||
msgid "Start time must be in HH:MM format."
|
||||
msgstr "Start time must be in HH:MM format."
|
||||
|
||||
#: app/validators.py:547 app/validators.py:612
|
||||
msgid "A start time is required."
|
||||
msgstr "A start time is required."
|
||||
|
||||
#: app/validators.py:553
|
||||
msgid "End time must be in HH:MM format."
|
||||
msgstr "End time must be in HH:MM format."
|
||||
|
||||
#: app/validators.py:554
|
||||
msgid "An end time is required."
|
||||
msgstr "An end time is required."
|
||||
|
||||
#: app/validators.py:558
|
||||
msgid "Points must be 2000 characters or less."
|
||||
msgstr "Points must be 2000 characters or less."
|
||||
|
||||
#: app/validators.py:573
|
||||
msgid "End time must be after start time."
|
||||
msgstr "End time must be after start time."
|
||||
|
||||
#: app/validators.py:602 app/validators.py:604
|
||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
|
||||
|
||||
#: app/validators.py:605
|
||||
msgid "A day is required."
|
||||
msgstr "A day is required."
|
||||
|
||||
#: app/validators.py:640
|
||||
msgid "Player selection is malformed."
|
||||
msgstr "Player selection is malformed."
|
||||
|
||||
#: app/validators.py:666 app/validators.py:776
|
||||
msgid "A title is required."
|
||||
msgstr "A title is required."
|
||||
|
||||
#: app/validators.py:675
|
||||
msgid "Invalid date format."
|
||||
msgstr "Invalid date format."
|
||||
|
||||
#: app/validators.py:680 app/validators.py:687
|
||||
msgid "Invalid time format."
|
||||
msgstr "Invalid time format."
|
||||
|
||||
#: app/validators.py:681
|
||||
msgid "Start time is required. Please select a time slot."
|
||||
msgstr "Start time is required. Please select a time slot."
|
||||
|
||||
#: app/validators.py:695
|
||||
msgid "Unknown match status."
|
||||
msgstr "Unknown match status."
|
||||
|
||||
#: app/validators.py:711
|
||||
msgid "The end time must come after the start time."
|
||||
msgstr "The end time must come after the start time."
|
||||
|
||||
#: app/validators.py:725
|
||||
msgid "Unknown match type."
|
||||
msgstr "Unknown match type."
|
||||
|
||||
#: app/validators.py:737
|
||||
msgid "A team cannot play against itself."
|
||||
msgstr "A team cannot play against itself."
|
||||
|
||||
#: app/validators.py:785
|
||||
#: app/validators.py:284 app/validators.py:924
|
||||
msgid "Unknown game."
|
||||
msgstr "Unknown game."
|
||||
|
||||
#: app/validators.py:790
|
||||
#: app/validators.py:291
|
||||
msgid "Gamertag must be between 1 and 120 characters."
|
||||
msgstr "Gamertag must be between 1 and 120 characters."
|
||||
|
||||
#: app/validators.py:297
|
||||
msgid "Platform must be 30 characters or less."
|
||||
msgstr "Platform must be 30 characters or less."
|
||||
|
||||
#: app/validators.py:306
|
||||
msgid "Unknown platform for this game."
|
||||
msgstr "Unknown platform for this game."
|
||||
|
||||
#: app/validators.py:343 app/validators.py:378
|
||||
msgid "Invalid role selected."
|
||||
msgstr "Invalid role selected."
|
||||
|
||||
#: app/validators.py:473 app/validators.py:596 app/validators.py:629
|
||||
msgid "Player must be selected."
|
||||
msgstr "Player must be selected."
|
||||
|
||||
#: app/validators.py:476
|
||||
msgid "Notes must be 2000 characters or less."
|
||||
msgstr "Notes must be 2000 characters or less."
|
||||
|
||||
#: app/validators.py:513 app/validators.py:993
|
||||
msgid "Invalid coach selection."
|
||||
msgstr "Invalid coach selection."
|
||||
|
||||
#: app/validators.py:519 app/validators.py:999
|
||||
msgid "Invalid manager selection."
|
||||
msgstr "Invalid manager selection."
|
||||
|
||||
#: app/validators.py:537 app/validators.py:595 app/validators.py:628
|
||||
msgid "Invalid player selection."
|
||||
msgstr "Invalid player selection."
|
||||
|
||||
#: app/validators.py:546
|
||||
msgid "Unknown roster status."
|
||||
msgstr "Unknown roster status."
|
||||
|
||||
#: app/validators.py:559
|
||||
msgid "Unknown tryout status."
|
||||
msgstr "Unknown tryout status."
|
||||
|
||||
#: app/validators.py:570
|
||||
msgid "Unknown registration status."
|
||||
msgstr "Unknown registration status."
|
||||
|
||||
#: app/validators.py:583
|
||||
msgid "Team name must be between 1 and 100 characters."
|
||||
msgstr "Team name must be between 1 and 100 characters."
|
||||
|
||||
#: app/validators.py:603
|
||||
msgid "Position must be 50 characters or less."
|
||||
msgstr "Position must be 50 characters or less."
|
||||
|
||||
#: app/validators.py:616
|
||||
msgid "Note content must be between 1 and 5000 characters."
|
||||
msgstr "Note content must be between 1 and 5000 characters."
|
||||
|
||||
#: app/validators.py:642
|
||||
msgid "Select at most one note context."
|
||||
msgstr "Select at most one note context."
|
||||
|
||||
#: app/validators.py:654
|
||||
msgid "Rejection reason must be 2000 characters or less."
|
||||
msgstr "Rejection reason must be 2000 characters or less."
|
||||
|
||||
#: app/validators.py:678
|
||||
msgid "Date must be in YYYY-MM-DD format."
|
||||
msgstr "Date must be in YYYY-MM-DD format."
|
||||
|
||||
#: app/validators.py:679
|
||||
msgid "A date is required."
|
||||
msgstr "A date is required."
|
||||
|
||||
#: app/validators.py:685 app/validators.py:750
|
||||
msgid "Start time must be in HH:MM format."
|
||||
msgstr "Start time must be in HH:MM format."
|
||||
|
||||
#: app/validators.py:686 app/validators.py:751
|
||||
msgid "A start time is required."
|
||||
msgstr "A start time is required."
|
||||
|
||||
#: app/validators.py:692
|
||||
msgid "End time must be in HH:MM format."
|
||||
msgstr "End time must be in HH:MM format."
|
||||
|
||||
#: app/validators.py:693
|
||||
msgid "An end time is required."
|
||||
msgstr "An end time is required."
|
||||
|
||||
#: app/validators.py:697
|
||||
msgid "Points must be 2000 characters or less."
|
||||
msgstr "Points must be 2000 characters or less."
|
||||
|
||||
#: app/validators.py:712
|
||||
msgid "End time must be after start time."
|
||||
msgstr "End time must be after start time."
|
||||
|
||||
#: app/validators.py:741 app/validators.py:743
|
||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
|
||||
|
||||
#: app/validators.py:744
|
||||
msgid "A day is required."
|
||||
msgstr "A day is required."
|
||||
|
||||
#: app/validators.py:779
|
||||
msgid "Player selection is malformed."
|
||||
msgstr "Player selection is malformed."
|
||||
|
||||
#: app/validators.py:805 app/validators.py:915
|
||||
msgid "A title is required."
|
||||
msgstr "A title is required."
|
||||
|
||||
#: app/validators.py:814
|
||||
msgid "Invalid date format."
|
||||
msgstr "Invalid date format."
|
||||
|
||||
#: app/validators.py:819 app/validators.py:826
|
||||
msgid "Invalid time format."
|
||||
msgstr "Invalid time format."
|
||||
|
||||
#: app/validators.py:820
|
||||
msgid "Start time is required. Please select a time slot."
|
||||
msgstr "Start time is required. Please select a time slot."
|
||||
|
||||
#: app/validators.py:834
|
||||
msgid "Unknown match status."
|
||||
msgstr "Unknown match status."
|
||||
|
||||
#: app/validators.py:850
|
||||
msgid "The end time must come after the start time."
|
||||
msgstr "The end time must come after the start time."
|
||||
|
||||
#: app/validators.py:864
|
||||
msgid "Unknown match type."
|
||||
msgstr "Unknown match type."
|
||||
|
||||
#: app/validators.py:876
|
||||
msgid "A team cannot play against itself."
|
||||
msgstr "A team cannot play against itself."
|
||||
|
||||
#: app/validators.py:929
|
||||
msgid "Invalid start date format."
|
||||
msgstr "Invalid start date format."
|
||||
|
||||
#: app/validators.py:791
|
||||
#: app/validators.py:930
|
||||
msgid "A start date is required."
|
||||
msgstr "A start date is required."
|
||||
|
||||
#: app/validators.py:797
|
||||
#: app/validators.py:936
|
||||
msgid "Invalid end date format."
|
||||
msgstr "Invalid end date format."
|
||||
|
||||
#: app/validators.py:805
|
||||
#: app/validators.py:944
|
||||
msgid "A tryout must allow at least one player."
|
||||
msgstr "A tryout must allow at least one player."
|
||||
|
||||
#: app/validators.py:808
|
||||
#: app/validators.py:947
|
||||
msgid "The player limit must be a whole number."
|
||||
msgstr "The player limit must be a whole number."
|
||||
|
||||
#: app/validators.py:820
|
||||
#: app/validators.py:959
|
||||
msgid "End date cannot be before start date."
|
||||
msgstr "End date cannot be before start date."
|
||||
|
||||
#: app/validators.py:847 app/validators.py:848
|
||||
#: app/validators.py:986 app/validators.py:987
|
||||
msgid "Team name is required."
|
||||
msgstr "Team name is required."
|
||||
|
||||
#: app/validators.py:872
|
||||
#: app/validators.py:1011
|
||||
msgid "Scores run from 1 to 10."
|
||||
msgstr "Scores run from 1 to 10."
|
||||
|
||||
#: app/validators.py:873
|
||||
#: app/validators.py:1012
|
||||
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:245
|
||||
#: app/routes/auth.py:246
|
||||
msgid "This account has been deactivated."
|
||||
msgstr "This account has been deactivated."
|
||||
|
||||
#: app/routes/auth.py:280
|
||||
#: app/routes/auth.py:281
|
||||
#, python-format
|
||||
msgid "Welcome back, %(username)s!"
|
||||
msgstr "Welcome back, %(username)s!"
|
||||
|
||||
#: app/routes/auth.py:310
|
||||
#: app/routes/auth.py:311
|
||||
msgid ""
|
||||
"Login unsuccessful. Please check your username and password, or ask a "
|
||||
"president for help."
|
||||
@@ -239,32 +279,32 @@ msgstr ""
|
||||
"Login unsuccessful. Please check your username and password, or ask a "
|
||||
"president for help."
|
||||
|
||||
#: app/routes/auth.py:376
|
||||
#: app/routes/auth.py:377
|
||||
msgid "Your registration could not be processed. Please try again."
|
||||
msgstr "Your registration could not be processed. Please try again."
|
||||
|
||||
#: app/routes/auth.py:411 app/routes/users/accounts.py:339
|
||||
#: app/routes/auth.py:419 app/routes/users/accounts.py:343
|
||||
msgid "Username already exists."
|
||||
msgstr "Username already exists."
|
||||
|
||||
#: app/routes/auth.py:415 app/routes/users/accounts.py:343
|
||||
#: app/routes/auth.py:423 app/routes/users/accounts.py:347
|
||||
msgid "Email already registered."
|
||||
msgstr "Email already registered."
|
||||
|
||||
#: app/routes/auth.py:422 app/routes/auth.py:617
|
||||
#: app/routes/auth.py:430 app/routes/auth.py:623
|
||||
#: app/routes/users/accounts.py:121
|
||||
msgid "This Discord account is already linked to another account."
|
||||
msgstr "This Discord account is already linked to another account."
|
||||
|
||||
#: app/routes/auth.py:466
|
||||
#: app/routes/auth.py:472
|
||||
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:491
|
||||
#: app/routes/auth.py:497
|
||||
msgid "Discord OAuth2 is not configured."
|
||||
msgstr "Discord OAuth2 is not configured."
|
||||
|
||||
#: app/routes/auth.py:540
|
||||
#: app/routes/auth.py:546
|
||||
msgid ""
|
||||
"Discord authorization could not be verified. Please start the connection "
|
||||
"again from this page."
|
||||
@@ -272,35 +312,35 @@ msgstr ""
|
||||
"Discord authorization could not be verified. Please start the connection "
|
||||
"again from this page."
|
||||
|
||||
#: app/routes/auth.py:549
|
||||
#: app/routes/auth.py:555
|
||||
msgid "Discord authorization failed. No code received."
|
||||
msgstr "Discord authorization failed. No code received."
|
||||
|
||||
#: app/routes/auth.py:573
|
||||
#: app/routes/auth.py:579
|
||||
msgid "Failed to connect to Discord. Please try again."
|
||||
msgstr "Failed to connect to Discord. Please try again."
|
||||
|
||||
#: app/routes/auth.py:577
|
||||
#: app/routes/auth.py:583
|
||||
msgid "Failed to obtain Discord access token."
|
||||
msgstr "Failed to obtain Discord access token."
|
||||
|
||||
#: app/routes/auth.py:592 app/routes/auth.py:602
|
||||
#: app/routes/auth.py:598 app/routes/auth.py:608
|
||||
msgid "Failed to fetch Discord user profile."
|
||||
msgstr "Failed to fetch Discord user profile."
|
||||
|
||||
#: app/routes/auth.py:609
|
||||
#: app/routes/auth.py:615
|
||||
msgid "Please log in to connect your Discord account."
|
||||
msgstr "Please log in to connect your Discord account."
|
||||
|
||||
#: app/routes/auth.py:628
|
||||
#: app/routes/auth.py:634
|
||||
msgid "Discord account connected!"
|
||||
msgstr "Discord account connected!"
|
||||
|
||||
#: app/routes/auth.py:675
|
||||
#: app/routes/auth.py:681
|
||||
msgid "Discord account connected! Your profile has been pre-filled."
|
||||
msgstr "Discord account connected! Your profile has been pre-filled."
|
||||
|
||||
#: app/routes/auth.py:703
|
||||
#: app/routes/auth.py:709
|
||||
msgid "You have been logged out."
|
||||
msgstr "You have been logged out."
|
||||
|
||||
@@ -334,10 +374,10 @@ 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:444
|
||||
#: app/routes/tryouts.py:460 app/routes/tryouts.py:480
|
||||
#: app/routes/tryouts.py:527 app/routes/tryouts.py:563
|
||||
#: app/routes/tryouts.py:582
|
||||
#: 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
|
||||
msgid "Permission denied."
|
||||
msgstr "Permission denied."
|
||||
|
||||
@@ -345,35 +385,35 @@ msgstr "Permission denied."
|
||||
msgid "That language is not available."
|
||||
msgstr "That language is not available."
|
||||
|
||||
#: app/routes/matches.py:364
|
||||
#: app/routes/matches.py:383
|
||||
msgid "You do not have permission to schedule matches for this tryout."
|
||||
msgstr "You do not have permission to schedule matches for this tryout."
|
||||
|
||||
#: app/routes/matches.py:368 app/routes/matches.py:448
|
||||
#: app/routes/matches.py:387 app/routes/matches.py:463
|
||||
msgid "This tryout has ended. Matches can no longer be created or modified."
|
||||
msgstr "This tryout has ended. Matches can no longer be created or modified."
|
||||
|
||||
#: app/routes/matches.py:430
|
||||
#: app/routes/matches.py:445
|
||||
msgid "Match scheduled successfully!"
|
||||
msgstr "Match scheduled successfully!"
|
||||
|
||||
#: app/routes/matches.py:444 app/routes/team_matches.py:220
|
||||
#: app/routes/matches.py:459 app/routes/team_matches.py:220
|
||||
msgid "You do not have permission to edit this match."
|
||||
msgstr "You do not have permission to edit this match."
|
||||
|
||||
#: app/routes/matches.py:539 app/routes/team_matches.py:250
|
||||
#: app/routes/matches.py:552 app/routes/team_matches.py:250
|
||||
msgid "Match updated successfully!"
|
||||
msgstr "Match updated successfully!"
|
||||
|
||||
#: app/routes/matches.py:575 app/routes/team_matches.py:265
|
||||
#: app/routes/matches.py:588 app/routes/team_matches.py:265
|
||||
msgid "You do not have permission to delete this match."
|
||||
msgstr "You do not have permission to delete this match."
|
||||
|
||||
#: app/routes/matches.py:578
|
||||
#: app/routes/matches.py:591
|
||||
msgid "This tryout has ended. Matches can no longer be deleted."
|
||||
msgstr "This tryout has ended. Matches can no longer be deleted."
|
||||
|
||||
#: app/routes/matches.py:591 app/routes/team_matches.py:269
|
||||
#: app/routes/matches.py:604 app/routes/team_matches.py:269
|
||||
msgid "Match deleted successfully."
|
||||
msgstr "Match deleted successfully."
|
||||
|
||||
@@ -476,7 +516,7 @@ msgstr "Coach removed from %(name)s."
|
||||
msgid "Manager removed from %(name)s."
|
||||
msgstr "Manager removed from %(name)s."
|
||||
|
||||
#: app/routes/teams.py:490 app/routes/tryouts.py:489 app/routes/tryouts.py:593
|
||||
#: app/routes/teams.py:490 app/routes/tryouts.py:524
|
||||
msgid "Please select a player."
|
||||
msgstr "Please select a player."
|
||||
|
||||
@@ -494,7 +534,7 @@ msgstr "%(username)s is already on %(name)s."
|
||||
msgid "%(username)s added to %(name)s!"
|
||||
msgstr "%(username)s added to %(name)s!"
|
||||
|
||||
#: app/routes/teams.py:527 app/routes/teams.py:601
|
||||
#: app/routes/teams.py:527 app/routes/teams.py:605
|
||||
#, python-format
|
||||
msgid "%(username)s is not on %(name)s."
|
||||
msgstr "%(username)s is not on %(name)s."
|
||||
@@ -504,130 +544,130 @@ msgstr "%(username)s is not on %(name)s."
|
||||
msgid "%(username)s removed from %(name)s."
|
||||
msgstr "%(username)s removed from %(name)s."
|
||||
|
||||
#: app/routes/teams.py:572 app/routes/teams.py:590
|
||||
#: app/routes/teams.py:572 app/routes/teams.py:594
|
||||
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:580
|
||||
#: app/routes/teams.py:584
|
||||
msgid "Team notes added successfully!"
|
||||
msgstr "Team notes added successfully!"
|
||||
|
||||
#: app/routes/teams.py:595 app/routes/users/notes.py:207
|
||||
#: app/routes/users/notes.py:250
|
||||
#: app/routes/teams.py:599 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:611
|
||||
#: app/routes/teams.py:619
|
||||
#, python-format
|
||||
msgid "Note added for %(username)s!"
|
||||
msgstr "Note added for %(username)s!"
|
||||
|
||||
#: app/routes/tryouts.py:98
|
||||
#: app/routes/tryouts.py:125
|
||||
msgid "You do not have permission to create tryouts."
|
||||
msgstr "You do not have permission to create tryouts."
|
||||
|
||||
#: app/routes/tryouts.py:145
|
||||
#: app/routes/tryouts.py:172
|
||||
msgid "Tryout created successfully!"
|
||||
msgstr "Tryout created successfully!"
|
||||
|
||||
#: app/routes/tryouts.py:158
|
||||
#: app/routes/tryouts.py:185
|
||||
msgid "You do not have permission to edit this tryout."
|
||||
msgstr "You do not have permission to edit this tryout."
|
||||
|
||||
#: app/routes/tryouts.py:162
|
||||
#: app/routes/tryouts.py:189
|
||||
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:202
|
||||
#: app/routes/tryouts.py:229
|
||||
msgid "Tryout updated successfully!"
|
||||
msgstr "Tryout updated successfully!"
|
||||
|
||||
#: app/routes/tryouts.py:242
|
||||
#: app/routes/tryouts.py:269
|
||||
msgid "You do not have permission to view this tryout."
|
||||
msgstr "You do not have permission to view this tryout."
|
||||
|
||||
#: app/routes/tryouts.py:411
|
||||
#: app/routes/tryouts.py:437
|
||||
msgid "Only players can register for tryouts."
|
||||
msgstr "Only players can register for tryouts."
|
||||
|
||||
#: app/routes/tryouts.py:415
|
||||
#: app/routes/tryouts.py:442
|
||||
msgid "This tryout is not accepting registrations."
|
||||
msgstr "This tryout is not accepting registrations."
|
||||
|
||||
#: app/routes/tryouts.py:422
|
||||
#: app/routes/tryouts.py:449
|
||||
msgid "You are already registered for this tryout."
|
||||
msgstr "You are already registered for this tryout."
|
||||
|
||||
#: app/routes/tryouts.py:428 app/routes/tryouts.py:511
|
||||
#: app/routes/tryouts.py:455 app/routes/tryouts.py:546
|
||||
msgid "This tryout is full."
|
||||
msgstr "This tryout is full."
|
||||
|
||||
#: app/routes/tryouts.py:434
|
||||
#: app/routes/tryouts.py:461
|
||||
msgid "Successfully registered for tryout!"
|
||||
msgstr "Successfully registered for tryout!"
|
||||
|
||||
#: app/routes/tryouts.py:450
|
||||
#: app/routes/tryouts.py:481
|
||||
#, python-format
|
||||
msgid "Tryout status updated to %(new_status)s."
|
||||
msgstr "Tryout status updated to %(new_status)s."
|
||||
|
||||
#: app/routes/tryouts.py:470
|
||||
#: app/routes/tryouts.py:505
|
||||
msgid "Registration status updated."
|
||||
msgstr "Registration status updated."
|
||||
|
||||
#: app/routes/tryouts.py:497
|
||||
#: app/routes/tryouts.py:532
|
||||
msgid "Can only register players."
|
||||
msgstr "Can only register players."
|
||||
|
||||
#: app/routes/tryouts.py:503
|
||||
#: app/routes/tryouts.py:538
|
||||
#, python-format
|
||||
msgid "%(username)s is already registered for this tryout."
|
||||
msgstr "%(username)s is already registered for this tryout."
|
||||
|
||||
#: app/routes/tryouts.py:517
|
||||
#: app/routes/tryouts.py:552
|
||||
#, python-format
|
||||
msgid "%(username)s registered for tryout!"
|
||||
msgstr "%(username)s registered for tryout!"
|
||||
|
||||
#: app/routes/tryouts.py:553
|
||||
#: app/routes/tryouts.py:588
|
||||
#, python-format
|
||||
msgid "%(username)s removed from tryout."
|
||||
msgstr "%(username)s removed from tryout."
|
||||
|
||||
#: app/routes/tryouts.py:571
|
||||
#: app/routes/tryouts.py:610
|
||||
#, python-format
|
||||
msgid "Team \"%(team_name)s\" created!"
|
||||
msgstr "Team \"%(team_name)s\" created!"
|
||||
|
||||
#: app/routes/tryouts.py:602
|
||||
#: app/routes/tryouts.py:643 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:608
|
||||
#: app/routes/tryouts.py:648
|
||||
msgid "Player is already on this team."
|
||||
msgstr "Player is already on this team."
|
||||
|
||||
#: app/routes/tryouts.py:613
|
||||
#: app/routes/tryouts.py:653
|
||||
msgid "Player added to team!"
|
||||
msgstr "Player added to team!"
|
||||
|
||||
#: app/routes/tryouts.py:623
|
||||
#: app/routes/tryouts.py:663
|
||||
msgid "You do not have permission to delete this tryout."
|
||||
msgstr "You do not have permission to delete this tryout."
|
||||
|
||||
#: app/routes/tryouts.py:659
|
||||
#: app/routes/tryouts.py:699
|
||||
msgid "Tryout deleted successfully."
|
||||
msgstr "Tryout deleted successfully."
|
||||
|
||||
#: app/routes/users/_shared.py:51
|
||||
#: app/routes/users/_shared.py:50
|
||||
msgid "No file selected."
|
||||
msgstr "No file selected."
|
||||
|
||||
#: app/routes/users/_shared.py:55
|
||||
#: app/routes/users/_shared.py:54
|
||||
msgid "Only PDF files are allowed for contracts."
|
||||
msgstr "Only PDF files are allowed for contracts."
|
||||
|
||||
#: app/routes/users/_shared.py:60
|
||||
#: app/routes/users/_shared.py:59
|
||||
msgid "That file is not a PDF, whatever its name says."
|
||||
msgstr "That file is not a PDF, whatever its name says."
|
||||
|
||||
@@ -643,11 +683,11 @@ msgstr "Only the president can edit users."
|
||||
msgid "Email already in use by another account."
|
||||
msgstr "Email already in use by another account."
|
||||
|
||||
#: app/routes/users/accounts.py:132
|
||||
#: app/routes/users/accounts.py:138
|
||||
msgid "You cannot change your own role. Ask another president to do it."
|
||||
msgstr "You cannot change your own role. Ask another president to do it."
|
||||
|
||||
#: app/routes/users/accounts.py:145
|
||||
#: app/routes/users/accounts.py:151
|
||||
msgid ""
|
||||
"This is the last active president. Promote another account before "
|
||||
"changing this one."
|
||||
@@ -655,29 +695,29 @@ msgstr ""
|
||||
"This is the last active president. Promote another account before "
|
||||
"changing this one."
|
||||
|
||||
#: app/routes/users/accounts.py:224
|
||||
#: app/routes/users/accounts.py:228
|
||||
#, python-format
|
||||
msgid "User %(username)s updated successfully!"
|
||||
msgstr "User %(username)s updated successfully!"
|
||||
|
||||
#: app/routes/users/accounts.py:245
|
||||
#: app/routes/users/accounts.py:249
|
||||
msgid "Only the president can delete users."
|
||||
msgstr "Only the president can delete users."
|
||||
|
||||
#: app/routes/users/accounts.py:249
|
||||
#: app/routes/users/accounts.py:253
|
||||
msgid "You cannot delete your own account."
|
||||
msgstr "You cannot delete your own account."
|
||||
|
||||
#: app/routes/users/accounts.py:308
|
||||
#: app/routes/users/accounts.py:312
|
||||
#, python-format
|
||||
msgid "User %(deleted_username)s has been removed."
|
||||
msgstr "User %(deleted_username)s has been removed."
|
||||
|
||||
#: app/routes/users/accounts.py:319
|
||||
#: app/routes/users/accounts.py:323
|
||||
msgid "Only the president can create users."
|
||||
msgstr "Only the president can create users."
|
||||
|
||||
#: app/routes/users/accounts.py:367
|
||||
#: app/routes/users/accounts.py:371
|
||||
#, python-format
|
||||
msgid "User %(full_name)s created as %(role)s!"
|
||||
msgstr "User %(full_name)s created as %(role)s!"
|
||||
@@ -715,54 +755,93 @@ msgstr "You do not have permission to download this contract."
|
||||
msgid "No signed contract available."
|
||||
msgstr "No signed contract available."
|
||||
|
||||
#: app/routes/users/notes.py:34
|
||||
#: app/routes/users/notes.py:43
|
||||
msgid "This page is for players only."
|
||||
msgstr "This page is for players only."
|
||||
|
||||
#: app/routes/users/notes.py:71
|
||||
#: app/routes/users/notes.py:80
|
||||
msgid "Only coaches can access the notes dashboard."
|
||||
msgstr "Only coaches can access the notes dashboard."
|
||||
|
||||
#: app/routes/users/notes.py:161
|
||||
#: app/routes/users/notes.py:172
|
||||
msgid "Only coaches can manage team notes."
|
||||
msgstr "Only coaches can manage team notes."
|
||||
|
||||
#: app/routes/users/notes.py:167
|
||||
#: app/routes/users/notes.py:178
|
||||
msgid "You are not assigned to a team."
|
||||
msgstr "You are not assigned to a team."
|
||||
|
||||
#: app/routes/users/notes.py:180
|
||||
#: app/routes/users/notes.py:195
|
||||
msgid "Team notes saved successfully!"
|
||||
msgstr "Team notes saved successfully!"
|
||||
|
||||
#: app/routes/users/notes.py:195
|
||||
#: app/routes/users/notes.py:210
|
||||
msgid "Only coaches can manage personal notes."
|
||||
msgstr "Only coaches can manage personal notes."
|
||||
|
||||
#: app/routes/users/notes.py:202 app/routes/users/notes.py:245
|
||||
#: app/routes/users/notes.py:296 app/routes/users/notes.py:350
|
||||
msgid "Player and content are required."
|
||||
msgstr "Player and content are required."
|
||||
|
||||
#: app/routes/users/notes.py:211 app/routes/users/notes.py:254
|
||||
#: app/routes/users/notes.py:300 app/routes/users/notes.py:354
|
||||
#: app/routes/users/notes.py:226 app/routes/users/notes.py:266
|
||||
#: app/routes/users/notes.py:346 app/routes/users/notes.py:411
|
||||
msgid "You can only write notes about players you work with."
|
||||
msgstr "You can only write notes about players you work with."
|
||||
|
||||
#: app/routes/users/notes.py:221 app/routes/users/notes.py:267
|
||||
#: app/routes/users/notes.py:236 app/routes/users/notes.py:306
|
||||
#, python-format
|
||||
msgid "Note added for %(username)s."
|
||||
msgstr "Note added for %(username)s."
|
||||
|
||||
#: app/routes/users/notes.py:235 app/routes/users/notes.py:281
|
||||
#: app/routes/users/notes.py:334
|
||||
#: app/routes/users/notes.py:250 app/routes/users/notes.py:320
|
||||
#: app/routes/users/notes.py:384
|
||||
msgid "Only coaches can add personal notes."
|
||||
msgstr "Only coaches can add personal notes."
|
||||
|
||||
#: app/routes/users/notes.py:311 app/routes/users/notes.py:365
|
||||
#: app/routes/users/notes.py:272
|
||||
msgid "You cannot use that match as note context."
|
||||
msgstr "You cannot use that match as note context."
|
||||
|
||||
#: app/routes/users/notes.py:275
|
||||
msgid "That player did not participate in the selected match."
|
||||
msgstr "That player did not participate in the selected match."
|
||||
|
||||
#: app/routes/users/notes.py:281
|
||||
msgid "You cannot use that tryout as note context."
|
||||
msgstr "You cannot use that tryout as note context."
|
||||
|
||||
#: app/routes/users/notes.py:284
|
||||
msgid "That player is not registered for the selected tryout."
|
||||
msgstr "That player is not registered for the selected tryout."
|
||||
|
||||
#: app/routes/users/notes.py:290
|
||||
msgid "You cannot use that team as note context."
|
||||
msgstr "You cannot use that team as note context."
|
||||
|
||||
#: app/routes/users/notes.py:293
|
||||
msgid "That player is not on the selected team."
|
||||
msgstr "That player is not on the selected team."
|
||||
|
||||
#: app/routes/users/notes.py:325
|
||||
msgid "You do not have permission to add notes for this tryout."
|
||||
msgstr "You do not have permission to add notes for this tryout."
|
||||
|
||||
#: app/routes/users/notes.py:342
|
||||
msgid "Invalid tryout context."
|
||||
msgstr "Invalid tryout context."
|
||||
|
||||
#: app/routes/users/notes.py:361 app/routes/users/notes.py:426
|
||||
msgid "Note added successfully."
|
||||
msgstr "Note added successfully."
|
||||
|
||||
#: app/routes/users/notes.py:389
|
||||
msgid "You do not have permission to add notes for this match."
|
||||
msgstr "You do not have permission to add notes for this match."
|
||||
|
||||
#: app/routes/users/notes.py:407
|
||||
msgid "Invalid match context."
|
||||
msgstr "Invalid match context."
|
||||
|
||||
#: app/routes/users/notes.py:415
|
||||
msgid "That player did not participate in this match."
|
||||
msgstr "That player did not participate in this match."
|
||||
|
||||
#: app/routes/users/one_on_one.py:23
|
||||
msgid "Only players can request One on One sessions."
|
||||
msgstr "Only players can request One on One sessions."
|
||||
@@ -804,7 +883,7 @@ msgstr "One on One request from %(player)s has been approved!"
|
||||
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:258
|
||||
#: app/routes/users/one_on_one.py:263
|
||||
#, python-format
|
||||
msgid "One on One request from %(player)s has been rejected."
|
||||
msgstr "One on One request from %(player)s has been rejected."
|
||||
@@ -817,7 +896,7 @@ msgstr "Username already taken."
|
||||
msgid "Email already in use."
|
||||
msgstr "Email already in use."
|
||||
|
||||
#: app/routes/users/profile.py:121
|
||||
#: app/routes/users/profile.py:131
|
||||
msgid "Profile updated successfully!"
|
||||
msgstr "Profile updated successfully!"
|
||||
|
||||
@@ -1213,7 +1292,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:216
|
||||
#: app/templates/pages/profile.html:213
|
||||
msgid "Loading availability grid..."
|
||||
msgstr "Loading availability grid..."
|
||||
|
||||
@@ -1222,7 +1301,7 @@ msgid "Save Availability"
|
||||
msgstr "Save Availability"
|
||||
|
||||
#: app/templates/pages/coach_availability.html:22
|
||||
#: app/templates/pages/profile.html:200 app/templates/pages/profile.html:220
|
||||
#: app/templates/pages/profile.html:197 app/templates/pages/profile.html:217
|
||||
msgid "Clear All"
|
||||
msgstr "Clear All"
|
||||
|
||||
@@ -1998,23 +2077,23 @@ msgstr ""
|
||||
msgid "Green indicators show player availability for the match date/time"
|
||||
msgstr "Green indicators show player availability for the match date/time"
|
||||
|
||||
#: app/templates/pages/match_form.html:625
|
||||
#: app/templates/pages/match_form.html:632
|
||||
msgid "Click time slots consecutively to set match duration"
|
||||
msgstr "Click time slots consecutively to set match duration"
|
||||
|
||||
#: app/templates/pages/match_form.html:892
|
||||
#: app/templates/pages/match_form.html:899
|
||||
msgid "No players registered"
|
||||
msgstr "No players registered"
|
||||
|
||||
#: app/templates/pages/match_form.html:925
|
||||
#: app/templates/pages/match_form.html:932
|
||||
msgid "T1"
|
||||
msgstr "T1"
|
||||
|
||||
#: app/templates/pages/match_form.html:926
|
||||
#: app/templates/pages/match_form.html:933
|
||||
msgid "T2"
|
||||
msgstr "T2"
|
||||
|
||||
#: app/templates/pages/match_form.html:931
|
||||
#: app/templates/pages/match_form.html:938
|
||||
msgid "No players available"
|
||||
msgstr "No players available"
|
||||
|
||||
@@ -2369,15 +2448,11 @@ msgstr ""
|
||||
"Select your available time blocks for matches (5pm to 12am). Green = "
|
||||
"selected, Gray = available to select."
|
||||
|
||||
#: app/templates/pages/profile.html:197
|
||||
msgid "Save Disponibilities"
|
||||
msgstr "Save Disponibilities"
|
||||
|
||||
#: app/templates/pages/profile.html:211
|
||||
#: app/templates/pages/profile.html:208
|
||||
msgid "My Coaching Availability"
|
||||
msgstr "My Coaching Availability"
|
||||
|
||||
#: app/templates/pages/profile.html:212
|
||||
#: app/templates/pages/profile.html:209
|
||||
msgid ""
|
||||
"Select time slots when you're available for One on One sessions (8am to "
|
||||
"10pm)."
|
||||
@@ -2385,7 +2460,7 @@ msgstr ""
|
||||
"Select time slots when you're available for One on One sessions (8am to "
|
||||
"10pm)."
|
||||
|
||||
#: app/templates/pages/profile.html:460
|
||||
#: app/templates/pages/profile.html:457
|
||||
msgid "Click or click-and-drag to select your available hours"
|
||||
msgstr "Click or click-and-drag to select your available hours"
|
||||
|
||||
@@ -3001,3 +3076,9 @@ msgstr "View Profile"
|
||||
|
||||
#~ msgid "Team Tryout Management System"
|
||||
#~ msgstr "Team Tryout Management System"
|
||||
|
||||
#~ msgid "Player and content are required."
|
||||
#~ msgstr "Player and content are required."
|
||||
|
||||
#~ msgid "Save Disponibilities"
|
||||
#~ msgstr "Save Disponibilities"
|
||||
|
||||
Binary file not shown.
@@ -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-16 23:22-0400\n"
|
||||
"POT-Creation-Date: 2026-08-17 14:18-0400\n"
|
||||
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: fr\n"
|
||||
@@ -23,8 +23,8 @@ msgstr ""
|
||||
msgid "Please log in to access this page."
|
||||
msgstr "Veuillez vous connecter pour accéder à cette page."
|
||||
|
||||
#: app/forms.py:37 app/routes/auth.py:228 app/routes/auth.py:387
|
||||
#: app/routes/users/contracts.py:98
|
||||
#: 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
|
||||
#, python-format
|
||||
msgid "%(field)s: %(msg)s"
|
||||
msgstr "%(field)s : %(msg)s"
|
||||
@@ -63,7 +63,7 @@ msgstr "Le nom d’utilisateur est obligatoire."
|
||||
msgid "Password is required."
|
||||
msgstr "Le mot de passe est obligatoire."
|
||||
|
||||
#: app/validators.py:227 app/validators.py:294
|
||||
#: app/validators.py:227 app/validators.py:324
|
||||
msgid "Username must be 3-80 characters."
|
||||
msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères."
|
||||
|
||||
@@ -71,8 +71,8 @@ msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères."
|
||||
msgid "Email must be 120 characters or less."
|
||||
msgstr "L’adresse courriel ne doit pas dépasser 120 caractères."
|
||||
|
||||
#: app/validators.py:246 app/validators.py:309 app/validators.py:340
|
||||
#: app/validators.py:403
|
||||
#: app/validators.py:246 app/validators.py:339 app/validators.py:370
|
||||
#: app/validators.py:433
|
||||
msgid "Full name is required."
|
||||
msgstr "Le nom complet est obligatoire."
|
||||
|
||||
@@ -80,160 +80,200 @@ msgstr "Le nom complet est obligatoire."
|
||||
msgid "Passwords do not match."
|
||||
msgstr "Les mots de passe ne concordent pas."
|
||||
|
||||
#: app/validators.py:313 app/validators.py:348
|
||||
msgid "Invalid role selected."
|
||||
msgstr "Rôle sélectionné invalide."
|
||||
|
||||
#: app/validators.py:443
|
||||
msgid "Player must be selected."
|
||||
msgstr "Vous devez choisir un joueur."
|
||||
|
||||
#: app/validators.py:446
|
||||
msgid "Notes must be 2000 characters or less."
|
||||
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
|
||||
|
||||
#: app/validators.py:483 app/validators.py:854
|
||||
msgid "Invalid coach selection."
|
||||
msgstr "Sélection de coach invalide."
|
||||
|
||||
#: app/validators.py:489 app/validators.py:860
|
||||
msgid "Invalid manager selection."
|
||||
msgstr "Sélection de gérant invalide."
|
||||
|
||||
#: app/validators.py:507
|
||||
msgid "Invalid player selection."
|
||||
msgstr "Sélection de joueur invalide."
|
||||
|
||||
#: app/validators.py:516
|
||||
msgid "Unknown roster status."
|
||||
msgstr "Statut d'effectif inconnu."
|
||||
|
||||
#: app/validators.py:539
|
||||
msgid "Date must be in YYYY-MM-DD format."
|
||||
msgstr "La date doit être au format AAAA-MM-JJ."
|
||||
|
||||
#: app/validators.py:540
|
||||
msgid "A date is required."
|
||||
msgstr "Une date est requise."
|
||||
|
||||
#: app/validators.py:546 app/validators.py:611
|
||||
msgid "Start time must be in HH:MM format."
|
||||
msgstr "L’heure de début doit être au format HH:MM."
|
||||
|
||||
#: app/validators.py:547 app/validators.py:612
|
||||
msgid "A start time is required."
|
||||
msgstr "Une heure de début est requise."
|
||||
|
||||
#: app/validators.py:553
|
||||
msgid "End time must be in HH:MM format."
|
||||
msgstr "L’heure de fin doit être au format HH:MM."
|
||||
|
||||
#: app/validators.py:554
|
||||
msgid "An end time is required."
|
||||
msgstr "Une heure de fin est requise."
|
||||
|
||||
#: app/validators.py:558
|
||||
msgid "Points must be 2000 characters or less."
|
||||
msgstr "Les points ne doivent pas dépasser 2000 caractères."
|
||||
|
||||
#: app/validators.py:573
|
||||
msgid "End time must be after start time."
|
||||
msgstr "L'heure de fin doit être postérieure à l'heure de début."
|
||||
|
||||
#: app/validators.py:602 app/validators.py:604
|
||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
|
||||
|
||||
#: app/validators.py:605
|
||||
msgid "A day is required."
|
||||
msgstr "Un jour est requis."
|
||||
|
||||
#: app/validators.py:640
|
||||
msgid "Player selection is malformed."
|
||||
msgstr "La sélection de joueurs est mal formée."
|
||||
|
||||
#: app/validators.py:666 app/validators.py:776
|
||||
msgid "A title is required."
|
||||
msgstr "Un titre est requis."
|
||||
|
||||
#: app/validators.py:675
|
||||
msgid "Invalid date format."
|
||||
msgstr "Format de date invalide."
|
||||
|
||||
#: app/validators.py:680 app/validators.py:687
|
||||
msgid "Invalid time format."
|
||||
msgstr "Format d’heure invalide."
|
||||
|
||||
#: app/validators.py:681
|
||||
msgid "Start time is required. Please select a time slot."
|
||||
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
||||
|
||||
#: app/validators.py:695
|
||||
msgid "Unknown match status."
|
||||
msgstr "Statut de match inconnu."
|
||||
|
||||
#: app/validators.py:711
|
||||
msgid "The end time must come after the start time."
|
||||
msgstr "L'heure de fin doit être postérieure à l'heure de début."
|
||||
|
||||
#: app/validators.py:725
|
||||
msgid "Unknown match type."
|
||||
msgstr "Type de match inconnu."
|
||||
|
||||
#: app/validators.py:737
|
||||
msgid "A team cannot play against itself."
|
||||
msgstr "Une équipe ne peut pas jouer contre elle-même."
|
||||
|
||||
#: app/validators.py:785
|
||||
#: app/validators.py:284 app/validators.py:924
|
||||
msgid "Unknown game."
|
||||
msgstr "Jeu inconnu."
|
||||
|
||||
#: app/validators.py:790
|
||||
#: app/validators.py:291
|
||||
msgid "Gamertag must be between 1 and 120 characters."
|
||||
msgstr "Le gamertag doit compter de 1 à 120 caractères."
|
||||
|
||||
#: app/validators.py:297
|
||||
msgid "Platform must be 30 characters or less."
|
||||
msgstr "La plateforme ne doit pas dépasser 30 caractères."
|
||||
|
||||
#: app/validators.py:306
|
||||
msgid "Unknown platform for this game."
|
||||
msgstr "Plateforme inconnue pour ce jeu."
|
||||
|
||||
#: app/validators.py:343 app/validators.py:378
|
||||
msgid "Invalid role selected."
|
||||
msgstr "Rôle sélectionné invalide."
|
||||
|
||||
#: app/validators.py:473 app/validators.py:596 app/validators.py:629
|
||||
msgid "Player must be selected."
|
||||
msgstr "Vous devez choisir un joueur."
|
||||
|
||||
#: app/validators.py:476
|
||||
msgid "Notes must be 2000 characters or less."
|
||||
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
|
||||
|
||||
#: app/validators.py:513 app/validators.py:993
|
||||
msgid "Invalid coach selection."
|
||||
msgstr "Sélection de coach invalide."
|
||||
|
||||
#: app/validators.py:519 app/validators.py:999
|
||||
msgid "Invalid manager selection."
|
||||
msgstr "Sélection de gérant invalide."
|
||||
|
||||
#: app/validators.py:537 app/validators.py:595 app/validators.py:628
|
||||
msgid "Invalid player selection."
|
||||
msgstr "Sélection de joueur invalide."
|
||||
|
||||
#: app/validators.py:546
|
||||
msgid "Unknown roster status."
|
||||
msgstr "Statut d'effectif inconnu."
|
||||
|
||||
#: app/validators.py:559
|
||||
msgid "Unknown tryout status."
|
||||
msgstr "Statut de sélection inconnu."
|
||||
|
||||
#: app/validators.py:570
|
||||
msgid "Unknown registration status."
|
||||
msgstr "Statut d’inscription inconnu."
|
||||
|
||||
#: app/validators.py:583
|
||||
msgid "Team name must be between 1 and 100 characters."
|
||||
msgstr "Le nom de l’équipe doit compter de 1 à 100 caractères."
|
||||
|
||||
#: app/validators.py:603
|
||||
msgid "Position must be 50 characters or less."
|
||||
msgstr "La position ne doit pas dépasser 50 caractères."
|
||||
|
||||
#: app/validators.py:616
|
||||
msgid "Note content must be between 1 and 5000 characters."
|
||||
msgstr "La note doit compter de 1 à 5000 caractères."
|
||||
|
||||
#: app/validators.py:642
|
||||
msgid "Select at most one note context."
|
||||
msgstr "Sélectionnez au plus un contexte pour la note."
|
||||
|
||||
#: app/validators.py:654
|
||||
msgid "Rejection reason must be 2000 characters or less."
|
||||
msgstr "Le motif de refus ne doit pas dépasser 2000 caractères."
|
||||
|
||||
#: app/validators.py:678
|
||||
msgid "Date must be in YYYY-MM-DD format."
|
||||
msgstr "La date doit être au format AAAA-MM-JJ."
|
||||
|
||||
#: app/validators.py:679
|
||||
msgid "A date is required."
|
||||
msgstr "Une date est requise."
|
||||
|
||||
#: app/validators.py:685 app/validators.py:750
|
||||
msgid "Start time must be in HH:MM format."
|
||||
msgstr "L’heure de début doit être au format HH:MM."
|
||||
|
||||
#: app/validators.py:686 app/validators.py:751
|
||||
msgid "A start time is required."
|
||||
msgstr "Une heure de début est requise."
|
||||
|
||||
#: app/validators.py:692
|
||||
msgid "End time must be in HH:MM format."
|
||||
msgstr "L’heure de fin doit être au format HH:MM."
|
||||
|
||||
#: app/validators.py:693
|
||||
msgid "An end time is required."
|
||||
msgstr "Une heure de fin est requise."
|
||||
|
||||
#: app/validators.py:697
|
||||
msgid "Points must be 2000 characters or less."
|
||||
msgstr "Les points ne doivent pas dépasser 2000 caractères."
|
||||
|
||||
#: app/validators.py:712
|
||||
msgid "End time must be after start time."
|
||||
msgstr "L'heure de fin doit être postérieure à l'heure de début."
|
||||
|
||||
#: app/validators.py:741 app/validators.py:743
|
||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
|
||||
|
||||
#: app/validators.py:744
|
||||
msgid "A day is required."
|
||||
msgstr "Un jour est requis."
|
||||
|
||||
#: app/validators.py:779
|
||||
msgid "Player selection is malformed."
|
||||
msgstr "La sélection de joueurs est mal formée."
|
||||
|
||||
#: app/validators.py:805 app/validators.py:915
|
||||
msgid "A title is required."
|
||||
msgstr "Un titre est requis."
|
||||
|
||||
#: app/validators.py:814
|
||||
msgid "Invalid date format."
|
||||
msgstr "Format de date invalide."
|
||||
|
||||
#: app/validators.py:819 app/validators.py:826
|
||||
msgid "Invalid time format."
|
||||
msgstr "Format d’heure invalide."
|
||||
|
||||
#: app/validators.py:820
|
||||
msgid "Start time is required. Please select a time slot."
|
||||
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
||||
|
||||
#: app/validators.py:834
|
||||
msgid "Unknown match status."
|
||||
msgstr "Statut de match inconnu."
|
||||
|
||||
#: app/validators.py:850
|
||||
msgid "The end time must come after the start time."
|
||||
msgstr "L'heure de fin doit être postérieure à l'heure de début."
|
||||
|
||||
#: app/validators.py:864
|
||||
msgid "Unknown match type."
|
||||
msgstr "Type de match inconnu."
|
||||
|
||||
#: app/validators.py:876
|
||||
msgid "A team cannot play against itself."
|
||||
msgstr "Une équipe ne peut pas jouer contre elle-même."
|
||||
|
||||
#: app/validators.py:929
|
||||
msgid "Invalid start date format."
|
||||
msgstr "Format de date de début invalide."
|
||||
|
||||
#: app/validators.py:791
|
||||
#: app/validators.py:930
|
||||
msgid "A start date is required."
|
||||
msgstr "Une date de début est requise."
|
||||
|
||||
#: app/validators.py:797
|
||||
#: app/validators.py:936
|
||||
msgid "Invalid end date format."
|
||||
msgstr "Format de date de fin invalide."
|
||||
|
||||
#: app/validators.py:805
|
||||
#: app/validators.py:944
|
||||
msgid "A tryout must allow at least one player."
|
||||
msgstr "Une sélection doit accepter au moins un joueur."
|
||||
|
||||
#: app/validators.py:808
|
||||
#: app/validators.py:947
|
||||
msgid "The player limit must be a whole number."
|
||||
msgstr "La limite de joueurs doit être un nombre entier."
|
||||
|
||||
#: app/validators.py:820
|
||||
#: app/validators.py:959
|
||||
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:847 app/validators.py:848
|
||||
#: app/validators.py:986 app/validators.py:987
|
||||
msgid "Team name is required."
|
||||
msgstr "Le nom de l’équipe est obligatoire."
|
||||
|
||||
#: app/validators.py:872
|
||||
#: app/validators.py:1011
|
||||
msgid "Scores run from 1 to 10."
|
||||
msgstr "Les notes vont de 1 à 10."
|
||||
|
||||
#: app/validators.py:873
|
||||
#: app/validators.py:1012
|
||||
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:245
|
||||
#: app/routes/auth.py:246
|
||||
msgid "This account has been deactivated."
|
||||
msgstr "Ce compte a été désactivé."
|
||||
|
||||
#: app/routes/auth.py:280
|
||||
#: app/routes/auth.py:281
|
||||
#, python-format
|
||||
msgid "Welcome back, %(username)s!"
|
||||
msgstr "Bon retour, %(username)s !"
|
||||
|
||||
#: app/routes/auth.py:310
|
||||
#: app/routes/auth.py:311
|
||||
msgid ""
|
||||
"Login unsuccessful. Please check your username and password, or ask a "
|
||||
"president for help."
|
||||
@@ -241,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:376
|
||||
#: app/routes/auth.py:377
|
||||
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:411 app/routes/users/accounts.py:339
|
||||
#: app/routes/auth.py:419 app/routes/users/accounts.py:343
|
||||
msgid "Username already exists."
|
||||
msgstr "Ce nom d’utilisateur est déjà pris."
|
||||
|
||||
#: app/routes/auth.py:415 app/routes/users/accounts.py:343
|
||||
#: app/routes/auth.py:423 app/routes/users/accounts.py:347
|
||||
msgid "Email already registered."
|
||||
msgstr "Cette adresse courriel est déjà enregistrée."
|
||||
|
||||
#: app/routes/auth.py:422 app/routes/auth.py:617
|
||||
#: app/routes/auth.py:430 app/routes/auth.py:623
|
||||
#: app/routes/users/accounts.py:121
|
||||
msgid "This Discord account is already linked to another account."
|
||||
msgstr "Ce compte Discord est déjà lié à un autre compte."
|
||||
|
||||
#: app/routes/auth.py:466
|
||||
#: app/routes/auth.py:472
|
||||
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:491
|
||||
#: app/routes/auth.py:497
|
||||
msgid "Discord OAuth2 is not configured."
|
||||
msgstr "La connexion Discord n’est pas configurée."
|
||||
|
||||
#: app/routes/auth.py:540
|
||||
#: app/routes/auth.py:546
|
||||
msgid ""
|
||||
"Discord authorization could not be verified. Please start the connection "
|
||||
"again from this page."
|
||||
@@ -274,35 +314,35 @@ msgstr ""
|
||||
"L’autorisation Discord n’a pas pu être vérifiée. Relancez la connexion "
|
||||
"depuis cette page."
|
||||
|
||||
#: app/routes/auth.py:549
|
||||
#: app/routes/auth.py:555
|
||||
msgid "Discord authorization failed. No code received."
|
||||
msgstr "L’autorisation Discord a échoué : aucun code reçu."
|
||||
|
||||
#: app/routes/auth.py:573
|
||||
#: app/routes/auth.py:579
|
||||
msgid "Failed to connect to Discord. Please try again."
|
||||
msgstr "Impossible de joindre Discord. Veuillez réessayer."
|
||||
|
||||
#: app/routes/auth.py:577
|
||||
#: app/routes/auth.py:583
|
||||
msgid "Failed to obtain Discord access token."
|
||||
msgstr "Impossible d’obtenir le jeton d’accès Discord."
|
||||
|
||||
#: app/routes/auth.py:592 app/routes/auth.py:602
|
||||
#: app/routes/auth.py:598 app/routes/auth.py:608
|
||||
msgid "Failed to fetch Discord user profile."
|
||||
msgstr "Impossible de récupérer le profil Discord."
|
||||
|
||||
#: app/routes/auth.py:609
|
||||
#: app/routes/auth.py:615
|
||||
msgid "Please log in to connect your Discord account."
|
||||
msgstr "Veuillez vous connecter pour lier votre compte Discord."
|
||||
|
||||
#: app/routes/auth.py:628
|
||||
#: app/routes/auth.py:634
|
||||
msgid "Discord account connected!"
|
||||
msgstr "Compte Discord connecté !"
|
||||
|
||||
#: app/routes/auth.py:675
|
||||
#: app/routes/auth.py:681
|
||||
msgid "Discord account connected! Your profile has been pre-filled."
|
||||
msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
|
||||
|
||||
#: app/routes/auth.py:703
|
||||
#: app/routes/auth.py:709
|
||||
msgid "You have been logged out."
|
||||
msgstr "Vous avez été déconnecté."
|
||||
|
||||
@@ -336,10 +376,10 @@ 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:444
|
||||
#: app/routes/tryouts.py:460 app/routes/tryouts.py:480
|
||||
#: app/routes/tryouts.py:527 app/routes/tryouts.py:563
|
||||
#: app/routes/tryouts.py:582
|
||||
#: 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
|
||||
msgid "Permission denied."
|
||||
msgstr "Accès refusé."
|
||||
|
||||
@@ -347,37 +387,37 @@ msgstr "Accès refusé."
|
||||
msgid "That language is not available."
|
||||
msgstr "Cette langue n’est pas disponible."
|
||||
|
||||
#: app/routes/matches.py:364
|
||||
#: app/routes/matches.py:383
|
||||
msgid "You do not have permission to schedule matches for this tryout."
|
||||
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection."
|
||||
|
||||
#: app/routes/matches.py:368 app/routes/matches.py:448
|
||||
#: app/routes/matches.py:387 app/routes/matches.py:463
|
||||
msgid "This tryout has ended. Matches can no longer be created or modified."
|
||||
msgstr ""
|
||||
"Cette sélection est terminée. Les matchs ne peuvent plus être créés ni "
|
||||
"modifiés."
|
||||
|
||||
#: app/routes/matches.py:430
|
||||
#: app/routes/matches.py:445
|
||||
msgid "Match scheduled successfully!"
|
||||
msgstr "Match planifié."
|
||||
|
||||
#: app/routes/matches.py:444 app/routes/team_matches.py:220
|
||||
#: app/routes/matches.py:459 app/routes/team_matches.py:220
|
||||
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:539 app/routes/team_matches.py:250
|
||||
#: app/routes/matches.py:552 app/routes/team_matches.py:250
|
||||
msgid "Match updated successfully!"
|
||||
msgstr "Match mis à jour."
|
||||
|
||||
#: app/routes/matches.py:575 app/routes/team_matches.py:265
|
||||
#: app/routes/matches.py:588 app/routes/team_matches.py:265
|
||||
msgid "You do not have permission to delete this match."
|
||||
msgstr "Vous n’avez pas les droits pour supprimer ce match."
|
||||
|
||||
#: app/routes/matches.py:578
|
||||
#: app/routes/matches.py:591
|
||||
msgid "This tryout has ended. Matches can no longer be deleted."
|
||||
msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés."
|
||||
|
||||
#: app/routes/matches.py:591 app/routes/team_matches.py:269
|
||||
#: app/routes/matches.py:604 app/routes/team_matches.py:269
|
||||
msgid "Match deleted successfully."
|
||||
msgstr "Match supprimé."
|
||||
|
||||
@@ -480,7 +520,7 @@ msgstr "Coach retiré de %(name)s."
|
||||
msgid "Manager removed from %(name)s."
|
||||
msgstr "Gérant retiré de %(name)s."
|
||||
|
||||
#: app/routes/teams.py:490 app/routes/tryouts.py:489 app/routes/tryouts.py:593
|
||||
#: app/routes/teams.py:490 app/routes/tryouts.py:524
|
||||
msgid "Please select a player."
|
||||
msgstr "Veuillez choisir un joueur."
|
||||
|
||||
@@ -498,7 +538,7 @@ msgstr "%(username)s fait déjà partie de %(name)s."
|
||||
msgid "%(username)s added to %(name)s!"
|
||||
msgstr "%(username)s a été ajouté à %(name)s."
|
||||
|
||||
#: app/routes/teams.py:527 app/routes/teams.py:601
|
||||
#: app/routes/teams.py:527 app/routes/teams.py:605
|
||||
#, python-format
|
||||
msgid "%(username)s is not on %(name)s."
|
||||
msgstr "%(username)s ne fait pas partie de %(name)s."
|
||||
@@ -508,130 +548,130 @@ msgstr "%(username)s ne fait pas partie de %(name)s."
|
||||
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:590
|
||||
#: app/routes/teams.py:572 app/routes/teams.py:594
|
||||
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:580
|
||||
#: app/routes/teams.py:584
|
||||
msgid "Team notes added successfully!"
|
||||
msgstr "Notes d’équipe ajoutées."
|
||||
|
||||
#: app/routes/teams.py:595 app/routes/users/notes.py:207
|
||||
#: app/routes/users/notes.py:250
|
||||
#: app/routes/teams.py:599 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:611
|
||||
#: app/routes/teams.py:619
|
||||
#, python-format
|
||||
msgid "Note added for %(username)s!"
|
||||
msgstr "Note ajoutée pour %(username)s."
|
||||
|
||||
#: app/routes/tryouts.py:98
|
||||
#: app/routes/tryouts.py:125
|
||||
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:145
|
||||
#: app/routes/tryouts.py:172
|
||||
msgid "Tryout created successfully!"
|
||||
msgstr "Sélection créée."
|
||||
|
||||
#: app/routes/tryouts.py:158
|
||||
#: app/routes/tryouts.py:185
|
||||
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:162
|
||||
#: app/routes/tryouts.py:189
|
||||
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:202
|
||||
#: app/routes/tryouts.py:229
|
||||
msgid "Tryout updated successfully!"
|
||||
msgstr "Sélection mise à jour."
|
||||
|
||||
#: app/routes/tryouts.py:242
|
||||
#: app/routes/tryouts.py:269
|
||||
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:411
|
||||
#: app/routes/tryouts.py:437
|
||||
msgid "Only players can register for tryouts."
|
||||
msgstr "Seuls les joueurs peuvent s’inscrire à une sélection."
|
||||
|
||||
#: app/routes/tryouts.py:415
|
||||
#: app/routes/tryouts.py:442
|
||||
msgid "This tryout is not accepting registrations."
|
||||
msgstr "Cette sélection n’accepte pas d’inscriptions."
|
||||
|
||||
#: app/routes/tryouts.py:422
|
||||
#: app/routes/tryouts.py:449
|
||||
msgid "You are already registered for this tryout."
|
||||
msgstr "Vous êtes déjà inscrit à cette sélection."
|
||||
|
||||
#: app/routes/tryouts.py:428 app/routes/tryouts.py:511
|
||||
#: app/routes/tryouts.py:455 app/routes/tryouts.py:546
|
||||
msgid "This tryout is full."
|
||||
msgstr "Cette sélection est complète."
|
||||
|
||||
#: app/routes/tryouts.py:434
|
||||
#: app/routes/tryouts.py:461
|
||||
msgid "Successfully registered for tryout!"
|
||||
msgstr "Inscription à la sélection réussie."
|
||||
|
||||
#: app/routes/tryouts.py:450
|
||||
#: app/routes/tryouts.py:481
|
||||
#, 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:470
|
||||
#: app/routes/tryouts.py:505
|
||||
msgid "Registration status updated."
|
||||
msgstr "Statut d’inscription mis à jour."
|
||||
|
||||
#: app/routes/tryouts.py:497
|
||||
#: app/routes/tryouts.py:532
|
||||
msgid "Can only register players."
|
||||
msgstr "Seuls des joueurs peuvent être inscrits."
|
||||
|
||||
#: app/routes/tryouts.py:503
|
||||
#: app/routes/tryouts.py:538
|
||||
#, 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:517
|
||||
#: app/routes/tryouts.py:552
|
||||
#, python-format
|
||||
msgid "%(username)s registered for tryout!"
|
||||
msgstr "%(username)s est inscrit à la sélection."
|
||||
|
||||
#: app/routes/tryouts.py:553
|
||||
#: app/routes/tryouts.py:588
|
||||
#, python-format
|
||||
msgid "%(username)s removed from tryout."
|
||||
msgstr "%(username)s a été retiré de la sélection."
|
||||
|
||||
#: app/routes/tryouts.py:571
|
||||
#: app/routes/tryouts.py:610
|
||||
#, python-format
|
||||
msgid "Team \"%(team_name)s\" created!"
|
||||
msgstr "Équipe « %(team_name)s » créée."
|
||||
|
||||
#: app/routes/tryouts.py:602
|
||||
#: app/routes/tryouts.py:643 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:608
|
||||
#: app/routes/tryouts.py:648
|
||||
msgid "Player is already on this team."
|
||||
msgstr "Ce joueur est déjà dans cette équipe."
|
||||
|
||||
#: app/routes/tryouts.py:613
|
||||
#: app/routes/tryouts.py:653
|
||||
msgid "Player added to team!"
|
||||
msgstr "Joueur ajouté à l’équipe."
|
||||
|
||||
#: app/routes/tryouts.py:623
|
||||
#: app/routes/tryouts.py:663
|
||||
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:659
|
||||
#: app/routes/tryouts.py:699
|
||||
msgid "Tryout deleted successfully."
|
||||
msgstr "Sélection supprimée."
|
||||
|
||||
#: app/routes/users/_shared.py:51
|
||||
#: app/routes/users/_shared.py:50
|
||||
msgid "No file selected."
|
||||
msgstr "Aucun fichier sélectionné."
|
||||
|
||||
#: app/routes/users/_shared.py:55
|
||||
#: app/routes/users/_shared.py:54
|
||||
msgid "Only PDF files are allowed for contracts."
|
||||
msgstr "Seuls les fichiers PDF sont acceptés pour les contrats."
|
||||
|
||||
#: app/routes/users/_shared.py:60
|
||||
#: app/routes/users/_shared.py:59
|
||||
msgid "That file is not a PDF, whatever its name says."
|
||||
msgstr "Ce fichier n’est pas un PDF, quel que soit son nom."
|
||||
|
||||
@@ -647,13 +687,13 @@ msgstr "Seul le président peut modifier des utilisateurs."
|
||||
msgid "Email already in use by another account."
|
||||
msgstr "Cette adresse courriel est déjà utilisée par un autre compte."
|
||||
|
||||
#: app/routes/users/accounts.py:132
|
||||
#: app/routes/users/accounts.py:138
|
||||
msgid "You cannot change your own role. Ask another president to do it."
|
||||
msgstr ""
|
||||
"Vous ne pouvez pas modifier votre propre rôle. Demandez à un autre "
|
||||
"président de le faire."
|
||||
|
||||
#: app/routes/users/accounts.py:145
|
||||
#: app/routes/users/accounts.py:151
|
||||
msgid ""
|
||||
"This is the last active president. Promote another account before "
|
||||
"changing this one."
|
||||
@@ -661,29 +701,29 @@ msgstr ""
|
||||
"C’est le dernier président actif. Promouvez un autre compte avant de "
|
||||
"modifier celui-ci."
|
||||
|
||||
#: app/routes/users/accounts.py:224
|
||||
#: app/routes/users/accounts.py:228
|
||||
#, python-format
|
||||
msgid "User %(username)s updated successfully!"
|
||||
msgstr "Utilisateur %(username)s mis à jour."
|
||||
|
||||
#: app/routes/users/accounts.py:245
|
||||
#: app/routes/users/accounts.py:249
|
||||
msgid "Only the president can delete users."
|
||||
msgstr "Seul le président peut supprimer des utilisateurs."
|
||||
|
||||
#: app/routes/users/accounts.py:249
|
||||
#: app/routes/users/accounts.py:253
|
||||
msgid "You cannot delete your own account."
|
||||
msgstr "Vous ne pouvez pas supprimer votre propre compte."
|
||||
|
||||
#: app/routes/users/accounts.py:308
|
||||
#: app/routes/users/accounts.py:312
|
||||
#, python-format
|
||||
msgid "User %(deleted_username)s has been removed."
|
||||
msgstr "L’utilisateur %(deleted_username)s a été supprimé."
|
||||
|
||||
#: app/routes/users/accounts.py:319
|
||||
#: app/routes/users/accounts.py:323
|
||||
msgid "Only the president can create users."
|
||||
msgstr "Seul le président peut créer des utilisateurs."
|
||||
|
||||
#: app/routes/users/accounts.py:367
|
||||
#: app/routes/users/accounts.py:371
|
||||
#, python-format
|
||||
msgid "User %(full_name)s created as %(role)s!"
|
||||
msgstr "Utilisateur %(full_name)s créé avec le rôle %(role)s."
|
||||
@@ -721,56 +761,95 @@ msgstr "Vous n’avez pas les droits pour télécharger ce contrat."
|
||||
msgid "No signed contract available."
|
||||
msgstr "Aucun contrat signé disponible."
|
||||
|
||||
#: app/routes/users/notes.py:34
|
||||
#: app/routes/users/notes.py:43
|
||||
msgid "This page is for players only."
|
||||
msgstr "Cette page est réservée aux joueurs."
|
||||
|
||||
#: app/routes/users/notes.py:71
|
||||
#: app/routes/users/notes.py:80
|
||||
msgid "Only coaches can access the notes dashboard."
|
||||
msgstr "Seuls les coachs ont accès au tableau des notes."
|
||||
|
||||
#: app/routes/users/notes.py:161
|
||||
#: app/routes/users/notes.py:172
|
||||
msgid "Only coaches can manage team notes."
|
||||
msgstr "Seuls les coachs peuvent gérer les notes d’équipe."
|
||||
|
||||
#: app/routes/users/notes.py:167
|
||||
#: app/routes/users/notes.py:178
|
||||
msgid "You are not assigned to a team."
|
||||
msgstr "Vous n’êtes assigné à aucune équipe."
|
||||
|
||||
#: app/routes/users/notes.py:180
|
||||
#: app/routes/users/notes.py:195
|
||||
msgid "Team notes saved successfully!"
|
||||
msgstr "Notes d’équipe enregistrées."
|
||||
|
||||
#: app/routes/users/notes.py:195
|
||||
#: app/routes/users/notes.py:210
|
||||
msgid "Only coaches can manage personal notes."
|
||||
msgstr "Seuls les coachs peuvent gérer les notes personnelles."
|
||||
|
||||
#: app/routes/users/notes.py:202 app/routes/users/notes.py:245
|
||||
#: app/routes/users/notes.py:296 app/routes/users/notes.py:350
|
||||
msgid "Player and content are required."
|
||||
msgstr "Le joueur et le contenu sont obligatoires."
|
||||
|
||||
#: app/routes/users/notes.py:211 app/routes/users/notes.py:254
|
||||
#: app/routes/users/notes.py:300 app/routes/users/notes.py:354
|
||||
#: app/routes/users/notes.py:226 app/routes/users/notes.py:266
|
||||
#: app/routes/users/notes.py:346 app/routes/users/notes.py:411
|
||||
msgid "You can only write notes about players you work with."
|
||||
msgstr ""
|
||||
"Vous ne pouvez écrire des notes que sur les joueurs avec qui vous "
|
||||
"travaillez."
|
||||
|
||||
#: app/routes/users/notes.py:221 app/routes/users/notes.py:267
|
||||
#: app/routes/users/notes.py:236 app/routes/users/notes.py:306
|
||||
#, python-format
|
||||
msgid "Note added for %(username)s."
|
||||
msgstr "Note ajoutée pour %(username)s."
|
||||
|
||||
#: app/routes/users/notes.py:235 app/routes/users/notes.py:281
|
||||
#: app/routes/users/notes.py:334
|
||||
#: app/routes/users/notes.py:250 app/routes/users/notes.py:320
|
||||
#: app/routes/users/notes.py:384
|
||||
msgid "Only coaches can add personal notes."
|
||||
msgstr "Seuls les coachs peuvent ajouter des notes personnelles."
|
||||
|
||||
#: app/routes/users/notes.py:311 app/routes/users/notes.py:365
|
||||
#: app/routes/users/notes.py:272
|
||||
msgid "You cannot use that match as note context."
|
||||
msgstr "Vous ne pouvez pas utiliser ce match comme contexte de note."
|
||||
|
||||
#: app/routes/users/notes.py:275
|
||||
msgid "That player did not participate in the selected match."
|
||||
msgstr "Ce joueur n’a pas participé au match sélectionné."
|
||||
|
||||
#: app/routes/users/notes.py:281
|
||||
msgid "You cannot use that tryout as note context."
|
||||
msgstr "Vous ne pouvez pas utiliser cette sélection comme contexte de note."
|
||||
|
||||
#: app/routes/users/notes.py:284
|
||||
msgid "That player is not registered for the selected tryout."
|
||||
msgstr "Ce joueur n’est pas inscrit à la sélection choisie."
|
||||
|
||||
#: app/routes/users/notes.py:290
|
||||
msgid "You cannot use that team as note context."
|
||||
msgstr "Vous ne pouvez pas utiliser cette équipe comme contexte de note."
|
||||
|
||||
#: app/routes/users/notes.py:293
|
||||
msgid "That player is not on the selected team."
|
||||
msgstr "Ce joueur ne fait pas partie de l’équipe sélectionnée."
|
||||
|
||||
#: app/routes/users/notes.py:325
|
||||
msgid "You do not have permission to add notes for this tryout."
|
||||
msgstr "Vous n’avez pas les droits pour ajouter des notes à cette sélection."
|
||||
|
||||
#: app/routes/users/notes.py:342
|
||||
msgid "Invalid tryout context."
|
||||
msgstr "Contexte de sélection invalide."
|
||||
|
||||
#: app/routes/users/notes.py:361 app/routes/users/notes.py:426
|
||||
msgid "Note added successfully."
|
||||
msgstr "Note ajoutée."
|
||||
|
||||
#: app/routes/users/notes.py:389
|
||||
msgid "You do not have permission to add notes for this match."
|
||||
msgstr "Vous n’avez pas les droits pour ajouter des notes à ce match."
|
||||
|
||||
#: app/routes/users/notes.py:407
|
||||
msgid "Invalid match context."
|
||||
msgstr "Contexte de match invalide."
|
||||
|
||||
#: app/routes/users/notes.py:415
|
||||
msgid "That player did not participate in this match."
|
||||
msgstr "Ce joueur n’a pas participé à ce match."
|
||||
|
||||
#: app/routes/users/one_on_one.py:23
|
||||
msgid "Only players can request One on One sessions."
|
||||
msgstr "Seuls les joueurs peuvent demander une rencontre individuelle."
|
||||
@@ -812,7 +891,7 @@ msgstr "La demande de rencontre de %(player)s a été approuvée."
|
||||
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:258
|
||||
#: app/routes/users/one_on_one.py:263
|
||||
#, 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."
|
||||
@@ -825,7 +904,7 @@ msgstr "Ce nom d’utilisateur est déjà pris."
|
||||
msgid "Email already in use."
|
||||
msgstr "Cette adresse courriel est déjà utilisée."
|
||||
|
||||
#: app/routes/users/profile.py:121
|
||||
#: app/routes/users/profile.py:131
|
||||
msgid "Profile updated successfully!"
|
||||
msgstr "Profil mis à jour."
|
||||
|
||||
@@ -1221,7 +1300,7 @@ msgstr ""
|
||||
"individuelles"
|
||||
|
||||
#: app/templates/pages/coach_availability.html:14
|
||||
#: app/templates/pages/profile.html:216
|
||||
#: app/templates/pages/profile.html:213
|
||||
msgid "Loading availability grid..."
|
||||
msgstr "Chargement de la grille de disponibilités..."
|
||||
|
||||
@@ -1230,7 +1309,7 @@ msgid "Save Availability"
|
||||
msgstr "Enregistrer les disponibilités"
|
||||
|
||||
#: app/templates/pages/coach_availability.html:22
|
||||
#: app/templates/pages/profile.html:200 app/templates/pages/profile.html:220
|
||||
#: app/templates/pages/profile.html:197 app/templates/pages/profile.html:217
|
||||
msgid "Clear All"
|
||||
msgstr "Tout effacer"
|
||||
|
||||
@@ -2011,23 +2090,23 @@ msgstr ""
|
||||
"Les indicateurs verts signalent les joueurs disponibles à la date et à "
|
||||
"l’heure du match"
|
||||
|
||||
#: app/templates/pages/match_form.html:625
|
||||
#: app/templates/pages/match_form.html:632
|
||||
msgid "Click time slots consecutively to set match duration"
|
||||
msgstr "Cliquez des plages consécutives pour définir la durée du match"
|
||||
|
||||
#: app/templates/pages/match_form.html:892
|
||||
#: app/templates/pages/match_form.html:899
|
||||
msgid "No players registered"
|
||||
msgstr "Aucun joueur inscrit"
|
||||
|
||||
#: app/templates/pages/match_form.html:925
|
||||
#: app/templates/pages/match_form.html:932
|
||||
msgid "T1"
|
||||
msgstr "É1"
|
||||
|
||||
#: app/templates/pages/match_form.html:926
|
||||
#: app/templates/pages/match_form.html:933
|
||||
msgid "T2"
|
||||
msgstr "É2"
|
||||
|
||||
#: app/templates/pages/match_form.html:931
|
||||
#: app/templates/pages/match_form.html:938
|
||||
msgid "No players available"
|
||||
msgstr "Aucun joueur disponible"
|
||||
|
||||
@@ -2382,15 +2461,11 @@ msgstr ""
|
||||
"Choisissez vos plages disponibles pour les matchs (17 h à minuit). Vert ="
|
||||
" sélectionné, gris = disponible."
|
||||
|
||||
#: app/templates/pages/profile.html:197
|
||||
msgid "Save Disponibilities"
|
||||
msgstr "Enregistrer mes disponibilités"
|
||||
|
||||
#: app/templates/pages/profile.html:211
|
||||
#: app/templates/pages/profile.html:208
|
||||
msgid "My Coaching Availability"
|
||||
msgstr "Mes disponibilités de coaching"
|
||||
|
||||
#: app/templates/pages/profile.html:212
|
||||
#: app/templates/pages/profile.html:209
|
||||
msgid ""
|
||||
"Select time slots when you're available for One on One sessions (8am to "
|
||||
"10pm)."
|
||||
@@ -2398,7 +2473,7 @@ msgstr ""
|
||||
"Choisissez les plages où vous êtes disponible pour des rencontres "
|
||||
"individuelles (8 h à 22 h)."
|
||||
|
||||
#: app/templates/pages/profile.html:460
|
||||
#: app/templates/pages/profile.html:457
|
||||
msgid "Click or click-and-drag to select your available hours"
|
||||
msgstr "Cliquez ou faites glisser pour choisir vos heures de disponibilité"
|
||||
|
||||
@@ -3025,3 +3100,9 @@ msgstr "Voir le profil"
|
||||
|
||||
#~ msgid "Team Tryout Management System"
|
||||
#~ msgstr "Système de gestion des sélections d’équipe"
|
||||
|
||||
#~ msgid "Player and content are required."
|
||||
#~ msgstr "Le joueur et le contenu sont obligatoires."
|
||||
|
||||
#~ msgid "Save Disponibilities"
|
||||
#~ msgstr "Enregistrer mes disponibilités"
|
||||
|
||||
+140
-1
@@ -23,7 +23,7 @@ from marshmallow import (
|
||||
validates_schema,
|
||||
)
|
||||
|
||||
from app.models import ESPORT_GAMES, USER_TYPES
|
||||
from app.models import ESPORT_GAMES, GAME_PLATFORMS, USER_TYPES
|
||||
|
||||
# =============================================================================
|
||||
# Custom Validators
|
||||
@@ -276,6 +276,36 @@ class RegisterSchema(StripMixin):
|
||||
raise ValidationError(_l('Passwords do not match.'), field_name='confirm_password')
|
||||
|
||||
|
||||
class GamertagSchema(StripMixin):
|
||||
"""One dynamic per-game identity submitted beside an account form."""
|
||||
|
||||
game = fields.String(
|
||||
required=True,
|
||||
validate=validate.OneOf(ESPORT_GAMES, error=_l('Unknown game.')),
|
||||
)
|
||||
gamertag = fields.String(
|
||||
required=True,
|
||||
validate=validate.Length(
|
||||
min=1,
|
||||
max=120,
|
||||
error=_l('Gamertag must be between 1 and 120 characters.'),
|
||||
),
|
||||
)
|
||||
platform = fields.String(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Length(max=30, error=_l('Platform must be 30 characters or less.')),
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_platform_for_game(self, data, **kwargs):
|
||||
"""A forged platform must belong to the selected game's list."""
|
||||
platform = data.get('platform')
|
||||
allowed = GAME_PLATFORMS.get(data.get('game'), ())
|
||||
if platform and platform not in allowed:
|
||||
raise ValidationError(_l('Unknown platform for this game.'), field_name='platform')
|
||||
|
||||
|
||||
class CreateUserSchema(StripMixin):
|
||||
"""Validate president-created user form input.
|
||||
|
||||
@@ -517,6 +547,115 @@ class TeamPlayerSchema(PlayerSelectionSchema):
|
||||
)
|
||||
|
||||
|
||||
TRYOUT_STATUSES = ('upcoming', 'in_progress', 'completed')
|
||||
TRYOUT_REGISTRATION_STATUSES = ('registered', 'attended', 'no_show')
|
||||
|
||||
|
||||
class TryoutStatusSchema(StripMixin):
|
||||
"""A state transition requested from the tryout detail page."""
|
||||
|
||||
status = fields.String(
|
||||
required=True,
|
||||
validate=validate.OneOf(TRYOUT_STATUSES, error=_l('Unknown tryout status.')),
|
||||
)
|
||||
|
||||
|
||||
class TryoutRegistrationStatusSchema(StripMixin):
|
||||
"""Attendance state for one tryout registration."""
|
||||
|
||||
status = fields.String(
|
||||
required=True,
|
||||
validate=validate.OneOf(
|
||||
TRYOUT_REGISTRATION_STATUSES,
|
||||
error=_l('Unknown registration status.'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TryoutTeamSchema(StripMixin):
|
||||
"""A tryout-local team created from its compact inline form."""
|
||||
|
||||
team_name = fields.String(
|
||||
required=True,
|
||||
validate=validate.Length(
|
||||
min=1,
|
||||
max=100,
|
||||
error=_l('Team name must be between 1 and 100 characters.'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TryoutTeamMemberSchema(StripMixin):
|
||||
"""A registered player and their optional position on a tryout team."""
|
||||
|
||||
player_id = fields.Integer(
|
||||
required=True,
|
||||
validate=validate.Range(min=1),
|
||||
error_messages={
|
||||
'invalid': _l('Invalid player selection.'),
|
||||
'required': _l('Player must be selected.'),
|
||||
},
|
||||
)
|
||||
position = fields.String(
|
||||
load_default='',
|
||||
validate=validate.Length(
|
||||
max=50,
|
||||
error=_l('Position must be 50 characters or less.'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class NoteContentSchema(StripMixin):
|
||||
"""Bounded text stored as a team or personal coaching note."""
|
||||
|
||||
content = fields.String(
|
||||
required=True,
|
||||
validate=validate.Length(
|
||||
min=1,
|
||||
max=5000,
|
||||
error=_l('Note content must be between 1 and 5000 characters.'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PersonalNoteSchema(NoteContentSchema):
|
||||
"""A personal note with at most one optional, typed context."""
|
||||
|
||||
player_id = fields.Integer(
|
||||
required=True,
|
||||
validate=validate.Range(min=1),
|
||||
error_messages={
|
||||
'invalid': _l('Invalid player selection.'),
|
||||
'required': _l('Player must be selected.'),
|
||||
},
|
||||
)
|
||||
match_id = fields.Integer(allow_none=True, load_default=None, validate=validate.Range(min=1))
|
||||
tryout_id = fields.Integer(allow_none=True, load_default=None, validate=validate.Range(min=1))
|
||||
team_id = fields.Integer(allow_none=True, load_default=None, validate=validate.Range(min=1))
|
||||
|
||||
@validates_schema
|
||||
def validate_one_context(self, data, **kwargs):
|
||||
"""A note cannot claim several unrelated contexts at once."""
|
||||
contexts = [data.get(name) for name in ('match_id', 'tryout_id', 'team_id')]
|
||||
if sum(value is not None for value in contexts) > 1:
|
||||
raise ValidationError(
|
||||
_l('Select at most one note context.'),
|
||||
field_name='context',
|
||||
)
|
||||
|
||||
|
||||
class OneOnOneRejectionSchema(StripMixin):
|
||||
"""Optional explanation sent to a player when a request is rejected."""
|
||||
|
||||
rejection_reason = fields.String(
|
||||
load_default='',
|
||||
validate=validate.Length(
|
||||
max=2000,
|
||||
error=_l('Rejection reason must be 2000 characters or less.'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class OneOnOneRequestSchema(StripMixin):
|
||||
"""A player asking their coach for a session (MNT-12).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user