From 78d528f75bf7b3f7f46a08f12d36d513960d54e6 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 18 Mar 2026 21:13:21 +0100 Subject: [PATCH] Filtre par montant #65 --- src/assets/css/style.css | 13 ++++++++- src/pages/observatoire.py | 60 ++++++++++++++++++++++++++++++++++++++- tests/test_main.py | 41 ++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 12dd156..f93c94a 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -201,7 +201,8 @@ p.version > a { margin-bottom: 6px; } -#filters input[type="text"] { +#filters input[type="text"], +#filters input[type="number"] { border: 1px #ccc solid; border-radius: 3px; padding-left: 8px; @@ -578,3 +579,13 @@ summary > h4 { display: none; } } + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} diff --git a/src/pages/observatoire.py b/src/pages/observatoire.py index 6ca7d10..f2bbe65 100644 --- a/src/pages/observatoire.py +++ b/src/pages/observatoire.py @@ -67,6 +67,8 @@ def _apply_filters( marche_type, considerations_sociales, considerations_environnementales, + montant_min=None, + montant_max=None, ) -> pl.LazyFrame: if year: lff = lff.filter(pl.col("dateNotification").dt.year() == int(year)) @@ -116,6 +118,12 @@ def _apply_filters( > 0 ) + if montant_min is not None: + lff = lff.filter(pl.col("montant") >= montant_min) + + if montant_max is not None: + lff = lff.filter(pl.col("montant") <= montant_max) + return lff @@ -271,6 +279,32 @@ Alors, on fait comment ? ), ), ), + dbc.Row( + [ + dbc.Col( + dcc.Input( + id="dashboard_montant_min", + placeholder="Montant min.", + type="number", + min=0, + debounce=True, + style={"width": "100%"}, + ), + width=6, + ), + dbc.Col( + dcc.Input( + id="dashboard_montant_max", + placeholder="Montant max.", + type="number", + min=0, + debounce=True, + style={"width": "100%"}, + ), + width=6, + ), + ] + ), dcc.Download(id="download-observatoire"), dbc.Button( "Télécharger au format Excel", @@ -313,6 +347,8 @@ Alors, on fait comment ? Output("dashboard_marche_type", "value"), Output("dashboard_marche_considerationsSociales", "value"), Output("dashboard_marche_considerationsEnvironnementales", "value"), + Output("dashboard_montant_min", "value"), + Output("dashboard_montant_max", "value"), Input("dashboard_url", "search"), Input("dashboard_url", "pathname"), State("observatoire-filters", "data"), @@ -334,6 +370,8 @@ def restore_filters(search, _pathname, stored_filters): None, None, None, + None, + None, ) if stored_filters: @@ -348,9 +386,11 @@ def restore_filters(search, _pathname, stored_filters): stored_filters.get("marche_type"), stored_filters.get("considerations_sociales"), stored_filters.get("considerations_environnementales"), + stored_filters.get("montant_min"), + stored_filters.get("montant_max"), ) - return (no_update,) * 10 + return (no_update,) * 12 @callback( @@ -365,6 +405,8 @@ def restore_filters(search, _pathname, stored_filters): Input("dashboard_marche_type", "value"), Input("dashboard_marche_considerationsSociales", "value"), Input("dashboard_marche_considerationsEnvironnementales", "value"), + Input("dashboard_montant_min", "value"), + Input("dashboard_montant_max", "value"), prevent_initial_call=True, ) def save_filters_to_storage( @@ -378,6 +420,8 @@ def save_filters_to_storage( marche_type, considerations_sociales, considerations_environnementales, + montant_min, + montant_max, ): return { "year": year, @@ -390,6 +434,8 @@ def save_filters_to_storage( "marche_type": marche_type, "considerations_sociales": considerations_sociales, "considerations_environnementales": considerations_environnementales, + "montant_min": montant_min, + "montant_max": montant_max, } @@ -453,6 +499,8 @@ def sync_observatoire_share_url(acheteur_id, titulaire_id, href): Input("dashboard_marche_type", "value"), Input("dashboard_marche_considerationsSociales", "value"), Input("dashboard_marche_considerationsEnvironnementales", "value"), + Input("dashboard_montant_min", "value"), + Input("dashboard_montant_max", "value"), ) def udpate_dashboard_cards( dashboard_year, @@ -465,6 +513,8 @@ def udpate_dashboard_cards( dashboard_marche_type, dashboard_marche_considerations_sociales, dashboard_marche_considerations_environnementales, + dashboard_montant_min, + dashboard_montant_max, ): lff: pl.LazyFrame = df.lazy() lff = lff.select( @@ -490,6 +540,8 @@ def udpate_dashboard_cards( dashboard_marche_type, dashboard_marche_considerations_sociales, dashboard_marche_considerations_environnementales, + montant_min=dashboard_montant_min, + montant_max=dashboard_montant_max, ) # Génération des métriques @@ -618,6 +670,8 @@ def udpate_dashboard_cards( State("dashboard_marche_type", "value"), State("dashboard_marche_considerationsSociales", "value"), State("dashboard_marche_considerationsEnvironnementales", "value"), + State("dashboard_montant_min", "value"), + State("dashboard_montant_max", "value"), prevent_initial_call=True, ) def download_observatoire( @@ -632,6 +686,8 @@ def download_observatoire( marche_type, considerations_sociales, considerations_environnementales, + montant_min, + montant_max, ): lff = _apply_filters( df.lazy(), @@ -645,6 +701,8 @@ def download_observatoire( marche_type, considerations_sociales, considerations_environnementales, + montant_min=montant_min, + montant_max=montant_max, ) def to_bytes(buffer): diff --git a/tests/test_main.py b/tests/test_main.py index f9d36b7..6a13762 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -216,6 +216,47 @@ def test_008_search_to_observatoire(dash_duo: DashComposite): ) +def test_010_observatoire_montant_filter(): + import datetime + + import polars as pl + + from pages.observatoire import _apply_filters + from src.app import ( + app, # noqa: F401 – instantiates the Dash app before register_page() calls + ) + + data = pl.DataFrame( + { + "uid": ["1", "2", "3"], + "montant": [100.0, 500.0, 1000.0], + "dateNotification": [datetime.date(2025, 1, 1)] * 3, + } + ) + + def apply(min_val=None, max_val=None): + return _apply_filters( + data.lazy(), + year="2025", + acheteur_id=None, + acheteur_categorie=None, + acheteur_departement_code=None, + titulaire_id=None, + titulaire_categorie=None, + titulaire_departement_code=None, + marche_type=None, + considerations_sociales=None, + considerations_environnementales=None, + montant_min=min_val, + montant_max=max_val, + ).collect() + + assert apply().height == 3 + assert apply(min_val=400).height == 2 # 500, 1000 + assert apply(max_val=500).height == 2 # 100, 500 + assert apply(min_val=200, max_val=600).height == 1 # 500 only + + def test_009_observatoire_filter_persistence(dash_duo: DashComposite): import time