"""Notes a coach keeps: about a team, and about individual players. The player-facing view of the same notes lives here too — my_notes — since it reads exactly what the coach routes write. """ from flask import flash, redirect, render_template, request, url_for from flask_babel import gettext as _ from flask_login import current_user, login_required from marshmallow import ValidationError from app.extensions import db from app.forms import flash_validation_errors, form_payload from app.models import ( Coach, Match, MatchParticipant, OneOnOneRequest, PersonalNote, Player, Team, TeamMember, TeamNote, Tryout, TryoutRegistration, User, ) from app.permissions import ( coach_can_access_player, coach_org_teams, coach_player_ids, coach_tryouts, ) from app.routes.users.blueprint import users_bp from app.validators import NoteContentSchema, PersonalNoteSchema @users_bp.route('/my-notes') @login_required def my_notes(): """View personal and team notes for the current player.""" if not isinstance(current_user, Player): flash(_('This page is for players only.'), 'info') return redirect(url_for('main.dashboard')) org_teams = current_user.get_org_teams() org_team = org_teams[0] if org_teams else None personal_notes = ( PersonalNote.query.filter_by( player_id=current_user.id, ) .order_by(PersonalNote.created_at.desc()) .all() ) team_notes = [] if org_team: team_notes = ( TeamNote.query.filter_by( org_team_id=org_team.id, ) .order_by(TeamNote.created_at.desc()) .all() ) return render_template( 'pages/player_personal_notes.html', org_team=org_team, personal_notes=personal_notes, team_notes=team_notes, ) @users_bp.route('/notes-dashboard') @login_required def notes_dashboard(): """Notes and One on One dashboard for coaches.""" if not isinstance(current_user, Coach): flash(_('Only coaches can access the notes dashboard.'), 'danger') return redirect(url_for('main.dashboard')) # The team-notes panel is still written against a single team; the # player list is not, and used to be narrowed to one team's squad while # the POST routes accepted every player the coach works with. The form # offered fewer players than the handler would take. org_teams = coach_org_teams(current_user) org_team = org_teams[0] if org_teams else None player_ids = coach_player_ids(current_user) players = ( User.query.filter(User.id.in_(player_ids)).order_by(User.username).all() if player_ids else [] ) team_notes = [] latest_team_note = None if org_team: team_notes = ( TeamNote.query.filter_by( org_team_id=org_team.id, ) .order_by(TeamNote.created_at.desc()) .all() ) latest_team_note = team_notes[0] if team_notes else None # A coach's own notes belong to them whether or not they hold a team; # this list was gated on org_team and came back empty without one. personal_notes = ( PersonalNote.query.filter_by( coach_id=current_user.id, ) .order_by(PersonalNote.created_at.desc()) .all() ) one_on_one_requests = [] if player_ids: one_on_one_requests = ( OneOnOneRequest.query.filter(OneOnOneRequest.player_id.in_(player_ids)) .order_by(OneOnOneRequest.created_at.desc()) .all() ) # For context selectors in the form # PersonalNote.team_id references a tryout-local Team, not OrgTeam. The # previous selector mixed the two namespaces and could either attach the # note to an unrelated team with the same integer id or fail its FK. # Every context list now comes from the tryouts this coach may manage. tryouts = list(reversed(coach_tryouts(current_user)))[:20] tryout_ids = [tryout.id for tryout in tryouts] matches = ( Match.query.filter(Match.tryout_id.in_(tryout_ids)) .order_by(Match.date.desc()) .limit(20) .all() if tryout_ids else [] ) teams = ( Team.query.filter(Team.tryout_id.in_(tryout_ids)).order_by(Team.name).all() if tryout_ids else [] ) return render_template( 'pages/notes.html', org_team=org_team, players=players, team_notes=team_notes, latest_team_note=latest_team_note, personal_notes=personal_notes, one_on_one_requests=one_on_one_requests, matches=matches, tryouts=tryouts, teams=teams, ) # --------------------------------------------------------------------------- # Manage Team Notes (POST) # --------------------------------------------------------------------------- @users_bp.route('/team-notes/manage', methods=['POST']) @login_required def manage_team_notes(): """Create or update team notes for the coach's org team.""" if not isinstance(current_user, Coach): flash(_('Only coaches can manage team notes.'), 'danger') return redirect(url_for('main.dashboard')) # Same team the dashboard displays notes for, resolved the same way. org_teams = coach_org_teams(current_user) if not org_teams: flash(_('You are not assigned to a team.'), 'danger') return redirect(url_for('users.notes_dashboard')) org_team = org_teams[0] try: data = NoteContentSchema().load(form_payload(list_fields=())) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.notes_dashboard')) note = TeamNote( org_team_id=org_team.id, coach_id=current_user.id, content=data['content'], ) db.session.add(note) db.session.commit() flash(_('Team notes saved successfully!'), 'success') return redirect(url_for('users.notes_dashboard')) # --------------------------------------------------------------------------- # Manage Personal Notes (POST, simple form) # --------------------------------------------------------------------------- @users_bp.route('/personal-notes/manage', methods=['POST']) @login_required def manage_personal_notes(): """Create a personal note for a player (coach only, simple form).""" if not isinstance(current_user, Coach): flash(_('Only coaches can manage personal notes.'), 'danger') return redirect(url_for('main.dashboard')) try: data = PersonalNoteSchema().load(form_payload(list_fields=())) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.notes_dashboard')) player_id = data['player_id'] 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('users.notes_dashboard')) if not coach_can_access_player(current_user, player_id): flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.notes_dashboard')) note = PersonalNote( player_id=player_id, coach_id=current_user.id, content=data['content'], ) db.session.add(note) db.session.commit() flash(_('Note added for %(username)s.', username=player.username), 'success') return redirect(url_for('users.notes_dashboard')) # --------------------------------------------------------------------------- # Add Personal Note (POST, full form with context) # --------------------------------------------------------------------------- @users_bp.route('/personal-notes/add', methods=['POST']) @login_required def add_personal_note(): """Create a personal note for a player with optional context (coach only).""" if not isinstance(current_user, Coach): flash(_('Only coaches can add personal notes.'), 'danger') return redirect(url_for('main.dashboard')) try: data = PersonalNoteSchema().load(form_payload(list_fields=())) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.notes_dashboard')) player_id = data['player_id'] 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('users.notes_dashboard')) if not coach_can_access_player(current_user, player_id): flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.notes_dashboard')) if data['match_id']: match = Match.query.get_or_404(data['match_id']) if not current_user.can_manage_this_tryout(match.tryout): flash(_('You cannot use that match as note context.'), 'danger') return redirect(url_for('users.notes_dashboard')) if not MatchParticipant.query.filter_by(match_id=match.id, player_id=player_id).first(): flash(_('That player did not participate in the selected match.'), 'danger') return redirect(url_for('users.notes_dashboard')) if data['tryout_id']: tryout = Tryout.query.get_or_404(data['tryout_id']) if not current_user.can_manage_this_tryout(tryout): flash(_('You cannot use that tryout as note context.'), 'danger') return redirect(url_for('users.notes_dashboard')) if not TryoutRegistration.query.filter_by(tryout_id=tryout.id, player_id=player_id).first(): flash(_('That player is not registered for the selected tryout.'), 'danger') return redirect(url_for('users.notes_dashboard')) if data['team_id']: team = Team.query.get_or_404(data['team_id']) if not current_user.can_manage_this_tryout(team.tryout): flash(_('You cannot use that team as note context.'), 'danger') return redirect(url_for('users.notes_dashboard')) if not TeamMember.query.filter_by(team_id=team.id, player_id=player_id).first(): flash(_('That player is not on the selected team.'), 'danger') return redirect(url_for('users.notes_dashboard')) note = PersonalNote( player_id=player_id, coach_id=current_user.id, content=data['content'], match_id=data['match_id'], tryout_id=data['tryout_id'], team_id=data['team_id'], ) db.session.add(note) db.session.commit() flash(_('Note added for %(username)s.', username=player.username), 'success') return redirect(url_for('users.notes_dashboard')) # --------------------------------------------------------------------------- # Add Note from Tryout context (GET + POST) # --------------------------------------------------------------------------- @users_bp.route('/personal-notes/tryout/', methods=['GET', 'POST']) @login_required def add_note_from_tryout(tryout_id): """Add a personal note for a player in the context of a tryout.""" if not isinstance(current_user, Coach): flash(_('Only coaches can add personal notes.'), 'danger') return redirect(url_for('main.dashboard')) tryout = Tryout.query.get_or_404(tryout_id) if not current_user.can_manage_this_tryout(tryout): flash(_('You do not have permission to add notes for this tryout.'), 'danger') return redirect(url_for('users.notes_dashboard')) preselected_player_id = request.args.get('player_id', type=int) # Get registrations as players for the select list registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() players = [r.player for r in registrations if r.player] if request.method == 'POST': try: data = PersonalNoteSchema().load(form_payload(list_fields=())) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) player_id = data['player_id'] if data['tryout_id'] not in (None, tryout_id): flash(_('Invalid tryout context.'), 'danger') return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) if not coach_can_access_player(current_user, player_id): flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) if not TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player_id).first(): flash(_('That player is not registered for this tryout.'), 'danger') return redirect(url_for('users.add_note_from_tryout', tryout_id=tryout_id)) note = PersonalNote( player_id=player_id, coach_id=current_user.id, content=data['content'], tryout_id=tryout_id, ) db.session.add(note) db.session.commit() flash(_('Note added successfully.'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return render_template( 'pages/add_note.html', context_type='tryout', tryout=tryout, players=players, preselected_player_id=preselected_player_id, team_notes=[], ) # --------------------------------------------------------------------------- # Add Note from Match context (GET + POST) # --------------------------------------------------------------------------- @users_bp.route('/personal-notes/match/', methods=['GET', 'POST']) @login_required def add_note_from_match(match_id): """Add a personal note for a player in the context of a match.""" if not isinstance(current_user, Coach): flash(_('Only coaches can add personal notes.'), 'danger') return redirect(url_for('main.dashboard')) match_obj = Match.query.get_or_404(match_id) if not current_user.can_manage_this_tryout(match_obj.tryout): flash(_('You do not have permission to add notes for this match.'), 'danger') return redirect(url_for('users.notes_dashboard')) # Get participants as players for the select list participants = MatchParticipant.query.filter_by(match_id=match_id).all() players = [p.player for p in participants if p.player] preselected_player_id = request.args.get('player_id', type=int) if request.method == 'POST': try: data = PersonalNoteSchema().load(form_payload(list_fields=())) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.add_note_from_match', match_id=match_id)) player_id = data['player_id'] if data['match_id'] not in (None, match_id): flash(_('Invalid match context.'), 'danger') return redirect(url_for('users.add_note_from_match', match_id=match_id)) if not coach_can_access_player(current_user, player_id): flash(_('You can only write notes about players you work with.'), 'danger') return redirect(url_for('users.add_note_from_match', match_id=match_id)) if not MatchParticipant.query.filter_by(match_id=match_id, player_id=player_id).first(): flash(_('That player did not participate in this match.'), 'danger') return redirect(url_for('users.add_note_from_match', match_id=match_id)) note = PersonalNote( player_id=player_id, coach_id=current_user.id, content=data['content'], match_id=match_id, ) db.session.add(note) db.session.commit() flash(_('Note added successfully.'), 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=match_obj.tryout_id)) return render_template( 'pages/add_note.html', context_type='match', tryout=match_obj, match=match_obj, players=players, preselected_player_id=preselected_player_id, team_notes=[], )