fix(data): reparer les trois suppressions cassees

DATA-004, DATA-005, DATA-006. L'audit les classait "forte probabilite" faute
de pouvoir les executer. Les tests les confirment : ce sont des bugs averes,
declenchables par tout manager ou administrateur depuis l'interface.

Erreurs reellement obtenues avant correction :
  NOT NULL constraint failed: match_participants.match_id
  NOT NULL constraint failed: team_matches.org_team_id

Supprimer un match (DATA-004)
  Match.participants n'avait pas de cascade. SQLAlchemy tentait donc de
  detacher les participants en mettant match_id a NULL, ce que la colonne
  refuse. Tout match ayant eu des participants etait indestructible.
  TeamMatch.participants declarait deja delete-orphan ; Match non.

Supprimer un tryout (DATA-006)
  Les PersonalNote pointant vers ses matchs, equipes ou vers lui-meme
  n'etaient pas traitees.

Supprimer une equipe (DATA-005)
  TeamNote.org_team_id et TeamMatch.org_team_id sont NOT NULL et n'etaient
  pas traites du tout. De plus la fonction validait trois fois : un echec au
  troisieme temps laissait les tryouts detaches et les joueurs retires sans
  que l'equipe soit supprimee -- un etat incoherent que rien ne rattrapait.
  Une seule transaction desormais.

Regle appliquee, uniforme
  Ce qui n'a de sens que dans le parent est supprime avec lui : participants,
  membres, notes d'equipe, matchs de saison.
  Ce qui lui survit est seulement detache : les notes personnelles sont les
  observations d'un coach sur un joueur, pas des donnees de tryout. Les
  supprimer avec le tryout detruirait du contenu sans rapport. Idem pour les
  contrats et les demandes de rencontre individuelle.

Fidelite des tests
  conftest.py active PRAGMA foreign_keys=ON. SQLite ignore les cles
  etrangeres par defaut ; PostgreSQL les applique toujours. Sans ce reglage,
  la suite pouvait valider une suppression qui echoue en production --
  precisement la classe de bug corrigee ici. Les 146 tests passent avec les
  contraintes actives.

9 tests, dont trois qui verifient que les entites survivantes survivent
vraiment : une note garde son contenu et perd son contexte, un tryout
survit a l'equipe qu'il visait.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-07 20:37:04 -04:00
co-authored by Claude Opus 5
parent a92600c305
commit d15a3de2a1
6 changed files with 269 additions and 17 deletions
+15 -9
View File
@@ -10,7 +10,7 @@ from app.extensions import db
from app.models import (
Admin, Manager, Coach, Player, Scout,
User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
OrgTeam, Match, MatchParticipant,
OrgTeam, Match, MatchParticipant, PersonalNote,
ESPORT_GAMES, GAME_POSITIONS,
)
from datetime import datetime
@@ -505,28 +505,34 @@ def delete_tryout(tryout_id):
flash('You do not have permission to delete this tryout.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
# Delete match participants for all matches in this tryout
match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()]
team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()]
# Personal notes outlive the tryout: they are a coach's observations
# about a player, not tryout data. Only their context links are cleared.
# Missing this step made the deletion fail on the foreign keys below.
PersonalNote.query.filter_by(tryout_id=tryout_id).update(
{'tryout_id': None}, synchronize_session=False)
if match_ids:
PersonalNote.query.filter(PersonalNote.match_id.in_(match_ids)).update(
{'match_id': None}, synchronize_session=False)
if team_ids:
PersonalNote.query.filter(PersonalNote.team_id.in_(team_ids)).update(
{'team_id': None}, synchronize_session=False)
if match_ids:
MatchParticipant.query.filter(
MatchParticipant.match_id.in_(match_ids)
).delete(synchronize_session=False)
# Delete matches
Match.query.filter(Match.id.in_(match_ids)).delete(synchronize_session=False)
# Delete team members for all teams in this tryout
team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()]
if team_ids:
TeamMember.query.filter(
TeamMember.team_id.in_(team_ids)
).delete(synchronize_session=False)
# Delete teams
Team.query.filter(Team.id.in_(team_ids)).delete(synchronize_session=False)
# Delete registrations
TryoutRegistration.query.filter_by(tryout_id=tryout_id).delete()
# Delete evaluations
Evaluation.query.filter_by(tryout_id=tryout_id).delete()
db.session.delete(tryout)