Améliorations sur le style de l'ag grid #47

This commit is contained in:
Colin Maudry
2026-07-11 16:58:36 +02:00
parent 117ce9a4ab
commit f580459bd5
4 changed files with 157 additions and 20 deletions
+70 -4
View File
@@ -2,7 +2,7 @@
import polars as pl
from src.db import count_marches, query_marches, schema
from src.db import count_marches, count_unique_marches, query_marches, schema
from src.figures import DATA_SCHEMA
from src.utils.cache import cache
from src.utils.query_ast import ast_to_sql, filtermodel_to_ast, sort_model_to_sql
@@ -21,6 +21,13 @@ def _cached_count(where_sql: str, params: tuple) -> int:
return count_marches(where_sql, params)
@cache.memoize()
def _cached_unique_count(where_sql: str, params: tuple) -> int:
"""Cache le COUNT(DISTINCT uid) sur (where_sql, params), même raison que
`_cached_count`."""
return count_unique_marches(where_sql, params)
def fetch_grid_page(
filter_model,
sort_model,
@@ -28,8 +35,8 @@ def fetch_grid_page(
end_row: int,
base_where_sql: str = "TRUE",
base_params: tuple = (),
) -> tuple[list[dict], int]:
"""Renvoie (row_data, total_count) pour un bloc [start_row, end_row)."""
) -> tuple[list[dict], int, int]:
"""Renvoie (row_data, total_count, total_unique_count) pour un bloc [start_row, end_row)."""
ast = filtermodel_to_ast(filter_model, schema)
filter_sql, filter_params = ast_to_sql(ast, schema)
where_sql = f"({base_where_sql}) AND ({filter_sql})"
@@ -37,6 +44,7 @@ def fetch_grid_page(
order_by = sort_model_to_sql(sort_model, schema) or None
total = _cached_count(where_sql, tuple(params))
total_unique = _cached_unique_count(where_sql, tuple(params))
limit = max(0, end_row - start_row)
page = query_marches(
@@ -47,7 +55,7 @@ def fetch_grid_page(
offset=start_row,
)
page = postprocess_page(page)
return page.to_dicts(), total
return page.to_dicts(), total, total_unique
def export_dataframe(filter_model, sort_model, hidden_columns) -> pl.DataFrame:
@@ -74,9 +82,33 @@ _LINK_COLUMNS = {
"acheteur_nom",
"titulaire_id",
"titulaire_nom",
"sourceDataset",
}
# Colonnes oui/non (cf. `booleans_to_strings` dans src.db) : valeur très
# courte, pas besoin de place.
_BOOLEAN_LIKE_COLUMNS = {
"attributionAvance",
"marcheInnovant",
"sousTraitanceDeclaree",
"considerationsSociales",
"considerationsEnvironnementales",
}
# Codes/identifiants qui ne suivent pas le pattern de suffixe `_id`/`_code`
# (ex. codeCPV, idAccordCadre) mais restent des valeurs courtes.
_SHORT_CODE_COLUMNS = {
"uid",
"id",
"codeCPV",
"idAccordCadre",
"lieuExecution_typeCode",
"sourceFile",
}
# Colonnes "nom" qui ne suivent pas le suffixe `_nom` (ex. sourceDataset).
_WIDE_LABEL_COLUMNS = {"sourceDataset"}
def _filter_for(col_type) -> str:
if col_type.is_numeric():
@@ -86,6 +118,28 @@ def _filter_for(col_type) -> str:
return "agTextColumnFilter"
def _column_width(col: str, col_type) -> dict:
"""Largeur par défaut selon la nature de la colonne.
`columnSize="responsiveSizeToFit"` (cf. ag_grid()) étire les colonnes
proportionnellement à cette largeur pour remplir l'espace disponible :
sans ça, les codes/booléens s'étirent autant que les noms/libellés.
"""
if col == "montant":
return {"width": 150, "maxWidth": 200}
if col_type == pl.Date:
return {"width": 140, "maxWidth": 160}
if col_type.is_numeric():
return {"width": 120, "maxWidth": 150}
if col in _BOOLEAN_LIKE_COLUMNS:
return {"width": 110, "maxWidth": 140}
if col in _SHORT_CODE_COLUMNS or col.endswith(("_id", "_code")):
return {"width": 130, "maxWidth": 170}
if col in _WIDE_LABEL_COLUMNS or col.endswith("_nom"):
return {"width": 200}
return {"width": 170}
def grid_column_defs(hidden_columns=None):
"""columnDefs dérivés du schéma DuckDB.
@@ -120,5 +174,17 @@ def grid_column_defs(hidden_columns=None):
)
if col in _LINK_COLUMNS:
col_def["cellRenderer"] = "markdown"
if col != "objet":
col_def.update(_column_width(col, col_type))
if col == "objet":
# autoHeight n'est pas supporté avec rowModelType="infinite" (la
# grille doit pouvoir calculer la position des lignes non
# chargées, donc une hauteur de ligne fixe) : cf. ag_grid(),
# rowHeight fixe côté dashGridOptions plutôt qu'autoHeight ici.
col_def["wrapText"] = True
col_def["minWidth"] = 360
# Le texte tronqué par la hauteur de ligne fixe reste consultable
# en entier via l'infobulle native AG Grid au survol.
col_def["tooltipField"] = "objet"
defs.append(col_def)
return defs