Merge branch 'feature/40_vue_marché' into dev
This commit is contained in:
@@ -38,7 +38,7 @@ python run.py
|
||||
|
||||
#### 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 🖼️
|
||||
- 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)) 💅
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ app.layout = html.Div(
|
||||
page["name"], href=page["relative_path"], className="nav"
|
||||
)
|
||||
for page in page_registry.values()
|
||||
if page["name"] not in ["Acheteur", "Titulaire"]
|
||||
if page["name"] not in ["Acheteur", "Titulaire", "Marché"]
|
||||
]
|
||||
),
|
||||
],
|
||||
|
||||
+33
-2
@@ -88,8 +88,8 @@ td[data-dash-column="objet"] {
|
||||
color: #333;
|
||||
width: 400px !important;
|
||||
max-width: 400px !important;
|
||||
height: 100px !important;
|
||||
max-height: 100px !important;
|
||||
height: 150px !important;
|
||||
max-height: 150px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,9 @@ summary > h3 {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wrapper > div {
|
||||
}
|
||||
|
||||
.org_title {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
@@ -207,3 +210,31 @@ summary > h3 {
|
||||
grid-column: 3;
|
||||
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
@@ -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)
|
||||
- 💾 [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)
|
||||
- 🚰 code source
|
||||
- [de decp.info](https://github.com/ColinMaudry/decp.info)
|
||||
|
||||
+6
-13
@@ -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.utils import (
|
||||
add_org_links_in_dict,
|
||||
add_links_in_dict,
|
||||
format_montant,
|
||||
format_number,
|
||||
get_annuaire_data,
|
||||
@@ -177,6 +177,7 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> pl.LazyFrame:
|
||||
lff = lff.fill_null("")
|
||||
lff = lff.select(
|
||||
"id",
|
||||
"uid",
|
||||
"objet",
|
||||
"dateNotification",
|
||||
"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"),
|
||||
)
|
||||
def get_last_marches_table(data) -> html.Div:
|
||||
columns = [
|
||||
"id",
|
||||
"objet",
|
||||
"dateNotification",
|
||||
"titulaire_nom",
|
||||
"montant",
|
||||
"codeCPV",
|
||||
"dureeMois",
|
||||
]
|
||||
|
||||
dff = pl.DataFrame(data)
|
||||
dff = format_montant(dff)
|
||||
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 = add_org_links_in_dict(data, "titulaire")
|
||||
data = add_links_in_dict(data, "titulaire")
|
||||
|
||||
table = html.Div(
|
||||
className="marches_table",
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import os
|
||||
import re
|
||||
import smtplib
|
||||
import time
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from dash import Input, Output, State, callback, dcc, html, register_page
|
||||
from flask import request
|
||||
|
||||
from src.utils import meta_content
|
||||
|
||||
name = "Contact"
|
||||
register_page(
|
||||
__name__,
|
||||
path="/contact",
|
||||
title=meta_content["title"],
|
||||
name=name,
|
||||
description=meta_content["description"],
|
||||
image_url=meta_content["image_url"],
|
||||
order=6,
|
||||
)
|
||||
|
||||
layout = html.Div(
|
||||
className="container",
|
||||
children=[
|
||||
html.H2("Contact", id="contact"),
|
||||
html.P(
|
||||
"Votre message arrivera directement dans ma boîte mail, et je reviendrai vers vous rapidement."
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
html.Label("Votre nom"),
|
||||
dcc.Input(
|
||||
id="input-name",
|
||||
type="text",
|
||||
style={"width": "100%", "padding": "8px", "margin": "8px 0"},
|
||||
),
|
||||
html.Label("Votre adresse email"),
|
||||
dcc.Input(
|
||||
id="input-email",
|
||||
type="email",
|
||||
style={"width": "100%", "padding": "8px", "margin": "8px 0"},
|
||||
),
|
||||
html.Label("Message"),
|
||||
dcc.Textarea(
|
||||
id="input-message",
|
||||
style={
|
||||
"width": "100%",
|
||||
"height": 200,
|
||||
"padding": "8px",
|
||||
"margin": "8px 0",
|
||||
},
|
||||
),
|
||||
html.Button(
|
||||
"Envoyer ",
|
||||
id="submit-button",
|
||||
n_clicks=0,
|
||||
style={"marginTop": "10px"},
|
||||
),
|
||||
html.Div(
|
||||
id="output-message", style={"marginTop": "10px", "color": "green"}
|
||||
),
|
||||
],
|
||||
style={
|
||||
"maxWidth": "600px",
|
||||
"margin": "auto",
|
||||
"padding": "20px",
|
||||
"lineHeight": "20px",
|
||||
},
|
||||
),
|
||||
dcc.Markdown("""
|
||||
- Bluesky : [@col1m.bsky.social](https://bsky.app/profile/col1m.bsky.social)
|
||||
- Mastodon : [col1m@mamot.fr](https://mamot.fr/@col1m)
|
||||
- LinkedIn : [colinmaudry](https://www.linkedin.com/in/colinmaudry/)
|
||||
- venez discuter de la transparence de la commande publique [sur le forum teamopendata.org](https://teamopendata.org/c/commande-publique/101)
|
||||
"""),
|
||||
],
|
||||
)
|
||||
|
||||
rate_limit_store = {} # {ip: last_timestamp}
|
||||
RATE_LIMIT_WINDOW = 300 # 5 minutes
|
||||
|
||||
|
||||
def is_rate_limited():
|
||||
ip = request.remote_addr
|
||||
now = time.time()
|
||||
last_time = rate_limit_store.get(ip)
|
||||
|
||||
if last_time and (now - last_time) < RATE_LIMIT_WINDOW:
|
||||
return True # rate limited !
|
||||
rate_limit_store[ip] = now # màj du timestamp
|
||||
return False
|
||||
|
||||
|
||||
def sanitize_email(email):
|
||||
return re.match(r"[^@]+@[^@]+\.[^@]+", email)
|
||||
|
||||
|
||||
@callback(
|
||||
Output("output-message", "children"),
|
||||
Input("submit-button", "n_clicks"),
|
||||
State("input-name", "value"),
|
||||
State("input-email", "value"),
|
||||
State("input-message", "value"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def send_email(n_clicks, form_name, form_email, form_message):
|
||||
if not all([form_name, form_email, form_message]):
|
||||
return html.Div(
|
||||
"Veuillez s'il vous plaît remplir tous les champs.", style={"color": "red"}
|
||||
)
|
||||
|
||||
client_ip = request.remote_addr
|
||||
if is_rate_limited():
|
||||
wait_time = int(RATE_LIMIT_WINDOW - (time.time() - rate_limit_store[client_ip]))
|
||||
return html.Div(
|
||||
f"⏳ J'ai mis en place une protection contre le spam, et vous m'avez écrit il y a moins de 5 minutes. "
|
||||
f"Veuillez s'il vous plaît attendre {wait_time} secondes avant de renvoyer un message.",
|
||||
style={"color": "black"},
|
||||
)
|
||||
|
||||
try:
|
||||
# Configuration du serveur SMTP
|
||||
smtp_server = os.getenv("SENDER_SERVER_DOMAIN")
|
||||
smtp_port = 587
|
||||
login_email = os.getenv("LOGIN_EMAIL")
|
||||
from_email = os.getenv("FROM_EMAIL")
|
||||
to_email = os.getenv("TO_EMAIL", from_email)
|
||||
login_password = os.getenv("LOGIN_PASSWORD")
|
||||
|
||||
print(
|
||||
smtp_server,
|
||||
smtp_port,
|
||||
login_email,
|
||||
from_email,
|
||||
to_email,
|
||||
login_password,
|
||||
sep="\n",
|
||||
)
|
||||
|
||||
# Création de l'email
|
||||
email = MIMEMultipart()
|
||||
email["From"] = from_email
|
||||
email["To"] = to_email
|
||||
email["Subject"] = f"[decp.info] Message de {form_name}"
|
||||
|
||||
body = f"""
|
||||
Nom : {form_name}
|
||||
Email : {form_email}
|
||||
|
||||
Message :
|
||||
{form_message}
|
||||
"""
|
||||
email.attach(MIMEText(body, "plain"))
|
||||
|
||||
# Send email
|
||||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||||
server.starttls()
|
||||
server.login(login_email, login_password)
|
||||
server.sendmail(from_email, to_email, email.as_string())
|
||||
server.quit()
|
||||
|
||||
return html.Div("✅ Envoi réussi", style={"color": "green"})
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return html.Div(
|
||||
f"❌ Échec de l'envoi du message : {str(e)}", style={"color": "red"}
|
||||
)
|
||||
+2
-2
@@ -5,7 +5,7 @@ import polars as pl
|
||||
from dash import Input, Output, State, callback, dash_table, dcc, html, register_page
|
||||
|
||||
from src.utils import (
|
||||
add_org_links,
|
||||
add_links,
|
||||
add_resource_link,
|
||||
filter_table_data,
|
||||
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)
|
||||
|
||||
# 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
|
||||
dff = add_resource_link(dff)
|
||||
|
||||
@@ -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
|
||||
@@ -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.utils import (
|
||||
add_org_links_in_dict,
|
||||
add_links_in_dict,
|
||||
format_montant,
|
||||
format_number,
|
||||
get_annuaire_data,
|
||||
@@ -181,6 +181,7 @@ def get_titulaire_marches_data(url, titulaire_year: str) -> pl.LazyFrame:
|
||||
)
|
||||
lff = lff.fill_null("")
|
||||
lff = lff.select(
|
||||
"id",
|
||||
"uid",
|
||||
"objet",
|
||||
"dateNotification",
|
||||
@@ -217,11 +218,13 @@ def get_last_marches_table(data) -> html.Div:
|
||||
|
||||
dff = pl.DataFrame(data)
|
||||
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()
|
||||
# Idéalement on utiliserait add_org_links(), mais le résultat attendu
|
||||
# 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(
|
||||
className="marches_table",
|
||||
@@ -248,8 +251,8 @@ def get_last_marches_table(data) -> html.Div:
|
||||
"whiteSpace": "normal",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": " acheteur_nom"},
|
||||
"minWidth": "200px",
|
||||
"if": {"column_id": "acheteur_nom"},
|
||||
"maxWidth": "400px",
|
||||
"textAlign": "left",
|
||||
"overflow": "hidden",
|
||||
"lineHeight": "18px",
|
||||
|
||||
+20
-12
@@ -49,7 +49,7 @@ def add_resource_link(dff: pl.DataFrame) -> pl.DataFrame:
|
||||
return dff
|
||||
|
||||
|
||||
def add_org_links(dff: pl.DataFrame):
|
||||
def add_links(dff: pl.DataFrame):
|
||||
dff = dff.with_columns(
|
||||
pl.when(pl.col("titulaire_typeIdentifiant") == "SIRET")
|
||||
.then(
|
||||
@@ -62,25 +62,29 @@ def add_org_links(dff: pl.DataFrame):
|
||||
.otherwise(pl.col("titulaire_id"))
|
||||
.alias("titulaire_id")
|
||||
)
|
||||
dff = dff.with_columns(
|
||||
(
|
||||
'<a href = "/acheteurs/'
|
||||
+ pl.col("acheteur_id")
|
||||
+ '" target="_blank">'
|
||||
+ pl.col("acheteur_id")
|
||||
+ "</a>"
|
||||
).alias("acheteur_id")
|
||||
)
|
||||
|
||||
for column, path in [("acheteur_id", "acheteurs"), ("uid", "marches")]:
|
||||
dff = dff.with_columns(
|
||||
(
|
||||
f'<a href = "/{path}/'
|
||||
+ pl.col(column)
|
||||
+ '" target="_blank">'
|
||||
+ pl.col(column)
|
||||
+ "</a>"
|
||||
).alias(column)
|
||||
)
|
||||
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 = []
|
||||
for marche in data:
|
||||
org_id = marche[org_type + "_id"]
|
||||
marche[org_type + "_nom"] = (
|
||||
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)
|
||||
return new_data
|
||||
|
||||
@@ -280,7 +284,7 @@ def setup_table_columns(dff, hideable: bool = True, exclude: list = None) -> tup
|
||||
|
||||
if column_object:
|
||||
tooltip[column_id] = {
|
||||
"value": f"""**{column_object.get("friendly_name", column_id)}**
|
||||
"value": f"""**{column_object.get("friendly_name")}** ({column_id})
|
||||
|
||||
"""
|
||||
+ column_object["description"],
|
||||
@@ -305,3 +309,7 @@ meta_content = {
|
||||
|
||||
# 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["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",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user