From 20dabecd675cba472b363db8198a53f77fbfe7dd Mon Sep 17 00:00:00 2001 From: GGThed Date: Tue, 11 Aug 2026 20:42:26 -0400 Subject: [PATCH] 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 --- app/routes/teams.py | 112 +++++-- app/routes/tryouts.py | 9 +- app/translations/en/LC_MESSAGES/messages.mo | Bin 45330 -> 45460 bytes app/translations/en/LC_MESSAGES/messages.po | 336 ++++++++++---------- app/translations/fr/LC_MESSAGES/messages.mo | Bin 49648 -> 49787 bytes app/translations/fr/LC_MESSAGES/messages.po | 336 ++++++++++---------- app/validators.py | 56 ++++ tests/test_authorization.py | 145 +++++++++ 8 files changed, 633 insertions(+), 361 deletions(-) diff --git a/app/routes/teams.py b/app/routes/teams.py index b876d89..1dabb16 100644 --- a/app/routes/teams.py +++ b/app/routes/teams.py @@ -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 `` 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')) diff --git a/app/routes/tryouts.py b/app/routes/tryouts.py index df213be..4d375e9 100644 --- a/app/routes/tryouts.py +++ b/app/routes/tryouts.py @@ -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() diff --git a/app/translations/en/LC_MESSAGES/messages.mo b/app/translations/en/LC_MESSAGES/messages.mo index 4ecf618788b153bf74d401664ea88d7683564433..23c3123b5ef362bd05882722c601ce3d82729c27 100644 GIT binary patch delta 10603 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 diff --git a/app/translations/en/LC_MESSAGES/messages.po b/app/translations/en/LC_MESSAGES/messages.po index 987f8c3..e4301be 100644 --- a/app/translations/en/LC_MESSAGES/messages.po +++ b/app/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: team-tryouts VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-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 \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." diff --git a/app/translations/fr/LC_MESSAGES/messages.mo b/app/translations/fr/LC_MESSAGES/messages.mo index 0d147095305db28655defe1fac891e5504cb66e7..b9f3180a3f7382439cb68f64561b4ec9a91a504d 100644 GIT binary patch delta 10600 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 Rm^om8MjDX2bZ@VK{{ay&sKfvO delta 10512 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 diff --git a/app/translations/fr/LC_MESSAGES/messages.po b/app/translations/fr/LC_MESSAGES/messages.po index 4a33801..690e0cc 100644 --- a/app/translations/fr/LC_MESSAGES/messages.po +++ b/app/translations/fr/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: team-tryouts VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-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 \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." diff --git a/app/validators.py b/app/validators.py index 2b6738f..230773b 100644 --- a/app/validators.py +++ b/app/validators.py @@ -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). diff --git a/tests/test_authorization.py b/tests/test_authorization.py index 3b9fcd3..af2cade 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -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'