"""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, request, url_for from flask_babel import gettext as _ from flask_login import current_user, login_required from app.extensions import db from app.models import ( Admin, Coach, Contract, Manager, OneOnOneRequest, OrgTeam, PersonalNote, Player, TeamMatch, TeamNote, TeamPlayer, Tryout, User, ) from app.permissions import visible_org_teams 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() ) all_players = User.query.filter_by(role='player').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) @teams_bp.route('/create', methods=['POST']) @login_required def create_team(): """Create a new organization team.""" if not current_user.can_manage_teams(): flash(_('You do not have permission to create teams.'), 'danger') return redirect(url_for('teams.list_teams')) name = request.form.get('name') coach_id = request.form.get('coach_id') manager_id = request.form.get('manager_id') if not name: flash(_('Team name is required.'), 'danger') return redirect(url_for('teams.list_teams')) existing = OrgTeam.query.filter_by(name=name).first() if existing: flash(_('Team "%(name)s" already exists.', name=name), 'danger') return redirect(url_for('teams.list_teams')) team = OrgTeam( name=name, coach_id=int(coach_id) if coach_id else None, manager_id=int(manager_id) if manager_id else None, created_by=current_user.id, ) db.session.add(team) db.session.flush() if coach_id: coach_user = User.query.get(int(coach_id)) if coach_user: team.coaches.append(coach_user) if manager_id: manager_user = User.query.get(int(manager_id)) if manager_user: team.managers.append(manager_user) db.session.commit() flash(_('Team "%(name)s" created successfully!', name=name), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//edit', methods=['POST']) @login_required def edit_team(team_id): """Edit an existing organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): flash(_('You do not have permission to edit this team.'), 'danger') return redirect(url_for('teams.list_teams')) name = request.form.get('name') coach_id = request.form.get('coach_id') manager_id = request.form.get('manager_id') if not name: flash(_('Team name is required.'), 'danger') return redirect(url_for('teams.list_teams')) existing = OrgTeam.query.filter(OrgTeam.name == name, OrgTeam.id != team_id).first() if existing: flash(_('Team "%(name)s" already exists.', name=name), 'danger') return redirect(url_for('teams.list_teams')) if request.form.get('sync_staff') == '1': coach_ids = request.form.getlist('coach_ids') manager_ids = request.form.getlist('manager_ids') team.coaches = [] for cid in coach_ids: if cid and cid.strip(): coach_user = User.query.get(int(cid)) if coach_user and isinstance(coach_user, Coach): team.coaches.append(coach_user) coach_list = team.coaches.all() team.coach_id = coach_list[0].id if coach_list else None team.managers = [] for mid in manager_ids: if mid and mid.strip(): manager_user = User.query.get(int(mid)) if manager_user and isinstance(manager_user, Manager): team.managers.append(manager_user) manager_list = team.managers.all() team.manager_id = manager_list[0].id if manager_list else None else: team.coach_id = int(coach_id) if coach_id else None team.manager_id = int(manager_id) if manager_id else None if coach_id: coach_user = User.query.get(int(coach_id)) if coach_user and not team.coaches.filter_by(id=coach_user.id).first(): team.coaches.append(coach_user) if manager_id: manager_user = User.query.get(int(manager_id)) if manager_user and not team.managers.filter_by(id=manager_user.id).first(): team.managers.append(manager_user) db.session.commit() flash(_('Team "%(name)s" updated successfully!', name=name), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//delete', methods=['POST']) @login_required def delete_team(team_id): """Delete an organization team.""" if not current_user.can_manage_teams(): flash(_('You do not have permission to delete teams.'), 'danger') return redirect(url_for('teams.list_teams')) team = OrgTeam.query.get_or_404(team_id) name = team.name # One transaction. This used to commit three times, so a failure at the # third step left the tryouts detached and the players removed without # the team being deleted — an inconsistent state nothing could undo. # # TeamNote.org_team_id and TeamMatch.org_team_id are NOT NULL, and were # not handled at all: deleting a team that had ever been used raised # IntegrityError. Contract.team_id and OneOnOneRequest.org_team_id are # nullable, and the rows outlive the team, so they are only detached. # Entities that only make sense as part of the team. TeamNote.query.filter_by(org_team_id=team_id).delete(synchronize_session=False) for team_match in TeamMatch.query.filter_by(org_team_id=team_id).all(): db.session.delete(team_match) # participants follow by cascade TeamPlayer.query.filter_by(org_team_id=team_id).delete(synchronize_session=False) # Entities that survive it. Tryout.query.filter_by(target_org_team_id=team_id).update( {'target_org_team_id': None}, synchronize_session=False ) Contract.query.filter_by(team_id=team_id).update({'team_id': None}, synchronize_session=False) OneOnOneRequest.query.filter_by(org_team_id=team_id).update( {'org_team_id': None}, synchronize_session=False ) db.session.delete(team) db.session.commit() flash(_('Team "%(name)s" deleted successfully.', name=name), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//add_coach', methods=['POST']) @login_required def add_coach(team_id): """Add a coach to an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) coach_id = request.form.get('coach_id') if not 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): flash(_('Only coaches can be assigned as coach.'), 'danger') return redirect(url_for('teams.list_teams')) if team.coaches.filter_by(id=coach.id).first(): flash( _( '%(username)s is already a coach of %(name)s.', username=coach.username, name=team.name, ), 'info', ) return redirect(url_for('teams.list_teams')) team.coaches.append(coach) if not team.coach_id: team.coach_id = coach.id db.session.commit() flash( _('%(username)s added as coach of %(name)s.', username=coach.username, name=team.name), 'success', ) return redirect(url_for('teams.list_teams')) @teams_bp.route('//add_manager', methods=['POST']) @login_required def add_manager(team_id): """Add a manager to an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) manager_id = request.form.get('manager_id') if not 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): flash(_('Only managers can be assigned as manager.'), 'danger') return redirect(url_for('teams.list_teams')) if team.managers.filter_by(id=manager.id).first(): flash( _( '%(username)s is already a manager of %(name)s.', username=manager.username, name=team.name, ), 'info', ) return redirect(url_for('teams.list_teams')) team.managers.append(manager) if not team.manager_id: team.manager_id = manager.id db.session.commit() flash( _('%(username)s added as manager of %(name)s.', username=manager.username, name=team.name), 'success', ) return redirect(url_for('teams.list_teams')) @teams_bp.route('//remove_coach', methods=['POST']) @login_required def remove_coach(team_id): """Remove a coach from an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): 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)) if coach and team.coaches.filter_by(id=coach.id).first(): team.coaches.remove(coach) if team.coach_id == coach.id: team.coach_id = None else: team.coaches = [] team.coach_id = None db.session.commit() flash(_('Coach removed from %(name)s.', name=team.name), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//remove_manager', methods=['POST']) @login_required def remove_manager(team_id): """Remove a manager from an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): 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)) if manager and team.managers.filter_by(id=manager.id).first(): team.managers.remove(manager) if team.manager_id == manager.id: team.manager_id = None else: team.managers = [] team.manager_id = None db.session.commit() flash(_('Manager removed from %(name)s.', name=team.name), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//add_player', methods=['POST']) @login_required def add_player(team_id): """Add a player to an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): 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: 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): flash(_('Can only assign players to teams.'), 'danger') return redirect(url_for('teams.list_teams')) existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first() if existing: flash( _('%(username)s is already on %(name)s.', username=player.username, name=team.name), 'info', ) return redirect(url_for('teams.list_teams')) tp = TeamPlayer(player_id=player.id, org_team_id=team.id, status=status) db.session.add(tp) db.session.commit() flash(_('%(username)s added to %(name)s!', username=player.username, name=team.name), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//remove_player/', methods=['POST']) @login_required def remove_player(team_id, player_id): """Remove a player from an organization team.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): flash(_('Permission denied.'), 'danger') return redirect(url_for('teams.list_teams')) player = User.query.get_or_404(player_id) tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first() if not tp: flash( _('%(username)s is not on %(name)s.', username=player.username, name=team.name), 'danger', ) return redirect(url_for('teams.list_teams')) db.session.delete(tp) db.session.commit() flash( _('%(username)s removed from %(name)s.', username=player.username, name=team.name), 'success', ) return redirect(url_for('teams.list_teams')) @teams_bp.route('//toggle_status/', methods=['POST']) @login_required def toggle_player_status(team_id, player_id): """Toggle a player's status between starter and substitute.""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): return jsonify({'error': 'Permission denied'}), 403 tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first() if not tp: return jsonify({'error': 'Player not found on this team'}), 404 tp.status = 'substitute' if tp.status == 'starter' else 'starter' db.session.commit() return jsonify( { 'success': True, 'player_id': player_id, 'new_status': tp.status, 'player_name': tp.player.username, } ) @teams_bp.route('//add-team-note', methods=['POST']) @login_required def add_team_note(team_id): """Add a team improvement note (coaches only).""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): flash(_('You do not have permission to add notes to this team.'), 'danger') return redirect(url_for('teams.list_teams')) content = request.form.get('content', '').strip() if content: note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=content) db.session.add(note) db.session.commit() flash(_('Team notes added successfully!'), 'success') return redirect(url_for('teams.list_teams')) @teams_bp.route('//add-player-note/', methods=['POST']) @login_required def add_player_note(team_id, player_id): """Add a personal note for a player (coaches only).""" team = OrgTeam.query.get_or_404(team_id) if not current_user.can_manage_this_org_team(team): flash(_('You do not have permission to add notes to this team.'), 'danger') return redirect(url_for('teams.list_teams')) player = User.query.get_or_404(player_id) if not isinstance(player, Player): flash(_('Can only add notes for players.'), 'danger') return redirect(url_for('teams.list_teams')) tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first() if not tp: flash( _('%(username)s is not on %(name)s.', username=player.username, name=team.name), 'danger', ) return redirect(url_for('teams.list_teams')) content = request.form.get('content', '').strip() if content: note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=content) db.session.add(note) db.session.commit() flash(_('Note added for %(username)s!', username=player.username), 'success') return redirect(url_for('teams.list_teams'))