Files
team-tryouts/app/routes/users/notes.py
T
GGThed d7a8907953
CI - Security, Lint & Tests / validate (push) Failing after 19m37s
fix(audit): moderniser les accès ORM
2026-08-17 15:02:29 -04:00

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 = db.get_or_404(User, 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 = db.get_or_404(User, 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 = db.get_or_404(Match, 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 = db.get_or_404(Tryout, 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 = db.get_or_404(Team, 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 = db.get_or_404(Tryout, 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 = db.get_or_404(Match, 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=[],
)