Merge branch 'feature/66_seo' into dev
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
#### 2.5.0 ()
|
||||
|
||||
- Renforcement du SEO avec une arborescence permettant l'accès aux marchés et des snippets JSON-LD
|
||||
|
||||
##### 2.4.1 (22 janvier 2026)
|
||||
|
||||
- Meilleure gestion des colonnes absentes du schéma
|
||||
|
||||
+7
-1
@@ -13,6 +13,10 @@ development = os.getenv("DEVELOPMENT").lower() == "true"
|
||||
|
||||
meta_tags = [
|
||||
{"name": "viewport", "content": "width=device-width, initial-scale=1"},
|
||||
{
|
||||
"name": "keywords",
|
||||
"content": "commande publique, decp, marchés publics, données essentielles",
|
||||
},
|
||||
]
|
||||
|
||||
if development:
|
||||
@@ -25,6 +29,7 @@ app = Dash(
|
||||
compress=True,
|
||||
meta_tags=meta_tags,
|
||||
)
|
||||
|
||||
# COSMO (belle font, blue),
|
||||
# UNITED (rouge, ubuntu font),
|
||||
# LUMEN (gros séparateur, blue clair),
|
||||
@@ -142,7 +147,8 @@ navbar = dbc.Navbar(
|
||||
)
|
||||
)
|
||||
for page in page_registry.values()
|
||||
if page["name"] not in ["Acheteur", "Titulaire", "Marché"]
|
||||
if page["name"]
|
||||
in ["Recherche", "À propos", "Tableau", "Statistiques"]
|
||||
],
|
||||
className="ms-auto",
|
||||
navbar=True,
|
||||
|
||||
@@ -254,7 +254,8 @@ summary > h3 {
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS for the /a-propos page Layout and Table of Contents */
|
||||
/* CSS for the /a-propos page Layout and Table of Contents
|
||||
Généré par Gemini 3.0 Pro */
|
||||
|
||||
.a-propos-container {
|
||||
display: flex;
|
||||
|
||||
+10
-2
@@ -169,14 +169,22 @@ def get_sources_tables(source_path) -> html.Div:
|
||||
+ pl.lit("</a>")
|
||||
).alias("nom")
|
||||
)
|
||||
df = df.drop("url")
|
||||
df = df.drop("url", "unique")
|
||||
df = df.sort(by=["nb_marchés"], descending=True)
|
||||
|
||||
columns = {
|
||||
"nom": "Nom de la source",
|
||||
"organisation": "Responsable de publication",
|
||||
"nb_marchés": "Nb de marchés",
|
||||
"nb_acheteurs": "Nb d'acheteurs",
|
||||
"code": "Code",
|
||||
}
|
||||
|
||||
datatable = dash_table.DataTable(
|
||||
id="source_table",
|
||||
columns=[
|
||||
{
|
||||
"name": i,
|
||||
"name": columns[i],
|
||||
"id": i,
|
||||
"presentation": "markdown",
|
||||
"type": "text",
|
||||
|
||||
@@ -122,6 +122,14 @@ Rien d’exceptionnel, je respecte simplement la loi, qui dit que certains outil
|
||||
|
||||
J’utilise pour cela [Matomo](https://matomo.org/), un outil [libre](https://matomo.org/free-software/), paramétré pour être en conformité avec [la recommandation « Cookies »](https://www.cnil.fr/fr/solutions-pour-les-cookies-de-mesure-daudience) de la CNIL. Cela signifie que votre adresse IP, par exemple, est anonymisée avant d’être enregistrée. Il m’est donc impossible d’associer vos visites sur ce site à votre personne."""
|
||||
),
|
||||
html.H4(
|
||||
"Liste des marchés par département", id="liste_marches"
|
||||
),
|
||||
dcc.Markdown(
|
||||
"""
|
||||
- [Marchés par département](/departements)
|
||||
"""
|
||||
),
|
||||
],
|
||||
),
|
||||
# Table of Contents Column
|
||||
|
||||
@@ -7,6 +7,7 @@ from src.callbacks import get_top_org_table
|
||||
from src.figures import DataTable, point_on_map
|
||||
from src.utils import (
|
||||
df,
|
||||
df_acheteurs,
|
||||
filter_table_data,
|
||||
format_number,
|
||||
get_annuaire_data,
|
||||
@@ -20,7 +21,12 @@ from src.utils import (
|
||||
|
||||
|
||||
def get_title(acheteur_id: str = None) -> str:
|
||||
return f"Acheteur {acheteur_id} | decp.info"
|
||||
acheteur_nom = (
|
||||
df_acheteurs.filter(pl.col("acheteur_id") == acheteur_id)
|
||||
.select("acheteur_nom")
|
||||
.item()
|
||||
)
|
||||
return f"Marchés publics attribués par {acheteur_nom} | decp.info"
|
||||
|
||||
|
||||
register_page(
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import polars as pl
|
||||
from dash import Input, Output, callback, dcc, html, register_page
|
||||
|
||||
from src.utils import departements, df_acheteurs_departement, df_titulaires_departement
|
||||
|
||||
name = "Département"
|
||||
|
||||
|
||||
def get_title(code):
|
||||
return f"Marchés publics de {departements[code]['departement']} | decp.info"
|
||||
|
||||
|
||||
def get_description(code):
|
||||
return f"Marchés publics passés dans le département {departements[code]['departement']} | decp.info"
|
||||
|
||||
|
||||
register_page(
|
||||
__name__,
|
||||
path_template="/departements/<code>",
|
||||
title=get_title,
|
||||
description=get_description,
|
||||
order=50,
|
||||
name=name,
|
||||
)
|
||||
|
||||
layout = html.Div(
|
||||
[
|
||||
dcc.Location(id="departement_url", refresh="callback-nav"),
|
||||
html.Div(id="departement_marches"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@callback(
|
||||
Output(component_id="departement_marches", component_property="children"),
|
||||
Input(component_id="departement_url", component_property="pathname"),
|
||||
)
|
||||
def departement_marches(url):
|
||||
departement = url.split("/")[-1]
|
||||
|
||||
def make_link_list(org_type) -> list:
|
||||
link_list = []
|
||||
if org_type == "acheteur":
|
||||
df = df_acheteurs_departement
|
||||
elif org_type == "titulaire":
|
||||
df = df_titulaires_departement
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
df = df.filter(pl.col(f"{org_type}_departement_code") == departement)
|
||||
|
||||
for row in df.iter_rows(named=True):
|
||||
li = html.Li(
|
||||
[
|
||||
dcc.Link(
|
||||
row[f"{org_type}_nom"],
|
||||
href=url + f"/{org_type}/{row[f'{org_type}_id']}",
|
||||
title=f"Marchés publics de {row[f'{org_type}_nom']}",
|
||||
),
|
||||
" ",
|
||||
dcc.Link(
|
||||
"(page dédiée)",
|
||||
href=f"/{org_type}s/{row[f'{org_type}_id']}",
|
||||
title=f"Page dédiée aux marchés publics de {row[f'{org_type}_nom']}",
|
||||
),
|
||||
]
|
||||
)
|
||||
link_list.append(li)
|
||||
return link_list
|
||||
|
||||
content = [
|
||||
html.H3("Acheteurs publics du département"),
|
||||
html.Ul(make_link_list("acheteur")),
|
||||
html.H3("Titulaires du département"),
|
||||
html.Ul(make_link_list("titulaire")),
|
||||
]
|
||||
|
||||
return content
|
||||
@@ -0,0 +1,25 @@
|
||||
from dash import dcc, html, register_page
|
||||
|
||||
from src.utils import departements
|
||||
|
||||
name = "Départements"
|
||||
|
||||
register_page(
|
||||
__name__,
|
||||
path="/departements",
|
||||
title="Marchés par département | decp.info",
|
||||
name="Départements",
|
||||
description="Tous les marchés publics, classés par départements",
|
||||
)
|
||||
|
||||
layout = html.Div(
|
||||
[
|
||||
html.H3("Départements"),
|
||||
html.Ul(
|
||||
[
|
||||
html.Li(dcc.Link(d["departement"], href=f"/departements/{k}"))
|
||||
for k, d in departements.items()
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
import polars as pl
|
||||
from dash import Input, Output, callback, dcc, html, register_page
|
||||
|
||||
from src.utils import (
|
||||
df_acheteurs,
|
||||
df_acheteurs_marches,
|
||||
df_titulaires,
|
||||
df_titulaires_marches,
|
||||
)
|
||||
|
||||
name = "Liste des marchés publics"
|
||||
|
||||
|
||||
def make_org_nom_verbe(org_type, org_id) -> tuple:
|
||||
if org_type == "titulaire":
|
||||
df = df_titulaires
|
||||
verbe = "remportés"
|
||||
elif org_type == "acheteur":
|
||||
df = df_acheteurs
|
||||
verbe = "attribués"
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
org_nom = (
|
||||
df.filter(pl.col(f"{org_type}_id") == org_id).select(f"{org_type}_nom").item()
|
||||
)
|
||||
|
||||
return org_nom, verbe
|
||||
|
||||
|
||||
def get_title(code, org_type, org_id):
|
||||
org_nom, verbe = make_org_nom_verbe(org_type, org_id)
|
||||
|
||||
return f"Marchés publics {verbe} par {org_nom} | decp.info"
|
||||
|
||||
|
||||
def get_description(code, org_type, org_id):
|
||||
org_nom, verbe = make_org_nom_verbe(org_type, org_id)
|
||||
|
||||
return f"Liste complète des marchés publics {verbe} par {org_nom} et publiés par decp.info. Cliquez sur les liens pour consulter les détails de chaque marché."
|
||||
|
||||
|
||||
register_page(
|
||||
__name__,
|
||||
path_template="/departements/<code>/<org_type>/<org_id>",
|
||||
title=get_title,
|
||||
description=get_description,
|
||||
order=40,
|
||||
name=name,
|
||||
)
|
||||
|
||||
layout = html.Div(
|
||||
[
|
||||
dcc.Location(id="liste_marches_url", refresh="callback-nav"),
|
||||
html.Div(id="liste_marches"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@callback(
|
||||
Output(component_id="liste_marches", component_property="children"),
|
||||
Input(component_id="liste_marches_url", component_property="pathname"),
|
||||
)
|
||||
def liste_marches(url):
|
||||
org_type = url.split("/")[-2]
|
||||
org_id = url.split("/")[-1]
|
||||
|
||||
def make_link_list() -> list:
|
||||
link_list = []
|
||||
if org_type == "acheteur":
|
||||
df = df_acheteurs_marches
|
||||
elif org_type == "titulaire":
|
||||
df = df_titulaires_marches
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
df = df.filter(pl.col(f"{org_type}_id") == org_id)
|
||||
|
||||
for row in df.iter_rows(named=True):
|
||||
li = html.Li(
|
||||
[
|
||||
dcc.Link(
|
||||
row["objet"],
|
||||
href=f"/marches/{row['uid']}",
|
||||
title=f"Marchés public attribué : {row['objet']}",
|
||||
)
|
||||
]
|
||||
)
|
||||
link_list.append(li)
|
||||
return link_list
|
||||
|
||||
nom, verbe = make_org_nom_verbe(org_type, org_id)
|
||||
|
||||
content = [
|
||||
html.H3(f"Marchés publics {verbe} par {nom}"),
|
||||
html.Ul(make_link_list()),
|
||||
]
|
||||
|
||||
return content
|
||||
+5
-6
@@ -34,9 +34,7 @@ layout = [
|
||||
dcc.Store(id="marche_data"),
|
||||
dcc.Store(id="titulaires_data"),
|
||||
dcc.Location(id="marche_url", refresh="callback-nav"),
|
||||
html.Script(
|
||||
type="application/ld+json", id="marche_jsonld", children=['{"test": "1"}']
|
||||
),
|
||||
html.Script(type="application/ld+json", id="marche_jsonld"),
|
||||
dbc.Container(
|
||||
className="marche_infos",
|
||||
children=[
|
||||
@@ -114,7 +112,7 @@ def get_marche_data(url) -> tuple[dict, list]:
|
||||
Input("titulaires_data", "data"),
|
||||
)
|
||||
def update_marche_info(marche, titulaires):
|
||||
def make_parameter(col):
|
||||
def make_parameter(col, bold=True):
|
||||
column_object = data_schema.get(col)
|
||||
column_name = column_object.get("title") if column_object else col
|
||||
|
||||
@@ -158,10 +156,11 @@ def update_marche_info(marche, titulaires):
|
||||
else:
|
||||
value = ""
|
||||
|
||||
param_content = html.P([column_name, " : ", html.Strong(value)])
|
||||
value = html.Strong(value) if bold else value
|
||||
param_content = html.P([column_name, " : ", value])
|
||||
return param_content
|
||||
|
||||
marche_objet = make_parameter("objet")
|
||||
marche_objet = make_parameter("objet", bold=False)
|
||||
|
||||
marche_infos = [
|
||||
make_parameter("id"),
|
||||
|
||||
+60
-2
@@ -18,8 +18,9 @@ from src.utils import (
|
||||
)
|
||||
from utils import prepare_table_data
|
||||
|
||||
update_date = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH"))
|
||||
update_date = datetime.fromtimestamp(update_date).strftime("%d/%m/%Y")
|
||||
update_date_timestamp = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH"))
|
||||
update_date = datetime.fromtimestamp(update_date_timestamp).strftime("%d/%m/%Y")
|
||||
update_date_iso = datetime.fromtimestamp(update_date_timestamp).isoformat()
|
||||
|
||||
|
||||
name = "Tableau"
|
||||
@@ -48,6 +49,63 @@ datatable = html.Div(
|
||||
|
||||
layout = [
|
||||
dcc.Location(id="tableau_url", refresh=False),
|
||||
html.Script(
|
||||
type="application/ld+json",
|
||||
id="dataset_jsonld",
|
||||
children=[
|
||||
json.dumps(
|
||||
{
|
||||
"@context": "https://schema.org/",
|
||||
"@type": "Dataset",
|
||||
"name": "Données essentielles des marchés publics français (DECP)",
|
||||
"description": "Données de marchés publics exhaustives décrivant les marchés publics attribués en France depuis 2018.",
|
||||
"url": "https://decp.info",
|
||||
"sameAs": "https://www.data.gouv.fr/datasets/608c055b35eb4e6ee20eb325",
|
||||
"keywords": [
|
||||
"marchés publics",
|
||||
"commande publique",
|
||||
"decp",
|
||||
"public procurement",
|
||||
],
|
||||
"license": "https://www.etalab.gouv.fr/licence-ouverte-open-licence",
|
||||
"isAccessibleForFree": True,
|
||||
"creator": {
|
||||
"@type": "Organization",
|
||||
"url": "https://colmo.tech",
|
||||
"name": "Colmo",
|
||||
"sameAs": "https://annuaire-entreprises.data.gouv.fr/entreprise/colmo-989393350",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"contactType": "Support et contact commercial",
|
||||
"email": "colin@colmo.tech",
|
||||
},
|
||||
},
|
||||
"includedInDataCatalog": {
|
||||
"@type": "DataCatalog",
|
||||
"name": "data.gouv.fr",
|
||||
},
|
||||
"distribution": [
|
||||
{
|
||||
"@type": "DataDownload",
|
||||
"encodingFormat": "CSV",
|
||||
"contentUrl": "https://www.data.gouv.fr/api/1/datasets/r/22847056-61df-452d-837d-8b8ceadbfc52",
|
||||
},
|
||||
{
|
||||
"@type": "DataDownload",
|
||||
"encodingFormat": "Parquet",
|
||||
"contentUrl": "https://www.data.gouv.fr/api/1/datasets/r/11cea8e8-df3e-4ed1-932b-781e2635e432",
|
||||
},
|
||||
],
|
||||
"temporalCoverage": f"2018-01-01/{update_date_iso[:10]}",
|
||||
"spatialCoverage": {
|
||||
"@type": "Place",
|
||||
"address": {"countryCode": "FR"},
|
||||
},
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
],
|
||||
),
|
||||
html.Div(
|
||||
html.Details(
|
||||
children=[
|
||||
|
||||
@@ -7,6 +7,7 @@ from src.callbacks import get_top_org_table
|
||||
from src.figures import DataTable, point_on_map
|
||||
from src.utils import (
|
||||
df,
|
||||
df_titulaires,
|
||||
filter_table_data,
|
||||
format_number,
|
||||
get_annuaire_data,
|
||||
@@ -20,7 +21,12 @@ from src.utils import (
|
||||
|
||||
|
||||
def get_title(titulaire_id: str = None) -> str:
|
||||
return f"Titulaire {titulaire_id} | decp.info"
|
||||
titulaire_nom = (
|
||||
df_titulaires.filter(pl.col("titulaire_id") == titulaire_id)
|
||||
.select("titulaire_nom")
|
||||
.item()
|
||||
)
|
||||
return f"Marchés publics remportés par {titulaire_nom} | decp.info"
|
||||
|
||||
|
||||
register_page(
|
||||
|
||||
+44
-24
@@ -145,11 +145,12 @@ def format_number(number) -> str:
|
||||
return number
|
||||
|
||||
|
||||
def unformat_montant(number: str) -> int:
|
||||
def unformat_montant(number: str) -> float:
|
||||
number = number.replace(" €", "")
|
||||
number = number.replace(" €", "").replace(" ", "")
|
||||
number = number.replace(",", ".")
|
||||
number = number.strip()
|
||||
return int(number)
|
||||
return float(number)
|
||||
|
||||
|
||||
def format_values(dff: pl.DataFrame) -> pl.DataFrame:
|
||||
@@ -701,27 +702,6 @@ def invert_columns(columns):
|
||||
return inverted_columns
|
||||
|
||||
|
||||
df: pl.DataFrame = get_decp_data()
|
||||
schema = df.collect_schema()
|
||||
|
||||
df_acheteurs = get_org_data(df, "acheteur")
|
||||
df_titulaires = get_org_data(df, "titulaire")
|
||||
|
||||
departements = get_departements()
|
||||
domain_name = (
|
||||
"test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info"
|
||||
)
|
||||
meta_content = {
|
||||
"image_url": f"https://{domain_name}/assets/decp.info.png",
|
||||
"title": "decp.info - exploration des marchés publics français",
|
||||
"description": (
|
||||
"Explorez et analysez les données des marchés publics français avec cet outil libre et gratuit. "
|
||||
"Pour une commande publique accessible à toutes et tous."
|
||||
),
|
||||
}
|
||||
data_schema = get_data_schema()
|
||||
|
||||
|
||||
def make_org_jsonld(org_id, org_type, org_name=None, type_org_id="SIRET") -> dict:
|
||||
org_types = {"acheteur": "GovernmentOrganization", "titulaire": "Organization"}
|
||||
address = None
|
||||
@@ -747,7 +727,8 @@ def make_org_jsonld(org_id, org_type, org_name=None, type_org_id="SIRET") -> dic
|
||||
jsonld = {
|
||||
"@type": org_types[org_type],
|
||||
"name": org_name,
|
||||
"url": f"https://decp.info/titulaires/{org_id}",
|
||||
"url": f"https://decp.info/{org_type}s/{org_id}",
|
||||
"sameAs": f"https://annuaire-entreprises.data.gouv.fr/etablissement/{org_id}",
|
||||
"identifier": {
|
||||
"@type": "PropertyValue",
|
||||
"propertyID": type_org_id.lower(),
|
||||
@@ -759,3 +740,42 @@ def make_org_jsonld(org_id, org_type, org_name=None, type_org_id="SIRET") -> dic
|
||||
jsonld["address"] = address
|
||||
|
||||
return jsonld
|
||||
|
||||
|
||||
df: pl.DataFrame = get_decp_data()
|
||||
schema = df.collect_schema()
|
||||
|
||||
df_acheteurs = get_org_data(df, "acheteur")
|
||||
df_titulaires = get_org_data(df, "titulaire")
|
||||
df_acheteurs_departement: pl.DataFrame = (
|
||||
df_acheteurs.select(["acheteur_id", "acheteur_nom", "acheteur_departement_code"])
|
||||
.unique()
|
||||
.sort("acheteur_nom")
|
||||
)
|
||||
df_titulaires_departement: pl.DataFrame = (
|
||||
df_titulaires.select(
|
||||
["titulaire_id", "titulaire_nom", "titulaire_departement_code"]
|
||||
)
|
||||
.unique()
|
||||
.sort("titulaire_nom")
|
||||
)
|
||||
df_acheteurs_marches: pl.DataFrame = (
|
||||
df.select("uid", "objet", "acheteur_id").unique().sort("acheteur_id")
|
||||
)
|
||||
df_titulaires_marches: pl.DataFrame = (
|
||||
df.select("uid", "objet", "titulaire_id").unique().sort("titulaire_id")
|
||||
)
|
||||
|
||||
departements = get_departements()
|
||||
domain_name = (
|
||||
"test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info"
|
||||
)
|
||||
meta_content = {
|
||||
"image_url": f"https://{domain_name}/assets/decp.info.png",
|
||||
"title": "decp.info - exploration des marchés publics français",
|
||||
"description": (
|
||||
"Explorez et analysez les données des marchés publics français avec cet outil libre et gratuit. "
|
||||
"Pour une commande publique accessible à toutes et tous."
|
||||
),
|
||||
}
|
||||
data_schema = get_data_schema()
|
||||
|
||||
Reference in New Issue
Block a user