perf: borner le calendrier et remplacer trois boucles par des requetes

PERF-002, PERF-003, PERF-004. Aucun changement de comportement : chaque
reecriture est accompagnee de tests qui enoncent la reponse attendue, pas
la methode.

PERF-002 -- /matches/api/events
Le flux parcourait `tryout.matches` pour chaque tryout visible -- pour un
president, tout l historique du club -- puis posait une requete
MatchParticipant PAR match pour savoir si la personne qui regarde y figure.
Le cout du calendrier croissait avec l historique, a chaque navigation.

FullCalendar envoie deja `start` et `end` sur une source d evenements de
type URL. Personne ne les lisait. La requete est desormais bornee, et les
participants de tous les matchs de la fenetre sont charges en une fois,
joueur compris. Des bornes illisibles sont ignorees plutot que refusees :
un calendrier qui en montre trop est un probleme de performance, un
calendrier qui renvoie 400 est une page blanche.

PERF-003 -- get_players_available_at_time
Chargeait tous les joueurs actifs, puis une requete PlayerDisponibility par
joueur, sur une colonne non indexee. Deux requetes desormais, quelle que
soit la taille du club. Mesure dans le test : 7 requetes pour 6 joueurs
avant, 2 apres.

PERF-004 -- decompte des evaluations en attente
Chargeait toutes les inscriptions du club et toutes les evaluations du
coach, construisait deux ensembles Python et les soustrayait -- deux
lectures de table entiere pour produire un entier. Un COUNT DISTINCT avec
NOT EXISTS.

Les tests couvrent ce que la reecriture aurait pu changer sans bruit : fin
de creneau exclusive, compte desactive exclu, joueur a cheval sur deux
creneaux compte une fois, evaluation d un autre coach qui ne libere pas la
ligne, double inscription comptee une fois (DB-006 n a pas encore atterri,
donc le cas existe).

PERF-001 (view_tryout) n est pas fait : c est le plus gros des quatre, il
touche la page la plus consultee et merite son propre lot.

23 tests ajoutes, 371 au total.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-08 18:00:36 -04:00
co-authored by Claude Opus 5
parent 0cd9a186ee
commit 9d0456c2fd
4 changed files with 473 additions and 34 deletions
+111
View File
@@ -147,3 +147,114 @@ class TestLegacyHostileData:
# as markup.
assert props['description'] == 'Normal text'
assert self.PAYLOAD not in props['description']
class TestTheWindow:
"""PERF-002 — the feed used to return every match of every visible
tryout, which for a president is the club's whole history, and then ask
one more question per match to find out whether the viewer was in it.
FullCalendar sends `start` and `end` with a URL event source, so the
bounds were already arriving; nothing read them."""
def test_a_match_inside_the_window_is_returned(
self, app, client, as_role, make_user, match_factory
):
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id)
payload = client.get(
'/matches/api/events?start=2030-05-01T00:00:00-04:00&end=2030-05-31T00:00:00-04:00'
).get_json()
assert _match_event(payload)['date'] == '2030-05-02'
def test_a_match_outside_the_window_is_not(
self, app, client, as_role, make_user, match_factory
):
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id)
payload = client.get('/matches/api/events?start=2031-01-01&end=2031-01-31').get_json()
assert [e for e in payload if e['id'].startswith('match_')] == []
def test_no_bounds_still_returns_everything(
self, app, client, as_role, make_user, match_factory
):
"""Bookmarks, the tryout detail page and anything else calling this
without bounds must keep working."""
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id)
payload = client.get('/matches/api/events').get_json()
assert _match_event(payload)['date'] == '2030-05-02'
def test_unparseable_bounds_are_ignored_rather_than_rejected(
self, app, client, as_role, make_user, match_factory
):
"""A calendar showing too much is a performance problem; one that
returns 400 is a blank page."""
player_id = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, player_id)
response = client.get('/matches/api/events?start=whenever&end=')
assert response.status_code == 200
assert _match_event(response.get_json())['date'] == '2030-05-02'
def test_the_viewers_own_participation_still_comes_back(
self, app, client, as_role, make_user, match_factory
):
"""Batching the participant lookup must not lose the one row the
modal uses to offer 'confirm attendance'."""
admin_id = make_user('admin')
player_id = as_role('player')
_tryout_id, match_id = match_factory(admin_id, player_id)
props = _match_event(client.get('/matches/api/events').get_json())['extendedProps']
assert props['user_participant_id'] is not None
assert props['user_attendance_confirmed'] is False
assert props['match_id'] == match_id
def test_someone_elses_participation_is_not_reported_as_yours(
self, app, client, as_role, make_user, match_factory
):
other_player = make_user('player')
admin_id = as_role('admin')
match_factory(admin_id, other_player)
props = _match_event(client.get('/matches/api/events').get_json())['extendedProps']
assert props['user_participant_id'] is None
class TestTheWindowParser:
def test_it_reads_the_date_out_of_an_iso_timestamp(self, app):
from datetime import date as date_type
from app.routes.matches import calendar_window
with app.test_request_context(
'/matches/api/events?start=2030-05-01T00:00:00-04:00&end=2030-05-31T23:59:59-04:00'
):
from flask import request
assert calendar_window(request.args) == (
date_type(2030, 5, 1),
date_type(2030, 5, 31),
)
def test_missing_or_broken_bounds_read_as_absent(self, app):
from app.routes.matches import calendar_window
with app.test_request_context('/matches/api/events?start=nope'):
from flask import request
assert calendar_window(request.args) == (None, None)