fix(tableau): filtres à conditions multiples ignorés + tri non réinitialisé (#41)
Deux bugs découverts en usage réel sur /tableau :
1. filtermodel_to_ast lisait condition1/condition2 pour un filtre à
deux conditions (ET/OU) sur une colonne — mais AG Grid >=29.2 (la
version 35.2.0 utilisée ici) encode ça via une liste `conditions`,
confirmé par la doc Dash AG Grid "Filter Model & Dash Callbacks" >
"Filter Model Multiple Conditions". Le filtre était donc
silencieusement ignoré (colonne exclue du AST, comme si aucun
filtre n'était posé), d'où des résultats sans rapport avec les
valeurs saisies. Corrigé : filtermodel_to_ast lit désormais
`conditions` (N éléments) en priorité, avec repli sur
condition1/condition2 (forme dépréciée mais "still accepted" selon
AG Grid). L'inverse (ast_to_filtermodel, utilisé au rappel d'une
vue sauvegardée) produit aussi la forme `conditions`.
2. Le bouton "Réinitialiser" ("Supprime tous les filtres et les tris")
ne réinitialisait que filterModel, jamais le tri. Corrigé via
resetColumnState (remet les colonnes à l'état de columnDefs, qui
reflète déjà la visibilité choisie via le sélecteur de colonnes —
seul le tri est donc affecté).
Reproduit et corrigé en TDD (tests/test_query_ast.py,
tests/test_grid.py), vérifié manuellement en navigateur par l'auteur
du rapport de bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -544,11 +544,16 @@ def toggle_tableau_columns(click_open, click_close, is_open):
|
||||
|
||||
@callback(
|
||||
Output("tableau_grid", "filterModel", allow_duplicate=True),
|
||||
Output("tableau_grid", "resetColumnState", allow_duplicate=True),
|
||||
Input("btn-tableau-reset", "n_clicks"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def reset_view(n_clicks):
|
||||
return {}
|
||||
# resetColumnState remet les colonnes (tri inclus) à l'état déclaré dans
|
||||
# columnDefs — qui reflète déjà la visibilité choisie via le sélecteur de
|
||||
# colonnes (cf. apply_hidden_columns), donc ça ne touche pas au choix de
|
||||
# colonnes affichées, seulement au tri.
|
||||
return {}, True
|
||||
|
||||
|
||||
@callback(
|
||||
|
||||
+42
-25
@@ -191,7 +191,14 @@ def _leaf(column: str, spec: dict):
|
||||
|
||||
|
||||
def filtermodel_to_ast(filter_model, schema):
|
||||
"""Traduit un filterModel AG Grid en AST. Colonnes combinées en And."""
|
||||
"""Traduit un filterModel AG Grid en AST. Colonnes combinées en And.
|
||||
|
||||
AG Grid >=29.2 (pinné à 35.2.0 ici) encode un filtre à conditions
|
||||
multiples via une liste `conditions` (N éléments, cf. `maxNumConditions`),
|
||||
et non plus via `condition1`/`condition2` (dépréciés depuis 29.2, mais
|
||||
l'API dit "still accept" ces clés en entrée — on les supporte donc aussi
|
||||
en repli, sans qu'AG Grid ne les envoie normalement).
|
||||
"""
|
||||
if not filter_model:
|
||||
return None
|
||||
children = []
|
||||
@@ -199,7 +206,12 @@ def filtermodel_to_ast(filter_model, schema):
|
||||
if column not in schema.names():
|
||||
logger.warning(f"Filtre sur colonne inconnue ignoré : {column!r}")
|
||||
continue
|
||||
if "operator" in spec: # deux conditions
|
||||
if "conditions" in spec: # forme courante AG Grid >=29.2 (N conditions)
|
||||
parts = [c for c in (_leaf(column, s) for s in spec["conditions"]) if c]
|
||||
if not parts:
|
||||
continue
|
||||
node = And(parts) if spec.get("operator") == "AND" else Or(parts)
|
||||
elif "operator" in spec: # forme dépréciée condition1/condition2
|
||||
c1 = _leaf(column, spec.get("condition1", {}))
|
||||
c2 = _leaf(column, spec.get("condition2", {}))
|
||||
parts = [c for c in (c1, c2) if c is not None]
|
||||
@@ -289,11 +301,12 @@ def _child_to_filterspec(child, schema: pl.Schema):
|
||||
"""Convertit un enfant du And de haut niveau en (colonne, spec filterModel).
|
||||
|
||||
Ne sait inverser que les deux formes produites par `filtermodel_to_ast` :
|
||||
une Condition seule, ou un And/Or à 2 enfants portant sur la MÊME colonne
|
||||
(filtre AG Grid natif à deux conditions). Toute autre forme (Not, And/Or
|
||||
imbriqué plus profondément, colonnes différentes, plus de 2 enfants) est
|
||||
ignorée avec un warning : un filterModel est par nature par-colonne et ne
|
||||
peut représenter une expression booléenne arbitraire (cf. #97).
|
||||
une Condition seule, ou un And/Or à N enfants (N = maxNumConditions)
|
||||
portant tous sur la MÊME colonne (filtre AG Grid natif multi-conditions,
|
||||
forme `conditions` — cf. AG Grid >=29.2). Toute autre forme (Not, And/Or
|
||||
imbriqué plus profondément, colonnes différentes) est ignorée avec un
|
||||
warning : un filterModel est par nature par-colonne et ne peut représenter
|
||||
une expression booléenne arbitraire (cf. #97).
|
||||
"""
|
||||
if isinstance(child, Condition):
|
||||
spec = _condition_to_filterspec(child, schema)
|
||||
@@ -301,24 +314,28 @@ def _child_to_filterspec(child, schema: pl.Schema):
|
||||
return None
|
||||
return child.column, spec
|
||||
|
||||
if isinstance(child, (And, Or)) and len(child.children) == 2:
|
||||
c1, c2 = child.children
|
||||
if (
|
||||
isinstance(c1, Condition)
|
||||
and isinstance(c2, Condition)
|
||||
and c1.column == c2.column
|
||||
):
|
||||
spec1 = _condition_to_filterspec(c1, schema)
|
||||
spec2 = _condition_to_filterspec(c2, schema)
|
||||
if spec1 is None or spec2 is None:
|
||||
return None
|
||||
operator = "AND" if isinstance(child, And) else "OR"
|
||||
return c1.column, {
|
||||
"filterType": spec1["filterType"],
|
||||
"operator": operator,
|
||||
"condition1": spec1,
|
||||
"condition2": spec2,
|
||||
}
|
||||
if isinstance(child, (And, Or)) and len(child.children) >= 2:
|
||||
conditions = child.children
|
||||
if not all(isinstance(c, Condition) for c in conditions):
|
||||
logger.warning(
|
||||
f"Nœud AST non représentable en filterModel, ignoré : {child!r}"
|
||||
)
|
||||
return None
|
||||
columns = {c.column for c in conditions}
|
||||
if len(columns) != 1:
|
||||
logger.warning(
|
||||
f"Nœud AST non représentable en filterModel, ignoré : {child!r}"
|
||||
)
|
||||
return None
|
||||
specs = [_condition_to_filterspec(c, schema) for c in conditions]
|
||||
if any(s is None for s in specs):
|
||||
return None
|
||||
operator = "AND" if isinstance(child, And) else "OR"
|
||||
return conditions[0].column, {
|
||||
"filterType": specs[0]["filterType"],
|
||||
"operator": operator,
|
||||
"conditions": specs,
|
||||
}
|
||||
|
||||
logger.warning(f"Nœud AST non représentable en filterModel, ignoré : {child!r}")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user