Merge tag 'v2.7.1' into dev
- Correction du partage de données filtrées entre dashboard et vue des données
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
#### 2.7.1 (23 mars 2026)
|
||||
|
||||
- Correction du partage de données filtrées entre dashboard et vue des données
|
||||
|
||||
#### 2.7.0 (23 mars 2026)
|
||||
|
||||
- Remplacement de la page Statistiques par l'observatoire
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# decp.info
|
||||
|
||||
> v2.7.0
|
||||
> v2.7.1
|
||||
> Outil d'exploration et de téléchargement des données essentielles de la commande publique.
|
||||
|
||||
=> [decp.info](https://decp.info)
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "decp.info"
|
||||
description = "Interface d'exploration et d'analyse des marchés publics français."
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
requires-python = ">= 3.10"
|
||||
authors = [
|
||||
{ name = "Colin Maudry", email = "colin@colmo.tech" }
|
||||
|
||||
+32
-146
@@ -90,8 +90,6 @@ OBSERVATOIRE_COLUMNS = [
|
||||
]
|
||||
]
|
||||
|
||||
DF_FILTERED: pl.DataFrame = pl.DataFrame()
|
||||
|
||||
layout = [
|
||||
dcc.Location(id="dashboard_url", refresh="callback-nav"),
|
||||
dcc.Store(id="observatoire-filters", storage_type="local"),
|
||||
@@ -324,7 +322,7 @@ Alors, on fait comment ?
|
||||
dbc.Col("Sous-traitance :", lg=5),
|
||||
dbc.Col(
|
||||
dbc.RadioItems(
|
||||
id="dashboard_marche_sousTraitanceDeclaree",
|
||||
id="dashboard_marche_sous_traitance_declaree",
|
||||
options=[
|
||||
{
|
||||
"label": "Tous",
|
||||
@@ -380,7 +378,7 @@ Alors, on fait comment ?
|
||||
dbc.Row(
|
||||
dbc.Col(
|
||||
dcc.Dropdown(
|
||||
id="dashboard_marche_considerationsSociales",
|
||||
id="dashboard_marche_considerations_sociales",
|
||||
placeholder="Considérations sociales",
|
||||
options=get_enum_values_as_dict(
|
||||
"considerationsSociales"
|
||||
@@ -394,7 +392,7 @@ Alors, on fait comment ?
|
||||
dbc.Row(
|
||||
dbc.Col(
|
||||
dcc.Dropdown(
|
||||
id="dashboard_marche_considerationsEnvironnementales",
|
||||
id="dashboard_marche_considerations_environnementales",
|
||||
placeholder="Considérations environnementales",
|
||||
multi=True,
|
||||
options=get_enum_values_as_dict(
|
||||
@@ -544,30 +542,14 @@ FILTER_PARAMS = [
|
||||
("dashboard_montant_max", "montant_max", False, None),
|
||||
("dashboard_marche_techniques", "techniques", True, None),
|
||||
("dashboard_marche_innovant", "innovant", False, "all"),
|
||||
("dashboard_marche_sousTraitanceDeclaree", "sous_traitance", False, "all"),
|
||||
("dashboard_marche_considerationsSociales", "social", True, None),
|
||||
("dashboard_marche_considerationsEnvironnementales", "env", True, None),
|
||||
("dashboard_marche_sous_traitance_declaree", "sous_traitance", False, "all"),
|
||||
("dashboard_marche_considerations_sociales", "social", True, None),
|
||||
("dashboard_marche_considerations_environnementales", "env", True, None),
|
||||
]
|
||||
|
||||
|
||||
@callback(
|
||||
Output("dashboard_year", "value"),
|
||||
Output("dashboard_acheteur_id", "value"),
|
||||
Output("dashboard_acheteur_categorie", "value"),
|
||||
Output("dashboard_acheteur_departement_code", "value"),
|
||||
Output("dashboard_titulaire_id", "value"),
|
||||
Output("dashboard_titulaire_categorie", "value"),
|
||||
Output("dashboard_titulaire_departement_code", "value"),
|
||||
Output("dashboard_marche_type", "value"),
|
||||
Output("dashboard_marche_objet", "value"),
|
||||
Output("dashboard_marche_code_cpv", "value"),
|
||||
Output("dashboard_montant_min", "value"),
|
||||
Output("dashboard_montant_max", "value"),
|
||||
Output("dashboard_marche_techniques", "value"),
|
||||
Output("dashboard_marche_innovant", "value"),
|
||||
Output("dashboard_marche_sousTraitanceDeclaree", "value"),
|
||||
Output("dashboard_marche_considerationsSociales", "value"),
|
||||
Output("dashboard_marche_considerationsEnvironnementales", "value"),
|
||||
*[Output(fp[0], "value") for fp in FILTER_PARAMS],
|
||||
Input("dashboard_url", "search"),
|
||||
Input("dashboard_url", "pathname"),
|
||||
State("observatoire-filters", "data"),
|
||||
@@ -670,73 +652,26 @@ def show_confirmation(n_clicks):
|
||||
|
||||
@callback(
|
||||
Output("cards", "children"),
|
||||
Input("dashboard_year", "value"),
|
||||
Input("dashboard_acheteur_id", "value"),
|
||||
Input("dashboard_acheteur_categorie", "value"),
|
||||
Input("dashboard_acheteur_departement_code", "value"),
|
||||
Input("dashboard_titulaire_id", "value"),
|
||||
Input("dashboard_titulaire_categorie", "value"),
|
||||
Input("dashboard_titulaire_departement_code", "value"),
|
||||
Input("dashboard_marche_type", "value"),
|
||||
Input("dashboard_marche_objet", "value"),
|
||||
Input("dashboard_marche_code_cpv", "value"),
|
||||
Input("dashboard_montant_min", "value"),
|
||||
Input("dashboard_montant_max", "value"),
|
||||
Input("dashboard_marche_techniques", "value"),
|
||||
Input("dashboard_marche_innovant", "value"),
|
||||
Input("dashboard_marche_sousTraitanceDeclaree", "value"),
|
||||
Input("dashboard_marche_considerationsSociales", "value"),
|
||||
Input("dashboard_marche_considerationsEnvironnementales", "value"),
|
||||
Output("observatoire-filters", "data"),
|
||||
*[Input(fp[0], "value") for fp in FILTER_PARAMS],
|
||||
)
|
||||
def udpate_dashboard_cards(
|
||||
dashboard_year,
|
||||
dashboard_acheteur_id,
|
||||
dashboard_acheteur_categorie,
|
||||
dashboard_acheteur_departement_code,
|
||||
dashboard_titulaire_id,
|
||||
dashboard_titulaire_categorie,
|
||||
dashboard_titulaire_departement_code,
|
||||
dashboard_marche_type,
|
||||
dashboard_marche_objet,
|
||||
dashboard_marche_code_cpv,
|
||||
dashboard_montant_min,
|
||||
dashboard_montant_max,
|
||||
dashboard_marche_techniques,
|
||||
dashboard_marche_innovant,
|
||||
dashboard_marche_sous_traitance_declaree,
|
||||
dashboard_marche_considerations_sociales,
|
||||
dashboard_marche_considerations_environnementales,
|
||||
):
|
||||
def udpate_dashboard_cards(*filter_values):
|
||||
lff: pl.LazyFrame = df.lazy()
|
||||
|
||||
# Filtrage des données
|
||||
lff = prepare_dashboard_data(
|
||||
lff=lff,
|
||||
year=dashboard_year,
|
||||
acheteur_id=dashboard_acheteur_id,
|
||||
acheteur_categorie=dashboard_acheteur_categorie,
|
||||
acheteur_departement_code=dashboard_acheteur_departement_code,
|
||||
titulaire_id=dashboard_titulaire_id,
|
||||
titulaire_categorie=dashboard_titulaire_categorie,
|
||||
titulaire_departement_code=dashboard_titulaire_departement_code,
|
||||
type=dashboard_marche_type,
|
||||
objet=dashboard_marche_objet,
|
||||
code_cpv=dashboard_marche_code_cpv,
|
||||
considerations_sociales=dashboard_marche_considerations_sociales,
|
||||
considerations_environnementales=dashboard_marche_considerations_environnementales,
|
||||
montant_min=dashboard_montant_min,
|
||||
montant_max=dashboard_montant_max,
|
||||
techniques=dashboard_marche_techniques,
|
||||
marche_innovant=dashboard_marche_innovant,
|
||||
sous_traitance_declaree=dashboard_marche_sous_traitance_declaree,
|
||||
)
|
||||
filter_params = {}
|
||||
for (input_id, url_key, is_multi, default), value in zip(
|
||||
FILTER_PARAMS, filter_values
|
||||
):
|
||||
filter_params[input_id] = value
|
||||
|
||||
print(filter_params)
|
||||
|
||||
lff = prepare_dashboard_data(lff=lff, **filter_params)
|
||||
|
||||
# Génération des métriques
|
||||
dff = lff.collect(engine="streaming")
|
||||
|
||||
global DF_FILTERED
|
||||
DF_FILTERED = dff
|
||||
|
||||
logger.debug("Filter data: " + str(dff.height))
|
||||
|
||||
df_per_uid = (
|
||||
@@ -832,73 +767,18 @@ def udpate_dashboard_cards(
|
||||
)
|
||||
)
|
||||
|
||||
return dbc.Row(children=cards + geographic_maps + other_cards)
|
||||
return dbc.Row(children=cards + geographic_maps + other_cards), filter_params
|
||||
|
||||
|
||||
@callback(
|
||||
Output("download-observatoire", "data"),
|
||||
Input("btn-download-observatoire", "n_clicks"),
|
||||
State("dashboard_year", "value"),
|
||||
State("dashboard_acheteur_id", "value"),
|
||||
State("dashboard_acheteur_categorie", "value"),
|
||||
State("dashboard_acheteur_departement_code", "value"),
|
||||
State("dashboard_titulaire_id", "value"),
|
||||
State("dashboard_titulaire_categorie", "value"),
|
||||
State("dashboard_titulaire_departement_code", "value"),
|
||||
State("dashboard_marche_type", "value"),
|
||||
State("dashboard_marche_objet", "value"),
|
||||
State("dashboard_marche_code_cpv", "value"),
|
||||
State("dashboard_montant_min", "value"),
|
||||
State("dashboard_montant_max", "value"),
|
||||
State("dashboard_marche_techniques", "value"),
|
||||
State("dashboard_marche_innovant", "value"),
|
||||
State("dashboard_marche_sousTraitanceDeclaree", "value"),
|
||||
State("dashboard_marche_considerationsSociales", "value"),
|
||||
State("dashboard_marche_considerationsEnvironnementales", "value"),
|
||||
State("observatoire-filters", "data"),
|
||||
State("observatoire-hidden-columns", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def download_observatoire(
|
||||
_n_clicks,
|
||||
dashboard_year,
|
||||
dashboard_acheteur_id,
|
||||
dashboard_acheteur_categorie,
|
||||
dashboard_acheteur_departement_code,
|
||||
dashboard_titulaire_id,
|
||||
dashboard_titulaire_categorie,
|
||||
dashboard_titulaire_departement_code,
|
||||
dashboard_marche_type,
|
||||
dashboard_marche_objet,
|
||||
dashboard_marche_code_cpv,
|
||||
dashboard_montant_min,
|
||||
dashboard_montant_max,
|
||||
dashboard_marche_techniques,
|
||||
dashboard_marche_innovant,
|
||||
dashboard_marche_sous_traitance_declaree,
|
||||
dashboard_considerations_sociales,
|
||||
dashboard_considerations_environnementales,
|
||||
hidden_columns,
|
||||
):
|
||||
lff = prepare_dashboard_data(
|
||||
lff=df.lazy(),
|
||||
year=dashboard_year,
|
||||
acheteur_id=dashboard_acheteur_id,
|
||||
acheteur_categorie=dashboard_acheteur_categorie,
|
||||
acheteur_departement_code=dashboard_acheteur_departement_code,
|
||||
titulaire_id=dashboard_titulaire_id,
|
||||
titulaire_categorie=dashboard_titulaire_categorie,
|
||||
titulaire_departement_code=dashboard_titulaire_departement_code,
|
||||
type=dashboard_marche_type,
|
||||
objet=dashboard_marche_objet,
|
||||
code_cpv=dashboard_marche_code_cpv,
|
||||
considerations_sociales=dashboard_considerations_sociales,
|
||||
considerations_environnementales=dashboard_considerations_environnementales,
|
||||
montant_min=dashboard_montant_min,
|
||||
montant_max=dashboard_montant_max,
|
||||
techniques=dashboard_marche_techniques,
|
||||
marche_innovant=dashboard_marche_innovant,
|
||||
sous_traitance_declaree=dashboard_marche_sous_traitance_declaree,
|
||||
)
|
||||
def download_observatoire(_n_clicks, filter_params, hidden_columns):
|
||||
lff = prepare_dashboard_data(lff=df.lazy(), **(filter_params or {}))
|
||||
|
||||
if hidden_columns:
|
||||
lff = lff.drop(hidden_columns)
|
||||
@@ -974,16 +854,22 @@ def toggle_observatoire_preview(n_clicks, is_open):
|
||||
Input("observatoire-preview-table", "page_size"),
|
||||
Input("observatoire-preview-table", "sort_by"),
|
||||
State("observatoire-preview-table", "data_timestamp"),
|
||||
State("observatoire-filters", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def populate_preview_table(
|
||||
is_open, filter_query, page_current, page_size, sort_by, data_timestamp
|
||||
is_open,
|
||||
filter_query,
|
||||
page_current,
|
||||
page_size,
|
||||
sort_by,
|
||||
data_timestamp,
|
||||
filter_params,
|
||||
):
|
||||
if not is_open:
|
||||
return (no_update,) * 9
|
||||
|
||||
global DF_FILTERED
|
||||
lff = DF_FILTERED.lazy()
|
||||
lff = prepare_dashboard_data(lff=df.lazy(), **(filter_params or {}))
|
||||
|
||||
return prepare_table_data(
|
||||
lff,
|
||||
|
||||
+51
-51
@@ -699,98 +699,98 @@ def prepare_table_data(
|
||||
|
||||
def prepare_dashboard_data(
|
||||
lff: pl.LazyFrame,
|
||||
year,
|
||||
acheteur_id,
|
||||
acheteur_categorie,
|
||||
acheteur_departement_code,
|
||||
titulaire_id,
|
||||
titulaire_categorie,
|
||||
titulaire_departement_code,
|
||||
type,
|
||||
objet,
|
||||
code_cpv,
|
||||
considerations_sociales,
|
||||
considerations_environnementales,
|
||||
techniques,
|
||||
marche_innovant,
|
||||
sous_traitance_declaree,
|
||||
montant_min=None,
|
||||
montant_max=None,
|
||||
dashboard_year,
|
||||
dashboard_acheteur_id,
|
||||
dashboard_acheteur_categorie,
|
||||
dashboard_acheteur_departement_code,
|
||||
dashboard_titulaire_id,
|
||||
dashboard_titulaire_categorie,
|
||||
dashboard_titulaire_departement_code,
|
||||
dashboard_marche_type,
|
||||
dashboard_marche_objet,
|
||||
dashboard_marche_code_cpv,
|
||||
dashboard_marche_considerations_sociales,
|
||||
dashboard_marche_considerations_environnementales,
|
||||
dashboard_marche_techniques,
|
||||
dashboard_marche_innovant,
|
||||
dashboard_marche_sous_traitance_declaree,
|
||||
dashboard_montant_min=None,
|
||||
dashboard_montant_max=None,
|
||||
) -> pl.LazyFrame:
|
||||
if year:
|
||||
lff = lff.filter(pl.col("dateNotification").dt.year() == int(year))
|
||||
if dashboard_year:
|
||||
lff = lff.filter(pl.col("dateNotification").dt.year() == int(dashboard_year))
|
||||
else:
|
||||
lff = lff.filter(
|
||||
pl.col("dateNotification") > (datetime.now() - timedelta(days=365))
|
||||
)
|
||||
|
||||
if acheteur_id:
|
||||
lff = lff.filter(pl.col("acheteur_id").str.contains(acheteur_id))
|
||||
if dashboard_acheteur_id:
|
||||
lff = lff.filter(pl.col("acheteur_id").str.contains(dashboard_acheteur_id))
|
||||
else:
|
||||
if acheteur_categorie:
|
||||
lff = lff.filter(pl.col("acheteur_categorie") == acheteur_categorie)
|
||||
if acheteur_departement_code:
|
||||
if dashboard_acheteur_categorie:
|
||||
lff = lff.filter(pl.col("acheteur_categorie") == dashboard_acheteur_categorie)
|
||||
if dashboard_acheteur_departement_code:
|
||||
lff = lff.filter(
|
||||
pl.col("acheteur_departement_code").is_in(acheteur_departement_code)
|
||||
pl.col("acheteur_departement_code").is_in(dashboard_acheteur_departement_code)
|
||||
)
|
||||
|
||||
if titulaire_id:
|
||||
lff = lff.filter(pl.col("titulaire_id").str.contains(titulaire_id))
|
||||
if dashboard_titulaire_id:
|
||||
lff = lff.filter(pl.col("titulaire_id").str.contains(dashboard_titulaire_id))
|
||||
else:
|
||||
if titulaire_categorie:
|
||||
lff = lff.filter(pl.col("titulaire_categorie") == titulaire_categorie)
|
||||
if titulaire_departement_code:
|
||||
if dashboard_titulaire_categorie:
|
||||
lff = lff.filter(pl.col("titulaire_categorie") == dashboard_titulaire_categorie)
|
||||
if dashboard_titulaire_departement_code:
|
||||
lff = lff.filter(
|
||||
pl.col("titulaire_departement_code").is_in(titulaire_departement_code)
|
||||
pl.col("titulaire_departement_code").is_in(dashboard_titulaire_departement_code)
|
||||
)
|
||||
|
||||
if type:
|
||||
lff = lff.filter(pl.col("type") == type)
|
||||
if dashboard_marche_type:
|
||||
lff = lff.filter(pl.col("type") == dashboard_marche_type)
|
||||
|
||||
if objet:
|
||||
lff = lff.filter(pl.col("objet").str.contains(f"(?i){objet}"))
|
||||
if dashboard_marche_objet:
|
||||
lff = lff.filter(pl.col("objet").str.contains(f"(?i){dashboard_marche_objet}"))
|
||||
|
||||
if code_cpv:
|
||||
lff = lff.filter(pl.col("codeCPV").str.starts_with(code_cpv))
|
||||
if dashboard_marche_code_cpv:
|
||||
lff = lff.filter(pl.col("codeCPV").str.starts_with(dashboard_marche_code_cpv))
|
||||
|
||||
if marche_innovant and marche_innovant != "all":
|
||||
lff = lff.filter(pl.col("marcheInnovant") == marche_innovant)
|
||||
if dashboard_marche_innovant and dashboard_marche_innovant != "all":
|
||||
lff = lff.filter(pl.col("marcheInnovant") == dashboard_marche_innovant)
|
||||
|
||||
if sous_traitance_declaree and sous_traitance_declaree != "all":
|
||||
lff = lff.filter(pl.col("sousTraitanceDeclaree") == sous_traitance_declaree)
|
||||
if dashboard_marche_sous_traitance_declaree and dashboard_marche_sous_traitance_declaree != "all":
|
||||
lff = lff.filter(pl.col("sousTraitanceDeclaree") == dashboard_marche_sous_traitance_declaree)
|
||||
|
||||
if techniques:
|
||||
if dashboard_marche_techniques:
|
||||
lff = lff.filter(
|
||||
pl.col("techniques")
|
||||
.str.split(", ")
|
||||
.list.set_intersection(techniques)
|
||||
.list.set_intersection(dashboard_marche_techniques)
|
||||
.list.len()
|
||||
> 0
|
||||
)
|
||||
|
||||
if considerations_sociales:
|
||||
if dashboard_marche_considerations_sociales:
|
||||
lff = lff.filter(
|
||||
pl.col("considerationsSociales")
|
||||
.str.split(", ")
|
||||
.list.set_intersection(considerations_sociales)
|
||||
.list.set_intersection(dashboard_marche_considerations_sociales)
|
||||
.list.len()
|
||||
> 0
|
||||
)
|
||||
|
||||
if considerations_environnementales:
|
||||
if dashboard_marche_considerations_environnementales:
|
||||
lff = lff.filter(
|
||||
pl.col("considerationsEnvironnementales")
|
||||
.str.split(", ")
|
||||
.list.set_intersection(considerations_environnementales)
|
||||
.list.set_intersection(dashboard_marche_considerations_environnementales)
|
||||
.list.len()
|
||||
> 0
|
||||
)
|
||||
|
||||
if montant_min is not None:
|
||||
lff = lff.filter(pl.col("montant") >= montant_min)
|
||||
if dashboard_montant_min is not None:
|
||||
lff = lff.filter(pl.col("montant") >= dashboard_montant_min)
|
||||
|
||||
if montant_max is not None:
|
||||
lff = lff.filter(pl.col("montant") <= montant_max)
|
||||
if dashboard_montant_max is not None:
|
||||
lff = lff.filter(pl.col("montant") <= dashboard_montant_max)
|
||||
|
||||
return lff
|
||||
|
||||
|
||||
+17
-17
@@ -234,23 +234,23 @@ def test_010_observatoire_montant_filter():
|
||||
def apply(min_val=None, max_val=None):
|
||||
return prepare_dashboard_data(
|
||||
data.lazy(),
|
||||
year="2025",
|
||||
acheteur_id=None,
|
||||
acheteur_categorie=None,
|
||||
acheteur_departement_code=None,
|
||||
titulaire_id=None,
|
||||
titulaire_categorie=None,
|
||||
titulaire_departement_code=None,
|
||||
type=None,
|
||||
objet=None,
|
||||
code_cpv=None,
|
||||
considerations_sociales=None,
|
||||
considerations_environnementales=None,
|
||||
techniques=None,
|
||||
marche_innovant=None,
|
||||
sous_traitance_declaree=None,
|
||||
montant_min=min_val,
|
||||
montant_max=max_val,
|
||||
dashboard_year="2025",
|
||||
dashboard_acheteur_id=None,
|
||||
dashboard_acheteur_categorie=None,
|
||||
dashboard_acheteur_departement_code=None,
|
||||
dashboard_titulaire_id=None,
|
||||
dashboard_titulaire_categorie=None,
|
||||
dashboard_titulaire_departement_code=None,
|
||||
dashboard_marche_type=None,
|
||||
dashboard_marche_objet=None,
|
||||
dashboard_marche_code_cpv=None,
|
||||
dashboard_marche_considerations_sociales=None,
|
||||
dashboard_marche_considerations_environnementales=None,
|
||||
dashboard_marche_techniques=None,
|
||||
dashboard_marche_innovant=None,
|
||||
dashboard_marche_sous_traitance_declaree=None,
|
||||
dashboard_montant_min=min_val,
|
||||
dashboard_montant_max=max_val,
|
||||
).collect()
|
||||
|
||||
assert apply().height == 3
|
||||
|
||||
Reference in New Issue
Block a user