ARCH-004 et la moitie NotificationService d ARCH-003. app/routes/users.py faisait 1 699 lignes et couvrait six sujets qui ne partageaient rien d autre qu un prefixe d URL. Il devient un paquet : blueprint.py l objet Blueprint, seul _shared.py helpers de formulaire, gamertags, validation de PDF accounts.py 348 l. liste, creation, edition, suppression, fiche availability.py 233 l. disponibilites joueur et creneaux coach contracts.py 206 l. depot, signature, telechargement notes.py 376 l. notes d equipe et notes nominatives one_on_one.py 259 l. demandes de seance individuelle profile.py 132 l. profil de la personne connectee Aucun fichier ne depasse 400 lignes -- le critere d acceptation de l audit. **Un seul blueprint, pas six.** Les endpoints restent `users.*`. Les renommer aurait touche 137 appels `url_for` dans les gabarits, pour un benefice nul : l objectif est un fichier qu on peut lire, pas une carte d URL a reapprendre. Les 30 endpoints sont identiques avant et apres, verifie sur url_map. send_discord_notification part dans app/services/notifications.py. Elle tirait `requests`, `logging` et le bot Discord dans un module dont le sujet est le traitement HTTP, et se trouvait coincee entre deux definitions de route. Son `except Exception` est conserve et documente : une notification qui n arrive pas ne doit pas annuler la transaction qu elle annoncait. tests/test_route_map.py, nouveau : il parcourt gabarits et code, releve tout endpoint nomme litteralement dans un url_for, et verifie qu il existe dans la carte. C est le mode de defaillance de ce genre de decoupage -- pas une erreur a l import, mais un BuildError chez la premiere personne qui ouvre la page concernee. Un test de garde verifie aussi que le scan trouve quelque chose, sinon le reste serait vide de sens. Piege rencontre, et corrige : test_role_change patchait `app.routes.users.update_user_gamertags`. Apres le decoupage ce nom est un reexport, pas celui qu accounts.py resout -- le patch aurait pu laisser la route appeler la vraie fonction et le test passer sans rien verifier. Ici monkeypatch a echoue bruyamment, mais la cible est desormais explicite et le test enregistre que la doublure a bien ete appelee. 347 tests passent (263 + 84, dont 82 parametres par la carte des routes). Co-Authored-By: Claude Opus 5 <[email protected]>
377 lines
13 KiB
Python
377 lines
13 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 app.extensions import db
|
|
from app.models import (
|
|
Coach,
|
|
Match,
|
|
MatchParticipant,
|
|
OneOnOneRequest,
|
|
OrgTeam,
|
|
PersonalNote,
|
|
Player,
|
|
TeamNote,
|
|
Tryout,
|
|
TryoutRegistration,
|
|
User,
|
|
)
|
|
from app.permissions import coach_can_access_player, coach_org_teams, coach_player_ids
|
|
from app.routes.users.blueprint import users_bp
|
|
|
|
|
|
@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
|
|
matches = (
|
|
Match.query.filter(
|
|
db.or_(Match.created_by == current_user.id, Match.status == 'scheduled'),
|
|
)
|
|
.order_by(Match.date.desc())
|
|
.limit(20)
|
|
.all()
|
|
)
|
|
tryouts = (
|
|
Tryout.query.filter_by(
|
|
created_by=current_user.id,
|
|
)
|
|
.order_by(Tryout.date.desc())
|
|
.limit(20)
|
|
.all()
|
|
)
|
|
teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
|
|
|
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]
|
|
|
|
content = request.form.get('content', '').strip()
|
|
if content:
|
|
note = TeamNote(
|
|
org_team_id=org_team.id,
|
|
coach_id=current_user.id,
|
|
content=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'))
|
|
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if not player_id or not content:
|
|
flash(_('Player and content are required.'), 'danger')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
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=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'))
|
|
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
match_id = request.form.get('match_id', type=int)
|
|
tryout_id = request.form.get('tryout_id', type=int)
|
|
team_id_str = request.form.get('team_id')
|
|
|
|
if not player_id or not content:
|
|
flash(_('Player and content are required.'), 'danger')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
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=content,
|
|
match_id=match_id if match_id else None,
|
|
tryout_id=tryout_id if tryout_id else None,
|
|
team_id=int(team_id_str) if team_id_str and team_id_str.isdigit() else None,
|
|
)
|
|
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)
|
|
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':
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if not player_id or not content:
|
|
flash(_('Player and content are required.'), '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))
|
|
|
|
note = PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=current_user.id,
|
|
content=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)
|
|
|
|
# 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':
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if not player_id or not content:
|
|
flash(_('Player and content are required.'), '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))
|
|
|
|
note = PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=current_user.id,
|
|
content=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=[],
|
|
)
|