QUA-002, premiere moitie. **Ce commit ne fait que reformater** : aucun changement de comportement, aucune ligne de logique touchee. 72 fichiers, 4 restaient deja conformes. Il est isole exprès, pour que `git log -p` sur les commits voisins reste lisible. `quote-style = "preserve"` etait deja pose dans pyproject.toml, ce qui evite le brassage guillemets simples / doubles : le diff porte sur les retours a la ligne, l indentation des appels longs et les virgules finales, pas sur le style de chaine. Verification : 263 tests passent avant et apres, ruff check propre. L activation en CI arrive dans le commit suivant, separement, pour que ce diff-ci ne contienne rien d autre. Co-Authored-By: Claude Opus 5 <[email protected]>
224 lines
8.1 KiB
Python
224 lines
8.1 KiB
Python
"""Deletion of entities that other rows point at.
|
|
|
|
DATA-004, DATA-005, DATA-006. Three delete routes clean up some of their
|
|
dependants and ignore others. Every ignored one is a foreign key pointing
|
|
at the row being removed, so the delete raises an IntegrityError, which
|
|
surfaces as a 500 and rolls back — the entity simply cannot be deleted once
|
|
it has been used.
|
|
|
|
These are reachable through the interface by any manager or admin, and the
|
|
audit could only rate them "likely" without executing them. That is what
|
|
this file is for.
|
|
"""
|
|
|
|
from datetime import date, time
|
|
|
|
import pytest
|
|
|
|
from app.extensions import db
|
|
from app.models import (
|
|
Match,
|
|
MatchParticipant,
|
|
OrgTeam,
|
|
PersonalNote,
|
|
Team,
|
|
TeamMatch,
|
|
TeamMember,
|
|
TeamNote,
|
|
TeamPlayer,
|
|
Tryout,
|
|
TryoutRegistration,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def world(app, make_user):
|
|
"""A tryout with a team, a match, a participant and a note on each.
|
|
|
|
Deliberately mirrors what an actual tryout looks like after a session:
|
|
the empty case already worked, the populated one is what breaks.
|
|
"""
|
|
admin_id = make_user('admin')
|
|
coach_id = make_user('coach')
|
|
player_id = make_user('player')
|
|
|
|
with app.app_context():
|
|
org_team = OrgTeam(name='Varsity', created_by=admin_id, coach_id=coach_id)
|
|
db.session.add(org_team)
|
|
db.session.flush()
|
|
|
|
tryout = Tryout(
|
|
title='Spring',
|
|
game='Valorant',
|
|
date=date(2030, 4, 1),
|
|
created_by=admin_id,
|
|
target_org_team_id=org_team.id,
|
|
)
|
|
db.session.add(tryout)
|
|
db.session.flush()
|
|
|
|
team = Team(tryout_id=tryout.id, name='Alpha', created_by=admin_id)
|
|
db.session.add(team)
|
|
db.session.flush()
|
|
|
|
match = Match(
|
|
tryout_id=tryout.id,
|
|
title='Scrim',
|
|
date=date(2030, 4, 2),
|
|
start_time=time(18, 0),
|
|
match_type='player_scrim',
|
|
created_by=admin_id,
|
|
)
|
|
db.session.add(match)
|
|
db.session.flush()
|
|
|
|
db.session.add_all(
|
|
[
|
|
TryoutRegistration(tryout_id=tryout.id, player_id=player_id),
|
|
TeamMember(team_id=team.id, player_id=player_id),
|
|
MatchParticipant(match_id=match.id, player_id=player_id),
|
|
TeamPlayer(player_id=player_id, org_team_id=org_team.id),
|
|
TeamNote(org_team_id=org_team.id, coach_id=coach_id, content='Team note'),
|
|
TeamMatch(
|
|
org_team_id=org_team.id,
|
|
title='Season match',
|
|
date=date(2030, 4, 5),
|
|
created_by=admin_id,
|
|
),
|
|
# A note referencing all three contexts at once.
|
|
PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=coach_id,
|
|
content='Watch the entries',
|
|
match_id=match.id,
|
|
team_id=team.id,
|
|
tryout_id=tryout.id,
|
|
),
|
|
]
|
|
)
|
|
db.session.commit()
|
|
|
|
return {
|
|
'admin_id': admin_id,
|
|
'coach_id': coach_id,
|
|
'player_id': player_id,
|
|
'org_team_id': org_team.id,
|
|
'tryout_id': tryout.id,
|
|
'team_id': team.id,
|
|
'match_id': match.id,
|
|
}
|
|
|
|
|
|
def _login_admin(client, app, admin_id, login):
|
|
from app.models import User
|
|
|
|
with app.app_context():
|
|
username = db.session.get(User, admin_id).username
|
|
login(username)
|
|
|
|
|
|
class TestDeleteMatch:
|
|
"""DATA-004 — Match.participants had no cascade, and PersonalNote.match_id
|
|
references matches.id."""
|
|
|
|
def test_a_populated_match_can_be_deleted(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
response = client.post(f"/matches/{world['match_id']}/delete", follow_redirects=False)
|
|
|
|
assert response.status_code < 500, 'deleting a used match raised'
|
|
with app.app_context():
|
|
assert db.session.get(Match, world['match_id']) is None
|
|
|
|
def test_its_participants_go_with_it(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
client.post(f"/matches/{world['match_id']}/delete", follow_redirects=True)
|
|
|
|
with app.app_context():
|
|
assert MatchParticipant.query.filter_by(match_id=world['match_id']).count() == 0
|
|
|
|
def test_notes_survive_but_lose_their_match_context(self, app, client, world, login):
|
|
"""A coach's observation keeps its value once the match is gone;
|
|
deleting it with the match would destroy unrelated content."""
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
client.post(f"/matches/{world['match_id']}/delete", follow_redirects=True)
|
|
|
|
with app.app_context():
|
|
note = PersonalNote.query.filter_by(player_id=world['player_id']).one()
|
|
assert note.content == 'Watch the entries'
|
|
assert note.match_id is None
|
|
|
|
|
|
class TestDeleteTryout:
|
|
"""DATA-006 — delete_tryout removed matches, teams, registrations and
|
|
evaluations, but not the PersonalNote rows pointing at any of them."""
|
|
|
|
def test_a_populated_tryout_can_be_deleted(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
response = client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=False)
|
|
|
|
assert response.status_code < 500
|
|
with app.app_context():
|
|
assert db.session.get(Tryout, world['tryout_id']) is None
|
|
|
|
def test_its_matches_and_teams_go_with_it(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=True)
|
|
|
|
with app.app_context():
|
|
assert db.session.get(Match, world['match_id']) is None
|
|
assert db.session.get(Team, world['team_id']) is None
|
|
assert TryoutRegistration.query.filter_by(tryout_id=world['tryout_id']).count() == 0
|
|
|
|
def test_notes_survive_the_tryout(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
client.post(f"/tryouts/{world['tryout_id']}/delete", follow_redirects=True)
|
|
|
|
with app.app_context():
|
|
note = PersonalNote.query.filter_by(player_id=world['player_id']).one()
|
|
assert note.tryout_id is None
|
|
assert note.match_id is None
|
|
assert note.team_id is None
|
|
|
|
|
|
class TestDeleteTeam:
|
|
"""DATA-005 — delete_team handled tryouts and TeamPlayer, but not
|
|
TeamNote or TeamMatch, both of which are NOT NULL foreign keys. It also
|
|
committed three times, so a failure at the third step left the tryouts
|
|
detached and the players removed without deleting the team."""
|
|
|
|
def test_a_populated_team_can_be_deleted(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
response = client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=False)
|
|
|
|
assert response.status_code < 500
|
|
with app.app_context():
|
|
assert db.session.get(OrgTeam, world['org_team_id']) is None
|
|
|
|
def test_its_notes_and_season_matches_go_with_it(self, app, client, world, login):
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=True)
|
|
|
|
with app.app_context():
|
|
assert TeamNote.query.filter_by(org_team_id=world['org_team_id']).count() == 0
|
|
assert TeamMatch.query.filter_by(org_team_id=world['org_team_id']).count() == 0
|
|
assert TeamPlayer.query.filter_by(org_team_id=world['org_team_id']).count() == 0
|
|
|
|
def test_the_tryout_survives_and_is_detached(self, app, client, world, login):
|
|
"""A tryout outlives the team it was aimed at."""
|
|
_login_admin(client, app, world['admin_id'], login)
|
|
|
|
client.post(f"/teams/{world['org_team_id']}/delete", follow_redirects=True)
|
|
|
|
with app.app_context():
|
|
tryout = db.session.get(Tryout, world['tryout_id'])
|
|
assert tryout is not None
|
|
assert tryout.target_org_team_id is None
|