filtres, tris fonctionnels (acheteur)

This commit is contained in:
Colin Maudry
2025-11-24 13:43:42 +01:00
parent 565a0cfd08
commit 8c98a60c75
4 changed files with 180 additions and 121 deletions
+7 -1
View File
@@ -133,6 +133,12 @@ td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-c
.marches_table { .marches_table {
font-family: "Open Sans", sans-serif; font-family: "Open Sans", sans-serif;
} }
.marches_table.stuck {
position: relative;
right: 200px;
}
.marches_table .cell-table tr:nth-child(even) td { .marches_table .cell-table tr:nth-child(even) td {
background-color: #feeeee; background-color: #feeeee;
font-family: "Open Sans", sans-serif; font-family: "Open Sans", sans-serif;
@@ -212,7 +218,7 @@ summary > h3 {
} }
#_pages_content { #_pages_content {
padding-top: 28px; padding: 28px 24px 0 24px;
} }
/* Vue acheteur/titulaire/recherche */ /* Vue acheteur/titulaire/recherche */
+47 -42
View File
@@ -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.callbacks import get_top_org_table
from src.figures import DataTable, point_on_map from src.figures import DataTable, point_on_map
from src.utils import ( from src.utils import (
add_links_in_dict,
df, df,
filter_table_data,
format_number, format_number,
format_values,
get_annuaire_data, get_annuaire_data,
get_default_hidden_columns,
get_departement_region, get_departement_region,
meta_content, meta_content,
setup_table_columns, prepare_table_data,
) )
register_page( register_page(
@@ -31,9 +29,11 @@ datatable = html.Div(
className="marches_table", className="marches_table",
children=DataTable( children=DataTable(
dtid="acheteur_datatable", dtid="acheteur_datatable",
page_action="native", page_action="custom",
filter_action="custom", filter_action="custom",
sort_action="custom",
page_size=10, 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.Store(id="acheteur_data", storage_type="memory"),
dcc.Location(id="url", refresh="callback-nav"), dcc.Location(id="url", refresh="callback-nav"),
html.Div( html.Div(
className="container",
children=[ children=[
html.Div( html.Div(
className="wrapper", className="wrapper",
@@ -112,7 +111,29 @@ layout = [
), ),
# récupérer les données de l'acheteur sur l'api annuaire # récupérer les données de l'acheteur sur l'api annuaire
html.H3("Derniers marchés publics attribués"), 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] acheteur_siret = url.split("/")[-1]
lff = df.lazy() lff = df.lazy()
lff = lff.filter(pl.col("acheteur_id") == acheteur_siret) 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": if acheteur_year and acheteur_year != "Toutes":
acheteur_year = int(acheteur_year) acheteur_year = int(acheteur_year)
lff = lff.filter(pl.col("dateNotification").dt.year() == 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( @callback(
Output(component_id="acheteur_datatable", component_property="data"), Output("acheteur_datatable", "data"),
Output(component_id="acheteur_datatable", component_property="columns"), Output("acheteur_datatable", "columns"),
Output(component_id="acheteur_datatable", component_property="tooltip_header"), Output("acheteur_datatable", "tooltip_header"),
Input(component_id="acheteur_data", component_property="data"), Output("acheteur_datatable", "data_timestamp"),
Input(component_id="acheteur_datatable", component_property="filter_query"), 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]: def get_last_marches_data(
lff: pl.LazyFrame = pl.LazyFrame(data) data, page_current, page_size, filter_query, sort_by, data_timestamp
if filter_query: ) -> list[dict]:
lff = filter_table_data(lff, filter_query) return prepare_table_data(
data, data_timestamp, filter_query, page_current, page_size, sort_by
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"],
) )
data = dff.to_dicts()
data = add_links_in_dict(data, "titulaire")
return data, columns, tooltip_header
@callback( @callback(
Output(component_id="top10_titulaires", component_property="children"), Output(component_id="top10_titulaires", component_property="children"),
+4 -66
View File
@@ -6,22 +6,17 @@ from dash import Input, Output, State, callback, dcc, html, register_page
from src.figures import DataTable from src.figures import DataTable
from src.utils import ( from src.utils import (
add_links,
add_resource_link,
df, df,
filter_table_data, filter_table_data,
format_number,
format_values,
get_default_hidden_columns, get_default_hidden_columns,
meta_content, meta_content,
setup_table_columns,
sort_table_data, sort_table_data,
) )
from utils import prepare_table_data
update_date = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH")) update_date = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH"))
update_date = datetime.fromtimestamp(update_date).strftime("%d/%m/%Y") update_date = datetime.fromtimestamp(update_date).strftime("%d/%m/%Y")
schema = df.collect_schema()
name = "Tableau" name = "Tableau"
register_page( register_page(
@@ -42,7 +37,7 @@ datatable = html.Div(
page_action="custom", page_action="custom",
filter_action="custom", filter_action="custom",
sort_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"), State("table", "data_timestamp"),
) )
def update_table(page_current, page_size, filter_query, sort_by, data_timestamp): def update_table(page_current, page_size, filter_query, sort_by, data_timestamp):
if os.getenv("DEVELOPMENT").lower() == "true": return prepare_table_data(
print(" + + + + + + + + + + + + + + + + + + ") None, data_timestamp, filter_query, page_current, page_size, sort_by
# 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,
) )
+115 -5
View File
@@ -7,7 +7,6 @@ from time import localtime, sleep
import polars as pl import polars as pl
import polars.selectors as cs import polars.selectors as cs
from httpx import get, post from httpx import get, post
from polars import Schema
from polars.exceptions import ComputeError from polars.exceptions import ComputeError
from unidecode import unidecode from unidecode import unidecode
@@ -356,18 +355,47 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup
return columns, tooltip return columns, tooltip
def get_default_hidden_columns(schema: Schema): 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") displayed_columns = os.getenv("DISPLAYED_COLUMNS")
hidden_columns = [] if displayed_columns is None:
if displayed_columns: raise ValueError("DISPLAYED_COLUMNS n'est pas configuré")
else:
displayed_columns = displayed_columns.replace(" ", "").split(",") displayed_columns = displayed_columns.replace(" ", "").split(",")
hidden_columns = []
for col in schema.names(): for col in schema.names():
if col in displayed_columns: if col in displayed_columns:
continue continue
else: else:
hidden_columns.append(col) hidden_columns.append(col)
return hidden_columns return hidden_columns
raise ValueError("DISPLAYED_COLUMNS n'est pas configuré")
def get_data_schema() -> dict: def get_data_schema() -> dict:
@@ -503,9 +531,91 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame:
return dff 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() df: pl.DataFrame = get_decp_data()
schema = df.collect_schema()
df_acheteurs = get_org_data(df, "acheteur") df_acheteurs = get_org_data(df, "acheteur")
df_titulaires = get_org_data(df, "titulaire") df_titulaires = get_org_data(df, "titulaire")
departements = get_departements() departements = get_departements()
domain_name = ( domain_name = (
"test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info" "test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info"