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:
+80
-32
@@ -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'))
|
||||
|
||||
|
||||
@@ -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.
@@ -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.
@@ -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 "L’heure 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 "L’heure 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 d’heure invalide."
|
||||
|
||||
#: app/validators.py:624
|
||||
msgid "Start time is required. Please select a time slot."
|
||||
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
||||
|
||||
#: app/validators.py: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 "L’heure 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 "L’heure 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 d’heure invalide."
|
||||
|
||||
#: app/validators.py:680
|
||||
msgid "Start time is required. Please select a time slot."
|
||||
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
||||
|
||||
#: app/validators.py: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 n’avez 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 n’avez 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 n’avez 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 n’avez 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 n’avez 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 n’est possible d’ajouter 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 n’avez 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 s’inscrire à une sélection."
|
||||
|
||||
#: app/routes/tryouts.py:408
|
||||
#: app/routes/tryouts.py:415
|
||||
msgid "This tryout is not accepting registrations."
|
||||
msgstr "Cette sélection n’accepte pas d’inscriptions."
|
||||
|
||||
#: 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 d’inscription 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 n’est 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 n’avez 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."
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -583,3 +583,148 @@ class TestTeamStaffAssignment:
|
||||
assert response.status_code < 500
|
||||
with app.app_context():
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user