fix(entity_grid): sortie MATCH unique pour le datasource (dash.set_props)

Le datasource _get_rows des grilles acheteur/titulaire mélangeait une sortie
MATCH (getRowsResponse de la grille pattern-matching) avec deux sorties fixes
(<org>-total / -total-unique). Dash l'interdit : le dev-renderer (mode debug de
run.py) lève « MATCH wildcards must be on the same keys for all Outputs » (4
erreurs : 2 par page). Le callback échoue au dispatch et la grille reste vide.

Correctif : _get_rows n'a plus qu'une sortie MATCH (getRowsResponse) ; les
stores totaux (id fixes) sont alimentés impérativement via dash.set_props.

Test de régression en debug=True (dev-renderer) : sans le correctif la grille ne
charge aucune ligne ; avec, lignes + compteur « N marchés » présents.
This commit is contained in:
Colin Maudry
2026-07-13 19:40:26 +02:00
parent f0a891b73f
commit 770b7cbdf8
2 changed files with 74 additions and 7 deletions
+24 -7
View File
@@ -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. (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 ( from src.figures import (
AG_GRID_LOCALE_FR, # noqa: F401 (réexport pratique) 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) gtype = grid_type(org_type)
# 1) Datasource server-side. Input MATCH (grille) → Output MATCH # 1) Datasource server-side. Une seule sortie MATCH (getRowsResponse de la
# (getRowsResponse) + Outputs fixes (stores totaux). Autorisé en Dash 4.4. # 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( @callback(
Output({"type": gtype, "entity_id": MATCH, "year": MATCH}, "getRowsResponse"), 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"), Input({"type": gtype, "entity_id": MATCH, "year": MATCH}, "getRowsRequest"),
prevent_initial_call=True, prevent_initial_call=True,
) )
def _get_rows(request): def _get_rows(request):
gid = ctx.triggered_id # {"type", "entity_id", "year"} gid = ctx.triggered_id # {"type", "entity_id", "year"}
if request is None or gid is None: 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: if request.get("filterModel") and request.get("startRow", 0) == 0:
import json import json
from src.utils.tracking import track_search from src.utils.tracking import track_search
track_search(json.dumps(request["filterModel"]), org_type) 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 # 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é # colonnes masquées. Le remontage réinitialise le filterModel (accepté
+50
View File
@@ -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}"