Vue marché simple #40

This commit is contained in:
Colin Maudry
2025-09-30 14:57:41 +02:00
parent 79d11b326e
commit 7c157d9d71
9 changed files with 260 additions and 37 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ python run.py
#### 2.1.0 #### 2.1.0
- Ajout des vues [acheteur](https://decp.info/acheteurs/24350013900189) et [titulaire](https://decp.info/titulaires/51903758414786) ([#28](https://github.com/ColinMaudry/decp.info/issues/28) et [#35](https://github.com/ColinMaudry/decp.info/issues/35)) 🔎 - Ajout des vues [acheteur](https://decp.info/acheteurs/24350013900189) ([#28](https://github.com/ColinMaudry/decp.info/issues/28)), [titulaire](https://decp.info/titulaires/51903758414786) ([#35](https://github.com/ColinMaudry/decp.info/issues/35)) et [marché](https://decp.info/marches/532239472000482025S00004) ([#40](https://github.com/ColinMaudry/decp.info/issues/40)) 🔎
- Ajout des balises HTML meta Open Graph et Twitter ([#39](https://github.com/ColinMaudry/decp.info/issues/39)) pour de beaux aperçus de liens 🖼️ - Ajout des balises HTML meta Open Graph et Twitter ([#39](https://github.com/ColinMaudry/decp.info/issues/39)) pour de beaux aperçus de liens 🖼️
- Formulaire de contact ([#48](https://github.com/ColinMaudry/decp.info/issues/48)) 📨 - Formulaire de contact ([#48](https://github.com/ColinMaudry/decp.info/issues/48)) 📨
- Nom de colonnes plus_agréables ([#33](https://github.com/ColinMaudry/decp.info/issues/33)) 💅 - Nom de colonnes plus_agréables ([#33](https://github.com/ColinMaudry/decp.info/issues/33)) 💅
+1 -1
View File
@@ -96,7 +96,7 @@ app.layout = html.Div(
page["name"], href=page["relative_path"], className="nav" page["name"], href=page["relative_path"], className="nav"
) )
for page in page_registry.values() for page in page_registry.values()
if page["name"] not in ["Acheteur", "Titulaire"] if page["name"] not in ["Acheteur", "Titulaire", "Marché"]
] ]
), ),
], ],
+33 -2
View File
@@ -88,8 +88,8 @@ td[data-dash-column="objet"] {
color: #333; color: #333;
width: 400px !important; width: 400px !important;
max-width: 400px !important; max-width: 400px !important;
height: 100px !important; height: 150px !important;
max-height: 100px !important; max-height: 150px !important;
overflow: hidden; overflow: hidden;
} }
@@ -179,6 +179,9 @@ summary > h3 {
justify-content: space-between; justify-content: space-between;
} }
.wrapper > div {
}
.org_title { .org_title {
grid-column: 1 / 3; grid-column: 1 / 3;
grid-row: 1; grid-row: 1;
@@ -207,3 +210,31 @@ summary > h3 {
grid-column: 3; grid-column: 3;
grid-row: 2; grid-row: 2;
} }
/* Vue marché */
.marche_infos p {
margin-top: 6px;
margin-bottom: 6px;
}
.marche_title {
grid-column: 1/3;
grid-row: 1;
}
.marche_map {
grid-column: 3;
grid-row: 2;
width: 400px;
}
.marche_infos_1 {
grid-column: 1;
grid-row: 2;
}
.marche_infos_2 {
grid-column: 2;
grid-row: 2;
}
+1 -1
View File
@@ -48,7 +48,7 @@ les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter
- ✉️ [inscription à la liste de diffusion](https://6254d9a3.sibforms.com/serve/MUIFAEonUVkoSVrdgey18CTgLyI16xw4yeu-M-YOUzhWE_AgfQfbgkyT7GvA_RYLro9MfuRqkzQxSvu7-uzbMSv2a2ZQPsliM7wtiiqIL8kR2zOvl6m11fb5qjcOxMAYsLiY_YBi3P7NY95CTJ8vRY4CpsDclF2iLooOElKkTgIgi5nePe7zAIrgiYM5v2EuALlGJZMEG9vBP-Cu) (annonces des mises à jour et évènements, maximum une fois par mois) - ✉️ [inscription à la liste de diffusion](https://6254d9a3.sibforms.com/serve/MUIFAEonUVkoSVrdgey18CTgLyI16xw4yeu-M-YOUzhWE_AgfQfbgkyT7GvA_RYLro9MfuRqkzQxSvu7-uzbMSv2a2ZQPsliM7wtiiqIL8kR2zOvl6m11fb5qjcOxMAYsLiY_YBi3P7NY95CTJ8vRY4CpsDclF2iLooOElKkTgIgi5nePe7zAIrgiYM5v2EuALlGJZMEG9vBP-Cu) (annonces des mises à jour et évènements, maximum une fois par mois)
- 💾 [données consolidées en Open Data](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire/) - 💾 [données consolidées en Open Data](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire/)
- 🗞️ [mon blog](https://colin.maudry.com), qui parle beaucoup de transparence des marchés publics - 🗞️ [mon blog](https://colin.maudry.com)
- 📔 [wiki du projet](https://github.com/ColinMaudry/decp-processing/wiki) - 📔 [wiki du projet](https://github.com/ColinMaudry/decp-processing/wiki)
- 🚰 code source - 🚰 code source
- [de decp.info](https://github.com/ColinMaudry/decp.info) - [de decp.info](https://github.com/ColinMaudry/decp.info)
+6 -13
View File
@@ -5,7 +5,7 @@ from dash import Input, Output, State, callback, dash_table, dcc, html, register
from src.figures import point_on_map from src.figures import point_on_map
from src.utils import ( from src.utils import (
add_org_links_in_dict, add_links_in_dict,
format_montant, format_montant,
format_number, format_number,
get_annuaire_data, get_annuaire_data,
@@ -177,6 +177,7 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> pl.LazyFrame:
lff = lff.fill_null("") lff = lff.fill_null("")
lff = lff.select( lff = lff.select(
"id", "id",
"uid",
"objet", "objet",
"dateNotification", "dateNotification",
"titulaire_id", "titulaire_id",
@@ -201,23 +202,15 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> pl.LazyFrame:
Input(component_id="acheteur_data", component_property="data"), Input(component_id="acheteur_data", component_property="data"),
) )
def get_last_marches_table(data) -> html.Div: def get_last_marches_table(data) -> html.Div:
columns = [
"id",
"objet",
"dateNotification",
"titulaire_nom",
"montant",
"codeCPV",
"dureeMois",
]
dff = pl.DataFrame(data) dff = pl.DataFrame(data)
dff = format_montant(dff) dff = format_montant(dff)
columns, tooltip = setup_table_columns( columns, tooltip = setup_table_columns(
dff, hideable=False, exclude=["titulaire_id", "titulaire_typeIdentifiant"] dff,
hideable=False,
exclude=["titulaire_id", "titulaire_typeIdentifiant", "uid"],
) )
data = dff.to_dicts() data = dff.to_dicts()
data = add_org_links_in_dict(data, "titulaire") data = add_links_in_dict(data, "titulaire")
table = html.Div( table = html.Div(
className="marches_table", className="marches_table",
+2 -2
View File
@@ -5,7 +5,7 @@ import polars as pl
from dash import Input, Output, State, callback, dash_table, dcc, html, register_page from dash import Input, Output, State, callback, dash_table, dcc, html, register_page
from src.utils import ( from src.utils import (
add_org_links, add_links,
add_resource_link, add_resource_link,
filter_table_data, filter_table_data,
format_montant, format_montant,
@@ -197,7 +197,7 @@ def update_table(page_current, page_size, filter_query, sort_by, data_timestamp)
dff = dff.slice(start_row, page_size) dff = dff.slice(start_row, page_size)
# Ajout des liens vers l'annuaire des entreprises # Ajout des liens vers l'annuaire des entreprises
dff = add_org_links(dff) dff = add_links(dff)
# Ajout des liens vers les fichiers Open Data # Ajout des liens vers les fichiers Open Data
dff = add_resource_link(dff) dff = add_resource_link(dff)
+188
View File
@@ -0,0 +1,188 @@
from datetime import datetime
import polars as pl
from dash import Input, Output, callback, dcc, html, register_page
from polars import selectors as cs
from src.utils import data_schema, format_montant, lf, meta_content
register_page(
__name__,
path_template="/marches/<uid>",
title=meta_content["title"],
name="Marché",
description=meta_content["description"],
image_url=meta_content["image_url"],
order=7,
)
layout = [
dcc.Store(id="marche_data"),
dcc.Store(id="titulaires_data"),
dcc.Location(id="url", refresh="callback-nav"),
html.Div(
className="container marche_infos",
children=[
html.P("Vous consultez un résumé des données de ce marché public"),
html.Ul(
[
html.Li(
"après son attribution aux titulaires qui l'ont remporté à la suite d'un appel d'offres (ou sans appel d'offres via une attribution directe)"
),
html.Li(
"après avoir appliqué les éventuelles modifications de montant, durée ou titulaires renseignées par l'acheteur"
),
]
),
html.P(
"Le montant total payé aux titulaires, la durée du marché et la liste des titulaires peuvent cependant encore évoluer jusqu'à la fin de l'exécution du marché."
),
html.Div(
className="wrapper",
children=[
html.Div(
className="marche_map",
id="marche_map",
children=[
html.H4("Titulaires"),
html.Ul(id="marche_infos_titulaires"),
],
),
html.Div(className="marche_infos_1", id="marche_infos_1"),
html.Div(className="marche_infos_2", id="marche_infos_2"),
],
),
],
),
]
@callback(
Output("marche_data", "data"),
Output("titulaires_data", "data"),
Input(component_id="url", component_property="pathname"),
)
def get_marche_data(url):
marche_uid = url.split("/")[-1]
# Récupération des données du marché à partir du lf global
lff = lf.filter(pl.col("uid") == pl.lit(marche_uid))
# Données des titulaires du marché
dff_titulaires = lff.select(cs.starts_with("titulaire")).collect(engine="streaming")
# Données du marché
dff_marche = (
lff.select(~cs.starts_with("titulaires")).unique().collect(engine="streaming")
)
dff_marche = format_montant(dff_marche)
assert dff_marche.height == 1
return dff_marche.to_dicts()[0], dff_titulaires.to_dicts()
@callback(
Output("marche_infos_1", "children"),
Output("marche_infos_2", "children"),
Output("marche_infos_titulaires", "children"),
Input("marche_data", "data"),
Input("titulaires_data", "data"),
)
def update_marche_info(marche, titulaires):
def make_parameter(col):
column_object = data_schema.get(col)
column_name = column_object.get("friendly_name") if column_object else col
if marche[col]:
if col == "acheteur_nom":
value = html.A(
href=f"/acheteurs/{marche['acheteur_id']}",
children=marche["acheteur_nom"],
)
elif col == "sourceDataset":
value = html.A(
href=marche["sourceFile"], children=marche["sourceDataset"]
)
column_name = "Source des données"
# Dates
elif col in ["dateNotification", "datePublicationDonnees"]:
print(marche[col])
value = datetime.fromisoformat(marche[col]).strftime("%d/%m/%Y")
# Listes
elif (
col
in [
"techniques",
"typesPrix",
"considerationsSociales",
"considerationsEnvironnementales",
]
and "," in marche[col]
):
col_values = marche[col].split(", ")
lines = []
for val in col_values:
lines.append(html.Li(val))
_content = html.Div(
[html.P([column_name, " : "]), html.Ul(children=lines)]
)
return _content
else:
value = marche.get(col)
else:
value = ""
param_content = html.P([column_name, " : ", html.Strong(value)])
return param_content
marche_infos = [
make_parameter("id"),
make_parameter("objet"),
make_parameter("dateNotification"), # date
make_parameter("nature"),
make_parameter("acheteur_nom"), # lien
make_parameter("montant"),
make_parameter("codeCPV"),
make_parameter("procedure"),
make_parameter("techniques"), # list
make_parameter("dureeMois"),
make_parameter("offresRecues"),
make_parameter("datePublicationDonnees"), # date
make_parameter("formePrix"),
make_parameter("typesPrix"), # list
make_parameter("attributionAvance"),
make_parameter("tauxAvance"),
make_parameter("marcheInnovant"), # label
make_parameter("modalitesExecution"),
make_parameter("considerationsSociales"), # list
make_parameter("considerationsEnvironnementales"), # list
make_parameter("ccag"),
make_parameter("sousTraitanceDeclaree"),
make_parameter("typeGroupementOperateurs"),
make_parameter("origineFrance"),
make_parameter("origineUE"),
make_parameter("idAccordCadre"),
make_parameter("sourceDataset"), # lien
]
half = round(len(marche_infos) / 2)
# pas inclus pour l'instant : lieu d'exécution, modifications
titulaires_lines = []
for titulaire in titulaires:
if titulaire["titulaire_typeIdentifiant"] == "SIRET":
content = html.Li(
html.A(
href=f"/titulaires/{titulaire['titulaire_id']}",
children=titulaire["titulaire_nom"],
)
)
else:
content = html.Li(titulaire["titulaire_nom"])
titulaires_lines.append(content)
return marche_infos[:half], marche_infos[half:], titulaires_lines
+8 -5
View File
@@ -5,7 +5,7 @@ from dash import Input, Output, State, callback, dash_table, dcc, html, register
from src.figures import point_on_map from src.figures import point_on_map
from src.utils import ( from src.utils import (
add_org_links_in_dict, add_links_in_dict,
format_montant, format_montant,
format_number, format_number,
get_annuaire_data, get_annuaire_data,
@@ -181,6 +181,7 @@ def get_titulaire_marches_data(url, titulaire_year: str) -> pl.LazyFrame:
) )
lff = lff.fill_null("") lff = lff.fill_null("")
lff = lff.select( lff = lff.select(
"id",
"uid", "uid",
"objet", "objet",
"dateNotification", "dateNotification",
@@ -217,11 +218,13 @@ def get_last_marches_table(data) -> html.Div:
dff = pl.DataFrame(data) dff = pl.DataFrame(data)
dff = format_montant(dff) dff = format_montant(dff)
columns, tooltip = setup_table_columns(dff, hideable=False, exclude=["acheteur_id"]) columns, tooltip = setup_table_columns(
dff, hideable=False, exclude=["acheteur_id", "id"]
)
data = dff.to_dicts() data = dff.to_dicts()
# Idéalement on utiliserait add_org_links(), mais le résultat attendu # Idéalement on utiliserait add_org_links(), mais le résultat attendu
# est différent de home.py (Tableau) # est différent de home.py (Tableau)
data = add_org_links_in_dict(data, "acheteur") data = add_links_in_dict(data, "acheteur")
table = html.Div( table = html.Div(
className="marches_table", className="marches_table",
@@ -248,8 +251,8 @@ def get_last_marches_table(data) -> html.Div:
"whiteSpace": "normal", "whiteSpace": "normal",
}, },
{ {
"if": {"column_id": " acheteur_nom"}, "if": {"column_id": "acheteur_nom"},
"minWidth": "200px", "maxWidth": "400px",
"textAlign": "left", "textAlign": "left",
"overflow": "hidden", "overflow": "hidden",
"lineHeight": "18px", "lineHeight": "18px",
+15 -7
View File
@@ -49,7 +49,7 @@ def add_resource_link(dff: pl.DataFrame) -> pl.DataFrame:
return dff return dff
def add_org_links(dff: pl.DataFrame): def add_links(dff: pl.DataFrame):
dff = dff.with_columns( dff = dff.with_columns(
pl.when(pl.col("titulaire_typeIdentifiant") == "SIRET") pl.when(pl.col("titulaire_typeIdentifiant") == "SIRET")
.then( .then(
@@ -62,25 +62,29 @@ def add_org_links(dff: pl.DataFrame):
.otherwise(pl.col("titulaire_id")) .otherwise(pl.col("titulaire_id"))
.alias("titulaire_id") .alias("titulaire_id")
) )
for column, path in [("acheteur_id", "acheteurs"), ("uid", "marches")]:
dff = dff.with_columns( dff = dff.with_columns(
( (
'<a href = "/acheteurs/' f'<a href = "/{path}/'
+ pl.col("acheteur_id") + pl.col(column)
+ '" target="_blank">' + '" target="_blank">'
+ pl.col("acheteur_id") + pl.col(column)
+ "</a>" + "</a>"
).alias("acheteur_id") ).alias(column)
) )
return dff return dff
def add_org_links_in_dict(data: list, org_type: str) -> list: def add_links_in_dict(data: list, org_type: str) -> list:
new_data = [] new_data = []
for marche in data: for marche in data:
org_id = marche[org_type + "_id"] org_id = marche[org_type + "_id"]
marche[org_type + "_nom"] = ( marche[org_type + "_nom"] = (
f'<a href="/{org_type}s/{org_id}">{marche[org_type + "_nom"]}</a>' f'<a href="/{org_type}s/{org_id}">{marche[org_type + "_nom"]}</a>'
) )
marche["id"] = f'<a href="/marches/{marche["uid"]}">{marche["id"]}</a>'
marche["uid"] = f'<a href="/marches/{marche["uid"]}">{marche["uid"]}</a>'
new_data.append(marche) new_data.append(marche)
return new_data return new_data
@@ -280,7 +284,7 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup
if column_object: if column_object:
tooltip[column_id] = { tooltip[column_id] = {
"value": f"""**{column_object.get("friendly_name", column_id)}** "value": f"""**{column_object.get("friendly_name")}** ({column_id})
""" """
+ column_object["description"], + column_object["description"],
@@ -305,3 +309,7 @@ meta_content = {
# Récupération du schéma des données tabulaires # Récupération du schéma des données tabulaires
data_schema: dict = get(os.getenv("DATA_SCHEMA_PATH"), follow_redirects=True).json() data_schema: dict = get(os.getenv("DATA_SCHEMA_PATH"), follow_redirects=True).json()
data_schema["source"] = {
"description": "Code de la source des données, avec un lien vers le fichier Open Data dont proviennent les données de ce marché public.",
"friendly_name": "Source des données",
}