ositah 25.6.dev1__py3-none-any.whl → 25.9.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ositah might be problematic. Click here for more details.
- ositah/app.py +17 -17
- ositah/apps/analysis.py +785 -785
- ositah/apps/configuration/callbacks.py +916 -916
- ositah/apps/configuration/main.py +546 -546
- ositah/apps/configuration/parameters.py +74 -74
- ositah/apps/configuration/tools.py +112 -112
- ositah/apps/export.py +1209 -1191
- ositah/apps/validation/callbacks.py +240 -240
- ositah/apps/validation/main.py +89 -89
- ositah/apps/validation/parameters.py +25 -25
- ositah/apps/validation/tables.py +646 -646
- ositah/apps/validation/tools.py +552 -552
- ositah/assets/arrow_down_up.svg +3 -3
- ositah/assets/ositah.css +53 -53
- ositah/assets/sort_ascending.svg +4 -4
- ositah/assets/sort_descending.svg +5 -5
- ositah/assets/sorttable.js +499 -499
- ositah/main.py +449 -449
- ositah/ositah.example.cfg +229 -229
- ositah/static/style.css +53 -53
- ositah/templates/base.html +22 -22
- ositah/templates/bootstrap_login.html +38 -38
- ositah/templates/login_form.html +26 -26
- ositah/utils/agents.py +124 -124
- ositah/utils/authentication.py +287 -287
- ositah/utils/cache.py +19 -19
- ositah/utils/core.py +13 -13
- ositah/utils/exceptions.py +64 -64
- ositah/utils/hito_db.py +51 -51
- ositah/utils/hito_db_model.py +253 -253
- ositah/utils/menus.py +339 -339
- ositah/utils/period.py +139 -139
- ositah/utils/projects.py +1179 -1178
- ositah/utils/teams.py +42 -42
- ositah/utils/utils.py +474 -474
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/METADATA +149 -150
- ositah-25.9.dev2.dist-info/RECORD +46 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/licenses/LICENSE +29 -29
- ositah-25.6.dev1.dist-info/RECORD +0 -46
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/WHEEL +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/entry_points.txt +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/top_level.txt +0 -0
ositah/utils/menus.py
CHANGED
|
@@ -1,339 +1,339 @@
|
|
|
1
|
-
# Module containing helper functions to build/manage the menus and graphic objects
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
|
|
5
|
-
import dash_bootstrap_components as dbc
|
|
6
|
-
from dash import dcc, html
|
|
7
|
-
from dash.dependencies import MATCH, Input, Output, State
|
|
8
|
-
|
|
9
|
-
from ositah.app import app
|
|
10
|
-
from ositah.utils.cache import clear_cached_data
|
|
11
|
-
from ositah.utils.exceptions import SessionDataMissing
|
|
12
|
-
from ositah.utils.period import get_declaration_periods, get_default_period_date
|
|
13
|
-
from ositah.utils.utils import GlobalParams, no_session_id_jumbotron
|
|
14
|
-
|
|
15
|
-
DATA_SELECTED_SOURCE_ID = "project-declaration-source"
|
|
16
|
-
DATA_SELECTION_SOURCE_ID = "project-declaration-source-button"
|
|
17
|
-
|
|
18
|
-
TEAM_SELECTED_VALUE_ID = "team-selected"
|
|
19
|
-
TEAM_SELECTION_MENU_ID = "team-selection-dropdown"
|
|
20
|
-
TEAM_SELECTION_DATE_ID = "team-selection-date"
|
|
21
|
-
|
|
22
|
-
VALIDATION_PERIOD_MENU_ID = "validation-period-dropdown"
|
|
23
|
-
VALIDATION_PERIOD_SELECTED_ID = "validation-period-selected"
|
|
24
|
-
|
|
25
|
-
# 'type' part of composite IDs
|
|
26
|
-
TABLE_TYPE_TABLE = "ositah-table"
|
|
27
|
-
TABLE_TYPE_DUMMY_STORE = "ositah-table-dummy-store"
|
|
28
|
-
|
|
29
|
-
LOAD_PROGRESS_BAR_ID = "validation-progress-bar"
|
|
30
|
-
LOAD_PROGRESS_BAR_INTERVAL_DURATION = 500 # Milliseconds
|
|
31
|
-
LOAD_PROGRESS_BAR_MAX_DURATION = 15 # Seconds
|
|
32
|
-
|
|
33
|
-
NEW_PAGE_INDICATOR_ID = "page-initial-load"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def team_list_dropdown(menu_id=TEAM_SELECTION_MENU_ID):
|
|
37
|
-
"""
|
|
38
|
-
Build a dropdown menu from the teams associated with the current user session.
|
|
39
|
-
|
|
40
|
-
:param menu_id: menu ID for the created dropdown menu
|
|
41
|
-
:return: dcc.Dropdown object or a jumbotron in case of errors
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
global_params = GlobalParams()
|
|
45
|
-
try:
|
|
46
|
-
session_data = global_params.session_data
|
|
47
|
-
if session_data.agent_teams and len(session_data.agent_teams) > 1:
|
|
48
|
-
default_team = ""
|
|
49
|
-
else:
|
|
50
|
-
default_team = session_data.agent_teams[0]
|
|
51
|
-
|
|
52
|
-
except SessionDataMissing:
|
|
53
|
-
return no_session_id_jumbotron()
|
|
54
|
-
|
|
55
|
-
periods = session_data.declaration_periods
|
|
56
|
-
if periods is None:
|
|
57
|
-
periods = get_declaration_periods()
|
|
58
|
-
session_data.declaration_periods = periods
|
|
59
|
-
default_period = get_default_period_date(
|
|
60
|
-
periods, global_params.declaration_options["default_date"]
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return dbc.Row(
|
|
64
|
-
[
|
|
65
|
-
dbc.Col(
|
|
66
|
-
[
|
|
67
|
-
dbc.Label(html.B("Equipe")),
|
|
68
|
-
dcc.Dropdown(
|
|
69
|
-
id=menu_id,
|
|
70
|
-
options=[
|
|
71
|
-
{"label": team, "value": team} for team in session_data.agent_teams
|
|
72
|
-
],
|
|
73
|
-
loading_state={"is_loading": True},
|
|
74
|
-
value=default_team,
|
|
75
|
-
placeholder="Sélectionner une équipe",
|
|
76
|
-
),
|
|
77
|
-
],
|
|
78
|
-
width=6,
|
|
79
|
-
class_name="team_list_dropdown",
|
|
80
|
-
),
|
|
81
|
-
dbc.Col(
|
|
82
|
-
[
|
|
83
|
-
dbc.Label(html.B("Période")),
|
|
84
|
-
dcc.Dropdown(
|
|
85
|
-
id=VALIDATION_PERIOD_MENU_ID,
|
|
86
|
-
options=[
|
|
87
|
-
{
|
|
88
|
-
"label": period.label,
|
|
89
|
-
"value": period.start_date,
|
|
90
|
-
}
|
|
91
|
-
for period in periods
|
|
92
|
-
],
|
|
93
|
-
value=default_period,
|
|
94
|
-
placeholder="Sélectionner une période",
|
|
95
|
-
),
|
|
96
|
-
],
|
|
97
|
-
width={"size": 4, "offset": 1},
|
|
98
|
-
class_name="team_list_dropdown",
|
|
99
|
-
),
|
|
100
|
-
]
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def build_accordion(button_number, button_content, hidden_text, tooltip=None, class_list=""):
|
|
105
|
-
"""
|
|
106
|
-
Function to build an accordion associated with the component passed in button_contents.
|
|
107
|
-
|
|
108
|
-
:param button_number: the button number, must be unique for each button
|
|
109
|
-
:param button_content: what will be put inside the button
|
|
110
|
-
:param hidden_text: text to be displayed when the accordion is open
|
|
111
|
-
:param tooltip: text to be displayed as an optional tooltip
|
|
112
|
-
:param class_list: optional class list to add to the dbc.Card
|
|
113
|
-
:return: the accordion element
|
|
114
|
-
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
return html.Div(
|
|
118
|
-
[
|
|
119
|
-
dbc.Accordion(
|
|
120
|
-
dbc.AccordionItem(
|
|
121
|
-
hidden_text,
|
|
122
|
-
title=button_content,
|
|
123
|
-
class_name=class_list,
|
|
124
|
-
),
|
|
125
|
-
id={"type": "accordion_toggle", "id": button_number},
|
|
126
|
-
start_collapsed=True,
|
|
127
|
-
),
|
|
128
|
-
dbc.Tooltip(
|
|
129
|
-
tooltip,
|
|
130
|
-
target={"type": "accordion_toggle", "id": button_number},
|
|
131
|
-
placement="left",
|
|
132
|
-
key=f"acccordion_tooltip_{button_number}",
|
|
133
|
-
),
|
|
134
|
-
]
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def create_progress_bar(
|
|
139
|
-
team: str = None,
|
|
140
|
-
duration: int = LOAD_PROGRESS_BAR_MAX_DURATION,
|
|
141
|
-
interval: float = LOAD_PROGRESS_BAR_INTERVAL_DURATION,
|
|
142
|
-
):
|
|
143
|
-
"""
|
|
144
|
-
Create a Div with a progress bar
|
|
145
|
-
|
|
146
|
-
:param team: currently selected team
|
|
147
|
-
:param duration: progress bar duration (seconds)
|
|
148
|
-
:param interval: interval duration (millisecondes
|
|
149
|
-
:return: Div
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
max_intervals = int(duration * 1000 / interval)
|
|
153
|
-
|
|
154
|
-
return html.Div(
|
|
155
|
-
[
|
|
156
|
-
(
|
|
157
|
-
html.Div(f"Chargement des données de l'équipe {team} en cours...")
|
|
158
|
-
if team
|
|
159
|
-
else html.Div()
|
|
160
|
-
),
|
|
161
|
-
dcc.Interval(
|
|
162
|
-
id="progress-interval",
|
|
163
|
-
max_intervals=max_intervals,
|
|
164
|
-
n_intervals=0,
|
|
165
|
-
interval=LOAD_PROGRESS_BAR_INTERVAL_DURATION,
|
|
166
|
-
),
|
|
167
|
-
dbc.Progress(id="progress", striped=True),
|
|
168
|
-
dcc.Store(id="progress-bar-max-intervals", data=max_intervals),
|
|
169
|
-
],
|
|
170
|
-
id=LOAD_PROGRESS_BAR_ID,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@app.callback(
|
|
175
|
-
[Output("progress", "value"), Output("progress", "label")],
|
|
176
|
-
Input("progress-interval", "n_intervals"),
|
|
177
|
-
State("progress-bar-max-intervals", "data"),
|
|
178
|
-
prevent_initial_call=True,
|
|
179
|
-
)
|
|
180
|
-
def update_progress_bar(n, max_intervals):
|
|
181
|
-
"""
|
|
182
|
-
Update the progress bar.
|
|
183
|
-
|
|
184
|
-
:param n: number of intervals since the beginning
|
|
185
|
-
:param max_intervals: maximum number of intervals (duration)
|
|
186
|
-
:return:
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
progress = int(round(n * 100 / max_intervals))
|
|
190
|
-
# only add text after 5% progress to ensure text isn't squashed too much
|
|
191
|
-
return progress, f"{progress} %" if progress >= 5 else ""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@app.callback(
|
|
195
|
-
Output({"type": "accordion_collapse", "id": MATCH}, "is_open"),
|
|
196
|
-
Input({"type": "accordion_toggle", "id": MATCH}, "n_clicks"),
|
|
197
|
-
State({"type": "accordion_collapse", "id": MATCH}, "is_open"),
|
|
198
|
-
prevent_initial_call=True,
|
|
199
|
-
)
|
|
200
|
-
def toggle_agent_accordion(n_clicks, is_open) -> bool:
|
|
201
|
-
"""
|
|
202
|
-
Callback function for the agent accordion.
|
|
203
|
-
|
|
204
|
-
:param value: number of times the link was clicked
|
|
205
|
-
:param is_open: whether the accordion is open or closed
|
|
206
|
-
:return: List of n Output
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
if n_clicks:
|
|
210
|
-
return not is_open
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
@app.callback(
|
|
214
|
-
Output(TEAM_SELECTION_MENU_ID, "value"),
|
|
215
|
-
Output(TEAM_SELECTED_VALUE_ID, "data"),
|
|
216
|
-
Output(TEAM_SELECTION_DATE_ID, "data"),
|
|
217
|
-
Output(VALIDATION_PERIOD_MENU_ID, "value"),
|
|
218
|
-
Output(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
219
|
-
Output(TEAM_SELECTION_MENU_ID, "loading_state"),
|
|
220
|
-
Input(TEAM_SELECTION_MENU_ID, "value"),
|
|
221
|
-
Input(VALIDATION_PERIOD_MENU_ID, "value"),
|
|
222
|
-
State(TEAM_SELECTED_VALUE_ID, "data"),
|
|
223
|
-
State(DATA_SELECTED_SOURCE_ID, "children"),
|
|
224
|
-
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
225
|
-
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
226
|
-
State(TEAM_SELECTION_MENU_ID, "loading_state"),
|
|
227
|
-
)
|
|
228
|
-
def save_team_and_period(
|
|
229
|
-
selected_team,
|
|
230
|
-
selected_period,
|
|
231
|
-
previous_team,
|
|
232
|
-
selected_source,
|
|
233
|
-
team_selection_date,
|
|
234
|
-
previous_period,
|
|
235
|
-
loading_state,
|
|
236
|
-
):
|
|
237
|
-
"""
|
|
238
|
-
Function to save the selected team and period in a dcc.Store. This will trigger other callbacks.
|
|
239
|
-
Also clear the data cache. The value of the team selection dropdown is written back: it
|
|
240
|
-
allows to update the initial value if one was saved into the dcc.Store.
|
|
241
|
-
|
|
242
|
-
:param selected_team: team dropdown menu value
|
|
243
|
-
:param selected_period: period dropdoan menu value
|
|
244
|
-
:param previous_team: previously selected team
|
|
245
|
-
:param selected_source: currently selected data source
|
|
246
|
-
:param team_selection_date: date of the last team selection
|
|
247
|
-
:param previous_period: previously selected period
|
|
248
|
-
:param loading_state: loading state of the team selection menu
|
|
249
|
-
:return: expected Output values
|
|
250
|
-
"""
|
|
251
|
-
|
|
252
|
-
# Handle the initial display of the selection menu: if it was already displayed in another
|
|
253
|
-
# page and a value was previously selected (saved in TEAM_SELECTED_VALUE_ID), use it as the
|
|
254
|
-
# initial value (set it to this value through the callback). Do the same for the validation
|
|
255
|
-
# period
|
|
256
|
-
|
|
257
|
-
global_params = GlobalParams()
|
|
258
|
-
try:
|
|
259
|
-
session_data = global_params.session_data
|
|
260
|
-
except SessionDataMissing:
|
|
261
|
-
return no_session_id_jumbotron(), "", ""
|
|
262
|
-
|
|
263
|
-
if "is_loading" in loading_state and loading_state["is_loading"]:
|
|
264
|
-
team = previous_team
|
|
265
|
-
# If previous_period is empty, means that it should be initialised with the pulldown
|
|
266
|
-
# menu default value (in selected_period the first time the callback is executed)
|
|
267
|
-
if previous_period == "":
|
|
268
|
-
period = selected_period
|
|
269
|
-
else:
|
|
270
|
-
period = previous_period
|
|
271
|
-
else:
|
|
272
|
-
team = selected_team
|
|
273
|
-
period = selected_period
|
|
274
|
-
|
|
275
|
-
# Cache must be changed if the team has been changed or if the selected source doesn't match
|
|
276
|
-
# the cached one
|
|
277
|
-
if (
|
|
278
|
-
team != previous_team
|
|
279
|
-
or period != previous_period
|
|
280
|
-
or session_data.project_declarations_source is None
|
|
281
|
-
or selected_source != session_data.project_declarations_source
|
|
282
|
-
):
|
|
283
|
-
clear_cached_data()
|
|
284
|
-
selection_date = f"{datetime.now()}"
|
|
285
|
-
else:
|
|
286
|
-
selection_date = team_selection_date
|
|
287
|
-
|
|
288
|
-
return team, team, selection_date, period, period, {"is_laoding": False}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# Client-side callback used to mark a table as sortable. To be marked sortable, a table must have
|
|
292
|
-
# an ID matching the ID type attribute TABLE_TYPE_TABLE and create a dcc.Store associated with
|
|
293
|
-
# an ID type attribute TABLE_TYPE_DUMMY_STORE
|
|
294
|
-
app.clientside_callback(
|
|
295
|
-
"""
|
|
296
|
-
function make_table_sortable(dummy, table_id) {
|
|
297
|
-
if (!(typeof table_id === 'string' || table_id instanceof String)) {
|
|
298
|
-
table_id = JSON.stringify(table_id, Object.keys(table_id).sort());
|
|
299
|
-
};
|
|
300
|
-
/*alert('Mark sortable table with ID='+table_id);*/
|
|
301
|
-
const tableObject = document.getElementById(table_id);
|
|
302
|
-
sorttable.makeSortable(tableObject);
|
|
303
|
-
return 0;
|
|
304
|
-
}
|
|
305
|
-
""",
|
|
306
|
-
Output({"type": TABLE_TYPE_DUMMY_STORE, "id": MATCH}, "data"),
|
|
307
|
-
Input({"type": TABLE_TYPE_TABLE, "id": MATCH}, "children"),
|
|
308
|
-
State({"type": TABLE_TYPE_TABLE, "id": MATCH}, "id"),
|
|
309
|
-
prevent_initial_call=True,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
# Emulate former Jumbotron
|
|
314
|
-
def ositah_jumbotron(title: str, main_text: str, details: str = None, title_class: str = None):
|
|
315
|
-
"""
|
|
316
|
-
Emulate Jumbotron component available in Bootstrap v4
|
|
317
|
-
|
|
318
|
-
:param title: Jumbotron title
|
|
319
|
-
:param main_text: main text
|
|
320
|
-
:param details: optional additional text
|
|
321
|
-
:return: html.div()
|
|
322
|
-
"""
|
|
323
|
-
|
|
324
|
-
return html.Div(
|
|
325
|
-
dbc.Container(
|
|
326
|
-
[
|
|
327
|
-
html.H1(title, className=f"display-3 {title_class if (title_class) else ''}"),
|
|
328
|
-
html.P(
|
|
329
|
-
main_text,
|
|
330
|
-
className="lead",
|
|
331
|
-
),
|
|
332
|
-
html.Hr(className="my-2"),
|
|
333
|
-
html.P(details),
|
|
334
|
-
],
|
|
335
|
-
fluid=True,
|
|
336
|
-
className="py-3",
|
|
337
|
-
),
|
|
338
|
-
className="p-3 bg-light rounded-3",
|
|
339
|
-
)
|
|
1
|
+
# Module containing helper functions to build/manage the menus and graphic objects
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import dash_bootstrap_components as dbc
|
|
6
|
+
from dash import dcc, html
|
|
7
|
+
from dash.dependencies import MATCH, Input, Output, State
|
|
8
|
+
|
|
9
|
+
from ositah.app import app
|
|
10
|
+
from ositah.utils.cache import clear_cached_data
|
|
11
|
+
from ositah.utils.exceptions import SessionDataMissing
|
|
12
|
+
from ositah.utils.period import get_declaration_periods, get_default_period_date
|
|
13
|
+
from ositah.utils.utils import GlobalParams, no_session_id_jumbotron
|
|
14
|
+
|
|
15
|
+
DATA_SELECTED_SOURCE_ID = "project-declaration-source"
|
|
16
|
+
DATA_SELECTION_SOURCE_ID = "project-declaration-source-button"
|
|
17
|
+
|
|
18
|
+
TEAM_SELECTED_VALUE_ID = "team-selected"
|
|
19
|
+
TEAM_SELECTION_MENU_ID = "team-selection-dropdown"
|
|
20
|
+
TEAM_SELECTION_DATE_ID = "team-selection-date"
|
|
21
|
+
|
|
22
|
+
VALIDATION_PERIOD_MENU_ID = "validation-period-dropdown"
|
|
23
|
+
VALIDATION_PERIOD_SELECTED_ID = "validation-period-selected"
|
|
24
|
+
|
|
25
|
+
# 'type' part of composite IDs
|
|
26
|
+
TABLE_TYPE_TABLE = "ositah-table"
|
|
27
|
+
TABLE_TYPE_DUMMY_STORE = "ositah-table-dummy-store"
|
|
28
|
+
|
|
29
|
+
LOAD_PROGRESS_BAR_ID = "validation-progress-bar"
|
|
30
|
+
LOAD_PROGRESS_BAR_INTERVAL_DURATION = 500 # Milliseconds
|
|
31
|
+
LOAD_PROGRESS_BAR_MAX_DURATION = 15 # Seconds
|
|
32
|
+
|
|
33
|
+
NEW_PAGE_INDICATOR_ID = "page-initial-load"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def team_list_dropdown(menu_id=TEAM_SELECTION_MENU_ID):
|
|
37
|
+
"""
|
|
38
|
+
Build a dropdown menu from the teams associated with the current user session.
|
|
39
|
+
|
|
40
|
+
:param menu_id: menu ID for the created dropdown menu
|
|
41
|
+
:return: dcc.Dropdown object or a jumbotron in case of errors
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
global_params = GlobalParams()
|
|
45
|
+
try:
|
|
46
|
+
session_data = global_params.session_data
|
|
47
|
+
if session_data.agent_teams and len(session_data.agent_teams) > 1:
|
|
48
|
+
default_team = ""
|
|
49
|
+
else:
|
|
50
|
+
default_team = session_data.agent_teams[0]
|
|
51
|
+
|
|
52
|
+
except SessionDataMissing:
|
|
53
|
+
return no_session_id_jumbotron()
|
|
54
|
+
|
|
55
|
+
periods = session_data.declaration_periods
|
|
56
|
+
if periods is None:
|
|
57
|
+
periods = get_declaration_periods()
|
|
58
|
+
session_data.declaration_periods = periods
|
|
59
|
+
default_period = get_default_period_date(
|
|
60
|
+
periods, global_params.declaration_options["default_date"]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return dbc.Row(
|
|
64
|
+
[
|
|
65
|
+
dbc.Col(
|
|
66
|
+
[
|
|
67
|
+
dbc.Label(html.B("Equipe")),
|
|
68
|
+
dcc.Dropdown(
|
|
69
|
+
id=menu_id,
|
|
70
|
+
options=[
|
|
71
|
+
{"label": team, "value": team} for team in session_data.agent_teams
|
|
72
|
+
],
|
|
73
|
+
loading_state={"is_loading": True},
|
|
74
|
+
value=default_team,
|
|
75
|
+
placeholder="Sélectionner une équipe",
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
width=6,
|
|
79
|
+
class_name="team_list_dropdown",
|
|
80
|
+
),
|
|
81
|
+
dbc.Col(
|
|
82
|
+
[
|
|
83
|
+
dbc.Label(html.B("Période")),
|
|
84
|
+
dcc.Dropdown(
|
|
85
|
+
id=VALIDATION_PERIOD_MENU_ID,
|
|
86
|
+
options=[
|
|
87
|
+
{
|
|
88
|
+
"label": period.label,
|
|
89
|
+
"value": period.start_date,
|
|
90
|
+
}
|
|
91
|
+
for period in periods
|
|
92
|
+
],
|
|
93
|
+
value=default_period,
|
|
94
|
+
placeholder="Sélectionner une période",
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
width={"size": 4, "offset": 1},
|
|
98
|
+
class_name="team_list_dropdown",
|
|
99
|
+
),
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_accordion(button_number, button_content, hidden_text, tooltip=None, class_list=""):
|
|
105
|
+
"""
|
|
106
|
+
Function to build an accordion associated with the component passed in button_contents.
|
|
107
|
+
|
|
108
|
+
:param button_number: the button number, must be unique for each button
|
|
109
|
+
:param button_content: what will be put inside the button
|
|
110
|
+
:param hidden_text: text to be displayed when the accordion is open
|
|
111
|
+
:param tooltip: text to be displayed as an optional tooltip
|
|
112
|
+
:param class_list: optional class list to add to the dbc.Card
|
|
113
|
+
:return: the accordion element
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
return html.Div(
|
|
118
|
+
[
|
|
119
|
+
dbc.Accordion(
|
|
120
|
+
dbc.AccordionItem(
|
|
121
|
+
hidden_text,
|
|
122
|
+
title=button_content,
|
|
123
|
+
class_name=class_list,
|
|
124
|
+
),
|
|
125
|
+
id={"type": "accordion_toggle", "id": button_number},
|
|
126
|
+
start_collapsed=True,
|
|
127
|
+
),
|
|
128
|
+
dbc.Tooltip(
|
|
129
|
+
tooltip,
|
|
130
|
+
target={"type": "accordion_toggle", "id": button_number},
|
|
131
|
+
placement="left",
|
|
132
|
+
key=f"acccordion_tooltip_{button_number}",
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def create_progress_bar(
|
|
139
|
+
team: str = None,
|
|
140
|
+
duration: int = LOAD_PROGRESS_BAR_MAX_DURATION,
|
|
141
|
+
interval: float = LOAD_PROGRESS_BAR_INTERVAL_DURATION,
|
|
142
|
+
):
|
|
143
|
+
"""
|
|
144
|
+
Create a Div with a progress bar
|
|
145
|
+
|
|
146
|
+
:param team: currently selected team
|
|
147
|
+
:param duration: progress bar duration (seconds)
|
|
148
|
+
:param interval: interval duration (millisecondes
|
|
149
|
+
:return: Div
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
max_intervals = int(duration * 1000 / interval)
|
|
153
|
+
|
|
154
|
+
return html.Div(
|
|
155
|
+
[
|
|
156
|
+
(
|
|
157
|
+
html.Div(f"Chargement des données de l'équipe {team} en cours...")
|
|
158
|
+
if team
|
|
159
|
+
else html.Div()
|
|
160
|
+
),
|
|
161
|
+
dcc.Interval(
|
|
162
|
+
id="progress-interval",
|
|
163
|
+
max_intervals=max_intervals,
|
|
164
|
+
n_intervals=0,
|
|
165
|
+
interval=LOAD_PROGRESS_BAR_INTERVAL_DURATION,
|
|
166
|
+
),
|
|
167
|
+
dbc.Progress(id="progress", striped=True),
|
|
168
|
+
dcc.Store(id="progress-bar-max-intervals", data=max_intervals),
|
|
169
|
+
],
|
|
170
|
+
id=LOAD_PROGRESS_BAR_ID,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.callback(
|
|
175
|
+
[Output("progress", "value"), Output("progress", "label")],
|
|
176
|
+
Input("progress-interval", "n_intervals"),
|
|
177
|
+
State("progress-bar-max-intervals", "data"),
|
|
178
|
+
prevent_initial_call=True,
|
|
179
|
+
)
|
|
180
|
+
def update_progress_bar(n, max_intervals):
|
|
181
|
+
"""
|
|
182
|
+
Update the progress bar.
|
|
183
|
+
|
|
184
|
+
:param n: number of intervals since the beginning
|
|
185
|
+
:param max_intervals: maximum number of intervals (duration)
|
|
186
|
+
:return:
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
progress = int(round(n * 100 / max_intervals))
|
|
190
|
+
# only add text after 5% progress to ensure text isn't squashed too much
|
|
191
|
+
return progress, f"{progress} %" if progress >= 5 else ""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.callback(
|
|
195
|
+
Output({"type": "accordion_collapse", "id": MATCH}, "is_open"),
|
|
196
|
+
Input({"type": "accordion_toggle", "id": MATCH}, "n_clicks"),
|
|
197
|
+
State({"type": "accordion_collapse", "id": MATCH}, "is_open"),
|
|
198
|
+
prevent_initial_call=True,
|
|
199
|
+
)
|
|
200
|
+
def toggle_agent_accordion(n_clicks, is_open) -> bool:
|
|
201
|
+
"""
|
|
202
|
+
Callback function for the agent accordion.
|
|
203
|
+
|
|
204
|
+
:param value: number of times the link was clicked
|
|
205
|
+
:param is_open: whether the accordion is open or closed
|
|
206
|
+
:return: List of n Output
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
if n_clicks:
|
|
210
|
+
return not is_open
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@app.callback(
|
|
214
|
+
Output(TEAM_SELECTION_MENU_ID, "value"),
|
|
215
|
+
Output(TEAM_SELECTED_VALUE_ID, "data"),
|
|
216
|
+
Output(TEAM_SELECTION_DATE_ID, "data"),
|
|
217
|
+
Output(VALIDATION_PERIOD_MENU_ID, "value"),
|
|
218
|
+
Output(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
219
|
+
Output(TEAM_SELECTION_MENU_ID, "loading_state"),
|
|
220
|
+
Input(TEAM_SELECTION_MENU_ID, "value"),
|
|
221
|
+
Input(VALIDATION_PERIOD_MENU_ID, "value"),
|
|
222
|
+
State(TEAM_SELECTED_VALUE_ID, "data"),
|
|
223
|
+
State(DATA_SELECTED_SOURCE_ID, "children"),
|
|
224
|
+
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
225
|
+
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
226
|
+
State(TEAM_SELECTION_MENU_ID, "loading_state"),
|
|
227
|
+
)
|
|
228
|
+
def save_team_and_period(
|
|
229
|
+
selected_team,
|
|
230
|
+
selected_period,
|
|
231
|
+
previous_team,
|
|
232
|
+
selected_source,
|
|
233
|
+
team_selection_date,
|
|
234
|
+
previous_period,
|
|
235
|
+
loading_state,
|
|
236
|
+
):
|
|
237
|
+
"""
|
|
238
|
+
Function to save the selected team and period in a dcc.Store. This will trigger other callbacks.
|
|
239
|
+
Also clear the data cache. The value of the team selection dropdown is written back: it
|
|
240
|
+
allows to update the initial value if one was saved into the dcc.Store.
|
|
241
|
+
|
|
242
|
+
:param selected_team: team dropdown menu value
|
|
243
|
+
:param selected_period: period dropdoan menu value
|
|
244
|
+
:param previous_team: previously selected team
|
|
245
|
+
:param selected_source: currently selected data source
|
|
246
|
+
:param team_selection_date: date of the last team selection
|
|
247
|
+
:param previous_period: previously selected period
|
|
248
|
+
:param loading_state: loading state of the team selection menu
|
|
249
|
+
:return: expected Output values
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
# Handle the initial display of the selection menu: if it was already displayed in another
|
|
253
|
+
# page and a value was previously selected (saved in TEAM_SELECTED_VALUE_ID), use it as the
|
|
254
|
+
# initial value (set it to this value through the callback). Do the same for the validation
|
|
255
|
+
# period
|
|
256
|
+
|
|
257
|
+
global_params = GlobalParams()
|
|
258
|
+
try:
|
|
259
|
+
session_data = global_params.session_data
|
|
260
|
+
except SessionDataMissing:
|
|
261
|
+
return no_session_id_jumbotron(), "", ""
|
|
262
|
+
|
|
263
|
+
if "is_loading" in loading_state and loading_state["is_loading"]:
|
|
264
|
+
team = previous_team
|
|
265
|
+
# If previous_period is empty, means that it should be initialised with the pulldown
|
|
266
|
+
# menu default value (in selected_period the first time the callback is executed)
|
|
267
|
+
if previous_period == "":
|
|
268
|
+
period = selected_period
|
|
269
|
+
else:
|
|
270
|
+
period = previous_period
|
|
271
|
+
else:
|
|
272
|
+
team = selected_team
|
|
273
|
+
period = selected_period
|
|
274
|
+
|
|
275
|
+
# Cache must be changed if the team has been changed or if the selected source doesn't match
|
|
276
|
+
# the cached one
|
|
277
|
+
if (
|
|
278
|
+
team != previous_team
|
|
279
|
+
or period != previous_period
|
|
280
|
+
or session_data.project_declarations_source is None
|
|
281
|
+
or selected_source != session_data.project_declarations_source
|
|
282
|
+
):
|
|
283
|
+
clear_cached_data()
|
|
284
|
+
selection_date = f"{datetime.now()}"
|
|
285
|
+
else:
|
|
286
|
+
selection_date = team_selection_date
|
|
287
|
+
|
|
288
|
+
return team, team, selection_date, period, period, {"is_laoding": False}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Client-side callback used to mark a table as sortable. To be marked sortable, a table must have
|
|
292
|
+
# an ID matching the ID type attribute TABLE_TYPE_TABLE and create a dcc.Store associated with
|
|
293
|
+
# an ID type attribute TABLE_TYPE_DUMMY_STORE
|
|
294
|
+
app.clientside_callback(
|
|
295
|
+
"""
|
|
296
|
+
function make_table_sortable(dummy, table_id) {
|
|
297
|
+
if (!(typeof table_id === 'string' || table_id instanceof String)) {
|
|
298
|
+
table_id = JSON.stringify(table_id, Object.keys(table_id).sort());
|
|
299
|
+
};
|
|
300
|
+
/*alert('Mark sortable table with ID='+table_id);*/
|
|
301
|
+
const tableObject = document.getElementById(table_id);
|
|
302
|
+
sorttable.makeSortable(tableObject);
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
""",
|
|
306
|
+
Output({"type": TABLE_TYPE_DUMMY_STORE, "id": MATCH}, "data"),
|
|
307
|
+
Input({"type": TABLE_TYPE_TABLE, "id": MATCH}, "children"),
|
|
308
|
+
State({"type": TABLE_TYPE_TABLE, "id": MATCH}, "id"),
|
|
309
|
+
prevent_initial_call=True,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Emulate former Jumbotron
|
|
314
|
+
def ositah_jumbotron(title: str, main_text: str, details: str = None, title_class: str = None):
|
|
315
|
+
"""
|
|
316
|
+
Emulate Jumbotron component available in Bootstrap v4
|
|
317
|
+
|
|
318
|
+
:param title: Jumbotron title
|
|
319
|
+
:param main_text: main text
|
|
320
|
+
:param details: optional additional text
|
|
321
|
+
:return: html.div()
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
return html.Div(
|
|
325
|
+
dbc.Container(
|
|
326
|
+
[
|
|
327
|
+
html.H1(title, className=f"display-3 {title_class if (title_class) else ''}"),
|
|
328
|
+
html.P(
|
|
329
|
+
main_text,
|
|
330
|
+
className="lead",
|
|
331
|
+
),
|
|
332
|
+
html.Hr(className="my-2"),
|
|
333
|
+
html.P(details),
|
|
334
|
+
],
|
|
335
|
+
fluid=True,
|
|
336
|
+
className="py-3",
|
|
337
|
+
),
|
|
338
|
+
className="p-3 bg-light rounded-3",
|
|
339
|
+
)
|