feat(grid): columnDefs + fabrique ag_grid (#41)
This commit is contained in:
@@ -2,6 +2,7 @@ import math
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
import dash_ag_grid as dag
|
||||||
import dash_bootstrap_components as dbc
|
import dash_bootstrap_components as dbc
|
||||||
import dash_leaflet as dl
|
import dash_leaflet as dl
|
||||||
import dash_leaflet.express as dlx
|
import dash_leaflet.express as dlx
|
||||||
@@ -1066,3 +1067,29 @@ def get_top_org_table(data, org_type: str, extra_columns: list, filters: bool =
|
|||||||
tooltip_header=tooltip,
|
tooltip_header=tooltip,
|
||||||
filter_action="native" if filters else "none",
|
filter_action="native" if filters else "none",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ag_grid(grid_id: str, column_defs: list[dict]) -> "dag.AgGrid":
|
||||||
|
"""Grille AG Grid server-side (infinite) pour la page Tableau.
|
||||||
|
|
||||||
|
Apparence de base d'AG Grid (aucun thème custom au Lot 1).
|
||||||
|
"""
|
||||||
|
return dag.AgGrid(
|
||||||
|
id=grid_id,
|
||||||
|
columnDefs=column_defs,
|
||||||
|
defaultColDef={"resizable": True, "minWidth": 120, "floatingFilter": True},
|
||||||
|
rowModelType="infinite",
|
||||||
|
dangerously_allow_code=True, # rend le HTML <a> des cellules liens
|
||||||
|
dashGridOptions={
|
||||||
|
"cacheBlockSize": 100,
|
||||||
|
"maxBlocksInCache": 10,
|
||||||
|
"rowBuffer": 0,
|
||||||
|
"infiniteInitialRowCount": 100,
|
||||||
|
"suppressCellFocus": True,
|
||||||
|
},
|
||||||
|
columnSize="responsiveSizeToFit",
|
||||||
|
style={"height": "70vh", "width": "100%"},
|
||||||
|
persistence=True,
|
||||||
|
persistence_type="local",
|
||||||
|
persisted_props=["filterModel", "columnState"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"""Datasource server-side pour AG Grid (infinite row model)."""
|
"""Datasource server-side pour AG Grid (infinite row model)."""
|
||||||
|
|
||||||
|
import polars as pl
|
||||||
|
|
||||||
from src.db import count_marches, query_marches, schema
|
from src.db import count_marches, query_marches, schema
|
||||||
|
from src.figures import DATA_SCHEMA
|
||||||
from src.utils.query_ast import ast_to_sql, filtermodel_to_ast, sort_model_to_sql
|
from src.utils.query_ast import ast_to_sql, filtermodel_to_ast, sort_model_to_sql
|
||||||
from src.utils.table import postprocess_page
|
from src.utils.table import postprocess_page
|
||||||
|
|
||||||
@@ -32,3 +35,60 @@ def fetch_grid_page(
|
|||||||
)
|
)
|
||||||
page = postprocess_page(page)
|
page = postprocess_page(page)
|
||||||
return page.to_dicts(), total
|
return page.to_dicts(), total
|
||||||
|
|
||||||
|
|
||||||
|
_LINK_COLUMNS = {
|
||||||
|
"marche",
|
||||||
|
"uid",
|
||||||
|
"acheteur_id",
|
||||||
|
"acheteur_nom",
|
||||||
|
"titulaire_id",
|
||||||
|
"titulaire_nom",
|
||||||
|
"sourceFile",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_for(col_type) -> str:
|
||||||
|
if col_type.is_numeric():
|
||||||
|
return "agNumberColumnFilter"
|
||||||
|
if col_type == pl.Date:
|
||||||
|
return "agDateColumnFilter"
|
||||||
|
return "agTextColumnFilter"
|
||||||
|
|
||||||
|
|
||||||
|
def grid_column_defs(hidden_columns=None):
|
||||||
|
"""columnDefs dérivés du schéma DuckDB.
|
||||||
|
|
||||||
|
'marche' (colonne loupe ajoutée par postprocess_page) est placée en tête.
|
||||||
|
"""
|
||||||
|
hidden = set(hidden_columns or [])
|
||||||
|
defs = [
|
||||||
|
{
|
||||||
|
"field": "marche",
|
||||||
|
"headerName": "",
|
||||||
|
"cellRenderer": "markdown",
|
||||||
|
"filter": False,
|
||||||
|
"sortable": False,
|
||||||
|
"maxWidth": 60,
|
||||||
|
"pinned": "left",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for col in schema.names():
|
||||||
|
meta = DATA_SCHEMA.get(col, {})
|
||||||
|
col_type = schema[col]
|
||||||
|
col_def = {
|
||||||
|
"field": col,
|
||||||
|
"headerName": meta.get("title", col),
|
||||||
|
"filter": _filter_for(col_type),
|
||||||
|
"floatingFilter": True,
|
||||||
|
"sortable": True,
|
||||||
|
"hide": col in hidden,
|
||||||
|
}
|
||||||
|
if meta.get("description"):
|
||||||
|
col_def["headerTooltip"] = (
|
||||||
|
f"{meta.get('title', col)} ({col}) — {meta['description']}"
|
||||||
|
)
|
||||||
|
if col in _LINK_COLUMNS:
|
||||||
|
col_def["cellRenderer"] = "markdown"
|
||||||
|
defs.append(col_def)
|
||||||
|
return defs
|
||||||
|
|||||||
+19
-1
@@ -1,4 +1,22 @@
|
|||||||
from src.utils.grid import fetch_grid_page
|
from src.utils.grid import fetch_grid_page, grid_column_defs
|
||||||
|
|
||||||
|
|
||||||
|
def test_column_defs_have_field_and_filter():
|
||||||
|
defs = grid_column_defs(hidden_columns=[])
|
||||||
|
by_field = {d["field"]: d for d in defs}
|
||||||
|
assert "objet" in by_field
|
||||||
|
# filtre texte par défaut
|
||||||
|
assert by_field["objet"]["filter"] == "agTextColumnFilter"
|
||||||
|
# montant est numérique
|
||||||
|
assert by_field["montant"]["filter"] == "agNumberColumnFilter"
|
||||||
|
# headerTooltip présent (définition de colonne)
|
||||||
|
assert "headerTooltip" in by_field["objet"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_column_defs_hidden_flag():
|
||||||
|
defs = grid_column_defs(hidden_columns=["objet"])
|
||||||
|
by_field = {d["field"]: d for d in defs}
|
||||||
|
assert by_field["objet"]["hide"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_grid_page_returns_rows_and_count():
|
def test_fetch_grid_page_returns_rows_and_count():
|
||||||
|
|||||||
Reference in New Issue
Block a user