Merge branch 'feature/mode_demploi_modal' into dev

This commit is contained in:
Colin Maudry
2026-01-30 21:14:03 +01:00
7 changed files with 116 additions and 83 deletions
+39 -6
View File
@@ -27,18 +27,50 @@ b {
font-weight: 600 !important; font-weight: 600 !important;
} }
h1,
h2,
h3,
h4,
h5 {
font-weight: 600;
}
h3 { h3 {
margin: 36px 0 20px 0; margin: 36px 0 20px 0;
} }
/* Base Button Styles */ /* Base Button Styles
button { button {
font-weight: 400; font-weight: 400;
background-color: #fff; background-color: #fff;
border-radius: 3px; border-radius: 3px;
appearance: auto; appearance: auto;
border: solid var(--primary-color) 1px; border: solid var(--primary-color) 1px;
/* couleur thème foncée */ } */
button.btn.btn-primary,
button.show-hide {
display: block;
border-radius: 3px;
outline: 0;
color: #fff;
border: 0;
height: 30px;
padding-top: 2px;
background-image: linear-gradient(
rgb(209, 96, 73),
rgb(179, 56, 33) 26%,
rgb(159, 36, 22)
);
}
button.btn.btn-primary:hover,
button.show-hide:hover {
background-image: linear-gradient(
rgb(239, 126, 103),
rgb(209, 86, 63) 26%,
rgb(189, 66, 52)
);
} }
button[disabled] { button[disabled] {
@@ -160,12 +192,13 @@ p.version > a {
.table-menu { .table-menu {
font-size: 16px; font-size: 16px;
margin: 12px 0 12px 0; margin: 12px 0 12px 0;
height: 36px; height: 50px;
display: flex;
align-items: baseline;
} }
.table-menu > * { .table-menu > * {
margin: 8px 16px 8px 0; margin: 8px 16px 8px 0;
float: left;
} }
#source_table { #source_table {
@@ -213,11 +246,11 @@ table.cell-table th {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
text-align: left; text-align: left;
font-weight: 600; font-weight: 600;
padding-right: 12px; padding: 2px 12px 4px 2px;
border: 1px solid rgb(179, 56, 33) !important; border: 1px solid rgb(179, 56, 33) !important;
background-color: rgb(179, 56, 33); background-color: rgb(179, 56, 33);
border-bottom: none !important; border-bottom: none !important;
height: 34px; height: 32px;
} }
.dash-table-container .dash-table-container
+17 -38
View File
@@ -6,7 +6,7 @@ import plotly.graph_objects as go
import polars as pl import polars as pl
from dash import dash_table, dcc, html from dash import dash_table, dcc, html
from src.utils import format_number from src.utils import data_schema, format_number
def get_map_count_marches(df: pl.DataFrame): def get_map_count_marches(df: pl.DataFrame):
@@ -91,11 +91,8 @@ def get_yearly_statistics(statistics, today_str) -> html.Div:
page_size=10, page_size=10,
sort_action="none", sort_action="none",
filter_action="none", filter_action="none",
style_header={ style_header={"fontFamily": "Inter", "fontSize": "16px"},
"border": "solid 1px rgb(179, 56, 33)", style_cell={"fontFamily": "Inter", "fontSize": "16px"},
"backgroundColor": "rgb(179, 56, 33)",
"color": "white",
},
) )
return html.Div(children=table, className="marches_table") return html.Div(children=table, className="marches_table")
@@ -210,11 +207,8 @@ def get_sources_tables(source_path) -> html.Div:
], ],
sort_action="native", sort_action="native",
markdown_options={"html": True}, markdown_options={"html": True},
style_header={ style_header={"fontFamily": "Inter", "fontSize": "16px"},
"border": "solid 1px rgb(179, 56, 33)", style_cell={"fontFamily": "Inter", "fontSize": "16px"},
"backgroundColor": "rgb(179, 56, 33)",
"color": "white",
},
) )
return html.Div(children=datatable) return html.Div(children=datatable)
@@ -295,20 +289,18 @@ class DataTable(dash_table.DataTable):
"lineHeight": "18px", "lineHeight": "18px",
"whiteSpace": "normal", "whiteSpace": "normal",
}, },
{
"if": {"column_id": "montant"},
"textAlign": "right",
},
{
"if": {"column_id": "dureeMois"},
"textAlign": "right",
},
{
"if": {"column_id": "titulaire_distance"},
"textAlign": "right",
},
] ]
for key in data_schema.keys():
field = data_schema[key]
if field["type"] in ["number", "integer"]:
rule = {
"if": {"column_id": field["name"]},
"textAlign": "right",
# "fontFamily": "Fira Code",
}
style_cell_conditional.append(rule)
# Initialisation de la classe parente avec les arguments # Initialisation de la classe parente avec les arguments
super().__init__( super().__init__(
id=dtid, id=dtid,
@@ -330,6 +322,8 @@ class DataTable(dash_table.DataTable):
style_cell_conditional=style_cell_conditional, style_cell_conditional=style_cell_conditional,
data_timestamp=0, data_timestamp=0,
markdown_options={"html": True}, markdown_options={"html": True},
style_header={"fontFamily": "Inter", "fontSize": "16px"},
style_cell={"fontFamily": "Inter", "fontSize": "16px"},
tooltip_duration=8000, tooltip_duration=8000,
tooltip_delay=350, tooltip_delay=350,
hidden_columns=hidden_columns, hidden_columns=hidden_columns,
@@ -360,13 +354,6 @@ def get_duplicate_matrix() -> html.Div:
Passez votre souris sur une case pour avoir les pourcentages exacts. À noter que ces statistiques sont produites avant le dédoublonnement qui a lieu avant la publication en Open Data et sur ce site.""") Passez votre souris sur une case pour avoir les pourcentages exacts. À noter que ces statistiques sont produites avant le dédoublonnement qui a lieu avant la publication en Open Data et sur ce site.""")
# Assuming result_df is your DataFrame with structure:
# | sourceDataset | unique | dataset1 | dataset2 | dataset3 |
# |---------------|--------|----------|----------|----------|
# | dataset1 | 0.8 | | 0.15 | 0.2 |
# | dataset2 | 0.75 | 0.15 | | 0.12 |
# | dataset3 | 0.85 | 0.2 | 0.12 | |
# Extract data # Extract data
z_data = result_df.select(pl.all().exclude("sourceDataset")).fill_null(0).to_numpy() z_data = result_df.select(pl.all().exclude("sourceDataset")).fill_null(0).to_numpy()
x_labels = result_df.columns[1:] # columns after "sourceDataset" x_labels = result_df.columns[1:] # columns after "sourceDataset"
@@ -378,12 +365,6 @@ def get_duplicate_matrix() -> html.Div:
z=z_data, z=z_data,
x=x_labels, x=x_labels,
y=y_labels, y=y_labels,
# colorscale=[
# [0, "white"], # 0% → white
# [0.10, "lightblue"], # 1% → light blue (soft start)
# [0.50, "steelblue"], # 50% → medium blue
# [1, "darkblue"], # 100% → dark blue
# ],
colorscale=[ colorscale=[
[0.0, "white"], # 0% → white [0.0, "white"], # 0% → white
[0.10, "lightsalmon"], # 10% → light warm tone [0.10, "lightsalmon"], # 10% → light warm tone
@@ -391,8 +372,6 @@ def get_duplicate_matrix() -> html.Div:
], ],
zmin=0, zmin=0,
zmax=1, zmax=1,
# texttemplate="%{z:.0%}", # Format as percentage
# textfont={"size": 10, "color": "black"}, # Smaller font
hoverongaps=False, hoverongaps=False,
showscale=True, showscale=True,
hovertemplate=( hovertemplate=(
+5 -3
View File
@@ -71,7 +71,7 @@ layout = [
className="org_year", className="org_year",
children=dcc.Dropdown( children=dcc.Dropdown(
id="acheteur_year", id="acheteur_year",
options=["Toutes"] options=["Toutes les années"]
+ [ + [
str(year) str(year)
for year in range( for year in range(
@@ -109,6 +109,7 @@ layout = [
html.Button( html.Button(
"Téléchargement au format Excel", "Téléchargement au format Excel",
id="btn-download-data-acheteur", id="btn-download-data-acheteur",
className="btn btn-primary",
), ),
dcc.Download(id="download-data-acheteur"), dcc.Download(id="download-data-acheteur"),
], ],
@@ -136,6 +137,7 @@ layout = [
html.Button( html.Button(
"Téléchargement désactivé au-delà de 65 000 lignes", "Téléchargement désactivé au-delà de 65 000 lignes",
id="btn-download-filtered-data-acheteur", id="btn-download-filtered-data-acheteur",
className="btn btn-primary",
disabled=True, disabled=True,
), ),
dcc.Download(id="acheteur-download-filtered-data"), dcc.Download(id="acheteur-download-filtered-data"),
@@ -229,7 +231,7 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> tuple:
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)
if acheteur_year and acheteur_year != "Toutes": if acheteur_year and acheteur_year != "Toutes les années":
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)
lff = lff.sort(["dateNotification", "uid"], descending=True, nulls_last=True) lff = lff.sort(["dateNotification", "uid"], descending=True, nulls_last=True)
@@ -290,7 +292,7 @@ def download_acheteur_data(
def to_bytes(buffer): def to_bytes(buffer):
df_to_download.write_excel( df_to_download.write_excel(
buffer, worksheet="DECP" if annee in ["Toutes", None] else annee buffer, worksheet="DECP" if annee in ["Toutes les années", None] else annee
) )
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
+5 -5
View File
@@ -46,20 +46,20 @@ layout = html.Div(
"width": "500px", "width": "500px",
"border": "1px solid #ccc", "border": "1px solid #ccc",
"borderRight": "none", "borderRight": "none",
"borderRadius": "4px 0 0 4px", "borderRadius": "3px 0 0 3px",
"padding": "5px 10px", "padding": "5px 10px",
"outline": "none", "outline": "none",
"height": "34px",
}, },
), ),
html.Button( html.Button(
"🔍", "=>",
id="search-button", id="search-button",
className="btn btn-primary",
style={ style={
"border": "1px solid #ccc", "border": "1px solid #ccc",
"borderRadius": "0 4px 4px 0", "borderRadius": "0 3px 3px 0",
"marginLeft": "0", "marginLeft": "0",
"backgroundColor": "#f0f0f0",
"cursor": "pointer",
"height": "auto", # Ensure it matches input height if necessary, often relying on padding/line-height "height": "auto", # Ensure it matches input height if necessary, often relying on padding/line-height
}, },
), ),
+35 -10
View File
@@ -4,6 +4,7 @@ import urllib.parse
import uuid import uuid
from datetime import datetime from datetime import datetime
import dash_bootstrap_components as dbc
import polars as pl import polars as pl
from dash import ( from dash import (
ClientsideFunction, ClientsideFunction,
@@ -125,11 +126,12 @@ layout = [
style={"maxWidth": "1000px"}, style={"maxWidth": "1000px"},
), ),
html.Div( html.Div(
html.Details( [
children=[ dbc.Button("Mode d'emploi", id="tableau_help_open"),
html.Summary( dbc.Modal(
html.H4("Mode d'emploi", style={"textDecoration": "underline"}), [
), dbc.ModalHeader(dbc.ModalTitle("Header")),
dbc.ModalBody(
dcc.Markdown( dcc.Markdown(
dangerously_allow_html=True, dangerously_allow_html=True,
children=f""" children=f"""
@@ -144,11 +146,9 @@ layout = [
- Champs textuels : la recherche retourne les valeurs qui contiennent le texte recherché et n'est pas sensible à la casse (majuscules/minuscules). - Champs textuels : la recherche retourne les valeurs qui contiennent le texte recherché et n'est pas sensible à la casse (majuscules/minuscules).
- Exemple : `rennes` retourne "RENNES METROPOLE". - Exemple : `rennes` retourne "RENNES METROPOLE".
- Les guillemets simples (apostrophe du 4) doivent être prédédées d'une barre oblique (AltGr + 8). Exemple : `services d\\\'assurances` - Les guillemets simples (apostrophe du 4) doivent être prédédées d'une barre oblique (AltGr + 8). Exemple : `services d\\\'assurances`
- Lorsque vous ouvrez une URL de vue (voir "Partager une vue" plus bas), le format équivalent `icontains rennes` est utilisé. Mais dans vos filtres pas besoin de taper `icontains` !
- Champs numériques (Durée en mois, Montant, ...) : vous pouvez... - Champs numériques (Durée en mois, Montant, ...) : vous pouvez...
- soit taper un nombre pour trouver les valeurs strictement égales. Exemple : `12` ne retourne que des 12 - soit taper un nombre pour trouver les valeurs strictement égales. Exemple : `12` ne retourne que des 12
- soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres. - soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres.
- lorsque vous ouvrez une URL de vue (voir "Partager une vue" plus bas), le format équivalent `i<` ou `i>` est utilisé, mais c'est un bug : vous n'avez pas besoin de taper le `i` pour appliquer ce filtre.
- Champs date (Date de notification, ...) : vous pouvez également utiliser **>** ou **<**. Exemples : - Champs date (Date de notification, ...) : vous pouvez également utiliser **>** ou **<**. Exemples :
- `< 2024-01-31` pour "avant le 31 janvier 2024" - `< 2024-01-31` pour "avant le 31 janvier 2024"
- `2024` pour "en 2024", `> 2022` pour "à partir de 2022". - `2024` pour "en 2024", `> 2022` pour "à partir de 2022".
@@ -189,9 +189,23 @@ layout = [
""", """,
), ),
],
id="instructions",
), ),
dbc.ModalFooter(
dbc.Button(
"Close",
id="tableau_help_close",
className="ms-auto",
n_clicks=0,
)
),
],
id="tableau_help",
is_open=False,
fullscreen="md-down",
scrollable=True,
size="lg",
),
],
id="header", id="header",
), ),
# html.Div( # html.Div(
@@ -210,7 +224,7 @@ layout = [
html.P("lignes", id="nb_rows"), html.P("lignes", id="nb_rows"),
html.Div(id="copy-container"), html.Div(id="copy-container"),
dcc.Input(id="share-url", readOnly=True, style={"display": "none"}), dcc.Input(id="share-url", readOnly=True, style={"display": "none"}),
html.Button( dbc.Button(
"Téléchargement désactivé au-delà de 65 000 lignes", "Téléchargement désactivé au-delà de 65 000 lignes",
id="btn-download-data", id="btn-download-data",
disabled=True, disabled=True,
@@ -389,3 +403,14 @@ def show_confirmation(n_clicks):
style={"color": "green", "fontWeight": "bold", "marginLeft": "10px"}, style={"color": "green", "fontWeight": "bold", "marginLeft": "10px"},
) )
return no_update return no_update
@callback(
Output("tableau_help", "is_open"),
[Input("tableau_help_open", "n_clicks"), Input("tableau_help_close", "n_clicks")],
[State("tableau_help", "is_open")],
)
def toggle_tableau_help(click_open, click_close, is_open):
if click_open or click_close:
return not is_open
return is_open
+3 -3
View File
@@ -71,7 +71,7 @@ layout = [
className="org_year", className="org_year",
children=dcc.Dropdown( children=dcc.Dropdown(
id="titulaire_year", id="titulaire_year",
options=["Toutes"] options=["Toutes les années"]
+ [ + [
str(year) str(year)
for year in range( for year in range(
@@ -235,7 +235,7 @@ def get_titulaire_marches_data(url, titulaire_year: str) -> tuple:
(pl.col("titulaire_id") == titulaire_siret) (pl.col("titulaire_id") == titulaire_siret)
& (pl.col("titulaire_typeIdentifiant") == "SIRET") & (pl.col("titulaire_typeIdentifiant") == "SIRET")
) )
if titulaire_year and titulaire_year != "Toutes": if titulaire_year and titulaire_year != "Toutes les années":
lff = lff.filter( lff = lff.filter(
pl.col("dateNotification").cast(pl.String).str.starts_with(titulaire_year) pl.col("dateNotification").cast(pl.String).str.starts_with(titulaire_year)
) )
@@ -305,7 +305,7 @@ def download_titulaire_data(
def to_bytes(buffer): def to_bytes(buffer):
df_to_download.write_excel( df_to_download.write_excel(
buffer, worksheet="DECP" if annee in ["Toutes", None] else annee buffer, worksheet="DECP" if annee in ["Toutes les années", None] else annee
) )
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
+6 -12
View File
@@ -284,7 +284,7 @@ def filter_table_data(
lff: pl.LazyFrame, filter_query: str, filter_source: str lff: pl.LazyFrame, filter_query: str, filter_source: str
) -> pl.LazyFrame: ) -> pl.LazyFrame:
_schema = lff.collect_schema() _schema = lff.collect_schema()
track_search(f"{filter_source}: {filter_query}") track_search(filter_query, filter_source)
filtering_expressions = filter_query.split(" && ") filtering_expressions = filter_query.split(" && ")
for filter_part in filtering_expressions: for filter_part in filtering_expressions:
col_name, operator, filter_value = split_filter_part(filter_part) col_name, operator, filter_value = split_filter_part(filter_part)
@@ -464,21 +464,15 @@ def get_data_schema() -> dict:
return new_schema return new_schema
def track_search(query): def track_search(query, category):
if ( if len(query) >= 4 and not development and os.getenv("MATOMO_DOMAIN"):
len(query) >= 4
and os.getenv("DEVELOPMENT").lower != "true"
and os.getenv("MATOMO_DOMAIN")
):
if os.getenv("DEVELOPMENT").lower() == "true":
url = "https://test.decp.info"
else:
url = "https://decp.info" url = "https://decp.info"
params = { params = {
"idsite": os.getenv("MATOMO_ID_SITE"), "idsite": os.getenv("MATOMO_ID_SITE"),
"url": url, "url": url,
"rec": "1", "rec": "1",
"action_name": "front_page_search", "action_name": "search" if category == "home_page_search" else "filter",
"search_cat": category,
"rand": uuid.uuid4().hex, "rand": uuid.uuid4().hex,
"apiv": "1", "apiv": "1",
"h": localtime().tm_hour, "h": localtime().tm_hour,
@@ -506,7 +500,7 @@ def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame:
return dff.select(pl.lit(False).alias("matches")) return dff.select(pl.lit(False).alias("matches"))
# Enregistrement des recherche dans Matomo # Enregistrement des recherche dans Matomo
track_search(query) track_search(query, "home_page_search")
# Normalize query # Normalize query
normalized_query = unidecode(query.strip()).upper() normalized_query = unidecode(query.strip()).upper()