diff --git a/src/figures.py b/src/figures.py index 77a2d07..974e902 100644 --- a/src/figures.py +++ b/src/figures.py @@ -2,6 +2,7 @@ import math from datetime import datetime from typing import Literal +import dash_ag_grid as dag import dash_bootstrap_components as dbc import dash_leaflet as dl 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, 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 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"], + ) diff --git a/src/utils/grid.py b/src/utils/grid.py index 674b11c..f175b97 100644 --- a/src/utils/grid.py +++ b/src/utils/grid.py @@ -1,6 +1,9 @@ """Datasource server-side pour AG Grid (infinite row model).""" +import polars as pl + 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.table import postprocess_page @@ -32,3 +35,60 @@ def fetch_grid_page( ) page = postprocess_page(page) 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 diff --git a/tests/test_grid.py b/tests/test_grid.py index 22020be..9d603ac 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -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():