fix(authz): finir SEC-16, que mon propre correctif avait laisse a moitie

La vague K a pose un schema sur create_team et edit_team et a laisse cinq
routes soeurs du meme fichier lire int(request.form.get(...)) : add_coach,
add_manager, remove_coach, remove_manager et add_player. Un identifiant non
numerique y etait un 500 dans chacune.

C'est exactement la lecon que ce projet repete depuis la vague D -- corriger
un motif fautif dans une seule couche le laisse dans les autres -- et cette
fois c'est le correctif lui-meme qui l'a commise. Notee comme telle.

Deux defauts de plus, trouves en finissant.

add_player ecrivait status tel quel dans une colonne NOT NULL String(20). Et
toggle_player_status lit "substitute si status == starter, sinon starter" :
une valeur inconnue devenait donc starter a la premiere bascule, c'est-a-dire
promouvait son porteur. Liste blanche dans TEAM_PLAYER_STATUSES.

Et aucune de ces routes ne regardait is_active_account. La requete qui
alimente la liste deroulante des joueurs ne le filtrait pas non plus, alors
que les deux requetes juste au-dessus, coachs et gerants, le posaient -- deux
lignes d'ecart, meme fichier. Un compte desactive etait donc propose et
accepte, alors que is_active_account est precisement ce qui dit que la
personne a quitte le club. Meme oubli dans tryouts.py.

_staff_member delegue desormais a _assignable au lieu de repeter isinstance :
deux fonctions du meme fichier repondant differemment a "ce compte peut-il
prendre ce role" est la forme de tous les defauts qu'a eus ce module.

Verifie par mutation : retirer le controle d'activite ou la liste blanche
fait tomber trois tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-11 20:42:26 -04:00
co-authored by Claude Opus 5
parent 838b247649
commit 20dabecd67
8 changed files with 633 additions and 361 deletions
+80 -32
View File
@@ -29,7 +29,7 @@ from app.models import (
User,
)
from app.permissions import visible_org_teams
from app.validators import OrgTeamSchema
from app.validators import OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@@ -55,7 +55,12 @@ def list_teams():
managers = (
User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
)
all_players = User.query.filter_by(role='player').order_by(User.username).all()
# is_active_account, like the two queries above it. Without it the "add
# player" select offered accounts that had been deactivated, and
# add_player accepted them.
all_players = (
User.query.filter_by(role='player', is_active_account=True).order_by(User.username).all()
)
return render_template(
'pages/teams.html',
teams=teams,
@@ -119,27 +124,58 @@ def my_teams():
return render_template('pages/my_teams.html', team_data=team_data, now=now)
def _staff_member(user_id, expected_class):
"""The user behind an id, only if they hold the role being assigned.
def _posted(schema):
"""Load a form through `schema`, or None when it will not load.
Returns None for a missing id, an unknown id, or an account of the wrong
role. That last case is the point (SEC-16): the id comes from a `<select>`
the browser rendered, so it is a value the client chooses, and nothing
checked it in two of the three places that used it. A forged submission
could therefore list a player among a team's coaches — the same defect
wave G fixed in `tryouts.py`, left standing here.
The five assignment routes below each answer a bad field with their own
flash and a redirect to the same page, so a shared "it did not validate"
return is enough; the field-level message is flashed on the way out.
"""
try:
return schema.load(form_payload())
except ValidationError as err:
flash_validation_errors(err)
return None
def _assignable(user, expected_class):
"""Whether this account may be given a role on a team.
Deactivated accounts were offered by the selects and accepted by the
routes. `is_active_account` is what stops someone logging in — a person
who has left the club — so putting them on a roster contradicts the one
control that says they are gone. The listings filtered it for coaches and
managers and not for players, two lines apart, which is how it went
unnoticed.
"""
return isinstance(user, expected_class) and bool(user.is_active_account)
def _staff_member(user_id, expected_class):
"""The user behind an id, only if they may hold the role being assigned.
Returns None for a missing id, an unknown id, an account of the wrong
role, or a deactivated one. The role check is the point (SEC-16): the id
comes from a `<select>` the browser rendered, so it is a value the client
chooses, and nothing checked it in two of the three places that used it.
A forged submission could therefore list a player among a team's coaches
— the same defect wave G fixed in `tryouts.py`, left standing here.
Defers to `_assignable` rather than repeating `isinstance`: two functions
in one file answering "may this account take this role" differently is
the shape of every defect this module has had.
Args:
user_id: Already an int or None, thanks to OrgTeamSchema.
user_id: Already an int or None, thanks to the schema.
expected_class: Coach or Manager.
Returns:
User | None: The account, when it is of the expected role.
User | None: The account, when it may take the role.
"""
if not user_id:
return None
user = db.session.get(User, user_id)
return user if isinstance(user, expected_class) else None
return user if user and _assignable(user, expected_class) else None
@teams_bp.route('/create', methods=['POST'])
@@ -305,13 +341,15 @@ def add_coach(team_id):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id')
if not coach_id:
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
if not data['coach_id']:
flash(_('Please select a coach.'), 'danger')
return redirect(url_for('teams.list_teams'))
coach = User.query.get_or_404(int(coach_id))
if not isinstance(coach, Coach):
coach = db.session.get(User, data['coach_id'])
if not coach or not _assignable(coach, Coach):
flash(_('Only coaches can be assigned as coach.'), 'danger')
return redirect(url_for('teams.list_teams'))
@@ -346,13 +384,15 @@ def add_manager(team_id):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id')
if not manager_id:
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
if not data['manager_id']:
flash(_('Please select a manager.'), 'danger')
return redirect(url_for('teams.list_teams'))
manager = User.query.get_or_404(int(manager_id))
if not isinstance(manager, Manager):
manager = db.session.get(User, data['manager_id'])
if not manager or not _assignable(manager, Manager):
flash(_('Only managers can be assigned as manager.'), 'danger')
return redirect(url_for('teams.list_teams'))
@@ -387,9 +427,12 @@ def remove_coach(team_id):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id')
if coach_id:
coach = User.query.get(int(coach_id))
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
if data['coach_id']:
coach = db.session.get(User, data['coach_id'])
if coach and team.coaches.filter_by(id=coach.id).first():
team.coaches.remove(coach)
if team.coach_id == coach.id:
@@ -412,9 +455,12 @@ def remove_manager(team_id):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id')
if manager_id:
manager = User.query.get(int(manager_id))
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
if data['manager_id']:
manager = db.session.get(User, data['manager_id'])
if manager and team.managers.filter_by(id=manager.id).first():
team.managers.remove(manager)
if team.manager_id == manager.id:
@@ -437,14 +483,16 @@ def add_player(team_id):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
player_id = request.form.get('player_id')
status = request.form.get('status', 'starter')
if not player_id:
data = _posted(TeamPlayerSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
if not data['player_id']:
flash(_('Please select a player.'), 'danger')
return redirect(url_for('teams.list_teams'))
player = User.query.get_or_404(int(player_id))
if not isinstance(player, Player):
status = data['status']
player = db.session.get(User, data['player_id'])
if not player or not _assignable(player, Player):
flash(_('Can only assign players to teams.'), 'danger')
return redirect(url_for('teams.list_teams'))
+8 -1
View File
@@ -303,7 +303,14 @@ def view_tryout(tryout_id):
all_players = None
if can_edit:
all_players = User.query.filter_by(role='player').order_by(User.username).all()
# is_active_account, like the manager and coach queries in this same
# module. Offering a deactivated account in a roster select
# contradicts the one control that says the person has left.
all_players = (
User.query.filter_by(role='player', is_active_account=True)
.order_by(User.username)
.all()
)
matches = (
Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
Binary file not shown.
+172 -164
View File
@@ -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-11 20:15-0400\n"
"POT-Creation-Date: 2026-08-11 20:39-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@@ -90,127 +90,135 @@ msgstr "Player must be selected."
msgid "Notes must be 2000 characters or less."
msgstr "Notes must be 2000 characters or less."
#: app/validators.py:482
msgid "Date must be in YYYY-MM-DD format."
msgstr "Date must be in YYYY-MM-DD format."
#: app/validators.py:483
msgid "A date is required."
msgstr "A date is required."
#: app/validators.py:489 app/validators.py:554
msgid "Start time must be in HH:MM format."
msgstr "Start time must be in HH:MM format."
#: app/validators.py:490 app/validators.py:555
msgid "A start time is required."
msgstr "A start time is required."
#: app/validators.py:496
msgid "End time must be in HH:MM format."
msgstr "End time must be in HH:MM format."
#: app/validators.py:497
msgid "An end time is required."
msgstr "An end time is required."
#: app/validators.py:501
msgid "Points must be 2000 characters or less."
msgstr "Points must be 2000 characters or less."
#: app/validators.py:516
msgid "End time must be after start time."
msgstr "End time must be after start time."
#: app/validators.py:545 app/validators.py:547
msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
#: app/validators.py:548
msgid "A day is required."
msgstr "A day is required."
#: app/validators.py:583
msgid "Player selection is malformed."
msgstr "Player selection is malformed."
#: app/validators.py:609 app/validators.py:719
msgid "A title is required."
msgstr "A title is required."
#: app/validators.py:618
msgid "Invalid date format."
msgstr "Invalid date format."
#: app/validators.py:623 app/validators.py:630
msgid "Invalid time format."
msgstr "Invalid time format."
#: app/validators.py:624
msgid "Start time is required. Please select a time slot."
msgstr "Start time is required. Please select a time slot."
#: app/validators.py:638
msgid "Unknown match status."
msgstr "Unknown match status."
#: app/validators.py:654
msgid "The end time must come after the start time."
msgstr "The end time must come after the start time."
#: app/validators.py:668
msgid "Unknown match type."
msgstr "Unknown match type."
#: app/validators.py:680
msgid "A team cannot play against itself."
msgstr "A team cannot play against itself."
#: app/validators.py:728
msgid "Unknown game."
msgstr "Unknown game."
#: app/validators.py:733
msgid "Invalid start date format."
msgstr "Invalid start date format."
#: app/validators.py:734
msgid "A start date is required."
msgstr "A start date is required."
#: app/validators.py:740
msgid "Invalid end date format."
msgstr "Invalid end date format."
#: app/validators.py:748
msgid "A tryout must allow at least one player."
msgstr "A tryout must allow at least one player."
#: app/validators.py:751
msgid "The player limit must be a whole number."
msgstr "The player limit must be a whole number."
#: app/validators.py:763
msgid "End date cannot be before start date."
msgstr "End date cannot be before start date."
#: app/validators.py:790 app/validators.py:791
msgid "Team name is required."
msgstr "Team name is required."
#: app/validators.py:797
#: app/validators.py:494 app/validators.py:853
msgid "Invalid coach selection."
msgstr "Invalid coach selection."
#: app/validators.py:803
#: app/validators.py:500 app/validators.py:859
msgid "Invalid manager selection."
msgstr "Invalid manager selection."
#: app/validators.py:815
#: app/validators.py:511
msgid "Invalid player selection."
msgstr "Invalid player selection."
#: app/validators.py:515
msgid "Unknown roster status."
msgstr "Unknown roster status."
#: app/validators.py:538
msgid "Date must be in YYYY-MM-DD format."
msgstr "Date must be in YYYY-MM-DD format."
#: app/validators.py:539
msgid "A date is required."
msgstr "A date is required."
#: app/validators.py:545 app/validators.py:610
msgid "Start time must be in HH:MM format."
msgstr "Start time must be in HH:MM format."
#: app/validators.py:546 app/validators.py:611
msgid "A start time is required."
msgstr "A start time is required."
#: app/validators.py:552
msgid "End time must be in HH:MM format."
msgstr "End time must be in HH:MM format."
#: app/validators.py:553
msgid "An end time is required."
msgstr "An end time is required."
#: app/validators.py:557
msgid "Points must be 2000 characters or less."
msgstr "Points must be 2000 characters or less."
#: app/validators.py:572
msgid "End time must be after start time."
msgstr "End time must be after start time."
#: app/validators.py:601 app/validators.py:603
msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
#: app/validators.py:604
msgid "A day is required."
msgstr "A day is required."
#: app/validators.py:639
msgid "Player selection is malformed."
msgstr "Player selection is malformed."
#: app/validators.py:665 app/validators.py:775
msgid "A title is required."
msgstr "A title is required."
#: app/validators.py:674
msgid "Invalid date format."
msgstr "Invalid date format."
#: app/validators.py:679 app/validators.py:686
msgid "Invalid time format."
msgstr "Invalid time format."
#: app/validators.py:680
msgid "Start time is required. Please select a time slot."
msgstr "Start time is required. Please select a time slot."
#: app/validators.py:694
msgid "Unknown match status."
msgstr "Unknown match status."
#: app/validators.py:710
msgid "The end time must come after the start time."
msgstr "The end time must come after the start time."
#: app/validators.py:724
msgid "Unknown match type."
msgstr "Unknown match type."
#: app/validators.py:736
msgid "A team cannot play against itself."
msgstr "A team cannot play against itself."
#: app/validators.py:784
msgid "Unknown game."
msgstr "Unknown game."
#: app/validators.py:789
msgid "Invalid start date format."
msgstr "Invalid start date format."
#: app/validators.py:790
msgid "A start date is required."
msgstr "A start date is required."
#: app/validators.py:796
msgid "Invalid end date format."
msgstr "Invalid end date format."
#: app/validators.py:804
msgid "A tryout must allow at least one player."
msgstr "A tryout must allow at least one player."
#: app/validators.py:807
msgid "The player limit must be a whole number."
msgstr "The player limit must be a whole number."
#: app/validators.py:819
msgid "End date cannot be before start date."
msgstr "End date cannot be before start date."
#: app/validators.py:846 app/validators.py:847
msgid "Team name is required."
msgstr "Team name is required."
#: app/validators.py:871
msgid "Scores run from 1 to 10."
msgstr "Scores run from 1 to 10."
#: app/validators.py:816
#: app/validators.py:872
msgid "A score must be a whole number from 1 to 10."
msgstr "A score must be a whole number from 1 to 10."
@@ -311,12 +319,12 @@ msgstr "Evaluation submitted successfully!"
msgid "Evaluation updated!"
msgstr "Evaluation updated!"
#: app/routes/evaluations.py:210 app/routes/teams.py:305
#: app/routes/teams.py:346 app/routes/teams.py:387 app/routes/teams.py:412
#: app/routes/teams.py:437 app/routes/teams.py:472 app/routes/tryouts.py:437
#: app/routes/tryouts.py:453 app/routes/tryouts.py:473
#: app/routes/tryouts.py:512 app/routes/tryouts.py:548
#: app/routes/tryouts.py:567
#: 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:519 app/routes/tryouts.py:555
#: app/routes/tryouts.py:574
msgid "Permission denied."
msgstr "Permission denied."
@@ -373,130 +381,130 @@ msgstr "Use My Team(s) to view your teams."
msgid "You do not have permission to view teams."
msgstr "You do not have permission to view teams."
#: app/routes/teams.py:74
#: app/routes/teams.py:79
msgid "This page is for players."
msgstr "This page is for players."
#: app/routes/teams.py:150
#: app/routes/teams.py:186
msgid "You do not have permission to create teams."
msgstr "You do not have permission to create teams."
#: app/routes/teams.py:161 app/routes/teams.py:203
#: app/routes/teams.py:197 app/routes/teams.py:239
#, python-format
msgid "Team \"%(name)s\" already exists."
msgstr "Team \"%(name)s\" already exists."
#: app/routes/teams.py:182
#: app/routes/teams.py:218
#, python-format
msgid "Team \"%(name)s\" created successfully!"
msgstr "Team \"%(name)s\" created successfully!"
#: app/routes/teams.py:192
#: app/routes/teams.py:228
msgid "You do not have permission to edit this team."
msgstr "You do not have permission to edit this team."
#: app/routes/teams.py:236
#: app/routes/teams.py:272
#, python-format
msgid "Team \"%(name)s\" updated successfully!"
msgstr "Team \"%(name)s\" updated successfully!"
#: app/routes/teams.py:264
#: app/routes/teams.py:300
msgid "You do not have permission to delete teams."
msgstr "You do not have permission to delete teams."
#: app/routes/teams.py:295
#: app/routes/teams.py:331
#, python-format
msgid "Team \"%(name)s\" deleted successfully."
msgstr "Team \"%(name)s\" deleted successfully."
#: app/routes/teams.py:310
#: app/routes/teams.py:348
msgid "Please select a coach."
msgstr "Please select a coach."
#: app/routes/teams.py:315
#: app/routes/teams.py:353
msgid "Only coaches can be assigned as coach."
msgstr "Only coaches can be assigned as coach."
#: app/routes/teams.py:321
#: app/routes/teams.py:359
#, python-format
msgid "%(username)s is already a coach of %(name)s."
msgstr "%(username)s is already a coach of %(name)s."
#: app/routes/teams.py:334
#: app/routes/teams.py:372
#, python-format
msgid "%(username)s added as coach of %(name)s."
msgstr "%(username)s added as coach of %(name)s."
#: app/routes/teams.py:351
#: app/routes/teams.py:391
msgid "Please select a manager."
msgstr "Please select a manager."
#: app/routes/teams.py:356
#: app/routes/teams.py:396
msgid "Only managers can be assigned as manager."
msgstr "Only managers can be assigned as manager."
#: app/routes/teams.py:362
#: app/routes/teams.py:402
#, python-format
msgid "%(username)s is already a manager of %(name)s."
msgstr "%(username)s is already a manager of %(name)s."
#: app/routes/teams.py:375
#: app/routes/teams.py:415
#, python-format
msgid "%(username)s added as manager of %(name)s."
msgstr "%(username)s added as manager of %(name)s."
#: app/routes/teams.py:402
#: app/routes/teams.py:445
#, python-format
msgid "Coach removed from %(name)s."
msgstr "Coach removed from %(name)s."
#: app/routes/teams.py:427
#: app/routes/teams.py:473
#, python-format
msgid "Manager removed from %(name)s."
msgstr "Manager removed from %(name)s."
#: app/routes/teams.py:443 app/routes/tryouts.py:477 app/routes/tryouts.py:578
#: app/routes/teams.py:490 app/routes/tryouts.py:484 app/routes/tryouts.py:585
msgid "Please select a player."
msgstr "Please select a player."
#: app/routes/teams.py:448
#: app/routes/teams.py:496
msgid "Can only assign players to teams."
msgstr "Can only assign players to teams."
#: app/routes/teams.py:454
#: app/routes/teams.py:502
#, python-format
msgid "%(username)s is already on %(name)s."
msgstr "%(username)s is already on %(name)s."
#: app/routes/teams.py:462
#: app/routes/teams.py:510
#, python-format
msgid "%(username)s added to %(name)s!"
msgstr "%(username)s added to %(name)s!"
#: app/routes/teams.py:479 app/routes/teams.py:553
#: app/routes/teams.py:527 app/routes/teams.py:601
#, python-format
msgid "%(username)s is not on %(name)s."
msgstr "%(username)s is not on %(name)s."
#: app/routes/teams.py:487
#: app/routes/teams.py:535
#, python-format
msgid "%(username)s removed from %(name)s."
msgstr "%(username)s removed from %(name)s."
#: app/routes/teams.py:524 app/routes/teams.py:542
#: app/routes/teams.py:572 app/routes/teams.py:590
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:532
#: app/routes/teams.py:580
msgid "Team notes added successfully!"
msgstr "Team notes added successfully!"
#: app/routes/teams.py:547 app/routes/users/notes.py:207
#: app/routes/teams.py:595 app/routes/users/notes.py:207
#: app/routes/users/notes.py:250
msgid "Can only add notes for players."
msgstr "Can only add notes for players."
#: app/routes/teams.py:563
#: app/routes/teams.py:611
#, python-format
msgid "Note added for %(username)s!"
msgstr "Note added for %(username)s!"
@@ -525,76 +533,76 @@ msgstr "Tryout updated successfully!"
msgid "You do not have permission to view this tryout."
msgstr "You do not have permission to view this tryout."
#: app/routes/tryouts.py:404
#: app/routes/tryouts.py:411
msgid "Only players can register for tryouts."
msgstr "Only players can register for tryouts."
#: app/routes/tryouts.py:408
#: app/routes/tryouts.py:415
msgid "This tryout is not accepting registrations."
msgstr "This tryout is not accepting registrations."
#: app/routes/tryouts.py:415
#: app/routes/tryouts.py:422
msgid "You are already registered for this tryout."
msgstr "You are already registered for this tryout."
#: app/routes/tryouts.py:421 app/routes/tryouts.py:496
#: app/routes/tryouts.py:428 app/routes/tryouts.py:503
msgid "This tryout is full."
msgstr "This tryout is full."
#: app/routes/tryouts.py:427
#: app/routes/tryouts.py:434
msgid "Successfully registered for tryout!"
msgstr "Successfully registered for tryout!"
#: app/routes/tryouts.py:443
#: app/routes/tryouts.py:450
#, python-format
msgid "Tryout status updated to %(new_status)s."
msgstr "Tryout status updated to %(new_status)s."
#: app/routes/tryouts.py:463
#: app/routes/tryouts.py:470
msgid "Registration status updated."
msgstr "Registration status updated."
#: app/routes/tryouts.py:482
#: app/routes/tryouts.py:489
msgid "Can only register players."
msgstr "Can only register players."
#: app/routes/tryouts.py:488
#: app/routes/tryouts.py:495
#, python-format
msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s is already registered for this tryout."
#: app/routes/tryouts.py:502
#: app/routes/tryouts.py:509
#, python-format
msgid "%(username)s registered for tryout!"
msgstr "%(username)s registered for tryout!"
#: app/routes/tryouts.py:538
#: app/routes/tryouts.py:545
#, python-format
msgid "%(username)s removed from tryout."
msgstr "%(username)s removed from tryout."
#: app/routes/tryouts.py:556
#: app/routes/tryouts.py:563
#, python-format
msgid "Team \"%(team_name)s\" created!"
msgstr "Team \"%(team_name)s\" created!"
#: app/routes/tryouts.py:587
#: app/routes/tryouts.py:594
msgid "That player is not registered for this tryout."
msgstr "That player is not registered for this tryout."
#: app/routes/tryouts.py:593
#: app/routes/tryouts.py:600
msgid "Player is already on this team."
msgstr "Player is already on this team."
#: app/routes/tryouts.py:598
#: app/routes/tryouts.py:605
msgid "Player added to team!"
msgstr "Player added to team!"
#: app/routes/tryouts.py:608
#: app/routes/tryouts.py:615
msgid "You do not have permission to delete this tryout."
msgstr "You do not have permission to delete this tryout."
#: app/routes/tryouts.py:644
#: app/routes/tryouts.py:651
msgid "Tryout deleted successfully."
msgstr "Tryout deleted successfully."
Binary file not shown.
+172 -164
View File
@@ -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-11 20:15-0400\n"
"POT-Creation-Date: 2026-08-11 20:39-0400\n"
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
@@ -92,127 +92,135 @@ msgstr "Vous devez choisir un joueur."
msgid "Notes must be 2000 characters or less."
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
#: app/validators.py:482
msgid "Date must be in YYYY-MM-DD format."
msgstr "La date doit être au format AAAA-MM-JJ."
#: app/validators.py:483
msgid "A date is required."
msgstr "Une date est requise."
#: app/validators.py:489 app/validators.py:554
msgid "Start time must be in HH:MM format."
msgstr "Lheure de début doit être au format HH:MM."
#: app/validators.py:490 app/validators.py:555
msgid "A start time is required."
msgstr "Une heure de début est requise."
#: app/validators.py:496
msgid "End time must be in HH:MM format."
msgstr "Lheure de fin doit être au format HH:MM."
#: app/validators.py:497
msgid "An end time is required."
msgstr "Une heure de fin est requise."
#: app/validators.py:501
msgid "Points must be 2000 characters or less."
msgstr "Les points ne doivent pas dépasser 2000 caractères."
#: app/validators.py:516
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:545 app/validators.py:547
msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
#: app/validators.py:548
msgid "A day is required."
msgstr "Un jour est requis."
#: app/validators.py:583
msgid "Player selection is malformed."
msgstr "La sélection de joueurs est mal formée."
#: app/validators.py:609 app/validators.py:719
msgid "A title is required."
msgstr "Un titre est requis."
#: app/validators.py:618
msgid "Invalid date format."
msgstr "Format de date invalide."
#: app/validators.py:623 app/validators.py:630
msgid "Invalid time format."
msgstr "Format dheure invalide."
#: app/validators.py:624
msgid "Start time is required. Please select a time slot."
msgstr "Lheure de début est obligatoire. Choisissez une plage horaire."
#: app/validators.py:638
msgid "Unknown match status."
msgstr "Statut de match inconnu."
#: app/validators.py:654
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:668
msgid "Unknown match type."
msgstr "Type de match inconnu."
#: app/validators.py:680
msgid "A team cannot play against itself."
msgstr "Une équipe ne peut pas jouer contre elle-même."
#: app/validators.py:728
msgid "Unknown game."
msgstr "Jeu inconnu."
#: app/validators.py:733
msgid "Invalid start date format."
msgstr "Format de date de début invalide."
#: app/validators.py:734
msgid "A start date is required."
msgstr "Une date de début est requise."
#: app/validators.py:740
msgid "Invalid end date format."
msgstr "Format de date de fin invalide."
#: app/validators.py:748
msgid "A tryout must allow at least one player."
msgstr "Une sélection doit accepter au moins un joueur."
#: app/validators.py:751
msgid "The player limit must be a whole number."
msgstr "La limite de joueurs doit être un nombre entier."
#: app/validators.py:763
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:790 app/validators.py:791
msgid "Team name is required."
msgstr "Le nom de l’équipe est obligatoire."
#: app/validators.py:797
#: app/validators.py:494 app/validators.py:853
msgid "Invalid coach selection."
msgstr "Sélection de coach invalide."
#: app/validators.py:803
#: app/validators.py:500 app/validators.py:859
msgid "Invalid manager selection."
msgstr "Sélection de gérant invalide."
#: app/validators.py:815
#: app/validators.py:511
msgid "Invalid player selection."
msgstr "Sélection de joueur invalide."
#: app/validators.py:515
msgid "Unknown roster status."
msgstr "Statut d'effectif inconnu."
#: app/validators.py:538
msgid "Date must be in YYYY-MM-DD format."
msgstr "La date doit être au format AAAA-MM-JJ."
#: app/validators.py:539
msgid "A date is required."
msgstr "Une date est requise."
#: app/validators.py:545 app/validators.py:610
msgid "Start time must be in HH:MM format."
msgstr "Lheure de début doit être au format HH:MM."
#: app/validators.py:546 app/validators.py:611
msgid "A start time is required."
msgstr "Une heure de début est requise."
#: app/validators.py:552
msgid "End time must be in HH:MM format."
msgstr "Lheure de fin doit être au format HH:MM."
#: app/validators.py:553
msgid "An end time is required."
msgstr "Une heure de fin est requise."
#: app/validators.py:557
msgid "Points must be 2000 characters or less."
msgstr "Les points ne doivent pas dépasser 2000 caractères."
#: app/validators.py:572
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:601 app/validators.py:603
msgid "Day must be 0 (Monday) to 6 (Sunday)."
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
#: app/validators.py:604
msgid "A day is required."
msgstr "Un jour est requis."
#: app/validators.py:639
msgid "Player selection is malformed."
msgstr "La sélection de joueurs est mal formée."
#: app/validators.py:665 app/validators.py:775
msgid "A title is required."
msgstr "Un titre est requis."
#: app/validators.py:674
msgid "Invalid date format."
msgstr "Format de date invalide."
#: app/validators.py:679 app/validators.py:686
msgid "Invalid time format."
msgstr "Format dheure invalide."
#: app/validators.py:680
msgid "Start time is required. Please select a time slot."
msgstr "Lheure de début est obligatoire. Choisissez une plage horaire."
#: app/validators.py:694
msgid "Unknown match status."
msgstr "Statut de match inconnu."
#: app/validators.py:710
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:724
msgid "Unknown match type."
msgstr "Type de match inconnu."
#: app/validators.py:736
msgid "A team cannot play against itself."
msgstr "Une équipe ne peut pas jouer contre elle-même."
#: app/validators.py:784
msgid "Unknown game."
msgstr "Jeu inconnu."
#: app/validators.py:789
msgid "Invalid start date format."
msgstr "Format de date de début invalide."
#: app/validators.py:790
msgid "A start date is required."
msgstr "Une date de début est requise."
#: app/validators.py:796
msgid "Invalid end date format."
msgstr "Format de date de fin invalide."
#: app/validators.py:804
msgid "A tryout must allow at least one player."
msgstr "Une sélection doit accepter au moins un joueur."
#: app/validators.py:807
msgid "The player limit must be a whole number."
msgstr "La limite de joueurs doit être un nombre entier."
#: app/validators.py:819
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:846 app/validators.py:847
msgid "Team name is required."
msgstr "Le nom de l’équipe est obligatoire."
#: app/validators.py:871
msgid "Scores run from 1 to 10."
msgstr "Les notes vont de 1 à 10."
#: app/validators.py:816
#: app/validators.py:872
msgid "A score must be a whole number from 1 to 10."
msgstr "Une note doit être un nombre entier de 1 à 10."
@@ -313,12 +321,12 @@ msgstr "Évaluation enregistrée."
msgid "Evaluation updated!"
msgstr "Évaluation mise à jour."
#: app/routes/evaluations.py:210 app/routes/teams.py:305
#: app/routes/teams.py:346 app/routes/teams.py:387 app/routes/teams.py:412
#: app/routes/teams.py:437 app/routes/teams.py:472 app/routes/tryouts.py:437
#: app/routes/tryouts.py:453 app/routes/tryouts.py:473
#: app/routes/tryouts.py:512 app/routes/tryouts.py:548
#: app/routes/tryouts.py:567
#: 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:519 app/routes/tryouts.py:555
#: app/routes/tryouts.py:574
msgid "Permission denied."
msgstr "Accès refusé."
@@ -377,130 +385,130 @@ msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
msgid "You do not have permission to view teams."
msgstr "Vous navez pas les droits pour consulter les équipes."
#: app/routes/teams.py:74
#: app/routes/teams.py:79
msgid "This page is for players."
msgstr "Cette page est réservée aux joueurs."
#: app/routes/teams.py:150
#: app/routes/teams.py:186
msgid "You do not have permission to create teams."
msgstr "Vous navez pas les droits pour créer une équipe."
#: app/routes/teams.py:161 app/routes/teams.py:203
#: app/routes/teams.py:197 app/routes/teams.py:239
#, python-format
msgid "Team \"%(name)s\" already exists."
msgstr "L’équipe « %(name)s » existe déjà."
#: app/routes/teams.py:182
#: app/routes/teams.py:218
#, python-format
msgid "Team \"%(name)s\" created successfully!"
msgstr "Équipe « %(name)s » créée."
#: app/routes/teams.py:192
#: app/routes/teams.py:228
msgid "You do not have permission to edit this team."
msgstr "Vous navez pas les droits pour modifier cette équipe."
#: app/routes/teams.py:236
#: app/routes/teams.py:272
#, python-format
msgid "Team \"%(name)s\" updated successfully!"
msgstr "Équipe « %(name)s » mise à jour."
#: app/routes/teams.py:264
#: app/routes/teams.py:300
msgid "You do not have permission to delete teams."
msgstr "Vous navez pas les droits pour supprimer une équipe."
#: app/routes/teams.py:295
#: app/routes/teams.py:331
#, python-format
msgid "Team \"%(name)s\" deleted successfully."
msgstr "Équipe « %(name)s » supprimée."
#: app/routes/teams.py:310
#: app/routes/teams.py:348
msgid "Please select a coach."
msgstr "Veuillez choisir un coach."
#: app/routes/teams.py:315
#: app/routes/teams.py:353
msgid "Only coaches can be assigned as coach."
msgstr "Seuls les coachs peuvent être assignés comme coach."
#: app/routes/teams.py:321
#: app/routes/teams.py:359
#, python-format
msgid "%(username)s is already a coach of %(name)s."
msgstr "%(username)s est déjà coach de %(name)s."
#: app/routes/teams.py:334
#: app/routes/teams.py:372
#, python-format
msgid "%(username)s added as coach of %(name)s."
msgstr "%(username)s a été ajouté comme coach de %(name)s."
#: app/routes/teams.py:351
#: app/routes/teams.py:391
msgid "Please select a manager."
msgstr "Veuillez choisir un gérant."
#: app/routes/teams.py:356
#: app/routes/teams.py:396
msgid "Only managers can be assigned as manager."
msgstr "Seuls les gérants peuvent être assignés comme gérant."
#: app/routes/teams.py:362
#: app/routes/teams.py:402
#, python-format
msgid "%(username)s is already a manager of %(name)s."
msgstr "%(username)s est déjà gérant de %(name)s."
#: app/routes/teams.py:375
#: app/routes/teams.py:415
#, python-format
msgid "%(username)s added as manager of %(name)s."
msgstr "%(username)s a été ajouté comme gérant de %(name)s."
#: app/routes/teams.py:402
#: app/routes/teams.py:445
#, python-format
msgid "Coach removed from %(name)s."
msgstr "Coach retiré de %(name)s."
#: app/routes/teams.py:427
#: app/routes/teams.py:473
#, python-format
msgid "Manager removed from %(name)s."
msgstr "Gérant retiré de %(name)s."
#: app/routes/teams.py:443 app/routes/tryouts.py:477 app/routes/tryouts.py:578
#: app/routes/teams.py:490 app/routes/tryouts.py:484 app/routes/tryouts.py:585
msgid "Please select a player."
msgstr "Veuillez choisir un joueur."
#: app/routes/teams.py:448
#: app/routes/teams.py:496
msgid "Can only assign players to teams."
msgstr "Seuls des joueurs peuvent être assignés à une équipe."
#: app/routes/teams.py:454
#: app/routes/teams.py:502
#, python-format
msgid "%(username)s is already on %(name)s."
msgstr "%(username)s fait déjà partie de %(name)s."
#: app/routes/teams.py:462
#: app/routes/teams.py:510
#, python-format
msgid "%(username)s added to %(name)s!"
msgstr "%(username)s a été ajouté à %(name)s."
#: app/routes/teams.py:479 app/routes/teams.py:553
#: app/routes/teams.py:527 app/routes/teams.py:601
#, python-format
msgid "%(username)s is not on %(name)s."
msgstr "%(username)s ne fait pas partie de %(name)s."
#: app/routes/teams.py:487
#: app/routes/teams.py:535
#, python-format
msgid "%(username)s removed from %(name)s."
msgstr "%(username)s a été retiré de %(name)s."
#: app/routes/teams.py:524 app/routes/teams.py:542
#: app/routes/teams.py:572 app/routes/teams.py:590
msgid "You do not have permission to add notes to this team."
msgstr "Vous navez pas les droits pour ajouter des notes à cette équipe."
#: app/routes/teams.py:532
#: app/routes/teams.py:580
msgid "Team notes added successfully!"
msgstr "Notes d’équipe ajoutées."
#: app/routes/teams.py:547 app/routes/users/notes.py:207
#: app/routes/teams.py:595 app/routes/users/notes.py:207
#: app/routes/users/notes.py:250
msgid "Can only add notes for players."
msgstr "Il nest possible dajouter des notes que pour des joueurs."
#: app/routes/teams.py:563
#: app/routes/teams.py:611
#, python-format
msgid "Note added for %(username)s!"
msgstr "Note ajoutée pour %(username)s."
@@ -529,76 +537,76 @@ msgstr "Sélection mise à jour."
msgid "You do not have permission to view this tryout."
msgstr "Vous navez pas les droits pour consulter cette sélection."
#: app/routes/tryouts.py:404
#: app/routes/tryouts.py:411
msgid "Only players can register for tryouts."
msgstr "Seuls les joueurs peuvent sinscrire à une sélection."
#: app/routes/tryouts.py:408
#: app/routes/tryouts.py:415
msgid "This tryout is not accepting registrations."
msgstr "Cette sélection naccepte pas dinscriptions."
#: app/routes/tryouts.py:415
#: app/routes/tryouts.py:422
msgid "You are already registered for this tryout."
msgstr "Vous êtes déjà inscrit à cette sélection."
#: app/routes/tryouts.py:421 app/routes/tryouts.py:496
#: app/routes/tryouts.py:428 app/routes/tryouts.py:503
msgid "This tryout is full."
msgstr "Cette sélection est complète."
#: app/routes/tryouts.py:427
#: app/routes/tryouts.py:434
msgid "Successfully registered for tryout!"
msgstr "Inscription à la sélection réussie."
#: app/routes/tryouts.py:443
#: app/routes/tryouts.py:450
#, 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:463
#: app/routes/tryouts.py:470
msgid "Registration status updated."
msgstr "Statut dinscription mis à jour."
#: app/routes/tryouts.py:482
#: app/routes/tryouts.py:489
msgid "Can only register players."
msgstr "Seuls des joueurs peuvent être inscrits."
#: app/routes/tryouts.py:488
#: app/routes/tryouts.py:495
#, 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:502
#: app/routes/tryouts.py:509
#, python-format
msgid "%(username)s registered for tryout!"
msgstr "%(username)s est inscrit à la sélection."
#: app/routes/tryouts.py:538
#: app/routes/tryouts.py:545
#, python-format
msgid "%(username)s removed from tryout."
msgstr "%(username)s a été retiré de la sélection."
#: app/routes/tryouts.py:556
#: app/routes/tryouts.py:563
#, python-format
msgid "Team \"%(team_name)s\" created!"
msgstr "Équipe « %(team_name)s » créée."
#: app/routes/tryouts.py:587
#: app/routes/tryouts.py:594
msgid "That player is not registered for this tryout."
msgstr "Ce joueur nest pas inscrit à cette sélection."
#: app/routes/tryouts.py:593
#: app/routes/tryouts.py:600
msgid "Player is already on this team."
msgstr "Ce joueur est déjà dans cette équipe."
#: app/routes/tryouts.py:598
#: app/routes/tryouts.py:605
msgid "Player added to team!"
msgstr "Joueur ajouté à l’équipe."
#: app/routes/tryouts.py:608
#: app/routes/tryouts.py:615
msgid "You do not have permission to delete this tryout."
msgstr "Vous navez pas les droits pour supprimer cette sélection."
#: app/routes/tryouts.py:644
#: app/routes/tryouts.py:651
msgid "Tryout deleted successfully."
msgstr "Sélection supprimée."
+56
View File
@@ -460,6 +460,62 @@ class UploadContractSchema(StripMixin):
)
#: Where a player stands on a team roster.
#:
#: `TeamPlayer.status` is a NOT NULL String(20) that `add_player` filled from
#: `request.form.get('status', 'starter')` with no check at all, so a forged
#: submission stored any string it liked. It then survived until someone
#: pressed the toggle, which reads `'substitute' if status == 'starter' else
#: 'starter'` — so an unknown value silently became `starter`, i.e. promoted
#: whoever held it (SEC-16).
TEAM_PLAYER_STATUSES = ('starter', 'substitute')
class TeamStaffSchema(StripMixin):
"""One staff id, posted by the add/remove coach and manager forms.
Both ids are optional here even though each route needs exactly one:
`remove_coach` treats an absent id as "remove every coach", and the add
routes already carry their own translated "Please select a coach."
message. Making the field required would replace that message with a
generic one for no gain.
What this schema is for is the conversion. Wave K closed `create_team`
and `edit_team` and left five sibling routes reading `int(...)` straight
off the form — a non-numeric id was a 500 in each. Fixing a pattern in
one place and not its neighbours is the mistake this project keeps
making; this is the same mistake, made by the fix for it.
"""
coach_id = fields.Integer(
allow_none=True,
load_default=None,
validate=validate.Range(min=1),
error_messages={'invalid': _l('Invalid coach selection.')},
)
manager_id = fields.Integer(
allow_none=True,
load_default=None,
validate=validate.Range(min=1),
error_messages={'invalid': _l('Invalid manager selection.')},
)
class TeamPlayerSchema(StripMixin):
"""A player being put on a team roster, and where they stand on it."""
player_id = fields.Integer(
allow_none=True,
load_default=None,
validate=validate.Range(min=1),
error_messages={'invalid': _l('Invalid player selection.')},
)
status = fields.String(
load_default='starter',
validate=validate.OneOf(TEAM_PLAYER_STATUSES, error=_l('Unknown roster status.')),
)
class OneOnOneRequestSchema(StripMixin):
"""A player asking their coach for a session (MNT-12).