ositah 25.6.dev1__py3-none-any.whl → 25.9.dev1__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 +1208 -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 +1178 -1178
- ositah/utils/teams.py +42 -42
- ositah/utils/utils.py +474 -474
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/METADATA +149 -150
- ositah-25.9.dev1.dist-info/RECORD +46 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/licenses/LICENSE +29 -29
- ositah-25.6.dev1.dist-info/RECORD +0 -46
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/WHEEL +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/entry_points.txt +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/top_level.txt +0 -0
ositah/apps/validation/tables.py
CHANGED
|
@@ -1,646 +1,646 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Functions to build tables associated with tab layouts
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import dash_bootstrap_components as dbc
|
|
6
|
-
import numpy as np
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from dash import dcc, html
|
|
9
|
-
from flask import session
|
|
10
|
-
|
|
11
|
-
from ositah.apps.validation.parameters import (
|
|
12
|
-
TABLE_COLUMN_VALIDATION,
|
|
13
|
-
TABLE_ID_DECLARATION_STATS,
|
|
14
|
-
TABLE_ID_MISSING_AGENTS,
|
|
15
|
-
TABLE_ID_VALIDATION,
|
|
16
|
-
VALIDATION_DECLARATIONS_SELECT_ALL,
|
|
17
|
-
VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED,
|
|
18
|
-
VALIDATION_DECLARATIONS_SELECT_VALIDATED,
|
|
19
|
-
)
|
|
20
|
-
from ositah.apps.validation.tools import (
|
|
21
|
-
activity_time_cell,
|
|
22
|
-
add_validation_declaration_selection_switch,
|
|
23
|
-
agent_project_time,
|
|
24
|
-
agent_tooltip_txt,
|
|
25
|
-
category_declarations,
|
|
26
|
-
define_declaration_thresholds,
|
|
27
|
-
get_all_validation_status,
|
|
28
|
-
validation_started,
|
|
29
|
-
)
|
|
30
|
-
from ositah.utils.agents import get_agents
|
|
31
|
-
from ositah.utils.exceptions import InvalidHitoProjectName, SessionDataMissing
|
|
32
|
-
from ositah.utils.menus import TABLE_TYPE_TABLE, build_accordion, ositah_jumbotron
|
|
33
|
-
from ositah.utils.projects import (
|
|
34
|
-
CATEGORY_DEFAULT,
|
|
35
|
-
DATA_SOURCE_HITO,
|
|
36
|
-
DATA_SOURCE_OSITAH,
|
|
37
|
-
get_team_projects,
|
|
38
|
-
time_unit,
|
|
39
|
-
)
|
|
40
|
-
from ositah.utils.utils import (
|
|
41
|
-
SEMESTER_WEEKS,
|
|
42
|
-
TEAM_LIST_ALL_AGENTS,
|
|
43
|
-
GlobalParams,
|
|
44
|
-
general_error_jumbotron,
|
|
45
|
-
no_session_id_jumbotron,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def build_validation_table(team, team_selection_date, declaration_set: int, period_date: str):
|
|
50
|
-
"""
|
|
51
|
-
Build the agent list of the selected team with their declarations. Returns a table.
|
|
52
|
-
|
|
53
|
-
:param team: selected team
|
|
54
|
-
:param team_selection_date: last time the team selection was changed
|
|
55
|
-
:param declaration_set: selected declaration set (all, validated or non-validated ones)
|
|
56
|
-
:param period_date: a date that must be inside the declaration period
|
|
57
|
-
:return: DBC table
|
|
58
|
-
"""
|
|
59
|
-
global_params = GlobalParams()
|
|
60
|
-
column_names = global_params.columns
|
|
61
|
-
try:
|
|
62
|
-
session_data = global_params.session_data
|
|
63
|
-
except SessionDataMissing:
|
|
64
|
-
return no_session_id_jumbotron()
|
|
65
|
-
|
|
66
|
-
if session["user_email"] in global_params.roles["read-only"]:
|
|
67
|
-
validation_disabled = True
|
|
68
|
-
else:
|
|
69
|
-
if session_data.role in global_params.validation_params["override_period"]:
|
|
70
|
-
validation_disabled = False
|
|
71
|
-
else:
|
|
72
|
-
validation_disabled = not validation_started(period_date)
|
|
73
|
-
|
|
74
|
-
validation_data = get_all_validation_status(period_date)
|
|
75
|
-
define_declaration_thresholds(period_date)
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
project_declarations = get_team_projects(
|
|
79
|
-
team, team_selection_date, period_date, DATA_SOURCE_HITO
|
|
80
|
-
)
|
|
81
|
-
except InvalidHitoProjectName as e:
|
|
82
|
-
return ositah_jumbotron(
|
|
83
|
-
"Error loading projects",
|
|
84
|
-
e.msg,
|
|
85
|
-
title_class="text-danger",
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
if project_declarations is None:
|
|
89
|
-
return html.Div(
|
|
90
|
-
[
|
|
91
|
-
dbc.Alert(
|
|
92
|
-
f"Aucune déclaration effectuée pour l'équipe '{team}'",
|
|
93
|
-
color="warning",
|
|
94
|
-
),
|
|
95
|
-
]
|
|
96
|
-
)
|
|
97
|
-
declarations = category_declarations(project_declarations)
|
|
98
|
-
|
|
99
|
-
if team == TEAM_LIST_ALL_AGENTS:
|
|
100
|
-
declaration_list = declarations
|
|
101
|
-
else:
|
|
102
|
-
declaration_list = declarations[declarations[column_names["team"]].str.match(team)]
|
|
103
|
-
declaration_list["validation_disabled"] = validation_disabled
|
|
104
|
-
|
|
105
|
-
data_columns = [CATEGORY_DEFAULT]
|
|
106
|
-
data_columns.extend(global_params.project_categories.keys())
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
validated_project_declarations = get_team_projects(
|
|
110
|
-
team, team_selection_date, period_date, DATA_SOURCE_OSITAH, use_cache=False
|
|
111
|
-
)
|
|
112
|
-
except InvalidHitoProjectName as e:
|
|
113
|
-
return ositah_jumbotron(
|
|
114
|
-
"Error loading projects",
|
|
115
|
-
e.msg,
|
|
116
|
-
title_class="text-danger",
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
if validated_project_declarations is None:
|
|
120
|
-
declaration_list["validated"] = False
|
|
121
|
-
declaration_list["hito_missing"] = False
|
|
122
|
-
validated_declarations_num = 0
|
|
123
|
-
hito_missing_num = 0
|
|
124
|
-
hito_missing_msg = ""
|
|
125
|
-
else:
|
|
126
|
-
validated_declarations_num = len(validated_project_declarations)
|
|
127
|
-
validated_declarations = category_declarations(
|
|
128
|
-
validated_project_declarations, use_cache=False
|
|
129
|
-
)
|
|
130
|
-
# Do an outer merge to detect validated declarations removed from Hito
|
|
131
|
-
declaration_list = declaration_list.merge(
|
|
132
|
-
validated_declarations,
|
|
133
|
-
how="outer",
|
|
134
|
-
on=column_names["agent_id"],
|
|
135
|
-
indicator=True,
|
|
136
|
-
suffixes=[None, "_val"],
|
|
137
|
-
)
|
|
138
|
-
declaration_list["validated"] = (declaration_list._merge == "both") | (
|
|
139
|
-
declaration_list._merge == "right_only"
|
|
140
|
-
)
|
|
141
|
-
declaration_list["hito_missing"] = declaration_list._merge == "right_only"
|
|
142
|
-
hito_missing_num = len(declaration_list.loc[declaration_list["hito_missing"]])
|
|
143
|
-
if hito_missing_num > 0:
|
|
144
|
-
columns_fillna = {c: 0 for c in [*data_columns, "total_hours", "percent_global"]}
|
|
145
|
-
declaration_list.loc[declaration_list["hito_missing"]] = declaration_list.fillna(
|
|
146
|
-
value=columns_fillna
|
|
147
|
-
)
|
|
148
|
-
declaration_list.loc[declaration_list["hito_missing"], column_names["fullname"]] = (
|
|
149
|
-
declaration_list[f"{column_names['fullname']}_val"]
|
|
150
|
-
)
|
|
151
|
-
declaration_list.loc[declaration_list["hito_missing"], column_names["team"]] = (
|
|
152
|
-
declaration_list[f"{column_names['team']}_val"]
|
|
153
|
-
)
|
|
154
|
-
declaration_list.loc[declaration_list["hito_missing"], "suspect"] = True
|
|
155
|
-
declaration_list.loc[declaration_list["hito_missing"], "validation_disabled"] = True
|
|
156
|
-
declaration_list.sort_values(by=column_names["fullname"], inplace=True)
|
|
157
|
-
hito_missing_msg = (
|
|
158
|
-
f" dont {hito_missing_num} supprimé"
|
|
159
|
-
f"{'s' if hito_missing_num > 1 else ''} de Hito"
|
|
160
|
-
)
|
|
161
|
-
else:
|
|
162
|
-
hito_missing_msg = ""
|
|
163
|
-
for category in data_columns:
|
|
164
|
-
time_ok_column = f"{category}_time_ok"
|
|
165
|
-
declaration_list.loc[declaration_list.validated, time_ok_column] = np.isclose(
|
|
166
|
-
declaration_list.loc[declaration_list.validated, category],
|
|
167
|
-
declaration_list.loc[declaration_list.validated, f"{category}_val"],
|
|
168
|
-
rtol=1e-5,
|
|
169
|
-
atol=0,
|
|
170
|
-
)
|
|
171
|
-
validated_number = len(declaration_list[declaration_list["validated"]])
|
|
172
|
-
|
|
173
|
-
if declaration_set == VALIDATION_DECLARATIONS_SELECT_ALL:
|
|
174
|
-
selected_declarations = declaration_list
|
|
175
|
-
elif declaration_set == VALIDATION_DECLARATIONS_SELECT_VALIDATED:
|
|
176
|
-
selected_declarations = declaration_list[declaration_list["validated"]]
|
|
177
|
-
elif declaration_set == VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED:
|
|
178
|
-
selected_declarations = declaration_list[~declaration_list["validated"]]
|
|
179
|
-
else:
|
|
180
|
-
return general_error_jumbotron(f"Invalid declaration set ID ({declaration_set})")
|
|
181
|
-
selected_declarations.sort_values(by=["fullname"], ignore_index=True, inplace=True)
|
|
182
|
-
|
|
183
|
-
columns = [*data_columns]
|
|
184
|
-
columns.insert(0, column_names["fullname"])
|
|
185
|
-
columns.append("percent_global")
|
|
186
|
-
rows_number = len(selected_declarations)
|
|
187
|
-
|
|
188
|
-
table_header = [
|
|
189
|
-
html.Thead(
|
|
190
|
-
html.Tr(
|
|
191
|
-
[
|
|
192
|
-
*[
|
|
193
|
-
html.Th(
|
|
194
|
-
[
|
|
195
|
-
html.Div(
|
|
196
|
-
[
|
|
197
|
-
html.I(f"{global_params.column_titles[c]} "),
|
|
198
|
-
]
|
|
199
|
-
),
|
|
200
|
-
html.Div(time_unit(c, english=False, parenthesis=True)),
|
|
201
|
-
],
|
|
202
|
-
className="text-center",
|
|
203
|
-
)
|
|
204
|
-
for c in columns
|
|
205
|
-
],
|
|
206
|
-
html.Th(TABLE_COLUMN_VALIDATION),
|
|
207
|
-
],
|
|
208
|
-
)
|
|
209
|
-
)
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
table_body = [
|
|
213
|
-
html.Tbody(
|
|
214
|
-
[
|
|
215
|
-
html.Tr(
|
|
216
|
-
[
|
|
217
|
-
html.Td(
|
|
218
|
-
build_accordion(
|
|
219
|
-
i,
|
|
220
|
-
selected_declarations.iloc[i - 1][column_names["fullname"]],
|
|
221
|
-
agent_project_time(
|
|
222
|
-
selected_declarations.iloc[i - 1][column_names["fullname"]]
|
|
223
|
-
),
|
|
224
|
-
agent_tooltip_txt(selected_declarations.iloc[i - 1], data_columns),
|
|
225
|
-
(
|
|
226
|
-
"validated_hito_missing"
|
|
227
|
-
if selected_declarations.iloc[i - 1]["hito_missing"]
|
|
228
|
-
else ""
|
|
229
|
-
),
|
|
230
|
-
),
|
|
231
|
-
className="accordion",
|
|
232
|
-
key=f"validation-table-cell-{i}-fullname",
|
|
233
|
-
),
|
|
234
|
-
*[
|
|
235
|
-
activity_time_cell(selected_declarations.iloc[i - 1], c, i)
|
|
236
|
-
for c in data_columns
|
|
237
|
-
],
|
|
238
|
-
activity_time_cell(selected_declarations.iloc[i - 1], "percent_global", i),
|
|
239
|
-
html.Td(
|
|
240
|
-
[
|
|
241
|
-
dbc.Checklist(
|
|
242
|
-
options=[
|
|
243
|
-
{
|
|
244
|
-
"label": "",
|
|
245
|
-
"value": 1,
|
|
246
|
-
"disabled": selected_declarations.iloc[i - 1][
|
|
247
|
-
"validation_disabled"
|
|
248
|
-
],
|
|
249
|
-
}
|
|
250
|
-
],
|
|
251
|
-
value=[
|
|
252
|
-
int(
|
|
253
|
-
selected_declarations.iloc[i - 1][
|
|
254
|
-
column_names["agent_id"]
|
|
255
|
-
]
|
|
256
|
-
in validation_data.index
|
|
257
|
-
)
|
|
258
|
-
],
|
|
259
|
-
id={"type": "validation-switch", "id": i},
|
|
260
|
-
key=f"validation-switch-{i}",
|
|
261
|
-
switch=True,
|
|
262
|
-
),
|
|
263
|
-
# The dcc.Store is created to ease validation callback management
|
|
264
|
-
# by passing the agent ID and providing an Output object (but
|
|
265
|
-
# nothing will be written in it).
|
|
266
|
-
dcc.Store(
|
|
267
|
-
id={"type": "validation-agent-id", "id": i},
|
|
268
|
-
data=selected_declarations.iloc[i - 1][
|
|
269
|
-
column_names["agent_id"]
|
|
270
|
-
],
|
|
271
|
-
),
|
|
272
|
-
],
|
|
273
|
-
className="align-middle",
|
|
274
|
-
key=f"validation-table-cell-{i}-switch",
|
|
275
|
-
),
|
|
276
|
-
],
|
|
277
|
-
key=f"validation-table-row-{i}",
|
|
278
|
-
)
|
|
279
|
-
for i in range(1, rows_number + 1)
|
|
280
|
-
]
|
|
281
|
-
)
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
return html.Div(
|
|
285
|
-
[
|
|
286
|
-
dbc.Alert(
|
|
287
|
-
[
|
|
288
|
-
html.Div(
|
|
289
|
-
html.B(
|
|
290
|
-
(
|
|
291
|
-
f"Nombre d'agents de l'équipe '{team}' ayant déclaré :"
|
|
292
|
-
f" {len(selected_declarations)} (agents validés={validated_number}"
|
|
293
|
-
f"{hito_missing_msg}"
|
|
294
|
-
f", déclarations validées/totales="
|
|
295
|
-
f"{validated_declarations_num}/{len(project_declarations)})"
|
|
296
|
-
),
|
|
297
|
-
className="agent_count",
|
|
298
|
-
)
|
|
299
|
-
),
|
|
300
|
-
html.Div(
|
|
301
|
-
html.Em(
|
|
302
|
-
(
|
|
303
|
-
f"Temps déclarés de l'équipe / total ="
|
|
304
|
-
f" {round(selected_declarations['percent_global'].mean(), 1)}%"
|
|
305
|
-
f" (100%={SEMESTER_WEEKS} semaines)"
|
|
306
|
-
)
|
|
307
|
-
)
|
|
308
|
-
),
|
|
309
|
-
]
|
|
310
|
-
),
|
|
311
|
-
html.P(),
|
|
312
|
-
add_validation_declaration_selection_switch(declaration_set),
|
|
313
|
-
dbc.Table(
|
|
314
|
-
table_header + table_body,
|
|
315
|
-
id={"type": TABLE_TYPE_TABLE, "id": TABLE_ID_VALIDATION},
|
|
316
|
-
bordered=True,
|
|
317
|
-
hover=True,
|
|
318
|
-
striped=True,
|
|
319
|
-
class_name="sortable",
|
|
320
|
-
),
|
|
321
|
-
]
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def build_missing_agents_table(team, team_selection_date, period_date: str):
|
|
326
|
-
"""
|
|
327
|
-
Function to build a table listing all agents that have not declared yet their time on projects
|
|
328
|
-
|
|
329
|
-
:param team: selected team
|
|
330
|
-
:param team_selection_date: last time the team selection was changed
|
|
331
|
-
:param period_date: a date that must be inside the declaration period
|
|
332
|
-
:return: missing agent page
|
|
333
|
-
"""
|
|
334
|
-
global_params = GlobalParams()
|
|
335
|
-
column_names = global_params.columns
|
|
336
|
-
declaration_options = global_params.declaration_options
|
|
337
|
-
|
|
338
|
-
try:
|
|
339
|
-
declarations = get_team_projects(team, team_selection_date, period_date)
|
|
340
|
-
except InvalidHitoProjectName as e:
|
|
341
|
-
return ositah_jumbotron(
|
|
342
|
-
"Error loading projects",
|
|
343
|
-
e.msg,
|
|
344
|
-
title_class="text-danger",
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
agent_list = get_agents(period_date, team)
|
|
348
|
-
|
|
349
|
-
if declarations is None:
|
|
350
|
-
missing_agents = agent_list
|
|
351
|
-
else:
|
|
352
|
-
missing_agents = build_missing_agents(declarations, agent_list)
|
|
353
|
-
|
|
354
|
-
rows_number = len(missing_agents)
|
|
355
|
-
table_columns = ["fullname", "team"]
|
|
356
|
-
|
|
357
|
-
table_header = [
|
|
358
|
-
html.Thead(
|
|
359
|
-
html.Tr(
|
|
360
|
-
[
|
|
361
|
-
html.Th(
|
|
362
|
-
[
|
|
363
|
-
html.I(f"{global_params.column_titles[c]} "),
|
|
364
|
-
]
|
|
365
|
-
)
|
|
366
|
-
for c in table_columns
|
|
367
|
-
],
|
|
368
|
-
)
|
|
369
|
-
)
|
|
370
|
-
]
|
|
371
|
-
|
|
372
|
-
table_body = [
|
|
373
|
-
html.Tbody(
|
|
374
|
-
[
|
|
375
|
-
html.Tr(
|
|
376
|
-
[
|
|
377
|
-
html.Td(
|
|
378
|
-
missing_agents.iloc[i][column_names[c]],
|
|
379
|
-
className="align-middle",
|
|
380
|
-
)
|
|
381
|
-
for c in table_columns
|
|
382
|
-
]
|
|
383
|
-
)
|
|
384
|
-
for i in range(rows_number)
|
|
385
|
-
]
|
|
386
|
-
)
|
|
387
|
-
]
|
|
388
|
-
|
|
389
|
-
return html.Div(
|
|
390
|
-
[
|
|
391
|
-
dbc.Alert(
|
|
392
|
-
[
|
|
393
|
-
html.Div(
|
|
394
|
-
html.B(
|
|
395
|
-
(
|
|
396
|
-
f"Nombre d'agents de l'équipe '{team}' sans déclaration :"
|
|
397
|
-
f" {len(missing_agents)}"
|
|
398
|
-
)
|
|
399
|
-
)
|
|
400
|
-
),
|
|
401
|
-
html.Div(
|
|
402
|
-
html.Em(
|
|
403
|
-
(
|
|
404
|
-
f"Statuts non inclus :"
|
|
405
|
-
f" {', '.join(declaration_options['optional_statutes'])}"
|
|
406
|
-
)
|
|
407
|
-
)
|
|
408
|
-
),
|
|
409
|
-
html.Div(
|
|
410
|
-
html.Em(
|
|
411
|
-
(
|
|
412
|
-
f"Equipes non incluses :"
|
|
413
|
-
f" {', '.join(declaration_options['optional_teams'])}"
|
|
414
|
-
)
|
|
415
|
-
)
|
|
416
|
-
),
|
|
417
|
-
],
|
|
418
|
-
class_name="agent_count",
|
|
419
|
-
),
|
|
420
|
-
html.P(),
|
|
421
|
-
dbc.Table(
|
|
422
|
-
table_header + table_body,
|
|
423
|
-
id={"type": TABLE_TYPE_TABLE, "id": TABLE_ID_MISSING_AGENTS},
|
|
424
|
-
bordered=True,
|
|
425
|
-
hover=True,
|
|
426
|
-
striped=True,
|
|
427
|
-
class_name="sortable",
|
|
428
|
-
),
|
|
429
|
-
]
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
def build_statistics_table(team, team_selection_date, period_date: str):
|
|
434
|
-
"""
|
|
435
|
-
Function to build a table listing the number of declarations and missing declarations per team
|
|
436
|
-
|
|
437
|
-
:param team: selected team
|
|
438
|
-
:param team_selection_date: last time the team selection was changed
|
|
439
|
-
:param period_date: a date that must be inside the declaration period
|
|
440
|
-
:return: missing agent page
|
|
441
|
-
"""
|
|
442
|
-
global_params = GlobalParams()
|
|
443
|
-
column_names = global_params.columns
|
|
444
|
-
declaration_options = global_params.declaration_options
|
|
445
|
-
|
|
446
|
-
try:
|
|
447
|
-
session_data = global_params.session_data
|
|
448
|
-
except SessionDataMissing:
|
|
449
|
-
return no_session_id_jumbotron()
|
|
450
|
-
|
|
451
|
-
try:
|
|
452
|
-
declarations = get_team_projects(team, team_selection_date, period_date)
|
|
453
|
-
except InvalidHitoProjectName as e:
|
|
454
|
-
return ositah_jumbotron(
|
|
455
|
-
"Error loading projects",
|
|
456
|
-
e.msg,
|
|
457
|
-
title_class="text-danger",
|
|
458
|
-
)
|
|
459
|
-
|
|
460
|
-
agent_list = get_agents(period_date, team)
|
|
461
|
-
|
|
462
|
-
add_no_team_row = False
|
|
463
|
-
if declarations is None:
|
|
464
|
-
team_declarations = pd.DataFrame({"declarations_number": 0}, index=[team])
|
|
465
|
-
team_declarations["missings_number"] = len(agent_list)
|
|
466
|
-
missing_declarations = pd.DataFrame()
|
|
467
|
-
else:
|
|
468
|
-
team_agent_declarations = declarations.drop_duplicates(
|
|
469
|
-
subset=[column_names["team"], column_names["fullname"]]
|
|
470
|
-
)
|
|
471
|
-
# If team is None, set it to empty string
|
|
472
|
-
if len(team_agent_declarations.loc[team_agent_declarations.team.isna(), "team"]) > 0:
|
|
473
|
-
team_agent_declarations.loc[team_agent_declarations.team.isna(), "team"] = ""
|
|
474
|
-
add_no_team_row = True
|
|
475
|
-
team_declarations = (
|
|
476
|
-
team_agent_declarations[column_names["team"]]
|
|
477
|
-
.value_counts()
|
|
478
|
-
.to_frame(name="declarations_number")
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
missing_agents = build_missing_agents(declarations, agent_list)
|
|
482
|
-
missing_declarations = (
|
|
483
|
-
missing_agents[column_names["team"]].value_counts().to_frame(name="missings_number")
|
|
484
|
-
)
|
|
485
|
-
# If team is None, set it to empty string
|
|
486
|
-
if len(missing_declarations.loc[missing_declarations.index == ""]) > 0:
|
|
487
|
-
add_no_team_row = True
|
|
488
|
-
|
|
489
|
-
team_list = pd.DataFrame(index=session_data.agent_teams)
|
|
490
|
-
if team == TEAM_LIST_ALL_AGENTS:
|
|
491
|
-
team_list.drop(index=TEAM_LIST_ALL_AGENTS, inplace=True)
|
|
492
|
-
if add_no_team_row:
|
|
493
|
-
team_list = pd.concat([team_list, pd.DataFrame(index=[""])])
|
|
494
|
-
else:
|
|
495
|
-
team_list = team_list[team_list.index.str.match(team)]
|
|
496
|
-
team_agents = agent_list[column_names["team"]].value_counts().to_frame(name="agents_number")
|
|
497
|
-
team_declarations = pd.merge(
|
|
498
|
-
team_declarations,
|
|
499
|
-
team_list,
|
|
500
|
-
how="outer",
|
|
501
|
-
left_index=True,
|
|
502
|
-
right_index=True,
|
|
503
|
-
sort=True,
|
|
504
|
-
).fillna(0)
|
|
505
|
-
team_declarations = pd.merge(team_declarations, team_agents, left_index=True, right_index=True)
|
|
506
|
-
|
|
507
|
-
if len(missing_declarations):
|
|
508
|
-
team_declarations = pd.merge(
|
|
509
|
-
team_declarations,
|
|
510
|
-
missing_declarations,
|
|
511
|
-
how="outer",
|
|
512
|
-
left_index=True,
|
|
513
|
-
right_index=True,
|
|
514
|
-
sort=True,
|
|
515
|
-
).fillna(0)
|
|
516
|
-
else:
|
|
517
|
-
team_declarations["missings_number"] = 0
|
|
518
|
-
|
|
519
|
-
declarations_total = int(sum(team_declarations["declarations_number"]))
|
|
520
|
-
missings_total = int(sum(team_declarations["missings_number"]))
|
|
521
|
-
|
|
522
|
-
data_columns = ["declarations_number", "missings_number"]
|
|
523
|
-
|
|
524
|
-
table_header = [
|
|
525
|
-
html.Thead(
|
|
526
|
-
html.Tr(
|
|
527
|
-
[
|
|
528
|
-
html.Th(
|
|
529
|
-
[
|
|
530
|
-
html.I(f"{global_params.column_titles['team']} "),
|
|
531
|
-
]
|
|
532
|
-
),
|
|
533
|
-
*[
|
|
534
|
-
html.Th(
|
|
535
|
-
html.Div(
|
|
536
|
-
[
|
|
537
|
-
html.I(f"{global_params.column_titles[c]} "),
|
|
538
|
-
]
|
|
539
|
-
),
|
|
540
|
-
className="text-center",
|
|
541
|
-
)
|
|
542
|
-
for c in data_columns
|
|
543
|
-
],
|
|
544
|
-
],
|
|
545
|
-
)
|
|
546
|
-
)
|
|
547
|
-
]
|
|
548
|
-
|
|
549
|
-
table_body = [
|
|
550
|
-
html.Tbody(
|
|
551
|
-
[
|
|
552
|
-
html.Tr(
|
|
553
|
-
[
|
|
554
|
-
html.Td(i),
|
|
555
|
-
*[
|
|
556
|
-
html.Td(
|
|
557
|
-
team_declarations.loc[i, column_names[c]],
|
|
558
|
-
className="text-center",
|
|
559
|
-
)
|
|
560
|
-
for c in data_columns
|
|
561
|
-
],
|
|
562
|
-
]
|
|
563
|
-
)
|
|
564
|
-
for i in team_declarations.index.values
|
|
565
|
-
]
|
|
566
|
-
)
|
|
567
|
-
]
|
|
568
|
-
|
|
569
|
-
return html.Div(
|
|
570
|
-
[
|
|
571
|
-
dbc.Alert(
|
|
572
|
-
[
|
|
573
|
-
html.Div(
|
|
574
|
-
html.B(
|
|
575
|
-
(
|
|
576
|
-
f"Statistiques des déclarations pour l'équipe '{team}' :"
|
|
577
|
-
f" effectuées={declarations_total}, manquantes={missings_total}"
|
|
578
|
-
)
|
|
579
|
-
)
|
|
580
|
-
),
|
|
581
|
-
html.Div(
|
|
582
|
-
html.Em(
|
|
583
|
-
(
|
|
584
|
-
f"Statuts non inclus dans les déclarations manquantes :"
|
|
585
|
-
f" {', '.join(declaration_options['optional_statutes'])}"
|
|
586
|
-
)
|
|
587
|
-
)
|
|
588
|
-
),
|
|
589
|
-
html.Div(
|
|
590
|
-
html.Em(
|
|
591
|
-
(
|
|
592
|
-
f"Equipes non incluses dans les déclarations manquantes :"
|
|
593
|
-
f" {', '.join(declaration_options['optional_teams'])}"
|
|
594
|
-
)
|
|
595
|
-
)
|
|
596
|
-
),
|
|
597
|
-
],
|
|
598
|
-
class_name="agent_count",
|
|
599
|
-
),
|
|
600
|
-
html.P(),
|
|
601
|
-
dbc.Table(
|
|
602
|
-
table_header + table_body,
|
|
603
|
-
id={"type": TABLE_TYPE_TABLE, "id": TABLE_ID_DECLARATION_STATS},
|
|
604
|
-
bordered=True,
|
|
605
|
-
hover=True,
|
|
606
|
-
striped=True,
|
|
607
|
-
class_name="sortable",
|
|
608
|
-
),
|
|
609
|
-
]
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
def build_missing_agents(declarations, agents, mandatory_only: bool = True):
|
|
614
|
-
"""
|
|
615
|
-
Build the missing agents list and return it as a dataframe. It allows not toking into
|
|
616
|
-
consideration the agent declarations for agents whose declaration is not mandatory
|
|
617
|
-
(e.g. fellows).
|
|
618
|
-
|
|
619
|
-
:param declarations: project declarations
|
|
620
|
-
:param agents: list of agents who are supposed to do a declaration
|
|
621
|
-
:param mandatory_only: ignore agents whose declaration is optional
|
|
622
|
-
:return: missing agents dataframe
|
|
623
|
-
"""
|
|
624
|
-
|
|
625
|
-
global_params = GlobalParams()
|
|
626
|
-
column_names = global_params.columns
|
|
627
|
-
|
|
628
|
-
declared_agents = pd.DataFrame(
|
|
629
|
-
{column_names["agent_id"]: declarations[column_names["agent_id"]].unique()}
|
|
630
|
-
)
|
|
631
|
-
if mandatory_only:
|
|
632
|
-
agent_list = agents[~agents.optional]
|
|
633
|
-
else:
|
|
634
|
-
agent_list = agents
|
|
635
|
-
missing_agents = pd.merge(
|
|
636
|
-
agent_list,
|
|
637
|
-
declared_agents,
|
|
638
|
-
on=column_names["agent_id"],
|
|
639
|
-
how="outer",
|
|
640
|
-
indicator=True,
|
|
641
|
-
)
|
|
642
|
-
missing_agents = missing_agents[missing_agents._merge == "left_only"].sort_values(
|
|
643
|
-
by=column_names["fullname"]
|
|
644
|
-
)
|
|
645
|
-
|
|
646
|
-
return missing_agents
|
|
1
|
+
"""
|
|
2
|
+
Functions to build tables associated with tab layouts
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import dash_bootstrap_components as dbc
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from dash import dcc, html
|
|
9
|
+
from flask import session
|
|
10
|
+
|
|
11
|
+
from ositah.apps.validation.parameters import (
|
|
12
|
+
TABLE_COLUMN_VALIDATION,
|
|
13
|
+
TABLE_ID_DECLARATION_STATS,
|
|
14
|
+
TABLE_ID_MISSING_AGENTS,
|
|
15
|
+
TABLE_ID_VALIDATION,
|
|
16
|
+
VALIDATION_DECLARATIONS_SELECT_ALL,
|
|
17
|
+
VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED,
|
|
18
|
+
VALIDATION_DECLARATIONS_SELECT_VALIDATED,
|
|
19
|
+
)
|
|
20
|
+
from ositah.apps.validation.tools import (
|
|
21
|
+
activity_time_cell,
|
|
22
|
+
add_validation_declaration_selection_switch,
|
|
23
|
+
agent_project_time,
|
|
24
|
+
agent_tooltip_txt,
|
|
25
|
+
category_declarations,
|
|
26
|
+
define_declaration_thresholds,
|
|
27
|
+
get_all_validation_status,
|
|
28
|
+
validation_started,
|
|
29
|
+
)
|
|
30
|
+
from ositah.utils.agents import get_agents
|
|
31
|
+
from ositah.utils.exceptions import InvalidHitoProjectName, SessionDataMissing
|
|
32
|
+
from ositah.utils.menus import TABLE_TYPE_TABLE, build_accordion, ositah_jumbotron
|
|
33
|
+
from ositah.utils.projects import (
|
|
34
|
+
CATEGORY_DEFAULT,
|
|
35
|
+
DATA_SOURCE_HITO,
|
|
36
|
+
DATA_SOURCE_OSITAH,
|
|
37
|
+
get_team_projects,
|
|
38
|
+
time_unit,
|
|
39
|
+
)
|
|
40
|
+
from ositah.utils.utils import (
|
|
41
|
+
SEMESTER_WEEKS,
|
|
42
|
+
TEAM_LIST_ALL_AGENTS,
|
|
43
|
+
GlobalParams,
|
|
44
|
+
general_error_jumbotron,
|
|
45
|
+
no_session_id_jumbotron,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_validation_table(team, team_selection_date, declaration_set: int, period_date: str):
|
|
50
|
+
"""
|
|
51
|
+
Build the agent list of the selected team with their declarations. Returns a table.
|
|
52
|
+
|
|
53
|
+
:param team: selected team
|
|
54
|
+
:param team_selection_date: last time the team selection was changed
|
|
55
|
+
:param declaration_set: selected declaration set (all, validated or non-validated ones)
|
|
56
|
+
:param period_date: a date that must be inside the declaration period
|
|
57
|
+
:return: DBC table
|
|
58
|
+
"""
|
|
59
|
+
global_params = GlobalParams()
|
|
60
|
+
column_names = global_params.columns
|
|
61
|
+
try:
|
|
62
|
+
session_data = global_params.session_data
|
|
63
|
+
except SessionDataMissing:
|
|
64
|
+
return no_session_id_jumbotron()
|
|
65
|
+
|
|
66
|
+
if session["user_email"] in global_params.roles["read-only"]:
|
|
67
|
+
validation_disabled = True
|
|
68
|
+
else:
|
|
69
|
+
if session_data.role in global_params.validation_params["override_period"]:
|
|
70
|
+
validation_disabled = False
|
|
71
|
+
else:
|
|
72
|
+
validation_disabled = not validation_started(period_date)
|
|
73
|
+
|
|
74
|
+
validation_data = get_all_validation_status(period_date)
|
|
75
|
+
define_declaration_thresholds(period_date)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
project_declarations = get_team_projects(
|
|
79
|
+
team, team_selection_date, period_date, DATA_SOURCE_HITO
|
|
80
|
+
)
|
|
81
|
+
except InvalidHitoProjectName as e:
|
|
82
|
+
return ositah_jumbotron(
|
|
83
|
+
"Error loading projects",
|
|
84
|
+
e.msg,
|
|
85
|
+
title_class="text-danger",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if project_declarations is None:
|
|
89
|
+
return html.Div(
|
|
90
|
+
[
|
|
91
|
+
dbc.Alert(
|
|
92
|
+
f"Aucune déclaration effectuée pour l'équipe '{team}'",
|
|
93
|
+
color="warning",
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
declarations = category_declarations(project_declarations)
|
|
98
|
+
|
|
99
|
+
if team == TEAM_LIST_ALL_AGENTS:
|
|
100
|
+
declaration_list = declarations
|
|
101
|
+
else:
|
|
102
|
+
declaration_list = declarations[declarations[column_names["team"]].str.match(team)]
|
|
103
|
+
declaration_list["validation_disabled"] = validation_disabled
|
|
104
|
+
|
|
105
|
+
data_columns = [CATEGORY_DEFAULT]
|
|
106
|
+
data_columns.extend(global_params.project_categories.keys())
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
validated_project_declarations = get_team_projects(
|
|
110
|
+
team, team_selection_date, period_date, DATA_SOURCE_OSITAH, use_cache=False
|
|
111
|
+
)
|
|
112
|
+
except InvalidHitoProjectName as e:
|
|
113
|
+
return ositah_jumbotron(
|
|
114
|
+
"Error loading projects",
|
|
115
|
+
e.msg,
|
|
116
|
+
title_class="text-danger",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if validated_project_declarations is None:
|
|
120
|
+
declaration_list["validated"] = False
|
|
121
|
+
declaration_list["hito_missing"] = False
|
|
122
|
+
validated_declarations_num = 0
|
|
123
|
+
hito_missing_num = 0
|
|
124
|
+
hito_missing_msg = ""
|
|
125
|
+
else:
|
|
126
|
+
validated_declarations_num = len(validated_project_declarations)
|
|
127
|
+
validated_declarations = category_declarations(
|
|
128
|
+
validated_project_declarations, use_cache=False
|
|
129
|
+
)
|
|
130
|
+
# Do an outer merge to detect validated declarations removed from Hito
|
|
131
|
+
declaration_list = declaration_list.merge(
|
|
132
|
+
validated_declarations,
|
|
133
|
+
how="outer",
|
|
134
|
+
on=column_names["agent_id"],
|
|
135
|
+
indicator=True,
|
|
136
|
+
suffixes=[None, "_val"],
|
|
137
|
+
)
|
|
138
|
+
declaration_list["validated"] = (declaration_list._merge == "both") | (
|
|
139
|
+
declaration_list._merge == "right_only"
|
|
140
|
+
)
|
|
141
|
+
declaration_list["hito_missing"] = declaration_list._merge == "right_only"
|
|
142
|
+
hito_missing_num = len(declaration_list.loc[declaration_list["hito_missing"]])
|
|
143
|
+
if hito_missing_num > 0:
|
|
144
|
+
columns_fillna = {c: 0 for c in [*data_columns, "total_hours", "percent_global"]}
|
|
145
|
+
declaration_list.loc[declaration_list["hito_missing"]] = declaration_list.fillna(
|
|
146
|
+
value=columns_fillna
|
|
147
|
+
)
|
|
148
|
+
declaration_list.loc[declaration_list["hito_missing"], column_names["fullname"]] = (
|
|
149
|
+
declaration_list[f"{column_names['fullname']}_val"]
|
|
150
|
+
)
|
|
151
|
+
declaration_list.loc[declaration_list["hito_missing"], column_names["team"]] = (
|
|
152
|
+
declaration_list[f"{column_names['team']}_val"]
|
|
153
|
+
)
|
|
154
|
+
declaration_list.loc[declaration_list["hito_missing"], "suspect"] = True
|
|
155
|
+
declaration_list.loc[declaration_list["hito_missing"], "validation_disabled"] = True
|
|
156
|
+
declaration_list.sort_values(by=column_names["fullname"], inplace=True)
|
|
157
|
+
hito_missing_msg = (
|
|
158
|
+
f" dont {hito_missing_num} supprimé"
|
|
159
|
+
f"{'s' if hito_missing_num > 1 else ''} de Hito"
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
hito_missing_msg = ""
|
|
163
|
+
for category in data_columns:
|
|
164
|
+
time_ok_column = f"{category}_time_ok"
|
|
165
|
+
declaration_list.loc[declaration_list.validated, time_ok_column] = np.isclose(
|
|
166
|
+
declaration_list.loc[declaration_list.validated, category],
|
|
167
|
+
declaration_list.loc[declaration_list.validated, f"{category}_val"],
|
|
168
|
+
rtol=1e-5,
|
|
169
|
+
atol=0,
|
|
170
|
+
)
|
|
171
|
+
validated_number = len(declaration_list[declaration_list["validated"]])
|
|
172
|
+
|
|
173
|
+
if declaration_set == VALIDATION_DECLARATIONS_SELECT_ALL:
|
|
174
|
+
selected_declarations = declaration_list
|
|
175
|
+
elif declaration_set == VALIDATION_DECLARATIONS_SELECT_VALIDATED:
|
|
176
|
+
selected_declarations = declaration_list[declaration_list["validated"]]
|
|
177
|
+
elif declaration_set == VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED:
|
|
178
|
+
selected_declarations = declaration_list[~declaration_list["validated"]]
|
|
179
|
+
else:
|
|
180
|
+
return general_error_jumbotron(f"Invalid declaration set ID ({declaration_set})")
|
|
181
|
+
selected_declarations.sort_values(by=["fullname"], ignore_index=True, inplace=True)
|
|
182
|
+
|
|
183
|
+
columns = [*data_columns]
|
|
184
|
+
columns.insert(0, column_names["fullname"])
|
|
185
|
+
columns.append("percent_global")
|
|
186
|
+
rows_number = len(selected_declarations)
|
|
187
|
+
|
|
188
|
+
table_header = [
|
|
189
|
+
html.Thead(
|
|
190
|
+
html.Tr(
|
|
191
|
+
[
|
|
192
|
+
*[
|
|
193
|
+
html.Th(
|
|
194
|
+
[
|
|
195
|
+
html.Div(
|
|
196
|
+
[
|
|
197
|
+
html.I(f"{global_params.column_titles[c]} "),
|
|
198
|
+
]
|
|
199
|
+
),
|
|
200
|
+
html.Div(time_unit(c, english=False, parenthesis=True)),
|
|
201
|
+
],
|
|
202
|
+
className="text-center",
|
|
203
|
+
)
|
|
204
|
+
for c in columns
|
|
205
|
+
],
|
|
206
|
+
html.Th(TABLE_COLUMN_VALIDATION),
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
table_body = [
|
|
213
|
+
html.Tbody(
|
|
214
|
+
[
|
|
215
|
+
html.Tr(
|
|
216
|
+
[
|
|
217
|
+
html.Td(
|
|
218
|
+
build_accordion(
|
|
219
|
+
i,
|
|
220
|
+
selected_declarations.iloc[i - 1][column_names["fullname"]],
|
|
221
|
+
agent_project_time(
|
|
222
|
+
selected_declarations.iloc[i - 1][column_names["fullname"]]
|
|
223
|
+
),
|
|
224
|
+
agent_tooltip_txt(selected_declarations.iloc[i - 1], data_columns),
|
|
225
|
+
(
|
|
226
|
+
"validated_hito_missing"
|
|
227
|
+
if selected_declarations.iloc[i - 1]["hito_missing"]
|
|
228
|
+
else ""
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
className="accordion",
|
|
232
|
+
key=f"validation-table-cell-{i}-fullname",
|
|
233
|
+
),
|
|
234
|
+
*[
|
|
235
|
+
activity_time_cell(selected_declarations.iloc[i - 1], c, i)
|
|
236
|
+
for c in data_columns
|
|
237
|
+
],
|
|
238
|
+
activity_time_cell(selected_declarations.iloc[i - 1], "percent_global", i),
|
|
239
|
+
html.Td(
|
|
240
|
+
[
|
|
241
|
+
dbc.Checklist(
|
|
242
|
+
options=[
|
|
243
|
+
{
|
|
244
|
+
"label": "",
|
|
245
|
+
"value": 1,
|
|
246
|
+
"disabled": selected_declarations.iloc[i - 1][
|
|
247
|
+
"validation_disabled"
|
|
248
|
+
],
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
value=[
|
|
252
|
+
int(
|
|
253
|
+
selected_declarations.iloc[i - 1][
|
|
254
|
+
column_names["agent_id"]
|
|
255
|
+
]
|
|
256
|
+
in validation_data.index
|
|
257
|
+
)
|
|
258
|
+
],
|
|
259
|
+
id={"type": "validation-switch", "id": i},
|
|
260
|
+
key=f"validation-switch-{i}",
|
|
261
|
+
switch=True,
|
|
262
|
+
),
|
|
263
|
+
# The dcc.Store is created to ease validation callback management
|
|
264
|
+
# by passing the agent ID and providing an Output object (but
|
|
265
|
+
# nothing will be written in it).
|
|
266
|
+
dcc.Store(
|
|
267
|
+
id={"type": "validation-agent-id", "id": i},
|
|
268
|
+
data=selected_declarations.iloc[i - 1][
|
|
269
|
+
column_names["agent_id"]
|
|
270
|
+
],
|
|
271
|
+
),
|
|
272
|
+
],
|
|
273
|
+
className="align-middle",
|
|
274
|
+
key=f"validation-table-cell-{i}-switch",
|
|
275
|
+
),
|
|
276
|
+
],
|
|
277
|
+
key=f"validation-table-row-{i}",
|
|
278
|
+
)
|
|
279
|
+
for i in range(1, rows_number + 1)
|
|
280
|
+
]
|
|
281
|
+
)
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
return html.Div(
|
|
285
|
+
[
|
|
286
|
+
dbc.Alert(
|
|
287
|
+
[
|
|
288
|
+
html.Div(
|
|
289
|
+
html.B(
|
|
290
|
+
(
|
|
291
|
+
f"Nombre d'agents de l'équipe '{team}' ayant déclaré :"
|
|
292
|
+
f" {len(selected_declarations)} (agents validés={validated_number}"
|
|
293
|
+
f"{hito_missing_msg}"
|
|
294
|
+
f", déclarations validées/totales="
|
|
295
|
+
f"{validated_declarations_num}/{len(project_declarations)})"
|
|
296
|
+
),
|
|
297
|
+
className="agent_count",
|
|
298
|
+
)
|
|
299
|
+
),
|
|
300
|
+
html.Div(
|
|
301
|
+
html.Em(
|
|
302
|
+
(
|
|
303
|
+
f"Temps déclarés de l'équipe / total ="
|
|
304
|
+
f" {round(selected_declarations['percent_global'].mean(), 1)}%"
|
|
305
|
+
f" (100%={SEMESTER_WEEKS} semaines)"
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
),
|
|
309
|
+
]
|
|
310
|
+
),
|
|
311
|
+
html.P(),
|
|
312
|
+
add_validation_declaration_selection_switch(declaration_set),
|
|
313
|
+
dbc.Table(
|
|
314
|
+
table_header + table_body,
|
|
315
|
+
id={"type": TABLE_TYPE_TABLE, "id": TABLE_ID_VALIDATION},
|
|
316
|
+
bordered=True,
|
|
317
|
+
hover=True,
|
|
318
|
+
striped=True,
|
|
319
|
+
class_name="sortable",
|
|
320
|
+
),
|
|
321
|
+
]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def build_missing_agents_table(team, team_selection_date, period_date: str):
|
|
326
|
+
"""
|
|
327
|
+
Function to build a table listing all agents that have not declared yet their time on projects
|
|
328
|
+
|
|
329
|
+
:param team: selected team
|
|
330
|
+
:param team_selection_date: last time the team selection was changed
|
|
331
|
+
:param period_date: a date that must be inside the declaration period
|
|
332
|
+
:return: missing agent page
|
|
333
|
+
"""
|
|
334
|
+
global_params = GlobalParams()
|
|
335
|
+
column_names = global_params.columns
|
|
336
|
+
declaration_options = global_params.declaration_options
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
declarations = get_team_projects(team, team_selection_date, period_date)
|
|
340
|
+
except InvalidHitoProjectName as e:
|
|
341
|
+
return ositah_jumbotron(
|
|
342
|
+
"Error loading projects",
|
|
343
|
+
e.msg,
|
|
344
|
+
title_class="text-danger",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
agent_list = get_agents(period_date, team)
|
|
348
|
+
|
|
349
|
+
if declarations is None:
|
|
350
|
+
missing_agents = agent_list
|
|
351
|
+
else:
|
|
352
|
+
missing_agents = build_missing_agents(declarations, agent_list)
|
|
353
|
+
|
|
354
|
+
rows_number = len(missing_agents)
|
|
355
|
+
table_columns = ["fullname", "team"]
|
|
356
|
+
|
|
357
|
+
table_header = [
|
|
358
|
+
html.Thead(
|
|
359
|
+
html.Tr(
|
|
360
|
+
[
|
|
361
|
+
html.Th(
|
|
362
|
+
[
|
|
363
|
+
html.I(f"{global_params.column_titles[c]} "),
|
|
364
|
+
]
|
|
365
|
+
)
|
|
366
|
+
for c in table_columns
|
|
367
|
+
],
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
table_body = [
|
|
373
|
+
html.Tbody(
|
|
374
|
+
[
|
|
375
|
+
html.Tr(
|
|
376
|
+
[
|
|
377
|
+
html.Td(
|
|
378
|
+
missing_agents.iloc[i][column_names[c]],
|
|
379
|
+
className="align-middle",
|
|
380
|
+
)
|
|
381
|
+
for c in table_columns
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
for i in range(rows_number)
|
|
385
|
+
]
|
|
386
|
+
)
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
return html.Div(
|
|
390
|
+
[
|
|
391
|
+
dbc.Alert(
|
|
392
|
+
[
|
|
393
|
+
html.Div(
|
|
394
|
+
html.B(
|
|
395
|
+
(
|
|
396
|
+
f"Nombre d'agents de l'équipe '{team}' sans déclaration :"
|
|
397
|
+
f" {len(missing_agents)}"
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
),
|
|
401
|
+
html.Div(
|
|
402
|
+
html.Em(
|
|
403
|
+
(
|
|
404
|
+
f"Statuts non inclus :"
|
|
405
|
+
f" {', '.join(declaration_options['optional_statutes'])}"
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
),
|
|
409
|
+
html.Div(
|
|
410
|
+
html.Em(
|
|
411
|
+
(
|
|
412
|
+
f"Equipes non incluses :"
|
|
413
|
+
f" {', '.join(declaration_options['optional_teams'])}"
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
),
|
|
417
|
+
],
|
|
418
|
+
class_name="agent_count",
|
|
419
|
+
),
|
|
420
|
+
html.P(),
|
|
421
|
+
dbc.Table(
|
|
422
|
+
table_header + table_body,
|
|
423
|
+
id={"type": TABLE_TYPE_TABLE, "id": TABLE_ID_MISSING_AGENTS},
|
|
424
|
+
bordered=True,
|
|
425
|
+
hover=True,
|
|
426
|
+
striped=True,
|
|
427
|
+
class_name="sortable",
|
|
428
|
+
),
|
|
429
|
+
]
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def build_statistics_table(team, team_selection_date, period_date: str):
|
|
434
|
+
"""
|
|
435
|
+
Function to build a table listing the number of declarations and missing declarations per team
|
|
436
|
+
|
|
437
|
+
:param team: selected team
|
|
438
|
+
:param team_selection_date: last time the team selection was changed
|
|
439
|
+
:param period_date: a date that must be inside the declaration period
|
|
440
|
+
:return: missing agent page
|
|
441
|
+
"""
|
|
442
|
+
global_params = GlobalParams()
|
|
443
|
+
column_names = global_params.columns
|
|
444
|
+
declaration_options = global_params.declaration_options
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
session_data = global_params.session_data
|
|
448
|
+
except SessionDataMissing:
|
|
449
|
+
return no_session_id_jumbotron()
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
declarations = get_team_projects(team, team_selection_date, period_date)
|
|
453
|
+
except InvalidHitoProjectName as e:
|
|
454
|
+
return ositah_jumbotron(
|
|
455
|
+
"Error loading projects",
|
|
456
|
+
e.msg,
|
|
457
|
+
title_class="text-danger",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
agent_list = get_agents(period_date, team)
|
|
461
|
+
|
|
462
|
+
add_no_team_row = False
|
|
463
|
+
if declarations is None:
|
|
464
|
+
team_declarations = pd.DataFrame({"declarations_number": 0}, index=[team])
|
|
465
|
+
team_declarations["missings_number"] = len(agent_list)
|
|
466
|
+
missing_declarations = pd.DataFrame()
|
|
467
|
+
else:
|
|
468
|
+
team_agent_declarations = declarations.drop_duplicates(
|
|
469
|
+
subset=[column_names["team"], column_names["fullname"]]
|
|
470
|
+
)
|
|
471
|
+
# If team is None, set it to empty string
|
|
472
|
+
if len(team_agent_declarations.loc[team_agent_declarations.team.isna(), "team"]) > 0:
|
|
473
|
+
team_agent_declarations.loc[team_agent_declarations.team.isna(), "team"] = ""
|
|
474
|
+
add_no_team_row = True
|
|
475
|
+
team_declarations = (
|
|
476
|
+
team_agent_declarations[column_names["team"]]
|
|
477
|
+
.value_counts()
|
|
478
|
+
.to_frame(name="declarations_number")
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
missing_agents = build_missing_agents(declarations, agent_list)
|
|
482
|
+
missing_declarations = (
|
|
483
|
+
missing_agents[column_names["team"]].value_counts().to_frame(name="missings_number")
|
|
484
|
+
)
|
|
485
|
+
# If team is None, set it to empty string
|
|
486
|
+
if len(missing_declarations.loc[missing_declarations.index == ""]) > 0:
|
|
487
|
+
add_no_team_row = True
|
|
488
|
+
|
|
489
|
+
team_list = pd.DataFrame(index=session_data.agent_teams)
|
|
490
|
+
if team == TEAM_LIST_ALL_AGENTS:
|
|
491
|
+
team_list.drop(index=TEAM_LIST_ALL_AGENTS, inplace=True)
|
|
492
|
+
if add_no_team_row:
|
|
493
|
+
team_list = pd.concat([team_list, pd.DataFrame(index=[""])])
|
|
494
|
+
else:
|
|
495
|
+
team_list = team_list[team_list.index.str.match(team)]
|
|
496
|
+
team_agents = agent_list[column_names["team"]].value_counts().to_frame(name="agents_number")
|
|
497
|
+
team_declarations = pd.merge(
|
|
498
|
+
team_declarations,
|
|
499
|
+
team_list,
|
|
500
|
+
how="outer",
|
|
501
|
+
left_index=True,
|
|
502
|
+
right_index=True,
|
|
503
|
+
sort=True,
|
|
504
|
+
).fillna(0)
|
|
505
|
+
team_declarations = pd.merge(team_declarations, team_agents, left_index=True, right_index=True)
|
|
506
|
+
|
|
507
|
+
if len(missing_declarations):
|
|
508
|
+
team_declarations = pd.merge(
|
|
509
|
+
team_declarations,
|
|
510
|
+
missing_declarations,
|
|
511
|
+
how="outer",
|
|
512
|
+
left_index=True,
|
|
513
|
+
right_index=True,
|
|
514
|
+
sort=True,
|
|
515
|
+
).fillna(0)
|
|
516
|
+
else:
|
|
517
|
+
team_declarations["missings_number"] = 0
|
|
518
|
+
|
|
519
|
+
declarations_total = int(sum(team_declarations["declarations_number"]))
|
|
520
|
+
missings_total = int(sum(team_declarations["missings_number"]))
|
|
521
|
+
|
|
522
|
+
data_columns = ["declarations_number", "missings_number"]
|
|
523
|
+
|
|
524
|
+
table_header = [
|
|
525
|
+
html.Thead(
|
|
526
|
+
html.Tr(
|
|
527
|
+
[
|
|
528
|
+
html.Th(
|
|
529
|
+
[
|
|
530
|
+
html.I(f"{global_params.column_titles['team']} "),
|
|
531
|
+
]
|
|
532
|
+
),
|
|
533
|
+
*[
|
|
534
|
+
html.Th(
|
|
535
|
+
html.Div(
|
|
536
|
+
[
|
|
537
|
+
html.I(f"{global_params.column_titles[c]} "),
|
|
538
|
+
]
|
|
539
|
+
),
|
|
540
|
+
className="text-center",
|
|
541
|
+
)
|
|
542
|
+
for c in data_columns
|
|
543
|
+
],
|
|
544
|
+
],
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
table_body = [
|
|
550
|
+
html.Tbody(
|
|
551
|
+
[
|
|
552
|
+
html.Tr(
|
|
553
|
+
[
|
|
554
|
+
html.Td(i),
|
|
555
|
+
*[
|
|
556
|
+
html.Td(
|
|
557
|
+
team_declarations.loc[i, column_names[c]],
|
|
558
|
+
className="text-center",
|
|
559
|
+
)
|
|
560
|
+
for c in data_columns
|
|
561
|
+
],
|
|
562
|
+
]
|
|
563
|
+
)
|
|
564
|
+
for i in team_declarations.index.values
|
|
565
|
+
]
|
|
566
|
+
)
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
return html.Div(
|
|
570
|
+
[
|
|
571
|
+
dbc.Alert(
|
|
572
|
+
[
|
|
573
|
+
html.Div(
|
|
574
|
+
html.B(
|
|
575
|
+
(
|
|
576
|
+
f"Statistiques des déclarations pour l'équipe '{team}' :"
|
|
577
|
+
f" effectuées={declarations_total}, manquantes={missings_total}"
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
),
|
|
581
|
+
html.Div(
|
|
582
|
+
html.Em(
|
|
583
|
+
(
|
|
584
|
+
f"Statuts non inclus dans les déclarations manquantes :"
|
|
585
|
+
f" {', '.join(declaration_options['optional_statutes'])}"
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
),
|
|
589
|
+
html.Div(
|
|
590
|
+
html.Em(
|
|
591
|
+
(
|
|
592
|
+
f"Equipes non incluses dans les déclarations manquantes :"
|
|
593
|
+
f" {', '.join(declaration_options['optional_teams'])}"
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
),
|
|
597
|
+
],
|
|
598
|
+
class_name="agent_count",
|
|
599
|
+
),
|
|
600
|
+
html.P(),
|
|
601
|
+
dbc.Table(
|
|
602
|
+
table_header + table_body,
|
|
603
|
+
id={"type": TABLE_TYPE_TABLE, "id": TABLE_ID_DECLARATION_STATS},
|
|
604
|
+
bordered=True,
|
|
605
|
+
hover=True,
|
|
606
|
+
striped=True,
|
|
607
|
+
class_name="sortable",
|
|
608
|
+
),
|
|
609
|
+
]
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def build_missing_agents(declarations, agents, mandatory_only: bool = True):
|
|
614
|
+
"""
|
|
615
|
+
Build the missing agents list and return it as a dataframe. It allows not toking into
|
|
616
|
+
consideration the agent declarations for agents whose declaration is not mandatory
|
|
617
|
+
(e.g. fellows).
|
|
618
|
+
|
|
619
|
+
:param declarations: project declarations
|
|
620
|
+
:param agents: list of agents who are supposed to do a declaration
|
|
621
|
+
:param mandatory_only: ignore agents whose declaration is optional
|
|
622
|
+
:return: missing agents dataframe
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
global_params = GlobalParams()
|
|
626
|
+
column_names = global_params.columns
|
|
627
|
+
|
|
628
|
+
declared_agents = pd.DataFrame(
|
|
629
|
+
{column_names["agent_id"]: declarations[column_names["agent_id"]].unique()}
|
|
630
|
+
)
|
|
631
|
+
if mandatory_only:
|
|
632
|
+
agent_list = agents[~agents.optional]
|
|
633
|
+
else:
|
|
634
|
+
agent_list = agents
|
|
635
|
+
missing_agents = pd.merge(
|
|
636
|
+
agent_list,
|
|
637
|
+
declared_agents,
|
|
638
|
+
on=column_names["agent_id"],
|
|
639
|
+
how="outer",
|
|
640
|
+
indicator=True,
|
|
641
|
+
)
|
|
642
|
+
missing_agents = missing_agents[missing_agents._merge == "left_only"].sort_values(
|
|
643
|
+
by=column_names["fullname"]
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
return missing_agents
|