438 lines
16 KiB
Python
438 lines
16 KiB
Python
"""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/<int:tryout_id>', 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/<int:match_id>', 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=[],
|
|
)
|