diff --git a/src/utils/entity_grid.py b/src/utils/entity_grid.py index 4417fc2..c806282 100644 --- a/src/utils/entity_grid.py +++ b/src/utils/entity_grid.py @@ -11,7 +11,18 @@ Les callbacks Dash sont enregistrés par register_entity_grid_callbacks() (cf. module séparé de wiring) ; ici, seules des fonctions pures testables. """ -from dash import ALL, MATCH, Input, Output, State, callback, ctx, dcc, no_update +from dash import ( + ALL, + MATCH, + Input, + Output, + State, + callback, + ctx, + dcc, + no_update, + set_props, +) from src.figures import ( AG_GRID_LOCALE_FR, # noqa: F401 (réexport pratique) @@ -126,26 +137,32 @@ def register_entity_grid_callbacks(org_type: str) -> None: """ gtype = grid_type(org_type) - # 1) Datasource server-side. Input MATCH (grille) → Output MATCH - # (getRowsResponse) + Outputs fixes (stores totaux). Autorisé en Dash 4.4. + # 1) Datasource server-side. Une seule sortie MATCH (getRowsResponse de la + # grille pattern-matching) : Dash INTERDIT de mélanger une sortie MATCH + # avec des sorties fixes (« MATCH wildcards must be on the same keys for + # all Outputs », erreur du dev-renderer en mode debug). Les stores totaux + # (id fixes) sont donc alimentés impérativement via dash.set_props. @callback( Output({"type": gtype, "entity_id": MATCH, "year": MATCH}, "getRowsResponse"), - Output(f"{org_type}-total", "data"), - Output(f"{org_type}-total-unique", "data"), Input({"type": gtype, "entity_id": MATCH, "year": MATCH}, "getRowsRequest"), prevent_initial_call=True, ) def _get_rows(request): gid = ctx.triggered_id # {"type", "entity_id", "year"} if request is None or gid is None: - return no_update, no_update, no_update + return no_update if request.get("filterModel") and request.get("startRow", 0) == 0: import json from src.utils.tracking import track_search track_search(json.dumps(request["filterModel"]), org_type) - return fetch_entity_page(org_type, gid["entity_id"], gid["year"], request) + response, total, total_unique = fetch_entity_page( + org_type, gid["entity_id"], gid["year"], request + ) + set_props(f"{org_type}-total", {"data": total}) + set_props(f"{org_type}-total-unique", {"data": total_unique}) + return response # 2) (Re)construit la grille au changement de fiche (URL), d'année, ou de # colonnes masquées. Le remontage réinitialise le filterModel (accepté diff --git a/tests/test_entity_grid_datasource.py b/tests/test_entity_grid_datasource.py new file mode 100644 index 0000000..2357b04 --- /dev/null +++ b/tests/test_entity_grid_datasource.py @@ -0,0 +1,50 @@ +"""Régression : le datasource server-side de la grille entité (_get_rows) doit +répondre sans erreur de callback Dash. + +Bug migration AG Grid (#41) : _get_rows mélangeait une sortie MATCH +(getRowsResponse de la grille pattern-matching) avec des sorties fixes +(acheteur-total / -total-unique). Dash l'interdit (« MATCH wildcards must be on +the same keys for all Outputs ») — l'erreur est levée par le dev-renderer en +mode debug (celui de `run.py`), pas par le renderer de production. Le callback +échoue alors au dispatch et la grille ne reçoit aucune ligne. + +Ce test tourne donc en `debug=True` (dev-renderer) pour exercer cette +validation, sinon le bug passe inaperçu. Le correctif : une seule sortie MATCH, +les stores totaux alimentés via dash.set_props. +""" + +import src.app # noqa: F401 # instancie l'app → register_page() +from src.db import query_marches + + +def _an_acheteur_id(): + return query_marches("TRUE", (), columns=["acheteur_id"])["acheteur_id"][0] + + +def test_entity_grid_datasource_returns_rows_in_debug(dash_duo): + from src.app import app + + ach = _an_acheteur_id() + + # debug=True → dev-renderer, qui valide la cohérence MATCH des Outputs. + dash_duo.start_server( + app, debug=True, use_reloader=False, dev_tools_hot_reload=False + ) + dash_duo.wait_for_text_to_equal(".logo > h1", "colibre", timeout=6) + dash_duo.wait_for_page(dash_duo.server_url + f"/acheteurs/{ach}") + + # La grille charge des lignes → _get_rows a répondu (pas d'erreur de dispatch). + dash_duo.wait_for_element( + "#acheteur-grid-container .ag-center-cols-container .ag-row", timeout=15 + ) + # nb_rows alimenté via les stores totaux mis à jour par set_props côté serveur. + dash_duo.wait_for_contains_text("#acheteur_nb_rows", "marchés", timeout=8) + assert "0 marchés" not in dash_duo.find_element("#acheteur_nb_rows").text + + # Aucune erreur « Mismatched MATCH wildcards » dans la console du dev-renderer. + match_errors = [ + entry + for entry in (dash_duo.get_logs() or []) + if "MATCH" in str(entry.get("message", "")) + ] + assert not match_errors, f"Erreur dev-renderer MATCH : {match_errors}"