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/export.py
CHANGED
|
@@ -1,1191 +1,1208 @@
|
|
|
1
|
-
# OSITAH sub-application exporting data to NSIP
|
|
2
|
-
import math
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
import dash_bootstrap_components as dbc
|
|
6
|
-
import numpy as np
|
|
7
|
-
from dash import dcc, html
|
|
8
|
-
from dash.dependencies import ALL, MATCH, Input, Output, State
|
|
9
|
-
from dash.exceptions import PreventUpdate
|
|
10
|
-
|
|
11
|
-
from ositah.app import app
|
|
12
|
-
from ositah.utils.agents import get_agents, get_nsip_agents
|
|
13
|
-
from ositah.utils.exceptions import SessionDataMissing
|
|
14
|
-
from ositah.utils.menus import (
|
|
15
|
-
DATA_SELECTED_SOURCE_ID,
|
|
16
|
-
TABLE_TYPE_TABLE,
|
|
17
|
-
TEAM_SELECTED_VALUE_ID,
|
|
18
|
-
TEAM_SELECTION_DATE_ID,
|
|
19
|
-
VALIDATION_PERIOD_SELECTED_ID,
|
|
20
|
-
create_progress_bar,
|
|
21
|
-
team_list_dropdown,
|
|
22
|
-
)
|
|
23
|
-
from ositah.utils.period import get_validation_period_dates
|
|
24
|
-
from ositah.utils.projects import (
|
|
25
|
-
DATA_SOURCE_OSITAH,
|
|
26
|
-
category_time_and_unit,
|
|
27
|
-
get_hito_projects,
|
|
28
|
-
get_nsip_declarations,
|
|
29
|
-
get_team_projects,
|
|
30
|
-
)
|
|
31
|
-
from ositah.utils.utils import (
|
|
32
|
-
HITO_ROLE_PROJECT_MGR,
|
|
33
|
-
HITO_ROLE_SUPER_ADMIN,
|
|
34
|
-
NSIP_COLUMN_NAMES,
|
|
35
|
-
TEAM_LIST_ALL_AGENTS,
|
|
36
|
-
TIME_UNIT_HOURS_EN,
|
|
37
|
-
GlobalParams,
|
|
38
|
-
no_session_id_jumbotron,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
EXPORT_TAB_MENU_ID = "report-tabs"
|
|
42
|
-
TAB_ID_EXPORT_NSIP = "nsip-export-page"
|
|
43
|
-
TAB_MENU_EXPORT_NSIP = "Export NSIP"
|
|
44
|
-
|
|
45
|
-
TABLE_NSIP_EXPORT_ID = "export-nsip"
|
|
46
|
-
|
|
47
|
-
NSIP_EXPORT_BUTTON_LABEL = "Export"
|
|
48
|
-
NSIP_EXPORT_BUTTON_ID = "export-nsip-selected-users"
|
|
49
|
-
NSIP_EXPORT_SELECT_ALL_ID = "export-nsip-select-all"
|
|
50
|
-
NSIP_EXPORT_ALL_SELECTED_ID = "export-nsip-all-selected"
|
|
51
|
-
NSIP_EXPORT_STATUS_ID = "export-nsip-status-msg"
|
|
52
|
-
NSIP_EXPORT_SELECTION_STATUS_ID = "export-nsip-selection-update-status"
|
|
53
|
-
|
|
54
|
-
EXPORT_LOAD_INDICATOR_ID = "export-nsip-load-indicator"
|
|
55
|
-
EXPORT_SAVED_LOAD_INDICATOR_ID = "export-nsip-saved-load-indicator"
|
|
56
|
-
EXPORT_LOAD_TRIGGER_INTERVAL_ID = "export-nsip-load-callback-interval"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
r"
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
dcc.Store(id=
|
|
114
|
-
dcc.Store(id=
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
:param
|
|
126
|
-
:param
|
|
127
|
-
:param
|
|
128
|
-
:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
declaration_list["
|
|
188
|
-
declaration_list
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
columns["
|
|
196
|
-
"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
declaration_list["
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
declaration_list
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
declaration_list.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
declaration_list["
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
declaration_list
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
#
|
|
345
|
-
|
|
346
|
-
declaration_list
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
declaration_list
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# declaration in
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
#
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
#
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
declaration_list
|
|
409
|
-
|
|
410
|
-
"selectable",
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
"selectable",
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
"selectable",
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
& (declaration_list["
|
|
424
|
-
"
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
#
|
|
431
|
-
|
|
432
|
-
declaration_list.loc[declaration_list["
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
#
|
|
438
|
-
|
|
439
|
-
declaration_list
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
f"
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
f"
|
|
472
|
-
f"{
|
|
473
|
-
f"
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
f"
|
|
480
|
-
f"
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
data_columns.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
dbc.Col(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
dcc.Store(id=
|
|
560
|
-
dcc.Store(id=
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
:param
|
|
578
|
-
:param
|
|
579
|
-
:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
#
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
:param
|
|
632
|
-
:param
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
:param
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
"
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
f"
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
# time
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
f"
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
f"
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
"
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
html.Div(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
f"
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
html.Div(
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
# agent
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
html.Div(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
"
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
"
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
Output(
|
|
788
|
-
Output(
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
Input(
|
|
793
|
-
Input(
|
|
794
|
-
Input(
|
|
795
|
-
Input(
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
State(
|
|
800
|
-
State(
|
|
801
|
-
State(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
:param
|
|
821
|
-
:param
|
|
822
|
-
:param
|
|
823
|
-
:param
|
|
824
|
-
:param
|
|
825
|
-
:param
|
|
826
|
-
:param
|
|
827
|
-
:param
|
|
828
|
-
:
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
time
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
:
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
:param
|
|
900
|
-
:
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
:
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
#
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
checkbox_forms.
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
Output(NSIP_EXPORT_BUTTON_ID, "
|
|
970
|
-
Output(NSIP_EXPORT_BUTTON_ID, "
|
|
971
|
-
|
|
972
|
-
Input(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
:
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
f"
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
Output(NSIP_EXPORT_STATUS_ID, "
|
|
1011
|
-
Output(NSIP_EXPORT_STATUS_ID, "
|
|
1012
|
-
Output(
|
|
1013
|
-
|
|
1014
|
-
Input(
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
:param
|
|
1026
|
-
:param
|
|
1027
|
-
:
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
# the
|
|
1046
|
-
#
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
#
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
action = "
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
f"
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
f" {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
)
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
:param
|
|
1188
|
-
:return:
|
|
1189
|
-
"""
|
|
1190
|
-
|
|
1191
|
-
return
|
|
1
|
+
# OSITAH sub-application exporting data to NSIP
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import dash_bootstrap_components as dbc
|
|
6
|
+
import numpy as np
|
|
7
|
+
from dash import dcc, html
|
|
8
|
+
from dash.dependencies import ALL, MATCH, Input, Output, State
|
|
9
|
+
from dash.exceptions import PreventUpdate
|
|
10
|
+
|
|
11
|
+
from ositah.app import app
|
|
12
|
+
from ositah.utils.agents import get_agents, get_nsip_agents
|
|
13
|
+
from ositah.utils.exceptions import SessionDataMissing
|
|
14
|
+
from ositah.utils.menus import (
|
|
15
|
+
DATA_SELECTED_SOURCE_ID,
|
|
16
|
+
TABLE_TYPE_TABLE,
|
|
17
|
+
TEAM_SELECTED_VALUE_ID,
|
|
18
|
+
TEAM_SELECTION_DATE_ID,
|
|
19
|
+
VALIDATION_PERIOD_SELECTED_ID,
|
|
20
|
+
create_progress_bar,
|
|
21
|
+
team_list_dropdown,
|
|
22
|
+
)
|
|
23
|
+
from ositah.utils.period import get_validation_period_dates
|
|
24
|
+
from ositah.utils.projects import (
|
|
25
|
+
DATA_SOURCE_OSITAH,
|
|
26
|
+
category_time_and_unit,
|
|
27
|
+
get_hito_projects,
|
|
28
|
+
get_nsip_declarations,
|
|
29
|
+
get_team_projects,
|
|
30
|
+
)
|
|
31
|
+
from ositah.utils.utils import (
|
|
32
|
+
HITO_ROLE_PROJECT_MGR,
|
|
33
|
+
HITO_ROLE_SUPER_ADMIN,
|
|
34
|
+
NSIP_COLUMN_NAMES,
|
|
35
|
+
TEAM_LIST_ALL_AGENTS,
|
|
36
|
+
TIME_UNIT_HOURS_EN,
|
|
37
|
+
GlobalParams,
|
|
38
|
+
no_session_id_jumbotron,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
EXPORT_TAB_MENU_ID = "report-tabs"
|
|
42
|
+
TAB_ID_EXPORT_NSIP = "nsip-export-page"
|
|
43
|
+
TAB_MENU_EXPORT_NSIP = "Export NSIP"
|
|
44
|
+
|
|
45
|
+
TABLE_NSIP_EXPORT_ID = "export-nsip"
|
|
46
|
+
|
|
47
|
+
NSIP_EXPORT_BUTTON_LABEL = "Export"
|
|
48
|
+
NSIP_EXPORT_BUTTON_ID = "export-nsip-selected-users"
|
|
49
|
+
NSIP_EXPORT_SELECT_ALL_ID = "export-nsip-select-all"
|
|
50
|
+
NSIP_EXPORT_ALL_SELECTED_ID = "export-nsip-all-selected"
|
|
51
|
+
NSIP_EXPORT_STATUS_ID = "export-nsip-status-msg"
|
|
52
|
+
NSIP_EXPORT_SELECTION_STATUS_ID = "export-nsip-selection-update-status"
|
|
53
|
+
|
|
54
|
+
EXPORT_LOAD_INDICATOR_ID = "export-nsip-load-indicator"
|
|
55
|
+
EXPORT_SAVED_LOAD_INDICATOR_ID = "export-nsip-saved-load-indicator"
|
|
56
|
+
EXPORT_LOAD_TRIGGER_INTERVAL_ID = "export-nsip-load-callback-interval"
|
|
57
|
+
EXPORT_MAX_FAILDED_UPDATES = 30
|
|
58
|
+
EXPORT_PROGRESS_BAR_MAX_DURATION = 8 # seconds
|
|
59
|
+
EXPORT_SAVED_ACTIVE_TAB_ID = "export-nsip-saved-active-tab"
|
|
60
|
+
|
|
61
|
+
EXPORT_NSIP_SYNC_INDICATOR_ID = "export-nsip-sync-indicator"
|
|
62
|
+
EXPORT_NSIP_SAVED_SYNC_INDICATOR_ID = "export-nsip-saved-sync-indicator"
|
|
63
|
+
EXPORT_NSIP_SYNC_FREQUENCY = 2.0 # Average number of sync operations per second
|
|
64
|
+
EXPORT_NSIP_SYNC_TRIGGER_INTERVAL_ID = "export-nsip-sync-callback-interval"
|
|
65
|
+
EXPORT_NSIP_MULTIPLE_CONTRACTS_ERROR = re.compile(
|
|
66
|
+
(
|
|
67
|
+
r'"Agent has active multi-contracts in same laboratory - manual action needed\s+'
|
|
68
|
+
r"\|\s+idAgentContract\s+:\s+(?P<id1>\d+)\s+\|\s+idAgentContract\s+:\s+(?P<id2>\d+)"
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
NSIP_DECLARATIONS_SELECT_ALL = 0
|
|
73
|
+
NSIP_DECLARATIONS_SELECT_UNSYNCHRONIZED = 1
|
|
74
|
+
NSIP_DECLARATIONS_SWITCH_ID = "export-nsip-declaration-set-switch"
|
|
75
|
+
NSIP_DECLARATIONS_SELECTED_ID = "export-nsip-selected-declaration-set"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def export_submenus():
|
|
79
|
+
"""
|
|
80
|
+
Build the tabs menus of the export subapplication
|
|
81
|
+
|
|
82
|
+
:return: DBC Tabs
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
return dbc.Tabs(
|
|
86
|
+
[
|
|
87
|
+
dbc.Tab(
|
|
88
|
+
id=TAB_ID_EXPORT_NSIP,
|
|
89
|
+
tab_id=TAB_ID_EXPORT_NSIP,
|
|
90
|
+
label=TAB_MENU_EXPORT_NSIP,
|
|
91
|
+
),
|
|
92
|
+
dcc.Store(id=NSIP_DECLARATIONS_SELECTED_ID, data=NSIP_DECLARATIONS_SELECT_ALL),
|
|
93
|
+
],
|
|
94
|
+
id=EXPORT_TAB_MENU_ID,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def export_layout():
|
|
99
|
+
"""
|
|
100
|
+
Build the layout for this application, after reading the data if necessary.
|
|
101
|
+
|
|
102
|
+
:return: application layout
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
return html.Div(
|
|
106
|
+
[
|
|
107
|
+
html.H1("Export des déclarations vers NSIP"),
|
|
108
|
+
team_list_dropdown(),
|
|
109
|
+
# The following dcc.Store is used to ensure that the the ijclab_export input exists
|
|
110
|
+
# before the export page is created
|
|
111
|
+
dcc.Store(id=DATA_SELECTED_SOURCE_ID, data=DATA_SOURCE_OSITAH),
|
|
112
|
+
html.Div(export_submenus(), id="export-submenus", style={"marginTop": "3em"}),
|
|
113
|
+
dcc.Store(id=EXPORT_LOAD_INDICATOR_ID, data=0),
|
|
114
|
+
dcc.Store(id=EXPORT_SAVED_LOAD_INDICATOR_ID, data=0),
|
|
115
|
+
dcc.Store(id=EXPORT_SAVED_ACTIVE_TAB_ID, data=""),
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def nsip_export_table(team, team_selection_date, period_date: str, declarations_set):
|
|
121
|
+
"""
|
|
122
|
+
Build a table ready to be exported to NSIP from validated declarations. The produced table
|
|
123
|
+
can then be exported as a CSV for ingestion by NSIP.
|
|
124
|
+
|
|
125
|
+
:param team: selected team
|
|
126
|
+
:param team_selection_date: last time the team selection was changed
|
|
127
|
+
:param period_date: a date that must be inside the declaration period
|
|
128
|
+
:param declarations_set: declaration set to use (all or only non-synchronized)
|
|
129
|
+
:return: dbc.Table
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if team is None:
|
|
133
|
+
return html.Div("")
|
|
134
|
+
|
|
135
|
+
global_params = GlobalParams()
|
|
136
|
+
columns = global_params.columns
|
|
137
|
+
try:
|
|
138
|
+
session_data = global_params.session_data
|
|
139
|
+
except SessionDataMissing:
|
|
140
|
+
return no_session_id_jumbotron()
|
|
141
|
+
|
|
142
|
+
start_date, end_date = get_validation_period_dates(period_date)
|
|
143
|
+
|
|
144
|
+
if session_data.role in [HITO_ROLE_PROJECT_MGR, HITO_ROLE_SUPER_ADMIN]:
|
|
145
|
+
export_disabled = False
|
|
146
|
+
else:
|
|
147
|
+
export_disabled = True
|
|
148
|
+
|
|
149
|
+
if session_data.nsip_declarations is None:
|
|
150
|
+
hito_projects = get_hito_projects()
|
|
151
|
+
declaration_list = get_team_projects(
|
|
152
|
+
team, team_selection_date, period_date, DATA_SOURCE_OSITAH
|
|
153
|
+
)
|
|
154
|
+
if declaration_list is None or declaration_list.empty:
|
|
155
|
+
return dbc.Alert(
|
|
156
|
+
f"Aucune données validées n'existe pour l'équipe '{team}'",
|
|
157
|
+
color="warning",
|
|
158
|
+
)
|
|
159
|
+
ositah_total_declarations_num = len(declaration_list)
|
|
160
|
+
declaration_list = declaration_list.merge(
|
|
161
|
+
hito_projects,
|
|
162
|
+
left_on="hito_project_id",
|
|
163
|
+
right_on="id",
|
|
164
|
+
suffixes=[None, "_y"],
|
|
165
|
+
)
|
|
166
|
+
agent_list = get_agents(period_date, team)
|
|
167
|
+
if team is None or team == TEAM_LIST_ALL_AGENTS:
|
|
168
|
+
# If no team is selected, merge left to include declarations from agents whose
|
|
169
|
+
# email_auth don't match between Hito and NSIP. It is typically the case for agents
|
|
170
|
+
# who are no longer active in NSIP (e.g. agents who left the lab during the
|
|
171
|
+
# declaration period).
|
|
172
|
+
merge_how = "left"
|
|
173
|
+
else:
|
|
174
|
+
merge_how = "inner"
|
|
175
|
+
declaration_list = declaration_list.merge(
|
|
176
|
+
agent_list,
|
|
177
|
+
how=merge_how,
|
|
178
|
+
left_on=columns["agent_id"],
|
|
179
|
+
right_on="id",
|
|
180
|
+
suffixes=[None, "_y"],
|
|
181
|
+
)
|
|
182
|
+
declaration_list[["time", "time_unit"]] = declaration_list.apply(
|
|
183
|
+
lambda r: category_time_and_unit(r["category"], r[columns["hours"]], english=True),
|
|
184
|
+
axis=1,
|
|
185
|
+
result_type="expand",
|
|
186
|
+
)
|
|
187
|
+
declaration_list["time"] = declaration_list["time"].astype(int, copy=False)
|
|
188
|
+
declaration_list["email_auth"] = declaration_list["email_auth"].str.lower()
|
|
189
|
+
declaration_list.sort_values(by="email_auth", inplace=True)
|
|
190
|
+
|
|
191
|
+
colums_to_delete = []
|
|
192
|
+
for columnn in declaration_list.columns.to_list():
|
|
193
|
+
if columnn not in [
|
|
194
|
+
*NSIP_COLUMN_NAMES.keys(),
|
|
195
|
+
columns["statut"],
|
|
196
|
+
columns["cem"],
|
|
197
|
+
"fullname",
|
|
198
|
+
]:
|
|
199
|
+
colums_to_delete.append(columnn)
|
|
200
|
+
if len(colums_to_delete) > 0:
|
|
201
|
+
declaration_list.drop(columns=colums_to_delete, inplace=True)
|
|
202
|
+
|
|
203
|
+
nsip_agents = get_nsip_agents()
|
|
204
|
+
declaration_list = declaration_list.merge(
|
|
205
|
+
nsip_agents,
|
|
206
|
+
how="left",
|
|
207
|
+
left_on="email_auth",
|
|
208
|
+
right_on="email_reseda",
|
|
209
|
+
suffixes=[None, "_nsipa"],
|
|
210
|
+
indicator=True,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# For OSITAH entries not found in NSIP, it may be that the agent left the lab during
|
|
214
|
+
# the declaration period: in this case there is an entry in NSIP where the RESEDA email
|
|
215
|
+
# has been replaced by a UUID allowing to update NSIP data. Check if such an entry
|
|
216
|
+
# exists in NSIP with a matching fullname and use it if it exists. It also happens if the
|
|
217
|
+
# agent email_auth changed during the period.
|
|
218
|
+
# To make matching easier, index is temporarily set to fullname instead of an integer.
|
|
219
|
+
nsip_missing_agent_names = declaration_list.loc[
|
|
220
|
+
declaration_list["_merge"] == "left_only", columns["fullname"]
|
|
221
|
+
].unique()
|
|
222
|
+
declaration_list.set_index(columns["fullname"], inplace=True)
|
|
223
|
+
nsip_agents.set_index(columns["fullname"], inplace=True)
|
|
224
|
+
for name in nsip_missing_agent_names:
|
|
225
|
+
if name in nsip_agents.index:
|
|
226
|
+
matching_inactive_nsip_agent = nsip_agents.loc[[name]]
|
|
227
|
+
if not matching_inactive_nsip_agent.empty:
|
|
228
|
+
# Should always work, raise an exception if it is not the case
|
|
229
|
+
declaration_list.update(matching_inactive_nsip_agent, errors="raise")
|
|
230
|
+
# Mark the entry as complete
|
|
231
|
+
declaration_list.loc[name, "_merge"] = "both"
|
|
232
|
+
declaration_list.reset_index(inplace=True)
|
|
233
|
+
|
|
234
|
+
declaration_list["nsip_agent_missing"] = declaration_list["_merge"] == "left_only"
|
|
235
|
+
declaration_list["optional"] = declaration_list[columns["statut"]].isin(
|
|
236
|
+
global_params.declaration_options["optional_statutes"]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# For EC (enseignant chercheurs), apply the ratio defined in the configuration (if any)
|
|
240
|
+
# to teaching hours declared to convert them into hours with students
|
|
241
|
+
if global_params.teaching_ratio:
|
|
242
|
+
ratio = global_params.teaching_ratio["ratio"]
|
|
243
|
+
cem = global_params.teaching_ratio["cem"]
|
|
244
|
+
if cem:
|
|
245
|
+
declaration_list.loc[
|
|
246
|
+
(declaration_list.nsip_master == global_params.teaching_ratio["masterproject"])
|
|
247
|
+
& (declaration_list[columns["cem"]].isin(cem)),
|
|
248
|
+
"time",
|
|
249
|
+
] = np.round(declaration_list["time"] / ratio)
|
|
250
|
+
else:
|
|
251
|
+
declaration_list.loc[
|
|
252
|
+
declaration_list.nsip_master == global_params.teaching_ratio["masterproject"],
|
|
253
|
+
"time",
|
|
254
|
+
] = np.round(declaration_list["time"] / ratio)
|
|
255
|
+
|
|
256
|
+
# Check that the number of hours doesn't exceed the maximum allowed for activities
|
|
257
|
+
# declared in hours
|
|
258
|
+
declaration_list["invalid_time"] = False
|
|
259
|
+
declaration_list.loc[
|
|
260
|
+
declaration_list["time_unit"] == TIME_UNIT_HOURS_EN, "invalid_time"
|
|
261
|
+
] = (declaration_list["time"] > global_params.declaration_options["max_hours"])
|
|
262
|
+
declaration_list.drop(columns="_merge", inplace=True)
|
|
263
|
+
|
|
264
|
+
nsip_declarations = get_nsip_declarations(start_date, team)
|
|
265
|
+
if nsip_declarations.empty:
|
|
266
|
+
# nsip_missing is True if the OSITAH declaration has no matching declaration in NSIP
|
|
267
|
+
declaration_list["nsip_missing"] = True
|
|
268
|
+
# time_unit_mismatch is True only if there is a matching declaration in NSIP and time
|
|
269
|
+
# unit differs
|
|
270
|
+
declaration_list["time_unit_mismatch"] = False
|
|
271
|
+
# mgr_val_time_mismatch indicates that validation time is different in OSITAH and NSIP
|
|
272
|
+
declaration_list["mgr_val_time_mismatch"] = False
|
|
273
|
+
# nsip_inconsistency is True only if there is a matching declaration in NSIP and
|
|
274
|
+
# time differs
|
|
275
|
+
declaration_list["nsip_inconsistency"] = False
|
|
276
|
+
# ositah_missing is True if a declaratipn is found in NSIP without a matching
|
|
277
|
+
# declaration in OSITAH
|
|
278
|
+
declaration_list["ositah_missing"] = False
|
|
279
|
+
# Other columns expected by the code below
|
|
280
|
+
declaration_list["id_declaration"] = np.NaN
|
|
281
|
+
else:
|
|
282
|
+
declaration_list["nsip_project_id"] = declaration_list["nsip_project_id"].astype(int)
|
|
283
|
+
declaration_list["nsip_reference_id"] = declaration_list["nsip_reference_id"].astype(
|
|
284
|
+
int
|
|
285
|
+
)
|
|
286
|
+
# In case nsip_declarations contains only references (no project), create the
|
|
287
|
+
# project.name column required later as it is used both for references and projects.
|
|
288
|
+
# Also copy reference.name into project.name if it is a reference.
|
|
289
|
+
if "project.name" not in nsip_declarations:
|
|
290
|
+
nsip_declarations["project.name"] = np.NaN
|
|
291
|
+
if "reference.name" not in nsip_declarations:
|
|
292
|
+
nsip_declarations["reference.name"] = np.NaN
|
|
293
|
+
nsip_declarations.loc[
|
|
294
|
+
nsip_declarations["project.name"].isna(),
|
|
295
|
+
"nsip_project",
|
|
296
|
+
] = nsip_declarations["reference.name"]
|
|
297
|
+
# Merge OSITAH declarations with those possibly present in NSIP
|
|
298
|
+
declaration_list = declaration_list.merge(
|
|
299
|
+
nsip_declarations,
|
|
300
|
+
how="outer",
|
|
301
|
+
left_on=["email_reseda", "nsip_project_id", "nsip_reference_id"],
|
|
302
|
+
right_on=["agent.email", "project.id", "reference.id"],
|
|
303
|
+
suffixes=[None, "_nsipd"],
|
|
304
|
+
indicator=True,
|
|
305
|
+
)
|
|
306
|
+
# Merge all declarations for an agent related to the same NSIP activity, if several
|
|
307
|
+
# local activities are associated with one NSIP one. First build the list of distinct
|
|
308
|
+
# NSIP activities and aggregate the related time, then merge back this time in the
|
|
309
|
+
# declaration list.
|
|
310
|
+
nsip_activity_identifier = [
|
|
311
|
+
"agent.id",
|
|
312
|
+
"project.id",
|
|
313
|
+
"reference.id",
|
|
314
|
+
"email_auth",
|
|
315
|
+
"nsip_project_id",
|
|
316
|
+
"nsip_reference_id",
|
|
317
|
+
]
|
|
318
|
+
for c in nsip_activity_identifier:
|
|
319
|
+
if declaration_list.dtypes[c] == "object":
|
|
320
|
+
declaration_list.loc[declaration_list[c].isna(), c] = ""
|
|
321
|
+
else:
|
|
322
|
+
declaration_list.loc[declaration_list[c].isna(), c] = 0
|
|
323
|
+
combined_declarations = (
|
|
324
|
+
declaration_list[[*nsip_activity_identifier, "time"]]
|
|
325
|
+
.groupby(by=nsip_activity_identifier, as_index=False, sort=False)
|
|
326
|
+
.sum()
|
|
327
|
+
)
|
|
328
|
+
declaration_list = declaration_list.drop(columns="time").drop_duplicates(
|
|
329
|
+
nsip_activity_identifier
|
|
330
|
+
)
|
|
331
|
+
declaration_list = declaration_list.merge(
|
|
332
|
+
combined_declarations,
|
|
333
|
+
how="inner",
|
|
334
|
+
on=nsip_activity_identifier,
|
|
335
|
+
suffixes=[None, "_cd"],
|
|
336
|
+
)
|
|
337
|
+
# time_unit_mismatch is True only if there is a matching declaration in NSIP and time
|
|
338
|
+
# unit differs
|
|
339
|
+
declaration_list["time_unit_mismatch"] = False
|
|
340
|
+
declaration_list.loc[declaration_list["_merge"] == "both", "time_unit_mismatch"] = (
|
|
341
|
+
declaration_list.loc[declaration_list["_merge"] == "both", "time_unit"]
|
|
342
|
+
!= declaration_list.loc[declaration_list["_merge"] == "both", "volume"]
|
|
343
|
+
)
|
|
344
|
+
# nsip_inconsistency is True only if there is a matching declaration in NSIP and
|
|
345
|
+
# time differs
|
|
346
|
+
declaration_list["nsip_inconsistency"] = False
|
|
347
|
+
declaration_list.loc[declaration_list["_merge"] == "both", "nsip_inconsistency"] = (
|
|
348
|
+
declaration_list.loc[declaration_list["_merge"] == "both", "time"]
|
|
349
|
+
!= declaration_list.loc[declaration_list["_merge"] == "both", "time_nsipd"]
|
|
350
|
+
)
|
|
351
|
+
# mgr_val_time_mismatch is True only if there is a matching declaration in NSIP and
|
|
352
|
+
# manager validation time differs
|
|
353
|
+
declaration_list["mgr_val_time_mismatch"] = False
|
|
354
|
+
declaration_list.loc[
|
|
355
|
+
declaration_list["_merge"] == "both", "mgr_val_time_mismatch"
|
|
356
|
+
] = declaration_list[declaration_list["_merge"] == "both"].apply(
|
|
357
|
+
lambda r: (
|
|
358
|
+
False
|
|
359
|
+
if r["managerValidationDate"]
|
|
360
|
+
and re.match(
|
|
361
|
+
r["managerValidationDate"], r["validation_time"].date().isoformat()
|
|
362
|
+
)
|
|
363
|
+
else True
|
|
364
|
+
),
|
|
365
|
+
axis=1,
|
|
366
|
+
)
|
|
367
|
+
# nsip_missing is True if the OSITAH declaration has no matching declaration in NSIP
|
|
368
|
+
declaration_list["nsip_missing"] = declaration_list["_merge"] == "left_only"
|
|
369
|
+
# ositah_missing is True if a declaration is found in NSIP without a matching
|
|
370
|
+
# declaration in OSITAH
|
|
371
|
+
declaration_list["ositah_missing"] = declaration_list["_merge"] == "right_only"
|
|
372
|
+
for ositah_column, nsip_column in {
|
|
373
|
+
"email_auth": "agent.email",
|
|
374
|
+
"fullname": "nsip_fullname",
|
|
375
|
+
"nsip_project_id": "project.id",
|
|
376
|
+
"nsip_reference_id": "reference.id",
|
|
377
|
+
"nsip_project": "project.name",
|
|
378
|
+
"time": "time_nsipd",
|
|
379
|
+
"time_unit": "volume",
|
|
380
|
+
}.items():
|
|
381
|
+
declaration_list.loc[declaration_list["ositah_missing"], ositah_column] = (
|
|
382
|
+
declaration_list[nsip_column]
|
|
383
|
+
)
|
|
384
|
+
declaration_list.loc[
|
|
385
|
+
declaration_list["nsip_agent_missing"].isna(), "nsip_agent_missing"
|
|
386
|
+
] = False
|
|
387
|
+
declaration_list.drop(columns="_merge", inplace=True)
|
|
388
|
+
|
|
389
|
+
# Mark declarations that are properly synced between OSITAH and NSIP for easier
|
|
390
|
+
# processing later
|
|
391
|
+
declaration_list["declaration_ok"] = ~declaration_list[
|
|
392
|
+
[
|
|
393
|
+
"nsip_missing",
|
|
394
|
+
"nsip_inconsistency",
|
|
395
|
+
"ositah_missing",
|
|
396
|
+
"nsip_missing",
|
|
397
|
+
"mgr_val_time_mismatch",
|
|
398
|
+
"invalid_time",
|
|
399
|
+
]
|
|
400
|
+
].any(axis=1)
|
|
401
|
+
|
|
402
|
+
# Define declarations that be selected for NSIP synchronisation as all declarations that
|
|
403
|
+
# are not ok, except those corresponding to agents missing in NSIP or that have no
|
|
404
|
+
# matching entries in OSITAH
|
|
405
|
+
if export_disabled:
|
|
406
|
+
declaration_list["selectable"] = False
|
|
407
|
+
else:
|
|
408
|
+
declaration_list["selectable"] = ~declaration_list["declaration_ok"]
|
|
409
|
+
declaration_list.loc[
|
|
410
|
+
declaration_list["selectable"] & declaration_list["ositah_missing"],
|
|
411
|
+
"selectable",
|
|
412
|
+
] = False
|
|
413
|
+
declaration_list.loc[
|
|
414
|
+
declaration_list["selectable"] & declaration_list["invalid_time"],
|
|
415
|
+
"selectable",
|
|
416
|
+
] = False
|
|
417
|
+
declaration_list.loc[
|
|
418
|
+
declaration_list["selectable"] & declaration_list["nsip_agent_missing"],
|
|
419
|
+
"selectable",
|
|
420
|
+
] = False
|
|
421
|
+
declaration_list.loc[
|
|
422
|
+
declaration_list["selectable"]
|
|
423
|
+
& (declaration_list["nsip_project_id"] == 0)
|
|
424
|
+
& (declaration_list["nsip_reference_id"] == 0),
|
|
425
|
+
"selectable",
|
|
426
|
+
] = False
|
|
427
|
+
|
|
428
|
+
# Reset selected state to False
|
|
429
|
+
declaration_list["selected"] = False
|
|
430
|
+
# Rset nsip_project_id and nsip_reference_id to NaN if they are equal to 0 so that the
|
|
431
|
+
# corresponding cell is empty
|
|
432
|
+
declaration_list.loc[declaration_list["nsip_project_id"] == 0, "nsip_project_id"] = np.NaN
|
|
433
|
+
declaration_list.loc[declaration_list["nsip_reference_id"] == 0, "nsip_reference_id"] = (
|
|
434
|
+
np.NaN
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Sort declarations by email_auth and add index value as column for easier further
|
|
438
|
+
# processing
|
|
439
|
+
declaration_list.sort_values(by="email_auth", inplace=True)
|
|
440
|
+
declaration_list["row_index"] = declaration_list.index
|
|
441
|
+
|
|
442
|
+
session_data.nsip_declarations = declaration_list
|
|
443
|
+
|
|
444
|
+
else:
|
|
445
|
+
declaration_list = session_data.nsip_declarations
|
|
446
|
+
ositah_total_declarations_num = session_data.total_declarations_num
|
|
447
|
+
|
|
448
|
+
declarations_ok = declaration_list[declaration_list["declaration_ok"]]
|
|
449
|
+
declarations_ok_num = len(declarations_ok)
|
|
450
|
+
agents_ok_num = len(declarations_ok["email_auth"].unique())
|
|
451
|
+
nsip_agent_missing_num = len(
|
|
452
|
+
declaration_list.loc[declaration_list["nsip_agent_missing"], "email_auth"].unique()
|
|
453
|
+
)
|
|
454
|
+
nsip_optional_missing_num = len(
|
|
455
|
+
declaration_list.loc[
|
|
456
|
+
declaration_list["nsip_agent_missing"] & declaration_list["optional"],
|
|
457
|
+
"email_auth",
|
|
458
|
+
].unique()
|
|
459
|
+
)
|
|
460
|
+
ositah_missing_num = len(declaration_list[declaration_list["ositah_missing"]])
|
|
461
|
+
ositah_validated_declarations_num = len(declaration_list) - ositah_missing_num
|
|
462
|
+
page_title = [
|
|
463
|
+
html.Div(
|
|
464
|
+
(
|
|
465
|
+
f"Export NSIP des contributions validées de '{team}' du"
|
|
466
|
+
f" {start_date.strftime('%Y-%m-%d')} au {end_date.strftime('%Y-%m-%d')}"
|
|
467
|
+
)
|
|
468
|
+
),
|
|
469
|
+
html.Div(
|
|
470
|
+
(
|
|
471
|
+
f"Déclarations totales={ositah_total_declarations_num} dont"
|
|
472
|
+
f" synchronisées/validées={declarations_ok_num}/"
|
|
473
|
+
f"{ositah_validated_declarations_num}, "
|
|
474
|
+
f" manquantes OSITAH={ositah_missing_num}"
|
|
475
|
+
)
|
|
476
|
+
),
|
|
477
|
+
html.Div(
|
|
478
|
+
(
|
|
479
|
+
f"(agents synchronisés={agents_ok_num},"
|
|
480
|
+
f" agents manquants dans NSIP={nsip_agent_missing_num} dont"
|
|
481
|
+
f" optionnels={nsip_optional_missing_num})"
|
|
482
|
+
)
|
|
483
|
+
),
|
|
484
|
+
]
|
|
485
|
+
if team and team != TEAM_LIST_ALL_AGENTS:
|
|
486
|
+
page_title.append(
|
|
487
|
+
html.Div(
|
|
488
|
+
(
|
|
489
|
+
"Certains agents peuvent apparaitre non synchronisés s'ils ont quitté"
|
|
490
|
+
f" le laboratoire: utiliser '{TEAM_LIST_ALL_AGENTS}' pour vérifier"
|
|
491
|
+
),
|
|
492
|
+
style={"fontStyle": "italic", "fontWeight": "bold"},
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if declarations_set == NSIP_DECLARATIONS_SELECT_ALL:
|
|
497
|
+
selected_declarations = declaration_list
|
|
498
|
+
else:
|
|
499
|
+
selected_declarations = declaration_list[~declaration_list["declaration_ok"]]
|
|
500
|
+
|
|
501
|
+
data_columns = list(NSIP_COLUMN_NAMES.keys())
|
|
502
|
+
data_columns.remove("email_auth")
|
|
503
|
+
|
|
504
|
+
table_header = [
|
|
505
|
+
html.Thead(
|
|
506
|
+
html.Tr(
|
|
507
|
+
[
|
|
508
|
+
html.Th(
|
|
509
|
+
[
|
|
510
|
+
(
|
|
511
|
+
dbc.Checkbox(id=NSIP_EXPORT_SELECT_ALL_ID)
|
|
512
|
+
if selected_declarations["selectable"].any()
|
|
513
|
+
else html.Div()
|
|
514
|
+
),
|
|
515
|
+
dcc.Store(id=NSIP_EXPORT_ALL_SELECTED_ID, data=0),
|
|
516
|
+
]
|
|
517
|
+
),
|
|
518
|
+
html.Th("email_reseda"),
|
|
519
|
+
*[html.Th(c) for c in data_columns],
|
|
520
|
+
]
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
table_body = []
|
|
526
|
+
for email in selected_declarations["email_auth"].unique():
|
|
527
|
+
tr_list = nsip_build_user_declarations(selected_declarations, email, data_columns)
|
|
528
|
+
table_body.extend(tr_list)
|
|
529
|
+
table_body = [html.Tbody(table_body)]
|
|
530
|
+
|
|
531
|
+
return html.Div(
|
|
532
|
+
[
|
|
533
|
+
html.Div(
|
|
534
|
+
[
|
|
535
|
+
dbc.Row(
|
|
536
|
+
[
|
|
537
|
+
dbc.Col(dbc.Alert(page_title), width=10),
|
|
538
|
+
dbc.Col(
|
|
539
|
+
[
|
|
540
|
+
dbc.Button(
|
|
541
|
+
NSIP_EXPORT_BUTTON_LABEL,
|
|
542
|
+
id=NSIP_EXPORT_BUTTON_ID,
|
|
543
|
+
disabled=True,
|
|
544
|
+
),
|
|
545
|
+
],
|
|
546
|
+
width={"size": 1, "offset": 1},
|
|
547
|
+
),
|
|
548
|
+
]
|
|
549
|
+
),
|
|
550
|
+
]
|
|
551
|
+
),
|
|
552
|
+
add_nsip_declaration_selection_switch(declarations_set),
|
|
553
|
+
html.Div(
|
|
554
|
+
dbc.Col(
|
|
555
|
+
dbc.Alert(dismissable=True, is_open=False, id=NSIP_EXPORT_STATUS_ID),
|
|
556
|
+
width=9,
|
|
557
|
+
)
|
|
558
|
+
),
|
|
559
|
+
dcc.Store(id=NSIP_EXPORT_SELECTION_STATUS_ID, data=0),
|
|
560
|
+
dcc.Store(id=EXPORT_NSIP_SYNC_INDICATOR_ID, data=0),
|
|
561
|
+
dcc.Store(id=EXPORT_NSIP_SAVED_SYNC_INDICATOR_ID, data=0),
|
|
562
|
+
html.P(""),
|
|
563
|
+
dbc.Table(
|
|
564
|
+
table_header + table_body,
|
|
565
|
+
id={"type": TABLE_TYPE_TABLE, "id": TABLE_NSIP_EXPORT_ID},
|
|
566
|
+
bordered=True,
|
|
567
|
+
hover=True,
|
|
568
|
+
),
|
|
569
|
+
]
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def nsip_build_user_declarations(declarations, agent_email, data_columns):
|
|
574
|
+
"""
|
|
575
|
+
Build the list of html.Tr corresponding to the various projects af a given user.
|
|
576
|
+
|
|
577
|
+
:param declarations: declarations dataframe
|
|
578
|
+
:param agent_email: user RESEDA email
|
|
579
|
+
:param data_columns: name of columns to add in each row
|
|
580
|
+
:return: list of Tr
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
user_declarations = declarations[declarations.email_auth == agent_email]
|
|
584
|
+
tr_list = [
|
|
585
|
+
# rowSpan must be len+1 because the first row attached to the email is in a separate Tr
|
|
586
|
+
# (rowSpan is in fact the number of Tr following this one attached to it)
|
|
587
|
+
html.Tr(
|
|
588
|
+
[
|
|
589
|
+
(
|
|
590
|
+
html.Td(
|
|
591
|
+
[
|
|
592
|
+
dbc.Checkbox(
|
|
593
|
+
id={"type": "nsip-export-user", "id": agent_email},
|
|
594
|
+
class_name="nsip-agent-selector",
|
|
595
|
+
value=False,
|
|
596
|
+
),
|
|
597
|
+
dcc.Store(
|
|
598
|
+
id={"type": "nsip-export-user-selected", "id": agent_email},
|
|
599
|
+
data=0,
|
|
600
|
+
),
|
|
601
|
+
],
|
|
602
|
+
className="align-middle ",
|
|
603
|
+
rowSpan=len(user_declarations) + 1,
|
|
604
|
+
)
|
|
605
|
+
if user_declarations["selectable"].any()
|
|
606
|
+
else html.Td(rowSpan=len(user_declarations) + 1)
|
|
607
|
+
),
|
|
608
|
+
nsip_build_poject_declaration_cell(user_declarations, "email_auth", None),
|
|
609
|
+
dcc.Store(
|
|
610
|
+
id={"type": "nsip-selected-user", "id": agent_email},
|
|
611
|
+
data=agent_email,
|
|
612
|
+
),
|
|
613
|
+
]
|
|
614
|
+
)
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
tr_list.extend(
|
|
618
|
+
[
|
|
619
|
+
html.Tr([nsip_build_poject_declaration_cell(row, c, i) for c in data_columns])
|
|
620
|
+
for i, row in declarations[declarations.email_auth == agent_email].iterrows()
|
|
621
|
+
]
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return tr_list
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def nsip_build_poject_declaration_cell(declaration, column, row_index):
|
|
628
|
+
"""
|
|
629
|
+
Build the column cell for one project declaration. Returns a html.Td.
|
|
630
|
+
|
|
631
|
+
:param declaration: project declaration row or rows if column='email_auth'
|
|
632
|
+
:param column: column name
|
|
633
|
+
:param row_index: row index in the dataframe: must be a unique id for the row. Ignored if
|
|
634
|
+
declaration is a dataframe.
|
|
635
|
+
:return: html.Td hor html.Th if column='email_auth'
|
|
636
|
+
"""
|
|
637
|
+
|
|
638
|
+
if column == "email_auth":
|
|
639
|
+
div_id = f"export-row-{declaration.iloc[0]['row_index']}-{column}"
|
|
640
|
+
cell_content = [html.Div(declaration.iloc[0]["email_auth"], id=div_id)]
|
|
641
|
+
else:
|
|
642
|
+
div_id = f"export-row-{row_index}-{column}"
|
|
643
|
+
cell_content = [html.Div(declaration[column], id=div_id)]
|
|
644
|
+
cell_opt_class, tooltip = project_declaration_class(declaration, column)
|
|
645
|
+
if tooltip:
|
|
646
|
+
cell_content.append(dbc.Tooltip(tooltip, target=div_id))
|
|
647
|
+
|
|
648
|
+
if column == "email_auth":
|
|
649
|
+
return html.Th(
|
|
650
|
+
cell_content,
|
|
651
|
+
className=f"align-middle {cell_opt_class}",
|
|
652
|
+
rowSpan=len(declaration) + 1,
|
|
653
|
+
)
|
|
654
|
+
else:
|
|
655
|
+
return html.Td(cell_content, className=f"align-middle {cell_opt_class}")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def project_declaration_class(declaration, column):
|
|
659
|
+
"""
|
|
660
|
+
Return the appropriate CSS class for each project declaration cell based on declaration
|
|
661
|
+
attributes
|
|
662
|
+
|
|
663
|
+
:param declaration: declaration row or rows if column='email_auth'
|
|
664
|
+
:param column: column for which CSS must be configured (allow to distingish between time and
|
|
665
|
+
time unit)
|
|
666
|
+
:return: CSS class names to add, tooltip text
|
|
667
|
+
"""
|
|
668
|
+
|
|
669
|
+
global_params = GlobalParams()
|
|
670
|
+
|
|
671
|
+
if column == "time_unit":
|
|
672
|
+
if declaration["time_unit_mismatch"]:
|
|
673
|
+
return (
|
|
674
|
+
"table-warning",
|
|
675
|
+
f"Unité incorrecte, requiert : {declaration['volume']}",
|
|
676
|
+
)
|
|
677
|
+
elif column == "time":
|
|
678
|
+
if declaration["declaration_ok"]:
|
|
679
|
+
return "table-success", None
|
|
680
|
+
elif declaration["ositah_missing"]:
|
|
681
|
+
# Flag only the declaration time for missing declarations in OSITAH
|
|
682
|
+
return (
|
|
683
|
+
"table-danger",
|
|
684
|
+
"Declaration trouvée dans NSIP mais absente ou non-validée dans OSITAH",
|
|
685
|
+
)
|
|
686
|
+
elif declaration["invalid_time"]:
|
|
687
|
+
return (
|
|
688
|
+
"table-danger",
|
|
689
|
+
(
|
|
690
|
+
f"Le temps déclaré pour ce type de projet ne peut pas dépasser"
|
|
691
|
+
f" {global_params.declaration_options['max_hours']} heures"
|
|
692
|
+
),
|
|
693
|
+
)
|
|
694
|
+
elif declaration["nsip_inconsistency"]:
|
|
695
|
+
# CSS class nsip_inconsistency is returned for time column only if there is a
|
|
696
|
+
# time mismatch
|
|
697
|
+
return (
|
|
698
|
+
"table-warning",
|
|
699
|
+
(
|
|
700
|
+
f"Le temps déclaré dans OSITAH est différent de celui de NSIP"
|
|
701
|
+
f" ({int(declaration['time_nsipd'])})"
|
|
702
|
+
),
|
|
703
|
+
)
|
|
704
|
+
elif column == "validation_time":
|
|
705
|
+
if declaration["mgr_val_time_mismatch"]:
|
|
706
|
+
return (
|
|
707
|
+
"table-warning",
|
|
708
|
+
(
|
|
709
|
+
f"La date de validation est différente de celle de NSIP"
|
|
710
|
+
f" ({declaration['managerValidationDate']})"
|
|
711
|
+
),
|
|
712
|
+
)
|
|
713
|
+
elif column in ["nsip_master", "nsip_project"]:
|
|
714
|
+
if math.isnan(declaration["nsip_project_id"]) and math.isnan(
|
|
715
|
+
declaration["nsip_reference_id"]
|
|
716
|
+
):
|
|
717
|
+
return (
|
|
718
|
+
"table-danger",
|
|
719
|
+
"Pas de projet correspondant dans NSIP: vérifier le referentiel Hito",
|
|
720
|
+
)
|
|
721
|
+
elif column == "email_auth":
|
|
722
|
+
fullname_txt = f"Nom: {declaration.loc[declaration.index[0], 'fullname']}"
|
|
723
|
+
if declaration["declaration_ok"].all():
|
|
724
|
+
return "table-success", fullname_txt
|
|
725
|
+
elif declaration["nsip_agent_missing"].any():
|
|
726
|
+
if declaration["optional"].all():
|
|
727
|
+
return "table-info", [
|
|
728
|
+
html.Div(fullname_txt),
|
|
729
|
+
html.Div(
|
|
730
|
+
(
|
|
731
|
+
f"Agent dont la déclaration est optionnelle non présent dans NSIP"
|
|
732
|
+
f" ({declaration.loc[declaration.index[0], 'statut']})"
|
|
733
|
+
)
|
|
734
|
+
),
|
|
735
|
+
]
|
|
736
|
+
else:
|
|
737
|
+
return "table-warning", [
|
|
738
|
+
html.Div(fullname_txt),
|
|
739
|
+
html.Div("Agent non trouvé dans NSIP"),
|
|
740
|
+
]
|
|
741
|
+
elif declaration["ositah_missing"].all():
|
|
742
|
+
# Set the cell class to table-danger on the agent email only if all declarations for the
|
|
743
|
+
# agent are missing
|
|
744
|
+
return "table-danger", [
|
|
745
|
+
html.Div(fullname_txt),
|
|
746
|
+
html.Div("Toutes les déclarations sont manquantes ou non validées dans OSITAH"),
|
|
747
|
+
]
|
|
748
|
+
else:
|
|
749
|
+
return "", fullname_txt
|
|
750
|
+
|
|
751
|
+
return "", None
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def add_nsip_declaration_selection_switch(current_set):
|
|
755
|
+
"""
|
|
756
|
+
Add a dbc.RadioItems to select whether to show all declaration or only the not
|
|
757
|
+
synchronized ones in NSIP export table.
|
|
758
|
+
|
|
759
|
+
:param current_set: currently selected declaration set
|
|
760
|
+
:return: dbc.RadioItems
|
|
761
|
+
"""
|
|
762
|
+
|
|
763
|
+
return dbc.Row(
|
|
764
|
+
[
|
|
765
|
+
dbc.RadioItems(
|
|
766
|
+
options=[
|
|
767
|
+
{
|
|
768
|
+
"label": "Toutes les déclarations",
|
|
769
|
+
"value": NSIP_DECLARATIONS_SELECT_ALL,
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
"label": "Déclarations non synchronisées uniquement",
|
|
773
|
+
"value": NSIP_DECLARATIONS_SELECT_UNSYNCHRONIZED,
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
value=current_set,
|
|
777
|
+
id=NSIP_DECLARATIONS_SWITCH_ID,
|
|
778
|
+
inline=True,
|
|
779
|
+
),
|
|
780
|
+
],
|
|
781
|
+
justify="center",
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@app.callback(
|
|
786
|
+
[
|
|
787
|
+
Output(TAB_ID_EXPORT_NSIP, "children"),
|
|
788
|
+
Output(EXPORT_SAVED_LOAD_INDICATOR_ID, "data"),
|
|
789
|
+
Output(EXPORT_SAVED_ACTIVE_TAB_ID, "data"),
|
|
790
|
+
],
|
|
791
|
+
[
|
|
792
|
+
Input(EXPORT_LOAD_INDICATOR_ID, "data"),
|
|
793
|
+
Input(EXPORT_TAB_MENU_ID, "active_tab"),
|
|
794
|
+
Input(TEAM_SELECTED_VALUE_ID, "data"),
|
|
795
|
+
Input(DATA_SELECTED_SOURCE_ID, "data"),
|
|
796
|
+
Input(NSIP_DECLARATIONS_SELECTED_ID, "data"),
|
|
797
|
+
],
|
|
798
|
+
[
|
|
799
|
+
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
800
|
+
State(EXPORT_SAVED_LOAD_INDICATOR_ID, "data"),
|
|
801
|
+
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
802
|
+
State(EXPORT_SAVED_ACTIVE_TAB_ID, "data"),
|
|
803
|
+
],
|
|
804
|
+
prevent_initial_call=True,
|
|
805
|
+
)
|
|
806
|
+
def display_export_table(
|
|
807
|
+
load_in_progress,
|
|
808
|
+
active_tab,
|
|
809
|
+
team,
|
|
810
|
+
data_source,
|
|
811
|
+
declaration_set,
|
|
812
|
+
team_selection_date,
|
|
813
|
+
previous_load_in_progress,
|
|
814
|
+
period_date: str,
|
|
815
|
+
previous_active_tab,
|
|
816
|
+
):
|
|
817
|
+
"""
|
|
818
|
+
Display active tab contents after a team or an active tab change.
|
|
819
|
+
|
|
820
|
+
:param load_in_progress: load in progress indicator
|
|
821
|
+
:param tab: tab name
|
|
822
|
+
:param team: selected team
|
|
823
|
+
:param data_source: Hito (non-validated declarations) or OSITAH (validated declarations)
|
|
824
|
+
:param declaration_set: declarations subset selected
|
|
825
|
+
:param team_selection_date: last time the team selection was changed
|
|
826
|
+
:param previous_load_in_progress: previous value of the load_in_progress indicator
|
|
827
|
+
:param period_date: a date that must be inside the declaration period
|
|
828
|
+
:param previous_active_tab: previously active tab
|
|
829
|
+
:return: tab content
|
|
830
|
+
"""
|
|
831
|
+
|
|
832
|
+
tab_contents = []
|
|
833
|
+
|
|
834
|
+
# Be sure to fill the return values in the same order as Output are declared
|
|
835
|
+
tab_list = [TAB_ID_EXPORT_NSIP]
|
|
836
|
+
for tab in tab_list:
|
|
837
|
+
if team and len(team) > 0 and tab == active_tab:
|
|
838
|
+
if load_in_progress > previous_load_in_progress and active_tab == previous_active_tab:
|
|
839
|
+
if active_tab == TAB_ID_EXPORT_NSIP:
|
|
840
|
+
tab_contents.append(
|
|
841
|
+
nsip_export_table(team, team_selection_date, period_date, declaration_set)
|
|
842
|
+
)
|
|
843
|
+
else:
|
|
844
|
+
tab_contents.append(
|
|
845
|
+
dbc.Alert("Erreur interne: tab non supporté"), color="warning"
|
|
846
|
+
)
|
|
847
|
+
previous_load_in_progress += 1
|
|
848
|
+
else:
|
|
849
|
+
component = html.Div(
|
|
850
|
+
[
|
|
851
|
+
create_progress_bar(team, duration=EXPORT_PROGRESS_BAR_MAX_DURATION),
|
|
852
|
+
dcc.Interval(
|
|
853
|
+
id=EXPORT_LOAD_TRIGGER_INTERVAL_ID,
|
|
854
|
+
n_intervals=0,
|
|
855
|
+
max_intervals=1,
|
|
856
|
+
interval=500,
|
|
857
|
+
),
|
|
858
|
+
]
|
|
859
|
+
)
|
|
860
|
+
tab_contents.append(component)
|
|
861
|
+
else:
|
|
862
|
+
tab_contents.append("")
|
|
863
|
+
|
|
864
|
+
tab_contents.extend([previous_load_in_progress, active_tab])
|
|
865
|
+
|
|
866
|
+
return tab_contents
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
@app.callback(
|
|
870
|
+
Output(EXPORT_LOAD_INDICATOR_ID, "data"),
|
|
871
|
+
Input(EXPORT_LOAD_TRIGGER_INTERVAL_ID, "n_intervals"),
|
|
872
|
+
State(EXPORT_SAVED_LOAD_INDICATOR_ID, "data"),
|
|
873
|
+
prevent_initial_call=True,
|
|
874
|
+
)
|
|
875
|
+
def export_tables_trigger(n, previous_load_indicator):
|
|
876
|
+
"""
|
|
877
|
+
Increment (change) input of display_export_table callback to get it fired a
|
|
878
|
+
second time after displaying the progress bar. The output component must be updated each
|
|
879
|
+
time the callback is entered to trigger the execution of the other callback, thus the
|
|
880
|
+
choice of incrementing it at each call.
|
|
881
|
+
|
|
882
|
+
:param n: n_interval property of the dcc.Interval (0 or 1)
|
|
883
|
+
:return: 1 increment to previous value
|
|
884
|
+
"""
|
|
885
|
+
|
|
886
|
+
return previous_load_indicator + 1
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
@app.callback(
|
|
890
|
+
Output({"type": "nsip-export-user-selected", "id": MATCH}, "data"),
|
|
891
|
+
Input({"type": "nsip-export-user", "id": MATCH}, "value"),
|
|
892
|
+
State({"type": "nsip-selected-user", "id": MATCH}, "data"),
|
|
893
|
+
prevent_initial_call=True,
|
|
894
|
+
)
|
|
895
|
+
def nsip_export_select_user(state, agent_email):
|
|
896
|
+
"""
|
|
897
|
+
Mark the user as selected for NSIP export.
|
|
898
|
+
|
|
899
|
+
:param state: checkbox state
|
|
900
|
+
:param agent_email: RESEDA email of the selected user
|
|
901
|
+
:return:
|
|
902
|
+
"""
|
|
903
|
+
|
|
904
|
+
global_params = GlobalParams()
|
|
905
|
+
try:
|
|
906
|
+
session_data = global_params.session_data
|
|
907
|
+
except SessionDataMissing:
|
|
908
|
+
return no_session_id_jumbotron()
|
|
909
|
+
declarations = session_data.nsip_declarations
|
|
910
|
+
|
|
911
|
+
if state:
|
|
912
|
+
declarations.loc[declarations.email_auth == agent_email, "selected"] = True
|
|
913
|
+
else:
|
|
914
|
+
declarations.loc[declarations.email_auth == agent_email, "selected"] = False
|
|
915
|
+
|
|
916
|
+
return state
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
@app.callback(
|
|
920
|
+
Output(NSIP_EXPORT_ALL_SELECTED_ID, "data"),
|
|
921
|
+
Input(NSIP_EXPORT_SELECT_ALL_ID, "value"),
|
|
922
|
+
prevent_initial_call=True,
|
|
923
|
+
)
|
|
924
|
+
def nsip_export_select_all_agents(checked):
|
|
925
|
+
"""
|
|
926
|
+
Mark all selectable agents as selected if checked=1 or unselect all otherwise
|
|
927
|
+
|
|
928
|
+
:param checked: checkbox value
|
|
929
|
+
:return: checkbox value
|
|
930
|
+
"""
|
|
931
|
+
|
|
932
|
+
global_params = GlobalParams()
|
|
933
|
+
try:
|
|
934
|
+
session_data = global_params.session_data
|
|
935
|
+
except SessionDataMissing:
|
|
936
|
+
return no_session_id_jumbotron()
|
|
937
|
+
declarations = session_data.nsip_declarations
|
|
938
|
+
|
|
939
|
+
declarations.loc[declarations.selectable, "selected"] = checked
|
|
940
|
+
|
|
941
|
+
return checked
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
# A client-side callback is used to update the selection indicator (checkbox) of all rows after
|
|
945
|
+
# clicking the "Select all" button
|
|
946
|
+
app.clientside_callback(
|
|
947
|
+
"""
|
|
948
|
+
function define_checkbox_status(checked) {
|
|
949
|
+
const checkbox_forms = document.querySelectorAll(".nsip-agent-selector");
|
|
950
|
+
checkbox_forms.forEach(function(cb_form) {
|
|
951
|
+
const agent = cb_form.querySelector("input");
|
|
952
|
+
/*console.log("Updating checkbox for "+agent.id);*/
|
|
953
|
+
if ( checked ) {
|
|
954
|
+
agent.checked = true;
|
|
955
|
+
} else {
|
|
956
|
+
agent.checked = false;
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
return checked;
|
|
960
|
+
}
|
|
961
|
+
""",
|
|
962
|
+
Output(NSIP_EXPORT_SELECTION_STATUS_ID, "data"),
|
|
963
|
+
Input(NSIP_EXPORT_ALL_SELECTED_ID, "data"),
|
|
964
|
+
prevent_initial_call=True,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
@app.callback(
|
|
969
|
+
Output(NSIP_EXPORT_BUTTON_ID, "children"),
|
|
970
|
+
Output(NSIP_EXPORT_BUTTON_ID, "title"),
|
|
971
|
+
Output(NSIP_EXPORT_BUTTON_ID, "disabled"),
|
|
972
|
+
Input({"type": "nsip-export-user-selected", "id": ALL}, "data"),
|
|
973
|
+
Input(NSIP_EXPORT_ALL_SELECTED_ID, "data"),
|
|
974
|
+
prevent_initial_call=True,
|
|
975
|
+
)
|
|
976
|
+
def nsip_export_selected_count(*_):
|
|
977
|
+
"""
|
|
978
|
+
Callback updating the export button label with the number of selected agent, each time a
|
|
979
|
+
selection is changed. The button is also disabled if no agent is selected. Cannot be merged
|
|
980
|
+
with nsip_export_select_user callback as MATCH and non MATCH output cannot be mixed.
|
|
981
|
+
|
|
982
|
+
:param *_: input values are ignored
|
|
983
|
+
:return: label of the export button
|
|
984
|
+
"""
|
|
985
|
+
|
|
986
|
+
global_params = GlobalParams()
|
|
987
|
+
try:
|
|
988
|
+
session_data = global_params.session_data
|
|
989
|
+
except SessionDataMissing:
|
|
990
|
+
return no_session_id_jumbotron()
|
|
991
|
+
declarations = session_data.nsip_declarations
|
|
992
|
+
|
|
993
|
+
selected_users = declarations[declarations.selected].email_auth.unique()
|
|
994
|
+
if len(selected_users):
|
|
995
|
+
selected_count = f" ({len(selected_users)})"
|
|
996
|
+
else:
|
|
997
|
+
selected_count = ""
|
|
998
|
+
|
|
999
|
+
return (
|
|
1000
|
+
f"{NSIP_EXPORT_BUTTON_LABEL}{selected_count}",
|
|
1001
|
+
(
|
|
1002
|
+
f"{len(selected_users)} agent{'s' if len(selected_users) > 1 else ''}"
|
|
1003
|
+
f" sélectionné{'s' if len(selected_users) > 1 else ''}"
|
|
1004
|
+
),
|
|
1005
|
+
False if len(selected_users) else True,
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
@app.callback(
|
|
1010
|
+
Output(NSIP_EXPORT_STATUS_ID, "children"),
|
|
1011
|
+
Output(NSIP_EXPORT_STATUS_ID, "is_open"),
|
|
1012
|
+
Output(NSIP_EXPORT_STATUS_ID, "color"),
|
|
1013
|
+
Output(EXPORT_NSIP_SAVED_SYNC_INDICATOR_ID, "data"),
|
|
1014
|
+
Input(NSIP_EXPORT_BUTTON_ID, "n_clicks"),
|
|
1015
|
+
Input(EXPORT_NSIP_SYNC_INDICATOR_ID, "data"),
|
|
1016
|
+
State(EXPORT_NSIP_SAVED_SYNC_INDICATOR_ID, "data"),
|
|
1017
|
+
prevent_initial_call=True,
|
|
1018
|
+
)
|
|
1019
|
+
def nsip_export_button(n_clicks, sync_indicator, previous_sync_indicator):
|
|
1020
|
+
"""
|
|
1021
|
+
Push into NSIP declarations for all the selected users. All project declarations for the user
|
|
1022
|
+
are added/updated. This callback is entered twice: the first time it displays a progress
|
|
1023
|
+
bar and start a dcc.Interval, the second time it does the real synchronisation work.
|
|
1024
|
+
|
|
1025
|
+
:param n_clicks: checkbox state
|
|
1026
|
+
:param sync_indicator: current value of sync indicator
|
|
1027
|
+
:param previous_sync_indicator: previous value of sync indicator
|
|
1028
|
+
:return: agent/project updates failed
|
|
1029
|
+
"""
|
|
1030
|
+
|
|
1031
|
+
global_params = GlobalParams()
|
|
1032
|
+
try:
|
|
1033
|
+
session_data = global_params.session_data
|
|
1034
|
+
except SessionDataMissing:
|
|
1035
|
+
return no_session_id_jumbotron()
|
|
1036
|
+
declarations = session_data.nsip_declarations
|
|
1037
|
+
selected_declarations = declarations[declarations.selectable & declarations.selected]
|
|
1038
|
+
failed_updates = []
|
|
1039
|
+
failed_exports = 0
|
|
1040
|
+
successful_exports = 0
|
|
1041
|
+
|
|
1042
|
+
if n_clicks and n_clicks >= 1:
|
|
1043
|
+
if sync_indicator > previous_sync_indicator:
|
|
1044
|
+
for row in selected_declarations.itertuples(index=False):
|
|
1045
|
+
# One of the possible error during declaration update is that the user has
|
|
1046
|
+
# multiple contracts attached to the lab for the current period. In this case
|
|
1047
|
+
# the update is retried with the second contract (generally the current one)
|
|
1048
|
+
# mentioned in the error message.
|
|
1049
|
+
retry_on_error = True
|
|
1050
|
+
retry_attempts = 0
|
|
1051
|
+
contract = None
|
|
1052
|
+
while retry_on_error:
|
|
1053
|
+
# nsip_project_id and nsip_reference_id are NaN if undefined and a NaN value is
|
|
1054
|
+
# not equal to itself!
|
|
1055
|
+
project_type = row.nsip_project_id == row.nsip_project_id
|
|
1056
|
+
activity_id = row.nsip_project_id if project_type else row.nsip_reference_id
|
|
1057
|
+
(
|
|
1058
|
+
status,
|
|
1059
|
+
http_status,
|
|
1060
|
+
http_reason,
|
|
1061
|
+
) = global_params.nsip.update_declaration(
|
|
1062
|
+
row.email_reseda,
|
|
1063
|
+
activity_id,
|
|
1064
|
+
project_type,
|
|
1065
|
+
row.time,
|
|
1066
|
+
row.validation_time,
|
|
1067
|
+
contract,
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
if status > 0 and http_status:
|
|
1071
|
+
m = EXPORT_NSIP_MULTIPLE_CONTRACTS_ERROR.match(http_reason)
|
|
1072
|
+
# Never retry more than once
|
|
1073
|
+
if m and (retry_attempts == 0):
|
|
1074
|
+
contract = m.group("id2")
|
|
1075
|
+
retry_attempts += 1
|
|
1076
|
+
print(
|
|
1077
|
+
(
|
|
1078
|
+
f"Agent {row.email_reseda} has several contracts for the"
|
|
1079
|
+
f" current period: retrying update with contract {contract}"
|
|
1080
|
+
)
|
|
1081
|
+
)
|
|
1082
|
+
else:
|
|
1083
|
+
retry_on_error = False
|
|
1084
|
+
else:
|
|
1085
|
+
retry_on_error = False
|
|
1086
|
+
|
|
1087
|
+
if status <= 0:
|
|
1088
|
+
# Log a message if the declaration was successfully added to NSIP to make
|
|
1089
|
+
# diagnostics easier. After a successful addition or update, http_status
|
|
1090
|
+
# contains the declaration ID instead of the http status
|
|
1091
|
+
if status == 0:
|
|
1092
|
+
action = "added to"
|
|
1093
|
+
else:
|
|
1094
|
+
action = "updated in"
|
|
1095
|
+
print(
|
|
1096
|
+
(
|
|
1097
|
+
f"Declaration {http_status} for user {row.email_auth} (NSIP ID:"
|
|
1098
|
+
f" {row.email_reseda}), {'project' if project_type else 'reference'}"
|
|
1099
|
+
f" ID {int(activity_id)} {action} NSIP"
|
|
1100
|
+
),
|
|
1101
|
+
flush=True,
|
|
1102
|
+
)
|
|
1103
|
+
successful_exports += 1
|
|
1104
|
+
|
|
1105
|
+
else:
|
|
1106
|
+
if http_status:
|
|
1107
|
+
http_error = f" (http status={http_status}, reason={http_reason})"
|
|
1108
|
+
else:
|
|
1109
|
+
http_error = ""
|
|
1110
|
+
print(
|
|
1111
|
+
(
|
|
1112
|
+
f"ERROR: update of declaration failed for user {row.email_auth}"
|
|
1113
|
+
f" (NSIP ID: {row.email_reseda}),"
|
|
1114
|
+
f" {'project' if project_type else 'reference'} ID"
|
|
1115
|
+
f" {int(activity_id)}{http_error}"
|
|
1116
|
+
),
|
|
1117
|
+
flush=True,
|
|
1118
|
+
)
|
|
1119
|
+
failed_exports += 1
|
|
1120
|
+
# Limit the number of explicit failed update displayed to avoid a too long
|
|
1121
|
+
# message
|
|
1122
|
+
if failed_exports < EXPORT_MAX_FAILDED_UPDATES:
|
|
1123
|
+
failed_updates.append(
|
|
1124
|
+
f"{row.email_auth}/{'projet' if project_type else 'reference'}"
|
|
1125
|
+
f"/{int(activity_id)}"
|
|
1126
|
+
)
|
|
1127
|
+
elif failed_exports == EXPORT_MAX_FAILDED_UPDATES:
|
|
1128
|
+
failed_updates.append("...")
|
|
1129
|
+
|
|
1130
|
+
previous_sync_indicator += 1
|
|
1131
|
+
else:
|
|
1132
|
+
component = html.Div(
|
|
1133
|
+
[
|
|
1134
|
+
create_progress_bar(
|
|
1135
|
+
duration=(len(selected_declarations) / EXPORT_NSIP_SYNC_FREQUENCY)
|
|
1136
|
+
),
|
|
1137
|
+
dcc.Interval(
|
|
1138
|
+
id=EXPORT_NSIP_SYNC_TRIGGER_INTERVAL_ID,
|
|
1139
|
+
n_intervals=0,
|
|
1140
|
+
max_intervals=1,
|
|
1141
|
+
interval=500,
|
|
1142
|
+
),
|
|
1143
|
+
]
|
|
1144
|
+
)
|
|
1145
|
+
return component, True, "success", previous_sync_indicator
|
|
1146
|
+
|
|
1147
|
+
else:
|
|
1148
|
+
raise PreventUpdate
|
|
1149
|
+
|
|
1150
|
+
if failed_exports == 0:
|
|
1151
|
+
update_status_msg = (
|
|
1152
|
+
f"Toutes les déclarations sélectionnées ({successful_exports})"
|
|
1153
|
+
f" ont été enregistrées dans NSIP"
|
|
1154
|
+
)
|
|
1155
|
+
color = "success"
|
|
1156
|
+
else:
|
|
1157
|
+
update_status_msg = [
|
|
1158
|
+
(
|
|
1159
|
+
f"{failed_exports} export{' a' if failed_exports == 1 else 's ont'} échoué :"
|
|
1160
|
+
f" {', '.join(failed_updates)}"
|
|
1161
|
+
),
|
|
1162
|
+
html.Br(),
|
|
1163
|
+
(
|
|
1164
|
+
f"{successful_exports} déclaration"
|
|
1165
|
+
f"{' a' if successful_exports == 1 else 's ont'}"
|
|
1166
|
+
f" été synchronisée{'' if successful_exports == 1 else 's'}"
|
|
1167
|
+
f" avec succès"
|
|
1168
|
+
),
|
|
1169
|
+
]
|
|
1170
|
+
color = "warning"
|
|
1171
|
+
return update_status_msg, True, color, previous_sync_indicator
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
@app.callback(
|
|
1175
|
+
Output(EXPORT_NSIP_SYNC_INDICATOR_ID, "data"),
|
|
1176
|
+
Input(EXPORT_NSIP_SYNC_TRIGGER_INTERVAL_ID, "n_intervals"),
|
|
1177
|
+
State(EXPORT_NSIP_SAVED_SYNC_INDICATOR_ID, "data"),
|
|
1178
|
+
prevent_initial_call=True,
|
|
1179
|
+
)
|
|
1180
|
+
def nsip_export_button_trigger(n, previous_load_indicator):
|
|
1181
|
+
"""
|
|
1182
|
+
Increment (change) input of nsip_export_button callback to get it fired a
|
|
1183
|
+
second time after displaying the progress bar. The output component must be updated each
|
|
1184
|
+
time the callback is entered to trigger the execution of the other callback, thus the
|
|
1185
|
+
choice of incrementing it at each call.
|
|
1186
|
+
|
|
1187
|
+
:param n: n_interval property of the dcc.Interval (0 or 1)
|
|
1188
|
+
:return: 1 increment to previous value
|
|
1189
|
+
"""
|
|
1190
|
+
|
|
1191
|
+
return previous_load_indicator + 1
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
@app.callback(
|
|
1195
|
+
Output(NSIP_DECLARATIONS_SELECTED_ID, "data"),
|
|
1196
|
+
Input(NSIP_DECLARATIONS_SWITCH_ID, "value"),
|
|
1197
|
+
prevent_initial_call=True,
|
|
1198
|
+
)
|
|
1199
|
+
def select_declarations_set(new_set):
|
|
1200
|
+
"""
|
|
1201
|
+
This callback is used to forward to the NSIP export callback the selected declarations set
|
|
1202
|
+
through a dcc.Store that exists permanently in the page.
|
|
1203
|
+
|
|
1204
|
+
:param new_set: selected declarations set
|
|
1205
|
+
:return: same value
|
|
1206
|
+
"""
|
|
1207
|
+
|
|
1208
|
+
return new_set
|