From 8c98a60c75463d6e37f0728a0b7fec38474f67f2 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 24 Nov 2025 13:43:42 +0100 Subject: [PATCH] filtres, tris fonctionnels (acheteur) --- src/assets/style.css | 8 ++- src/pages/acheteur.py | 89 +++++++++++++++------------- src/pages/tableau.py | 70 ++-------------------- src/utils.py | 134 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 180 insertions(+), 121 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index c009a16..4f91b10 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -133,6 +133,12 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c .marches_table { font-family: "Open Sans", sans-serif; } + +.marches_table.stuck { + position: relative; + right: 200px; +} + .marches_table .cell-table tr:nth-child(even) td { background-color: #feeeee; font-family: "Open Sans", sans-serif; @@ -212,7 +218,7 @@ summary > h3 { } #_pages_content { - padding-top: 28px; + padding: 28px 24px 0 24px; } /* Vue acheteur/titulaire/recherche */ diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 520230d..36c3295 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -6,15 +6,13 @@ from dash import Input, Output, State, callback, dcc, html, register_page from src.callbacks import get_top_org_table from src.figures import DataTable, point_on_map from src.utils import ( - add_links_in_dict, df, - filter_table_data, format_number, - format_values, get_annuaire_data, + get_default_hidden_columns, get_departement_region, meta_content, - setup_table_columns, + prepare_table_data, ) register_page( @@ -31,9 +29,11 @@ datatable = html.Div( className="marches_table", children=DataTable( dtid="acheteur_datatable", - page_action="native", + page_action="custom", filter_action="custom", + sort_action="custom", page_size=10, + hidden_columns=get_default_hidden_columns(page="acheteur"), ), ) @@ -41,7 +41,6 @@ layout = [ dcc.Store(id="acheteur_data", storage_type="memory"), dcc.Location(id="url", refresh="callback-nav"), html.Div( - className="container", children=[ html.Div( className="wrapper", @@ -112,7 +111,29 @@ layout = [ ), # récupérer les données de l'acheteur sur l'api annuaire html.H3("Derniers marchés publics attribués"), - html.Div(id="acheteur_last_marches", children=datatable), + dcc.Loading( + overlay_style={"visibility": "visible", "filter": "blur(2px)"}, + id="loading-home", + type="default", + children=[ + html.Div( + [ + html.P("lignes", id="acheteur_nb_rows"), + html.Button( + "Téléchargement désactivé au-delà de 65 000 lignes", + id="btn-download-data-acheteur", + disabled=True, + ), + dcc.Download(id="acheteur-download-data"), + dcc.Store( + id="acheteur_filtered_data", storage_type="memory" + ), + ], + className="table-menu", + ), + datatable, + ], + ), ], ), ] @@ -194,19 +215,6 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> list[dict]: acheteur_siret = url.split("/")[-1] lff = df.lazy() lff = lff.filter(pl.col("acheteur_id") == acheteur_siret) - lff = lff.select( - "id", - "uid", - "objet", - "dateNotification", - "titulaire_id", - "titulaire_typeIdentifiant", - "titulaire_nom", - "distance", - "montant", - "codeCPV", - "dureeMois", - ) if acheteur_year and acheteur_year != "Toutes": acheteur_year = int(acheteur_year) lff = lff.filter(pl.col("dateNotification").dt.year() == acheteur_year) @@ -218,31 +226,28 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> list[dict]: @callback( - Output(component_id="acheteur_datatable", component_property="data"), - Output(component_id="acheteur_datatable", component_property="columns"), - Output(component_id="acheteur_datatable", component_property="tooltip_header"), - Input(component_id="acheteur_data", component_property="data"), - Input(component_id="acheteur_datatable", component_property="filter_query"), + Output("acheteur_datatable", "data"), + Output("acheteur_datatable", "columns"), + Output("acheteur_datatable", "tooltip_header"), + Output("acheteur_datatable", "data_timestamp"), + Output("acheteur_nb_rows", "children"), + Output("btn-download-data-acheteur", "disabled"), + Output("btn-download-data-acheteur", "children"), + Output("btn-download-data-acheteur", "title"), + Input("acheteur_data", "data"), + Input("acheteur_datatable", "page_current"), + Input("acheteur_datatable", "page_size"), + Input("acheteur_datatable", "filter_query"), + Input("acheteur_datatable", "sort_by"), + State("acheteur_datatable", "data_timestamp"), ) -def get_last_marches_data(data, filter_query) -> tuple[list[dict], list, list]: - lff: pl.LazyFrame = pl.LazyFrame(data) - if filter_query: - lff = filter_table_data(lff, filter_query) - - lff = lff.cast(pl.String) - dff: pl.DataFrame = format_values(lff.collect()) - columns, tooltip_header = setup_table_columns( - dff, - hideable=False, - exclude=["titulaire_id", "titulaire_typeIdentifiant", "uid"], +def get_last_marches_data( + data, page_current, page_size, filter_query, sort_by, data_timestamp +) -> list[dict]: + return prepare_table_data( + data, data_timestamp, filter_query, page_current, page_size, sort_by ) - data = dff.to_dicts() - - data = add_links_in_dict(data, "titulaire") - - return data, columns, tooltip_header - @callback( Output(component_id="top10_titulaires", component_property="children"), diff --git a/src/pages/tableau.py b/src/pages/tableau.py index ab3f712..ba938b5 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -6,22 +6,17 @@ from dash import Input, Output, State, callback, dcc, html, register_page from src.figures import DataTable from src.utils import ( - add_links, - add_resource_link, df, filter_table_data, - format_number, - format_values, get_default_hidden_columns, meta_content, - setup_table_columns, sort_table_data, ) +from utils import prepare_table_data update_date = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH")) update_date = datetime.fromtimestamp(update_date).strftime("%d/%m/%Y") -schema = df.collect_schema() name = "Tableau" register_page( @@ -42,7 +37,7 @@ datatable = html.Div( page_action="custom", filter_action="custom", sort_action="custom", - hidden_columns=get_default_hidden_columns(schema), + hidden_columns=get_default_hidden_columns(None), ), ) @@ -138,65 +133,8 @@ layout = [ State("table", "data_timestamp"), ) def update_table(page_current, page_size, filter_query, sort_by, data_timestamp): - if os.getenv("DEVELOPMENT").lower() == "true": - print(" + + + + + + + + + + + + + + + + + + ") - - # Application des filtres - lff: pl.LazyFrame = df.lazy() # start from the original data - if filter_query: - lff = filter_table_data(lff, filter_query) - - if len(sort_by) > 0: - lff = sort_table_data(lff, sort_by) - - # Matérialisation des filtres - dff: pl.DataFrame = lff.collect() - - height = dff.height - nb_rows = f"{format_number(height)} lignes ({format_number(dff.select('uid').unique().height)} marchés)" - - # Pagination des données - start_row = page_current * page_size - # end_row = (page_current + 1) * page_size - dff = dff.slice(start_row, page_size) - - # Tout devient string - dff = dff.cast(pl.String) - - # Remplace les strings null par "", mais pas les numeric null - dff = dff.fill_null("") - - # Ajout des liens vers l'annuaire des entreprises - dff = add_links(dff) - - # Ajout des liens vers les fichiers Open Data - dff = add_resource_link(dff) - - # Formatage des montants - dff = format_values(dff) - - columns, tooltip = setup_table_columns(dff) - - dicts = dff.to_dicts() - - if height > 65000: - download_disabled = True - download_text = "Téléchargement désactivé au-delà de 65 000 lignes" - download_title = "Excel ne supporte pas d'avoir plus de 65 000 URLs dans une même feuille de calcul. Contactez-moi pour me présenter votre besoin en téléchargement afin que je puisse adapter la solution." - else: - download_disabled = False - download_text = "Télécharger au format Excel" - download_title = "" - - return ( - dicts, - columns, - tooltip, - data_timestamp + 1, - nb_rows, - download_disabled, - download_text, - download_title, + return prepare_table_data( + None, data_timestamp, filter_query, page_current, page_size, sort_by ) diff --git a/src/utils.py b/src/utils.py index 084938c..d520f74 100644 --- a/src/utils.py +++ b/src/utils.py @@ -7,7 +7,6 @@ from time import localtime, sleep import polars as pl import polars.selectors as cs from httpx import get, post -from polars import Schema from polars.exceptions import ComputeError from unidecode import unidecode @@ -356,18 +355,47 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup return columns, tooltip -def get_default_hidden_columns(schema: Schema): - displayed_columns = os.getenv("DISPLAYED_COLUMNS") +def get_default_hidden_columns(page): + if page == "acheteur": + displayed_columns = [ + "uid", + "objet", + "dateNotification", + "titulaire_id", + "titulaire_typeIdentifiant", + "titulaire_nom", + "distance", + "montant", + "codeCPV", + "dureeRestanteMois", + ] + elif page == "titulaire": + displayed_columns = [ + "uid", + "objet", + "dateNotification", + "acheteur_id", + "acheteur_nom", + "distance", + "montant", + "codeCPV", + "dureeRestanteMois", + ] + else: + displayed_columns = os.getenv("DISPLAYED_COLUMNS") + if displayed_columns is None: + raise ValueError("DISPLAYED_COLUMNS n'est pas configuré") + else: + displayed_columns = displayed_columns.replace(" ", "").split(",") + hidden_columns = [] - if displayed_columns: - displayed_columns = displayed_columns.replace(" ", "").split(",") - for col in schema.names(): - if col in displayed_columns: - continue - else: - hidden_columns.append(col) - return hidden_columns - raise ValueError("DISPLAYED_COLUMNS n'est pas configuré") + + for col in schema.names(): + if col in displayed_columns: + continue + else: + hidden_columns.append(col) + return hidden_columns def get_data_schema() -> dict: @@ -503,9 +531,91 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame: return dff +def prepare_table_data( + data, data_timestamp, filter_query, page_current, page_size, sort_by +): + """ + Fonction de préparation des données pour les datatables, afin de permettre une gestion fine des logiques, + notamment pour les filtres et les tris. + :param data + :param data_timestamp: + :param filter_query: + :param page_current: + :param page_size: + :param sort_by: + :return: + """ + + if os.getenv("DEVELOPMENT").lower() == "true": + print(" + + + + + + + + + + + + + + + + + + ") + + # Récupération des données + if data: + lff: pl.LazyFrame = pl.LazyFrame(data) + else: + lff: pl.LazyFrame = df.lazy() # start from the original data + + # Application des filtres + if filter_query: + lff = filter_table_data(lff, filter_query) + + # Application des tris + if len(sort_by) > 0: + lff = sort_table_data(lff, sort_by) + + # Matérialisation des filtres + dff: pl.DataFrame = lff.collect() + height = dff.height + nb_rows = f"{format_number(height)} lignes ({format_number(dff.select('uid').unique().height)} marchés)" + + # Pagination des données + start_row = page_current * page_size + # end_row = (page_current + 1) * page_size + dff = dff.slice(start_row, page_size) + + # Tout devient string + dff = dff.cast(pl.String) + + # Remplace les strings null par "", mais pas les numeric null + dff = dff.fill_null("") + + # Ajout des liens vers l'annuaire des entreprises + dff = add_links(dff) + + # Ajout des liens vers les fichiers Open Data + if "sourceFile" in dff.columns: + dff = add_resource_link(dff) + + # Formatage des montants + dff = format_values(dff) + columns, tooltip = setup_table_columns(dff) + dicts = dff.to_dicts() + if height > 65000: + download_disabled = True + download_text = "Téléchargement désactivé au-delà de 65 000 lignes" + download_title = "Excel ne supporte pas d'avoir plus de 65 000 URLs dans une même feuille de calcul. Contactez-moi pour me présenter votre besoin en téléchargement afin que je puisse adapter la solution." + else: + download_disabled = False + download_text = "Télécharger au format Excel" + download_title = "" + return ( + dicts, + columns, + tooltip, + data_timestamp + 1, + nb_rows, + download_disabled, + download_text, + download_title, + ) + + df: pl.DataFrame = get_decp_data() +schema = df.collect_schema() + df_acheteurs = get_org_data(df, "acheteur") df_titulaires = get_org_data(df, "titulaire") + departements = get_departements() domain_name = ( "test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info"