"""Organization team management routes. Uses polymorphic isinstance checks instead of role-string comparisons. """ from datetime import datetime from flask import Blueprint, flash, jsonify, redirect, render_template, url_for from flask_babel import gettext as _ from flask_login import current_user, login_required from marshmallow import ValidationError from app.api import json_endpoint from app.extensions import db from app.forms import flash_validation_errors, form_payload from app.models import ( Admin, Coach, Contract, Manager, OneOnOneRequest, OrgTeam, PersonalNote, Player, TeamMatch, TeamNote, TeamPlayer, Tryout, User, ) from app.permissions import visible_org_teams from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema teams_bp = Blueprint('teams', __name__, url_prefix='/teams') @teams_bp.route('') @login_required def list_teams(): """List all organization teams visible to the current user.""" can_manage = current_user.can_manage_teams() if isinstance(current_user, Player): flash(_('Use My Team(s) to view your teams.'), 'info') return redirect(url_for('teams.my_teams')) if not isinstance(current_user, (Admin, Coach, Manager)): flash(_('You do not have permission to view teams.'), 'danger') return redirect(url_for('main.dashboard')) teams = visible_org_teams(current_user) coaches = ( User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all() ) managers = ( User.query.filter_by(role='manager', is_active_account=True).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, coaches=coaches, managers=managers, all_players=all_players, can_manage=can_manage, ) @teams_bp.route('/my-teams') @login_required def my_teams(): """View the player's own teams with upcoming matches.""" if not isinstance(current_user, Player): flash(_('This page is for players.'), 'info') return redirect(url_for('teams.list_teams')) from app.models import TeamMatch, TeamMatchParticipant player_teams = current_user.get_org_teams() now = datetime.utcnow() team_data = [] for org_team in player_teams: matches = ( TeamMatch.query.filter( TeamMatch.org_team_id == org_team.id, TeamMatch.status == 'scheduled', ) .order_by(TeamMatch.date.asc(), TeamMatch.start_time.asc()) .all() ) matches_data = [] for tm in matches: confirmed, total = tm.get_confirmed_count() participant = TeamMatchParticipant.query.filter_by( team_match_id=tm.id, player_id=current_user.id, ).first() matches_data.append( { 'match': tm, 'participant_id': participant.id if participant else None, 'is_confirmed': participant.is_confirmed if participant else False, 'confirmed_count': confirmed, 'total_count': total, } ) team_data.append( { 'team': org_team, 'matches': matches_data, 'coaches': org_team.get_coaches(), 'managers': org_team.get_managers(), } ) return render_template('pages/my_teams.html', team_data=team_data, now=now) def _posted(schema): """Load a form through `schema`, or None when it will not load. 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 `