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, User,
) )
from app.permissions import visible_org_teams 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') teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@@ -55,7 +55,12 @@ def list_teams():
managers = ( managers = (
User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all() 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( return render_template(
'pages/teams.html', 'pages/teams.html',
teams=teams, teams=teams,
@@ -119,27 +124,58 @@ def my_teams():
return render_template('pages/my_teams.html', team_data=team_data, now=now) return render_template('pages/my_teams.html', team_data=team_data, now=now)
def _staff_member(user_id, expected_class): def _posted(schema):
"""The user behind an id, only if they hold the role being assigned. """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 The five assignment routes below each answer a bad field with their own
role. That last case is the point (SEC-16): the id comes from a `<select>` flash and a redirect to the same page, so a shared "it did not validate"
the browser rendered, so it is a value the client chooses, and nothing return is enough; the field-level message is flashed on the way out.
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 try:
wave G fixed in `tryouts.py`, left standing here. 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: 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. expected_class: Coach or Manager.
Returns: 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: if not user_id:
return None return None
user = db.session.get(User, user_id) 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']) @teams_bp.route('/create', methods=['POST'])
@@ -305,13 +341,15 @@ def add_coach(team_id):
flash(_('Permission denied.'), 'danger') flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id') data = _posted(TeamStaffSchema())
if not coach_id: if data is None:
return redirect(url_for('teams.list_teams'))
if not data['coach_id']:
flash(_('Please select a coach.'), 'danger') flash(_('Please select a coach.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
coach = User.query.get_or_404(int(coach_id)) coach = db.session.get(User, data['coach_id'])
if not isinstance(coach, Coach): if not coach or not _assignable(coach, Coach):
flash(_('Only coaches can be assigned as coach.'), 'danger') flash(_('Only coaches can be assigned as coach.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@@ -346,13 +384,15 @@ def add_manager(team_id):
flash(_('Permission denied.'), 'danger') flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id') data = _posted(TeamStaffSchema())
if not manager_id: if data is None:
return redirect(url_for('teams.list_teams'))
if not data['manager_id']:
flash(_('Please select a manager.'), 'danger') flash(_('Please select a manager.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
manager = User.query.get_or_404(int(manager_id)) manager = db.session.get(User, data['manager_id'])
if not isinstance(manager, Manager): if not manager or not _assignable(manager, Manager):
flash(_('Only managers can be assigned as manager.'), 'danger') flash(_('Only managers can be assigned as manager.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@@ -387,9 +427,12 @@ def remove_coach(team_id):
flash(_('Permission denied.'), 'danger') flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id') data = _posted(TeamStaffSchema())
if coach_id: if data is None:
coach = User.query.get(int(coach_id)) 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(): if coach and team.coaches.filter_by(id=coach.id).first():
team.coaches.remove(coach) team.coaches.remove(coach)
if team.coach_id == coach.id: if team.coach_id == coach.id:
@@ -412,9 +455,12 @@ def remove_manager(team_id):
flash(_('Permission denied.'), 'danger') flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id') data = _posted(TeamStaffSchema())
if manager_id: if data is None:
manager = User.query.get(int(manager_id)) 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(): if manager and team.managers.filter_by(id=manager.id).first():
team.managers.remove(manager) team.managers.remove(manager)
if team.manager_id == manager.id: if team.manager_id == manager.id:
@@ -437,14 +483,16 @@ def add_player(team_id):
flash(_('Permission denied.'), 'danger') flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
player_id = request.form.get('player_id') data = _posted(TeamPlayerSchema())
status = request.form.get('status', 'starter') if data is None:
if not player_id: return redirect(url_for('teams.list_teams'))
if not data['player_id']:
flash(_('Please select a player.'), 'danger') flash(_('Please select a player.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
player = User.query.get_or_404(int(player_id)) status = data['status']
if not isinstance(player, Player): player = db.session.get(User, data['player_id'])
if not player or not _assignable(player, Player):
flash(_('Can only assign players to teams.'), 'danger') flash(_('Can only assign players to teams.'), 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
+8 -1
View File
@@ -303,7 +303,14 @@ def view_tryout(tryout_id):
all_players = None all_players = None
if can_edit: 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 = ( matches = (
Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all() 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 "" msgstr ""
"Project-Id-Version: team-tryouts VERSION\n" "Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -90,127 +90,135 @@ msgstr "Player must be selected."
msgid "Notes must be 2000 characters or less." msgid "Notes must be 2000 characters or less."
msgstr "Notes must be 2000 characters or less." msgstr "Notes must be 2000 characters or less."
#: app/validators.py:482 #: app/validators.py:494 app/validators.py:853
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
msgid "Invalid coach selection." msgid "Invalid coach selection."
msgstr "Invalid coach selection." msgstr "Invalid coach selection."
#: app/validators.py:803 #: app/validators.py:500 app/validators.py:859
msgid "Invalid manager selection." msgid "Invalid manager selection."
msgstr "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." msgid "Scores run from 1 to 10."
msgstr "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." msgid "A score must be a whole number from 1 to 10."
msgstr "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!" msgid "Evaluation updated!"
msgstr "Evaluation updated!" msgstr "Evaluation updated!"
#: app/routes/evaluations.py:210 app/routes/teams.py:305 #: app/routes/evaluations.py:210 app/routes/teams.py:341
#: app/routes/teams.py:346 app/routes/teams.py:387 app/routes/teams.py:412 #: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455
#: app/routes/teams.py:437 app/routes/teams.py:472 app/routes/tryouts.py:437 #: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:444
#: app/routes/tryouts.py:453 app/routes/tryouts.py:473 #: app/routes/tryouts.py:460 app/routes/tryouts.py:480
#: app/routes/tryouts.py:512 app/routes/tryouts.py:548 #: app/routes/tryouts.py:519 app/routes/tryouts.py:555
#: app/routes/tryouts.py:567 #: app/routes/tryouts.py:574
msgid "Permission denied." msgid "Permission denied."
msgstr "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." msgid "You do not have permission to view teams."
msgstr "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." msgid "This page is for players."
msgstr "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." msgid "You do not have permission to create teams."
msgstr "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 #, python-format
msgid "Team \"%(name)s\" already exists." msgid "Team \"%(name)s\" already exists."
msgstr "Team \"%(name)s\" already exists." msgstr "Team \"%(name)s\" already exists."
#: app/routes/teams.py:182 #: app/routes/teams.py:218
#, python-format #, python-format
msgid "Team \"%(name)s\" created successfully!" msgid "Team \"%(name)s\" created successfully!"
msgstr "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." msgid "You do not have permission to edit this team."
msgstr "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 #, python-format
msgid "Team \"%(name)s\" updated successfully!" msgid "Team \"%(name)s\" updated successfully!"
msgstr "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." msgid "You do not have permission to delete teams."
msgstr "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 #, python-format
msgid "Team \"%(name)s\" deleted successfully." msgid "Team \"%(name)s\" deleted successfully."
msgstr "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." msgid "Please select a coach."
msgstr "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." msgid "Only coaches can be assigned as coach."
msgstr "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 #, python-format
msgid "%(username)s is already a coach of %(name)s." msgid "%(username)s is already a coach of %(name)s."
msgstr "%(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 #, python-format
msgid "%(username)s added as coach of %(name)s." msgid "%(username)s added as coach of %(name)s."
msgstr "%(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." msgid "Please select a manager."
msgstr "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." msgid "Only managers can be assigned as manager."
msgstr "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 #, python-format
msgid "%(username)s is already a manager of %(name)s." msgid "%(username)s is already a manager of %(name)s."
msgstr "%(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 #, python-format
msgid "%(username)s added as manager of %(name)s." msgid "%(username)s added as manager of %(name)s."
msgstr "%(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 #, python-format
msgid "Coach removed from %(name)s." msgid "Coach removed from %(name)s."
msgstr "Coach removed from %(name)s." msgstr "Coach removed from %(name)s."
#: app/routes/teams.py:427 #: app/routes/teams.py:473
#, python-format #, python-format
msgid "Manager removed from %(name)s." msgid "Manager removed from %(name)s."
msgstr "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." msgid "Please select a player."
msgstr "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." msgid "Can only assign players to teams."
msgstr "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 #, python-format
msgid "%(username)s is already on %(name)s." msgid "%(username)s is already on %(name)s."
msgstr "%(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 #, python-format
msgid "%(username)s added to %(name)s!" msgid "%(username)s added to %(name)s!"
msgstr "%(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 #, python-format
msgid "%(username)s is not on %(name)s." msgid "%(username)s is not on %(name)s."
msgstr "%(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 #, python-format
msgid "%(username)s removed from %(name)s." msgid "%(username)s removed from %(name)s."
msgstr "%(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." msgid "You do not have permission to add notes to this team."
msgstr "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!" msgid "Team notes added successfully!"
msgstr "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 #: app/routes/users/notes.py:250
msgid "Can only add notes for players." msgid "Can only add notes for players."
msgstr "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 #, python-format
msgid "Note added for %(username)s!" msgid "Note added for %(username)s!"
msgstr "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." msgid "You do not have permission to view this tryout."
msgstr "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." msgid "Only players can register for tryouts."
msgstr "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." msgid "This tryout is not accepting registrations."
msgstr "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." msgid "You are already registered for this tryout."
msgstr "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." msgid "This tryout is full."
msgstr "This tryout is full." msgstr "This tryout is full."
#: app/routes/tryouts.py:427 #: app/routes/tryouts.py:434
msgid "Successfully registered for tryout!" msgid "Successfully registered for tryout!"
msgstr "Successfully registered for tryout!" msgstr "Successfully registered for tryout!"
#: app/routes/tryouts.py:443 #: app/routes/tryouts.py:450
#, python-format #, python-format
msgid "Tryout status updated to %(new_status)s." msgid "Tryout status updated to %(new_status)s."
msgstr "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." msgid "Registration status updated."
msgstr "Registration status updated." msgstr "Registration status updated."
#: app/routes/tryouts.py:482 #: app/routes/tryouts.py:489
msgid "Can only register players." msgid "Can only register players."
msgstr "Can only register players." msgstr "Can only register players."
#: app/routes/tryouts.py:488 #: app/routes/tryouts.py:495
#, python-format #, python-format
msgid "%(username)s is already registered for this tryout." msgid "%(username)s is already registered for this tryout."
msgstr "%(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 #, python-format
msgid "%(username)s registered for tryout!" msgid "%(username)s registered for tryout!"
msgstr "%(username)s registered for tryout!" msgstr "%(username)s registered for tryout!"
#: app/routes/tryouts.py:538 #: app/routes/tryouts.py:545
#, python-format #, python-format
msgid "%(username)s removed from tryout." msgid "%(username)s removed from tryout."
msgstr "%(username)s removed from tryout." msgstr "%(username)s removed from tryout."
#: app/routes/tryouts.py:556 #: app/routes/tryouts.py:563
#, python-format #, python-format
msgid "Team \"%(team_name)s\" created!" msgid "Team \"%(team_name)s\" created!"
msgstr "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." msgid "That player is not registered for this tryout."
msgstr "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." msgid "Player is already on this team."
msgstr "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!" msgid "Player added to team!"
msgstr "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." msgid "You do not have permission to delete this tryout."
msgstr "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." msgid "Tryout deleted successfully."
msgstr "Tryout deleted successfully." msgstr "Tryout deleted successfully."
Binary file not shown.
+172 -164
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: team-tryouts VERSION\n" "Project-Id-Version: team-tryouts VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: 2026-08-07 20:22-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n" "Language: fr\n"
@@ -92,127 +92,135 @@ msgstr "Vous devez choisir un joueur."
msgid "Notes must be 2000 characters or less." msgid "Notes must be 2000 characters or less."
msgstr "Les notes ne doivent pas dépasser 2000 caractères." msgstr "Les notes ne doivent pas dépasser 2000 caractères."
#: app/validators.py:482 #: app/validators.py:494 app/validators.py:853
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
msgid "Invalid coach selection." msgid "Invalid coach selection."
msgstr "Sélection de coach invalide." msgstr "Sélection de coach invalide."
#: app/validators.py:803 #: app/validators.py:500 app/validators.py:859
msgid "Invalid manager selection." msgid "Invalid manager selection."
msgstr "Sélection de gérant invalide." 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." msgid "Scores run from 1 to 10."
msgstr "Les notes vont de 1 à 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." msgid "A score must be a whole number from 1 to 10."
msgstr "Une note doit être un nombre entier de 1 à 10." msgstr "Une note doit être un nombre entier de 1 à 10."
@@ -313,12 +321,12 @@ msgstr "Évaluation enregistrée."
msgid "Evaluation updated!" msgid "Evaluation updated!"
msgstr "Évaluation mise à jour." msgstr "Évaluation mise à jour."
#: app/routes/evaluations.py:210 app/routes/teams.py:305 #: app/routes/evaluations.py:210 app/routes/teams.py:341
#: app/routes/teams.py:346 app/routes/teams.py:387 app/routes/teams.py:412 #: app/routes/teams.py:384 app/routes/teams.py:427 app/routes/teams.py:455
#: app/routes/teams.py:437 app/routes/teams.py:472 app/routes/tryouts.py:437 #: app/routes/teams.py:483 app/routes/teams.py:520 app/routes/tryouts.py:444
#: app/routes/tryouts.py:453 app/routes/tryouts.py:473 #: app/routes/tryouts.py:460 app/routes/tryouts.py:480
#: app/routes/tryouts.py:512 app/routes/tryouts.py:548 #: app/routes/tryouts.py:519 app/routes/tryouts.py:555
#: app/routes/tryouts.py:567 #: app/routes/tryouts.py:574
msgid "Permission denied." msgid "Permission denied."
msgstr "Accès refusé." 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." msgid "You do not have permission to view teams."
msgstr "Vous navez pas les droits pour consulter les équipes." 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." msgid "This page is for players."
msgstr "Cette page est réservée aux joueurs." 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." msgid "You do not have permission to create teams."
msgstr "Vous navez pas les droits pour créer une équipe." 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 #, python-format
msgid "Team \"%(name)s\" already exists." msgid "Team \"%(name)s\" already exists."
msgstr "L’équipe « %(name)s » existe déjà." msgstr "L’équipe « %(name)s » existe déjà."
#: app/routes/teams.py:182 #: app/routes/teams.py:218
#, python-format #, python-format
msgid "Team \"%(name)s\" created successfully!" msgid "Team \"%(name)s\" created successfully!"
msgstr "Équipe « %(name)s » créée." 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." msgid "You do not have permission to edit this team."
msgstr "Vous navez pas les droits pour modifier cette équipe." msgstr "Vous navez pas les droits pour modifier cette équipe."
#: app/routes/teams.py:236 #: app/routes/teams.py:272
#, python-format #, python-format
msgid "Team \"%(name)s\" updated successfully!" msgid "Team \"%(name)s\" updated successfully!"
msgstr "Équipe « %(name)s » mise à jour." 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." msgid "You do not have permission to delete teams."
msgstr "Vous navez pas les droits pour supprimer une équipe." msgstr "Vous navez pas les droits pour supprimer une équipe."
#: app/routes/teams.py:295 #: app/routes/teams.py:331
#, python-format #, python-format
msgid "Team \"%(name)s\" deleted successfully." msgid "Team \"%(name)s\" deleted successfully."
msgstr "Équipe « %(name)s » supprimée." msgstr "Équipe « %(name)s » supprimée."
#: app/routes/teams.py:310 #: app/routes/teams.py:348
msgid "Please select a coach." msgid "Please select a coach."
msgstr "Veuillez choisir un coach." msgstr "Veuillez choisir un coach."
#: app/routes/teams.py:315 #: app/routes/teams.py:353
msgid "Only coaches can be assigned as coach." msgid "Only coaches can be assigned as coach."
msgstr "Seuls les coachs peuvent être assignés comme coach." msgstr "Seuls les coachs peuvent être assignés comme coach."
#: app/routes/teams.py:321 #: app/routes/teams.py:359
#, python-format #, python-format
msgid "%(username)s is already a coach of %(name)s." msgid "%(username)s is already a coach of %(name)s."
msgstr "%(username)s est déjà coach de %(name)s." msgstr "%(username)s est déjà coach de %(name)s."
#: app/routes/teams.py:334 #: app/routes/teams.py:372
#, python-format #, python-format
msgid "%(username)s added as coach of %(name)s." msgid "%(username)s added as coach of %(name)s."
msgstr "%(username)s a été ajouté comme coach de %(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." msgid "Please select a manager."
msgstr "Veuillez choisir un gérant." msgstr "Veuillez choisir un gérant."
#: app/routes/teams.py:356 #: app/routes/teams.py:396
msgid "Only managers can be assigned as manager." msgid "Only managers can be assigned as manager."
msgstr "Seuls les gérants peuvent être assignés comme gérant." msgstr "Seuls les gérants peuvent être assignés comme gérant."
#: app/routes/teams.py:362 #: app/routes/teams.py:402
#, python-format #, python-format
msgid "%(username)s is already a manager of %(name)s." msgid "%(username)s is already a manager of %(name)s."
msgstr "%(username)s est déjà gérant de %(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 #, python-format
msgid "%(username)s added as manager of %(name)s." msgid "%(username)s added as manager of %(name)s."
msgstr "%(username)s a été ajouté comme gérant de %(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 #, python-format
msgid "Coach removed from %(name)s." msgid "Coach removed from %(name)s."
msgstr "Coach retiré de %(name)s." msgstr "Coach retiré de %(name)s."
#: app/routes/teams.py:427 #: app/routes/teams.py:473
#, python-format #, python-format
msgid "Manager removed from %(name)s." msgid "Manager removed from %(name)s."
msgstr "Gérant retiré de %(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." msgid "Please select a player."
msgstr "Veuillez choisir un joueur." msgstr "Veuillez choisir un joueur."
#: app/routes/teams.py:448 #: app/routes/teams.py:496
msgid "Can only assign players to teams." msgid "Can only assign players to teams."
msgstr "Seuls des joueurs peuvent être assignés à une équipe." msgstr "Seuls des joueurs peuvent être assignés à une équipe."
#: app/routes/teams.py:454 #: app/routes/teams.py:502
#, python-format #, python-format
msgid "%(username)s is already on %(name)s." msgid "%(username)s is already on %(name)s."
msgstr "%(username)s fait déjà partie de %(name)s." msgstr "%(username)s fait déjà partie de %(name)s."
#: app/routes/teams.py:462 #: app/routes/teams.py:510
#, python-format #, python-format
msgid "%(username)s added to %(name)s!" msgid "%(username)s added to %(name)s!"
msgstr "%(username)s a été ajouté à %(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 #, python-format
msgid "%(username)s is not on %(name)s." msgid "%(username)s is not on %(name)s."
msgstr "%(username)s ne fait pas partie de %(name)s." msgstr "%(username)s ne fait pas partie de %(name)s."
#: app/routes/teams.py:487 #: app/routes/teams.py:535
#, python-format #, python-format
msgid "%(username)s removed from %(name)s." msgid "%(username)s removed from %(name)s."
msgstr "%(username)s a été retiré de %(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." msgid "You do not have permission to add notes to this team."
msgstr "Vous navez pas les droits pour ajouter des notes à cette équipe." 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!" msgid "Team notes added successfully!"
msgstr "Notes d’équipe ajoutées." 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 #: app/routes/users/notes.py:250
msgid "Can only add notes for players." msgid "Can only add notes for players."
msgstr "Il nest possible dajouter des notes que pour des joueurs." msgstr "Il nest possible dajouter des notes que pour des joueurs."
#: app/routes/teams.py:563 #: app/routes/teams.py:611
#, python-format #, python-format
msgid "Note added for %(username)s!" msgid "Note added for %(username)s!"
msgstr "Note ajoutée pour %(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." msgid "You do not have permission to view this tryout."
msgstr "Vous navez pas les droits pour consulter cette sélection." 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." msgid "Only players can register for tryouts."
msgstr "Seuls les joueurs peuvent sinscrire à une sélection." 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." msgid "This tryout is not accepting registrations."
msgstr "Cette sélection naccepte pas dinscriptions." 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." msgid "You are already registered for this tryout."
msgstr "Vous êtes déjà inscrit à cette sélection." 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." msgid "This tryout is full."
msgstr "Cette sélection est complète." msgstr "Cette sélection est complète."
#: app/routes/tryouts.py:427 #: app/routes/tryouts.py:434
msgid "Successfully registered for tryout!" msgid "Successfully registered for tryout!"
msgstr "Inscription à la sélection réussie." msgstr "Inscription à la sélection réussie."
#: app/routes/tryouts.py:443 #: app/routes/tryouts.py:450
#, python-format #, python-format
msgid "Tryout status updated to %(new_status)s." msgid "Tryout status updated to %(new_status)s."
msgstr "Statut de la sélection mis à jour : %(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." msgid "Registration status updated."
msgstr "Statut dinscription mis à jour." msgstr "Statut dinscription mis à jour."
#: app/routes/tryouts.py:482 #: app/routes/tryouts.py:489
msgid "Can only register players." msgid "Can only register players."
msgstr "Seuls des joueurs peuvent être inscrits." msgstr "Seuls des joueurs peuvent être inscrits."
#: app/routes/tryouts.py:488 #: app/routes/tryouts.py:495
#, python-format #, python-format
msgid "%(username)s is already registered for this tryout." msgid "%(username)s is already registered for this tryout."
msgstr "%(username)s est déjà inscrit à cette sélection." msgstr "%(username)s est déjà inscrit à cette sélection."
#: app/routes/tryouts.py:502 #: app/routes/tryouts.py:509
#, python-format #, python-format
msgid "%(username)s registered for tryout!" msgid "%(username)s registered for tryout!"
msgstr "%(username)s est inscrit à la sélection." msgstr "%(username)s est inscrit à la sélection."
#: app/routes/tryouts.py:538 #: app/routes/tryouts.py:545
#, python-format #, python-format
msgid "%(username)s removed from tryout." msgid "%(username)s removed from tryout."
msgstr "%(username)s a été retiré de la sélection." msgstr "%(username)s a été retiré de la sélection."
#: app/routes/tryouts.py:556 #: app/routes/tryouts.py:563
#, python-format #, python-format
msgid "Team \"%(team_name)s\" created!" msgid "Team \"%(team_name)s\" created!"
msgstr "Équipe « %(team_name)s » créée." 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." msgid "That player is not registered for this tryout."
msgstr "Ce joueur nest pas inscrit à cette sélection." 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." msgid "Player is already on this team."
msgstr "Ce joueur est déjà dans cette équipe." msgstr "Ce joueur est déjà dans cette équipe."
#: app/routes/tryouts.py:598 #: app/routes/tryouts.py:605
msgid "Player added to team!" msgid "Player added to team!"
msgstr "Joueur ajouté à l’équipe." 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." msgid "You do not have permission to delete this tryout."
msgstr "Vous navez pas les droits pour supprimer cette sélection." 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." msgid "Tryout deleted successfully."
msgstr "Sélection supprimée." 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): class OneOnOneRequestSchema(StripMixin):
"""A player asking their coach for a session (MNT-12). """A player asking their coach for a session (MNT-12).
+145
View File
@@ -583,3 +583,148 @@ class TestTeamStaffAssignment:
assert response.status_code < 500 assert response.status_code < 500
with app.app_context(): with app.app_context():
assert OrgTeam.query.count() == 0 assert OrgTeam.query.count() == 0
class TestTeamRosterAssignment:
"""SEC-16, the half wave K missed.
Wave K put a schema on `create_team` and `edit_team` and left five
sibling routes reading `int(request.form.get(...))` — `add_coach`,
`add_manager`, `remove_coach`, `remove_manager` and `add_player`. Each
was a 500 on a non-numeric id. `add_player` also accepted any `status`
string into a NOT NULL column, and none of them checked whether the
account had been deactivated.
Fixing a pattern in one place and not its neighbours is the mistake this
project keeps making. Here it was made by the fix for it.
"""
@pytest.fixture
def team(self, app, make_user):
from app.models import OrgTeam
admin_id = make_user('admin')
with app.app_context():
org_team = OrgTeam(name='Varsity', created_by=admin_id)
db.session.add(org_team)
db.session.commit()
return org_team.id
@pytest.mark.parametrize(
('path', 'field'),
[
('add_coach', 'coach_id'),
('add_manager', 'manager_id'),
('remove_coach', 'coach_id'),
('remove_manager', 'manager_id'),
('add_player', 'player_id'),
],
)
def test_a_non_numeric_id_is_not_a_500(self, app, client, team, as_role, path, field):
as_role('admin')
response = client.post(f'/teams/{team}/{path}', data={field: 'not-a-number'})
assert response.status_code < 500
def test_a_real_coach_is_added(self, app, client, team, as_role, make_user):
"""The premise for the two tests below."""
from app.models import OrgTeam
coach_id = make_user('coach')
as_role('admin')
client.post(f'/teams/{team}/add_coach', data={'coach_id': str(coach_id)})
with app.app_context():
org_team = db.session.get(OrgTeam, team)
assert [c.id for c in org_team.coaches.all()] == [coach_id]
def test_a_player_cannot_be_added_as_coach(self, app, client, team, as_role, make_user):
from app.models import OrgTeam
player_id = make_user('player')
as_role('admin')
client.post(f'/teams/{team}/add_coach', data={'coach_id': str(player_id)})
with app.app_context():
assert db.session.get(OrgTeam, team).coaches.all() == []
def test_a_deactivated_coach_cannot_be_added(self, app, client, team, as_role, make_user):
"""`is_active_account` is what says the person has left the club.
Putting them back on a roster contradicts it."""
from app.models import OrgTeam, User
coach_id = make_user('coach')
with app.app_context():
db.session.get(User, coach_id).is_active_account = False
db.session.commit()
as_role('admin')
client.post(f'/teams/{team}/add_coach', data={'coach_id': str(coach_id)})
with app.app_context():
assert db.session.get(OrgTeam, team).coaches.all() == []
def test_a_deactivated_player_cannot_be_added(self, app, client, team, as_role, make_user):
from app.models import TeamPlayer, User
player_id = make_user('player')
with app.app_context():
db.session.get(User, player_id).is_active_account = False
db.session.commit()
as_role('admin')
client.post(f'/teams/{team}/add_player', data={'player_id': str(player_id)})
with app.app_context():
assert TeamPlayer.query.filter_by(org_team_id=team).count() == 0
def test_a_deactivated_player_is_not_offered(self, app, client, as_role, make_user):
"""The select and the route agreed on nothing: the query had no
is_active_account filter while the two beside it did."""
from app.models import User
player_id = make_user('player', username='ghostplayer')
with app.app_context():
db.session.get(User, player_id).is_active_account = False
db.session.commit()
as_role('admin')
body = client.get('/teams').get_data(as_text=True)
assert 'ghostplayer' not in body
def test_an_unknown_roster_status_is_refused(self, app, client, team, as_role, make_user):
"""`status` went into a NOT NULL String(20) unchecked, and the toggle
reads anything that is not 'starter' as substitute — so an unknown
value silently promoted its holder on the next press."""
from app.models import TeamPlayer
player_id = make_user('player')
as_role('admin')
client.post(
f'/teams/{team}/add_player',
data={'player_id': str(player_id), 'status': 'captain-for-life'},
)
with app.app_context():
rows = TeamPlayer.query.filter_by(org_team_id=team).all()
assert [r.status for r in rows] != ['captain-for-life']
def test_a_known_status_is_kept(self, app, client, team, as_role, make_user):
from app.models import TeamPlayer
player_id = make_user('player')
as_role('admin')
client.post(
f'/teams/{team}/add_player',
data={'player_id': str(player_id), 'status': 'substitute'},
)
with app.app_context():
row = TeamPlayer.query.filter_by(org_team_id=team).one()
assert row.status == 'substitute'