Compare commits

..

8 Commits

Author SHA1 Message Date
Colin Maudry 2f3ce6a467 Amélioration du message d'annonce 2025-11-11 09:04:11 +01:00
Colin Maudry efd9bd4238 Merge branch 'hotfix/2.1.7' 2025-11-11 08:39:58 +01:00
Colin Maudry 39844071ad Changelog 2.1.7 2025-11-11 08:39:41 +01:00
Colin Maudry 5c64177c6a Remplacement du formulaire de contact par email 2025-11-11 08:37:47 +01:00
Colin Maudry ee91e7a91f Merge branch 'hotfix/2.1.6' 2025-10-15 19:44:09 +02:00
Colin Maudry 392f862baa Changelog 2.1.6 2025-10-15 19:44:02 +02:00
Colin Maudry 199476ec30 Stabilisation affichage fiche marché #40 2025-10-15 19:42:53 +02:00
Colin Maudry c98a36f759 Fix fiche marche 2025-10-14 12:54:47 +02:00
7 changed files with 143 additions and 130 deletions
+9 -1
View File
@@ -1,6 +1,6 @@
# decp.info # decp.info
> v2.1.4 > v2.1.7
Outil d'exploration et de téléchargement des données essentielles de la commande publique. Outil d'exploration et de téléchargement des données essentielles de la commande publique.
@@ -38,6 +38,14 @@ Ne pas oublier de mettre à jour les fichier .env.
## Notes de version ## Notes de version
##### 2.1.7 (11 novembre 2025)
- Remplacement du formulaire de contact par une adresse email
##### 2.1.6 (15 octobre 2025)
- Stabilisation de la vue marché
##### 2.1.5 (10 octobre 2025) ##### 2.1.5 (10 octobre 2025)
- réparation des filtres (notamment < > sur les montants) - réparation des filtres (notamment < > sur les montants)
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "decp.info" name = "decp.info"
description = "Interface d'exploration et d'analyse des marchés publics français." description = "Interface d'exploration et d'analyse des marchés publics français."
version = "2.1.5" version = "2.1.7"
requires-python = ">= 3.10" requires-python = ">= 3.10"
authors = [ authors = [
{ name = "Colin Maudry", email = "colin+decp@maudry.com" } { name = "Colin Maudry", email = "colin+decp@maudry.com" }
+6
View File
@@ -90,6 +90,12 @@ app.layout = html.Div(
], ],
className="logo", className="logo",
), ),
html.Div(
id="announcements",
children=dcc.Markdown(
"️ Le formulaire de contact, défectueux, a été remplacé par [une adresse email](/contact)."
),
),
html.Div( html.Div(
[ [
dcc.Link( dcc.Link(
+7
View File
@@ -142,6 +142,13 @@ td[data-dash-column="objet"] {
margin: 0 0 20px 20px; margin: 0 0 20px 20px;
} }
/* Annonces */
#announcements {
margin-top: 25px;
max-width: 30%;
font-size: 90%;
}
/* Menu de navigation */ /* Menu de navigation */
.navbar { .navbar {
+117 -120
View File
@@ -1,11 +1,7 @@
import os
import re import re
import smtplib
import time 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 dash import dcc, html, register_page
from flask import request from flask import request
from src.utils import meta_content from src.utils import meta_content
@@ -25,51 +21,52 @@ layout = html.Div(
className="container", className="container",
children=[ children=[
html.H2("Contact", id="contact"), html.H2("Contact", id="contact"),
html.P( # html.P(
"Votre message arrivera directement dans ma boîte mail, et je reviendrai vers vous rapidement." # "Votre message arrivera directement dans ma boîte mail, et je reviendrai vers vous rapidement."
), # ),
html.Div( # html.Div(
[ # [
html.Label("Votre nom"), # html.Label("Votre nom"),
dcc.Input( # dcc.Input(
id="input-name", # id="input-name",
type="text", # type="text",
style={"width": "100%", "padding": "8px", "margin": "8px 0"}, # style={"width": "100%", "padding": "8px", "margin": "8px 0"},
), # ),
html.Label("Votre adresse email"), # html.Label("Votre adresse email"),
dcc.Input( # dcc.Input(
id="input-email", # id="input-email",
type="email", # type="email",
style={"width": "100%", "padding": "8px", "margin": "8px 0"}, # style={"width": "100%", "padding": "8px", "margin": "8px 0"},
), # ),
html.Label("Message"), # html.Label("Message"),
dcc.Textarea( # dcc.Textarea(
id="input-message", # id="input-message",
style={ # style={
"width": "100%", # "width": "100%",
"height": 200, # "height": 200,
"padding": "8px", # "padding": "8px",
"margin": "8px 0", # "margin": "8px 0",
}, # },
), # ),
html.Button( # html.Button(
"Envoyer ", # "Envoyer ",
id="submit-button", # id="submit-button",
n_clicks=0, # n_clicks=0,
style={"marginTop": "10px"}, # style={"marginTop": "10px"},
), # ),
html.Div( # html.Div(
id="output-message", style={"marginTop": "10px", "color": "green"} # id="output-message", style={"marginTop": "10px", "color": "green"}
), # ),
], # ],
style={ # style={
"maxWidth": "600px", # "maxWidth": "600px",
"margin": "auto", # "margin": "auto",
"padding": "20px", # "padding": "20px",
"lineHeight": "20px", # "lineHeight": "20px",
}, # },
), # ),
dcc.Markdown(""" dcc.Markdown("""
- Email : [colin+decp@maudry.com](mailto:colin+decp@maudry.com)
- Bluesky : [@col1m.bsky.social](https://bsky.app/profile/col1m.bsky.social) - Bluesky : [@col1m.bsky.social](https://bsky.app/profile/col1m.bsky.social)
- Mastodon : [col1m@mamot.fr](https://mamot.fr/@col1m) - Mastodon : [col1m@mamot.fr](https://mamot.fr/@col1m)
- LinkedIn : [colinmaudry](https://www.linkedin.com/in/colinmaudry/) - LinkedIn : [colinmaudry](https://www.linkedin.com/in/colinmaudry/)
@@ -97,74 +94,74 @@ def sanitize_email(email):
return re.match(r"[^@]+@[^@]+\.[^@]+", email) return re.match(r"[^@]+@[^@]+\.[^@]+", email)
@callback( # @callback(
Output("output-message", "children"), # Output("output-message", "children"),
Input("submit-button", "n_clicks"), # Input("submit-button", "n_clicks"),
State("input-name", "value"), # State("input-name", "value"),
State("input-email", "value"), # State("input-email", "value"),
State("input-message", "value"), # State("input-message", "value"),
prevent_initial_call=True, # prevent_initial_call=True,
) # )
def send_email(n_clicks, form_name, form_email, form_message): # def send_email(n_clicks, form_name, form_email, form_message):
if not all([form_name, form_email, form_message]): # if not all([form_name, form_email, form_message]):
return html.Div( # return html.Div(
"Veuillez s'il vous plaît remplir tous les champs.", style={"color": "red"} # "Veuillez s'il vous plaît remplir tous les champs.", style={"color": "red"}
) # )
#
client_ip = request.remote_addr # client_ip = request.remote_addr
if is_rate_limited(): # if is_rate_limited():
wait_time = int(RATE_LIMIT_WINDOW - (time.time() - rate_limit_store[client_ip])) # wait_time = int(RATE_LIMIT_WINDOW - (time.time() - rate_limit_store[client_ip]))
return html.Div( # 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"⏳ 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.", # f"Veuillez s'il vous plaît attendre {wait_time} secondes avant de renvoyer un message.",
style={"color": "black"}, # style={"color": "black"},
) # )
#
try: # try:
# Configuration du serveur SMTP # # Configuration du serveur SMTP
smtp_server = os.getenv("SENDER_SERVER_DOMAIN") # smtp_server = os.getenv("SENDER_SERVER_DOMAIN")
smtp_port = 587 # smtp_port = 587
login_email = os.getenv("LOGIN_EMAIL") # login_email = os.getenv("LOGIN_EMAIL")
from_email = os.getenv("FROM_EMAIL") # from_email = os.getenv("FROM_EMAIL")
to_email = os.getenv("TO_EMAIL", from_email) # to_email = os.getenv("TO_EMAIL", from_email)
login_password = os.getenv("LOGIN_PASSWORD") # login_password = os.getenv("LOGIN_PASSWORD")
#
print( # print(
smtp_server, # smtp_server,
smtp_port, # smtp_port,
login_email, # login_email,
from_email, # from_email,
to_email, # to_email,
login_password, # login_password,
sep="\n", # sep="\n",
) # )
#
# Création de l'email # # Création de l'email
email = MIMEMultipart() # email = MIMEMultipart()
email["From"] = from_email # email["From"] = from_email
email["To"] = to_email # email["To"] = to_email
email["Subject"] = f"[decp.info] Message de {form_name}" # email["Subject"] = f"[decp.info] Message de {form_name}"
#
body = f""" # body = f"""
Nom : {form_name} # Nom : {form_name}
Email : {form_email} # Email : {form_email}
#
Message : # Message :
{form_message} # {form_message}
""" # """
email.attach(MIMEText(body, "plain")) # email.attach(MIMEText(body, "plain"))
#
# Send email # # Send email
server = smtplib.SMTP(smtp_server, smtp_port) # server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # server.starttls()
server.login(login_email, login_password) # server.login(login_email, login_password)
server.sendmail(from_email, to_email, email.as_string()) # server.sendmail(from_email, to_email, email.as_string())
server.quit() # server.quit()
#
return html.Div("✅ Envoi réussi", style={"color": "green"}) # return html.Div("✅ Envoi réussi", style={"color": "green"})
#
except Exception as e: # except Exception as e:
print(e) # print(e)
return html.Div( # return html.Div(
f"❌ Échec de l'envoi du message : {str(e)}", style={"color": "red"} # f"❌ Échec de l'envoi du message : {str(e)}", style={"color": "red"}
) # )
+2 -8
View File
@@ -66,6 +66,7 @@ def get_marche_data(url) -> tuple[dict, list]:
marche_uid = url.split("/")[-1] marche_uid = url.split("/")[-1]
# Récupération des données du marché à partir du df global # Récupération des données du marché à partir du df global
lff = df.lazy() lff = df.lazy()
lff = lff.filter(pl.col("uid") == pl.lit(marche_uid)) lff = lff.filter(pl.col("uid") == pl.lit(marche_uid))
@@ -73,13 +74,9 @@ def get_marche_data(url) -> tuple[dict, list]:
dff_titulaires = lff.select(cs.starts_with("titulaire")).collect(engine="streaming") dff_titulaires = lff.select(cs.starts_with("titulaire")).collect(engine="streaming")
# Données du marché # Données du marché
dff_marche = ( dff_marche = lff.unique("uid").collect(engine="streaming")
lff.select(~cs.starts_with("titulaires")).unique().collect(engine="streaming")
)
dff_marche = format_montant(dff_marche) dff_marche = format_montant(dff_marche)
assert dff_marche.height == 1
return dff_marche.to_dicts()[0], dff_titulaires.to_dicts() return dff_marche.to_dicts()[0], dff_titulaires.to_dicts()
@@ -93,7 +90,6 @@ def get_marche_data(url) -> tuple[dict, list]:
def update_marche_info(marche, titulaires): def update_marche_info(marche, titulaires):
def make_parameter(col): def make_parameter(col):
column_object = data_schema.get(col) column_object = data_schema.get(col)
print(column_object)
column_name = column_object.get("title") if column_object else col column_name = column_object.get("title") if column_object else col
if marche[col]: if marche[col]:
@@ -110,8 +106,6 @@ def update_marche_info(marche, titulaires):
# Dates # Dates
elif col in ["dateNotification", "datePublicationDonnees"]: elif col in ["dateNotification", "datePublicationDonnees"]:
print(marche[col])
value = datetime.fromisoformat(marche[col]).strftime("%d/%m/%Y") value = datetime.fromisoformat(marche[col]).strftime("%d/%m/%Y")
# Listes # Listes
+1
View File
@@ -126,6 +126,7 @@ def format_number(number) -> str:
def format_montant(dff: pl.DataFrame) -> pl.DataFrame: def format_montant(dff: pl.DataFrame) -> pl.DataFrame:
def format_function(expr, scale=None): def format_function(expr, scale=None):
# https://stackoverflow.com/a/78636786 # https://stackoverflow.com/a/78636786
expr = expr.cast(pl.String)
expr = expr.str.splitn(".", 2) expr = expr.str.splitn(".", 2)
num = expr.struct[0] num = expr.struct[0]