Skip to content

Commit 7ed68d0

Browse files
Export page (#253)
* add download archive * get cam info
1 parent 538ca61 commit 7ed68d0

File tree

6 files changed

+224
-0
lines changed

6 files changed

+224
-0
lines changed

app/callbacks/display_callbacks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ def update_live_stream_button(lang):
5454
return [translate("live_stream", lang)]
5555

5656

57+
@app.callback(Output("export", "children"), Input("language", "data"))
58+
def update_export_button(lang):
59+
return translate("export", lang)
60+
61+
5762
@app.callback(
5863
Output("start-live-stream", "children"),
5964
Input("language", "data"),

app/callbacks/export_callbacks.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright (C) 2023-2025, Pyronear.
2+
3+
# This program is licensed under the Apache License 2.0.
4+
# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
5+
6+
from io import StringIO
7+
8+
import logging_config
9+
import pandas as pd
10+
from dash import Input, Output, State, dcc
11+
from dash.dependencies import Input, Output
12+
from dash.exceptions import PreventUpdate
13+
from main import app
14+
from translations import translate
15+
16+
import config as cfg
17+
from services import get_client
18+
19+
logger = logging_config.configure_logging(cfg.DEBUG, cfg.SENTRY_DSN)
20+
21+
22+
@app.callback(
23+
Output("export-status-text", "children"),
24+
Output("export-trigger", "data"),
25+
Input("export-button", "n_clicks"),
26+
State("language", "data"),
27+
prevent_initial_call=True,
28+
)
29+
def trigger_export(n_clicks, lang):
30+
return translate("downloading_in_progress", lang), True
31+
32+
33+
@app.callback(
34+
Output("export-download", "data"),
35+
Output("export-status-text-done", "children"),
36+
Input("export-trigger", "data"),
37+
State("export-start-date", "date"),
38+
State("export-end-date", "date"),
39+
State("user_token", "data"),
40+
State("language", "data"),
41+
State("api_cameras", "data"),
42+
prevent_initial_call=True,
43+
)
44+
def handle_export(trigger, start_date, end_date, user_token, lang, api_cameras):
45+
if not trigger or not user_token or not start_date or not end_date:
46+
raise PreventUpdate
47+
48+
try:
49+
cameras_df = pd.read_json(StringIO(api_cameras), orient="split")
50+
cameras_df = cameras_df.rename(columns={"id": "camera_id_metadata"})
51+
cameras_df = cameras_df[["camera_id_metadata", "name", "angle_of_view", "lat", "lon"]]
52+
except Exception as e:
53+
return None, f"Erreur lecture des caméras : {e}"
54+
55+
client = get_client(user_token)
56+
57+
try:
58+
all_sequences = []
59+
current = pd.to_datetime(start_date)
60+
final = pd.to_datetime(end_date)
61+
62+
while current <= final:
63+
response = client.fetch_sequences_from_date(current.strftime("%Y-%m-%d"), limit=100)
64+
daily_df = pd.DataFrame(response.json())
65+
if not daily_df.empty:
66+
all_sequences.append(daily_df)
67+
current += pd.Timedelta(days=1)
68+
69+
if not all_sequences:
70+
return None, translate("no_data_found", lang)
71+
72+
export_df = pd.concat(all_sequences, ignore_index=True)
73+
74+
if "camera_id" in export_df.columns:
75+
export_df = export_df.merge(cameras_df, how="left", left_on="camera_id", right_on="camera_id_metadata")
76+
export_df = export_df.drop(columns=["camera_id", "camera_id_metadata"])
77+
else:
78+
return None, "camera_id column missing", False
79+
80+
# Reorder columns
81+
ordered_columns = [
82+
"id",
83+
"name",
84+
"azimuth",
85+
"cone_angle",
86+
"started_at",
87+
"last_seen_at",
88+
"is_wildfire",
89+
"angle_of_view",
90+
"lat",
91+
"lon",
92+
]
93+
# Keep only those columns that exist
94+
export_df = export_df[[col for col in ordered_columns if col in export_df.columns]]
95+
96+
return dcc.send_data_frame(export_df.to_csv, "export.csv", index=False), translate("download_ready", lang)
97+
98+
except Exception as e:
99+
return None, str(e)
100+
101+
102+
@app.callback(Output("export-title", "children"), Input("language", "data"))
103+
def update_export_title(lang):
104+
return translate("export_title", lang)
105+
106+
107+
@app.callback(Output("export-start-date-label", "children"), Input("language", "data"))
108+
def update_export_start_label(lang):
109+
return translate("start_date", lang)
110+
111+
112+
@app.callback(Output("export-end-date-label", "children"), Input("language", "data"))
113+
def update_export_end_label(lang):
114+
return translate("end_date", lang)
115+
116+
117+
@app.callback(Output("export-button", "children"), Input("language", "data"))
118+
def update_export_button_text(lang):
119+
return translate("prepare_archive", lang)

app/components/navbar.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ def Navbar(lang="fr"):
7373
color="light",
7474
style={"fontSize": "16px"},
7575
),
76+
# 📤 Export
77+
dbc.Button(
78+
id="export-button-navbar",
79+
children=["📤 ", html.Span(id="export")],
80+
href="/export",
81+
color="light",
82+
style={"fontSize": "16px"},
83+
),
7684
# 🌐 Langues
7785
dcc.Dropdown(
7886
id="language-selector",

app/index.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import callbacks.data_callbacks
1111
import callbacks.display_callbacks
12+
import callbacks.export_callbacks
1213
import callbacks.live_callbacks
1314
import dash
1415
import logging_config
@@ -20,6 +21,7 @@
2021
import config as cfg
2122
from pages.blinking_alarm import blinking_alarm_layout
2223
from pages.cameras_status import cameras_status_layout
24+
from pages.export import export_layout
2325
from pages.homepage import homepage_layout
2426
from pages.live_stream import live_stream_layout
2527
from pages.login import login_layout
@@ -71,6 +73,9 @@ def display_page(
7173
if pathname == "/live-stream":
7274
return live_stream_layout(user_token, api_cameras, available_stream, lang=lang)
7375

76+
if pathname == "/export":
77+
return export_layout(lang=lang)
78+
7479
logger.warning("Unable to find page for pathname: %s", pathname)
7580
return html.Div([html.P("Unable to find this page.", className="alert alert-warning")])
7681

app/pages/export.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (C) 2020-2025, Pyronear.
2+
3+
# This program is licensed under the Apache License 2.0.
4+
# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
5+
6+
import dash_bootstrap_components as dbc
7+
from dash import dcc, html
8+
from translations import translate
9+
10+
11+
def export_layout(lang="fr"):
12+
return html.Div(
13+
[
14+
html.H2(translate("export_title", lang), id="export-title"),
15+
html.Div(
16+
[
17+
html.Div(
18+
[
19+
html.Label(translate("start_date", lang), id="export-start-date-label"),
20+
html.Div(
21+
dcc.DatePickerSingle(
22+
id="export-start-date",
23+
display_format="YYYY-MM-DD",
24+
),
25+
style={"marginTop": "5px", "marginBottom": "10px"},
26+
),
27+
],
28+
style={"marginRight": "20px"},
29+
),
30+
html.Div([
31+
html.Label(translate("end_date", lang), id="export-end-date-label"),
32+
html.Div(
33+
dcc.DatePickerSingle(
34+
id="export-end-date",
35+
display_format="YYYY-MM-DD",
36+
),
37+
style={"marginTop": "5px", "marginBottom": "10px"},
38+
),
39+
]),
40+
],
41+
style={"display": "flex", "justifyContent": "center", "marginBottom": "20px"},
42+
),
43+
dbc.Button(
44+
id="export-button",
45+
children=translate("download", lang),
46+
color="primary",
47+
className="mb-1",
48+
style={"marginBottom": "15px"},
49+
n_clicks=0,
50+
),
51+
html.Div(id="export-status-text", style={"marginBottom": "10px", "fontStyle": "italic"}),
52+
html.Div(id="export-status-text-done", style={"marginBottom": "10px", "fontStyle": "italic"}),
53+
dcc.Download(id="export-download"),
54+
dcc.Store(id="export-trigger", data=False),
55+
],
56+
style={"textAlign": "center"},
57+
)

app/translations.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"home": "Alertes",
1616
"live_stream": "Levée de doute",
1717
"datepicker": "Choisir une date",
18+
"export": "Exporter",
1819
# cameras status
1920
"breadcrumb": "Dashboard des caméras",
2021
"page_title": "Dashboard de l'état des caméras",
@@ -69,6 +70,15 @@
6970
"username_placeholder": "UTILISATEUR",
7071
"password_placeholder": "MOT DE PASSE",
7172
"login_button_text": "Connexion",
73+
# Export
74+
"export_title": "Exporter les données",
75+
"start_date": "Date de début",
76+
"end_date": "Date de fin",
77+
"prepare_archive": "Préparer l'archive",
78+
"download": "Télécharger",
79+
"downloading_in_progress": "Chargement en cours...",
80+
"download_ready": "Téléchargement prêt",
81+
"no_data_found": "Aucune donnée trouvée pour cette période.",
7282
},
7383
"es": {
7484
# data callbacks
@@ -80,6 +90,7 @@
8090
"home": "Alertas",
8191
"live_stream": "Transmisión en Vivo",
8292
"datepicker": "Elegir una fecha",
93+
"export": "Exportar",
8394
# cameras status
8495
"breadcrumb": "Panel de cámaras",
8596
"page_title": "Panel de control del estado de la cámara",
@@ -136,6 +147,15 @@
136147
"username_placeholder": "NOMBRE DE USUARIO",
137148
"password_placeholder": "CONTRASEÑA",
138149
"login_button_text": "Iniciar sesión",
150+
# Export
151+
"export_title": "Exportar datos",
152+
"start_date": "Fecha de inicio",
153+
"end_date": "Fecha de fin",
154+
"prepare_archive": "Preparar archivo",
155+
"download": "Descargar",
156+
"downloading_in_progress": "Cargando...",
157+
"download_ready": "Descarga lista",
158+
"no_data_found": "No se encontraron datos para este período.",
139159
},
140160
"en": {
141161
# data callbacks
@@ -147,6 +167,7 @@
147167
"home": "Alerts",
148168
"live_stream": "Live Stream",
149169
"datepicker": "Pick a date",
170+
"export": "Export",
150171
# cameras status
151172
"breadcrumb": "Camera Dashboard",
152173
"page_title": "Camera Status Dashboard",
@@ -201,6 +222,15 @@
201222
"username_placeholder": "USERNAME",
202223
"password_placeholder": "PASSWORD",
203224
"login_button_text": "Log In",
225+
# Export
226+
"export_title": "Export data",
227+
"start_date": "Start date",
228+
"end_date": "End date",
229+
"prepare_archive": "Prepare archive",
230+
"download": "Download",
231+
"downloading_in_progress": "Loading in progress...",
232+
"download_ready": "Download ready",
233+
"no_data_found": "No data found for this period.",
204234
},
205235
}
206236

0 commit comments

Comments
 (0)