242 lines
8.4 KiB
Python
242 lines
8.4 KiB
Python
import math
|
|
|
|
import dash_bootstrap_components as dbc
|
|
from dash import Input, Output, State, callback, dcc, html, register_page
|
|
|
|
from src.db import count_unique_marches
|
|
from src.figures import DataTable
|
|
from src.pages.a_propos.abonnement import abonnement_features
|
|
from src.utils.cache import cache
|
|
from src.utils.data import DF_ACHETEURS, DF_TITULAIRES
|
|
from src.utils.search import search_org
|
|
from src.utils.seo import META_CONTENT
|
|
from src.utils.table import format_number, setup_table_columns
|
|
|
|
NAME = "Recherche"
|
|
|
|
_STAT_STYLE = {"fontWeight": "bold", "color": "var(--primary-color-text)"}
|
|
|
|
|
|
@cache.memoize(timeout=12 * 60 * 60)
|
|
def _tagline_stats() -> tuple[str, str, str]:
|
|
nb_acheteurs = format_number(round(DF_ACHETEURS.height, -3))
|
|
nb_titulaires = format_number(round(DF_TITULAIRES.height, -3))
|
|
nb_marches_millions = math.floor(count_unique_marches() / 100_000) / 10
|
|
unite_marches = "million" if nb_marches_millions < 2 else "millions"
|
|
nb_marches = f"{nb_marches_millions:.1f}".replace(".", ",") + " " + unite_marches
|
|
return nb_acheteurs, nb_titulaires, nb_marches
|
|
|
|
|
|
def _tagline() -> html.P:
|
|
nb_acheteurs, nb_titulaires, nb_marches = _tagline_stats()
|
|
return html.P(
|
|
[
|
|
"Recherchez parmi ",
|
|
html.Span(nb_acheteurs, style=_STAT_STYLE),
|
|
" acheteurs et ",
|
|
html.Span(nb_titulaires, style=_STAT_STYLE),
|
|
" titulaires dans une base de données de plus de ",
|
|
html.Span(nb_marches, style=_STAT_STYLE),
|
|
" de marchés publics",
|
|
]
|
|
)
|
|
|
|
|
|
_features_gratuites = dcc.Markdown("""
|
|
- Recherche d'acheteurs et de titulaires
|
|
- [Tableau](/tableau) filtrable et personnalisable sur des dizaines de colonnes et exports Excel
|
|
- Fiches détaillées : acheteur, titulaire, marché
|
|
- Cartes, statistiques et [observatoire](/observatoire) national personnalisable
|
|
- 100 % open source et Open Data
|
|
""")
|
|
|
|
home_intro = html.Div(
|
|
id="home_intro",
|
|
className="container",
|
|
style={"maxWidth": "900px", "marginTop": "1rem"},
|
|
children=[
|
|
html.P(
|
|
[
|
|
html.Span(
|
|
"colibre",
|
|
style={
|
|
"fontWeight": 600,
|
|
"fontSize": "1.5rem",
|
|
"color": "var(--primary-color-text)",
|
|
},
|
|
),
|
|
" ouvre les données des marchés publics français à toutes et "
|
|
"tous : recherchez, filtrez, cartographiez, téléchargez.",
|
|
],
|
|
className="text-center",
|
|
style={
|
|
"maxWidth": "750px",
|
|
"fontSize": "1.5rem",
|
|
"margin": "6rem auto 1.5rem",
|
|
},
|
|
),
|
|
dbc.Row(
|
|
[
|
|
dbc.Col(
|
|
[html.H4("✅ Gratuit, sans compte"), _features_gratuites],
|
|
md=6,
|
|
style={
|
|
"borderRight": "1px solid var(--bs-border-color)",
|
|
"paddingRight": "2rem",
|
|
},
|
|
),
|
|
dbc.Col(
|
|
[
|
|
html.H4("⭐ Avec un abonnement"),
|
|
abonnement_features,
|
|
dcc.Link(
|
|
"En savoir plus et s'abonner",
|
|
href="/a-propos/abonnement",
|
|
style={"marginLeft": "20px"},
|
|
),
|
|
],
|
|
md=6,
|
|
style={"paddingLeft": "2rem"},
|
|
),
|
|
],
|
|
className="align-items-start",
|
|
),
|
|
],
|
|
)
|
|
|
|
register_page(
|
|
__name__,
|
|
path="/",
|
|
title="Outils pour l'exploration des marchés publics | colibre",
|
|
name=NAME,
|
|
description="Explorez et analysez les données des marchés publics français. Pour une commande publique accessible à toutes et tous.",
|
|
image_url=META_CONTENT["image_url"],
|
|
order=0,
|
|
)
|
|
|
|
|
|
def layout(**_):
|
|
return html.Div(
|
|
className="container",
|
|
children=[
|
|
html.Div(
|
|
className="tagline",
|
|
children=_tagline(),
|
|
),
|
|
html.Div(
|
|
style={
|
|
"display": "flex",
|
|
"justifyContent": "center",
|
|
"marginTop": "30px",
|
|
"marginBottom": "30px",
|
|
},
|
|
children=[
|
|
dcc.Input(
|
|
id="search",
|
|
type="text",
|
|
placeholder="Nom d'acheteur/entreprise, SIREN/SIRET, code département",
|
|
autoFocus=True,
|
|
style={
|
|
"margin": "0",
|
|
"width": "500px",
|
|
"border": "1px solid #ccc",
|
|
"borderRight": "none",
|
|
"borderRadius": "3px 0 0 3px",
|
|
"padding": "5px 10px",
|
|
"outline": "none",
|
|
"height": "34px",
|
|
},
|
|
),
|
|
html.Button(
|
|
"=>",
|
|
id="search-button",
|
|
className="btn btn-secondary",
|
|
style={
|
|
"border": "1px solid #ccc",
|
|
"borderRadius": "0 3px 3px 0",
|
|
"marginLeft": "0",
|
|
"height": "34px",
|
|
# Le padding vertical (.375rem) et le line-height
|
|
# (1.5) de .btn dépassent la hauteur fixe de 34px :
|
|
# le glyphe débordait vers le bas. On centre en flex
|
|
# et on neutralise le padding vertical.
|
|
"display": "flex",
|
|
"alignItems": "center",
|
|
"justifyContent": "center",
|
|
"paddingTop": "0",
|
|
"paddingBottom": "0",
|
|
"lineHeight": "1",
|
|
},
|
|
),
|
|
],
|
|
),
|
|
html.P(
|
|
[
|
|
"...ou bien filtrez les marchés publics dans la vue ",
|
|
dcc.Link("Tableau", href="/tableau"),
|
|
],
|
|
style={"textAlign": "center"},
|
|
id="mention_tableau",
|
|
),
|
|
home_intro,
|
|
# html.Div(
|
|
# className="search_options",
|
|
# children=[dcc.RadioItems(options=["Acheteur(s)"])],
|
|
# ),
|
|
dbc.Row(id="search_results"),
|
|
],
|
|
)
|
|
|
|
|
|
@callback(
|
|
Output("search_results", "children"),
|
|
Output("mention_tableau", "style"),
|
|
Output("home_intro", "style"),
|
|
Input("search", "n_submit"),
|
|
Input("search-button", "n_clicks"),
|
|
State("search", "value"),
|
|
prevent_initial_call=True,
|
|
)
|
|
def update_search_results(n_submit, n_clicks, query):
|
|
if query and len(query) >= 1:
|
|
cols = []
|
|
|
|
for org_type in ["acheteur", "titulaire"]:
|
|
if org_type == "acheteur":
|
|
dff = DF_ACHETEURS
|
|
elif org_type == "titulaire":
|
|
dff = DF_TITULAIRES
|
|
else:
|
|
raise ValueError(f"{org_type} is not supported")
|
|
|
|
# Search acheteurs and titulaires using the same function
|
|
results = search_org(dff, query, org_type=org_type)
|
|
count = results.height
|
|
|
|
# Format output
|
|
columns, tooltip = setup_table_columns(results, hideable=False)
|
|
|
|
col = (
|
|
dbc.Col(
|
|
children=[
|
|
html.H3(f"{org_type.title()}s : {count}"),
|
|
DataTable(
|
|
dtid=f"results_{org_type}_datatable",
|
|
columns=columns,
|
|
data=results.to_dicts(),
|
|
page_size=10,
|
|
sort_action="none",
|
|
filter_action="none",
|
|
),
|
|
],
|
|
md=6,
|
|
)
|
|
if count > 0
|
|
else html.P(f"Aucun {org_type} trouvé.")
|
|
)
|
|
cols.append(col)
|
|
|
|
style = {"textAlign": "center", "display": "none"}
|
|
return cols, style, {"display": "none"}
|
|
return html.P(""), {"textAlign": "center"}, home_intro.style
|