feat(tableau): export Excel via DuckDB (AST->SQL) (#41)
Rewrite download_data to read filterModel/columnState from the AG Grid component and add export_dataframe (compiles filterModel to SQL via filtermodel_to_ast/ast_to_sql, builds ORDER BY, excludes hidden columns, queries DuckDB via query_marches). Replaces the old Polars filter_query pipeline tied to the removed DataTable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+15
-20
@@ -5,7 +5,6 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import dash_bootstrap_components as dbc
|
import dash_bootstrap_components as dbc
|
||||||
import polars as pl
|
|
||||||
from dash import (
|
from dash import (
|
||||||
ClientsideFunction,
|
ClientsideFunction,
|
||||||
Input,
|
Input,
|
||||||
@@ -20,7 +19,7 @@ from dash import (
|
|||||||
)
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from src.db import query_marches, schema
|
from src.db import schema
|
||||||
from src.figures import ag_grid, make_column_picker
|
from src.figures import ag_grid, make_column_picker
|
||||||
from src.pages._compte_shell import current_user_has_subscription
|
from src.pages._compte_shell import current_user_has_subscription
|
||||||
from src.saved_views import db as saved_views_db
|
from src.saved_views import db as saved_views_db
|
||||||
@@ -31,10 +30,8 @@ from src.utils.seo import META_CONTENT
|
|||||||
from src.utils.table import (
|
from src.utils.table import (
|
||||||
COLUMNS,
|
COLUMNS,
|
||||||
build_view_query,
|
build_view_query,
|
||||||
filter_table_data,
|
|
||||||
get_default_hidden_columns,
|
get_default_hidden_columns,
|
||||||
invert_columns,
|
invert_columns,
|
||||||
sort_table_data,
|
|
||||||
write_styled_excel,
|
write_styled_excel,
|
||||||
)
|
)
|
||||||
from src.utils.tracking import track_search
|
from src.utils.tracking import track_search
|
||||||
@@ -467,27 +464,25 @@ def update_meta(total):
|
|||||||
@callback(
|
@callback(
|
||||||
Output("download-data", "data"),
|
Output("download-data", "data"),
|
||||||
Input("btn-download-data", "n_clicks"),
|
Input("btn-download-data", "n_clicks"),
|
||||||
State("tableau_datatable", "filter_query"),
|
State("tableau_grid", "filterModel"),
|
||||||
State("tableau_datatable", "sort_by"),
|
State("tableau_grid", "columnState"),
|
||||||
State("tableau_datatable", "hidden_columns"),
|
|
||||||
prevent_initial_call=True,
|
prevent_initial_call=True,
|
||||||
)
|
)
|
||||||
def download_data(n_clicks, filter_query, sort_by, hidden_columns: list | None = None):
|
def download_data(n_clicks, filter_model, column_state):
|
||||||
lff: pl.LazyFrame = query_marches().lazy()
|
from src.utils.grid import export_dataframe
|
||||||
|
|
||||||
# Les colonnes masquées sont supprimées
|
sort_model = [
|
||||||
if hidden_columns:
|
{"colId": c["colId"], "sort": c["sort"]}
|
||||||
lff = lff.drop(hidden_columns)
|
for c in (column_state or [])
|
||||||
|
if c.get("sort")
|
||||||
if filter_query:
|
]
|
||||||
track_search(filter_query, "tab download")
|
hidden_columns = [c["colId"] for c in (column_state or []) if c.get("hide")]
|
||||||
lff = filter_table_data(lff, filter_query)
|
if filter_model:
|
||||||
|
track_search(json.dumps(filter_model), "tab download")
|
||||||
if sort_by and len(sort_by) > 0:
|
df = export_dataframe(filter_model, sort_model, hidden_columns)
|
||||||
lff = sort_table_data(lff, sort_by)
|
|
||||||
|
|
||||||
def to_bytes(buffer):
|
def to_bytes(buffer):
|
||||||
write_styled_excel(lff.collect(engine="streaming"), buffer)
|
write_styled_excel(df, buffer)
|
||||||
|
|
||||||
date = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
date = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||||
return dcc.send_bytes(to_bytes, filename=f"decp_{date}.xlsx")
|
return dcc.send_bytes(to_bytes, filename=f"decp_{date}.xlsx")
|
||||||
|
|||||||
@@ -37,6 +37,23 @@ def fetch_grid_page(
|
|||||||
return page.to_dicts(), total
|
return page.to_dicts(), total
|
||||||
|
|
||||||
|
|
||||||
|
def export_dataframe(filter_model, sort_model, hidden_columns) -> pl.DataFrame:
|
||||||
|
"""Renvoie les lignes filtrées/triées pour l'export Excel.
|
||||||
|
|
||||||
|
Colonnes masquées exclues, valeurs brutes (non post-traitées HTML).
|
||||||
|
"""
|
||||||
|
ast = filtermodel_to_ast(filter_model, schema)
|
||||||
|
filter_sql, params = ast_to_sql(ast, schema)
|
||||||
|
order_by = sort_model_to_sql(sort_model, schema) or None
|
||||||
|
visible = [c for c in schema.names() if c not in set(hidden_columns or [])]
|
||||||
|
return query_marches(
|
||||||
|
where_sql=filter_sql,
|
||||||
|
params=params,
|
||||||
|
columns=visible,
|
||||||
|
order_by=order_by,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_LINK_COLUMNS = {
|
_LINK_COLUMNS = {
|
||||||
"marche",
|
"marche",
|
||||||
"uid",
|
"uid",
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,4 @@
|
|||||||
from src.utils.grid import fetch_grid_page, grid_column_defs
|
from src.utils.grid import export_dataframe, fetch_grid_page, grid_column_defs
|
||||||
|
|
||||||
|
|
||||||
def test_column_defs_have_field_and_filter():
|
def test_column_defs_have_field_and_filter():
|
||||||
@@ -40,3 +40,14 @@ def test_fetch_grid_page_filter_reduces_count():
|
|||||||
def test_fetch_grid_page_offset_slicing():
|
def test_fetch_grid_page_offset_slicing():
|
||||||
rows, _ = fetch_grid_page(None, None, 0, 5)
|
rows, _ = fetch_grid_page(None, None, 0, 5)
|
||||||
assert len(rows) <= 5
|
assert len(rows) <= 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_dataframe_excludes_hidden_columns():
|
||||||
|
df = export_dataframe(None, None, hidden_columns=["objet"])
|
||||||
|
assert "objet" not in df.columns
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_dataframe_applies_filter():
|
||||||
|
fm = {"objet": {"filterType": "text", "type": "contains", "filter": "zzzzzznope"}}
|
||||||
|
df = export_dataframe(fm, None, hidden_columns=[])
|
||||||
|
assert df.height == 0
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ def test_003_tableau_download(dash_duo: DashComposite):
|
|||||||
print(app.server.name)
|
print(app.server.name)
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
download_data(1, "", [], None),
|
download_data(1, None, None),
|
||||||
download_acheteur_data(1, "/acheteurs/123", "2025", "ACHETEUR 1"),
|
download_acheteur_data(1, "/acheteurs/123", "2025", "ACHETEUR 1"),
|
||||||
download_titulaire_data(1, "/titulaires/345", "2025", "TITULAIRE 1"),
|
download_titulaire_data(1, "/titulaires/345", "2025", "TITULAIRE 1"),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user