ositah 24.4.dev1__py3-none-any.whl → 24.7.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.
Files changed (47) hide show
  1. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/METADATA +1 -1
  2. ositah-24.7.dev1.dist-info/RECORD +6 -0
  3. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/WHEEL +1 -1
  4. ositah/__init__.py +0 -0
  5. ositah/app.py +0 -17
  6. ositah/apps/__init__.py +0 -0
  7. ositah/apps/analysis.py +0 -774
  8. ositah/apps/configuration/__init__.py +0 -0
  9. ositah/apps/configuration/callbacks.py +0 -917
  10. ositah/apps/configuration/main.py +0 -542
  11. ositah/apps/configuration/parameters.py +0 -74
  12. ositah/apps/configuration/tools.py +0 -112
  13. ositah/apps/export.py +0 -1172
  14. ositah/apps/validation/__init__.py +0 -0
  15. ositah/apps/validation/callbacks.py +0 -240
  16. ositah/apps/validation/main.py +0 -89
  17. ositah/apps/validation/parameters.py +0 -25
  18. ositah/apps/validation/tables.py +0 -654
  19. ositah/apps/validation/tools.py +0 -533
  20. ositah/assets/arrow_down_up.svg +0 -4
  21. ositah/assets/ositah.css +0 -54
  22. ositah/assets/sort_ascending.svg +0 -5
  23. ositah/assets/sort_descending.svg +0 -6
  24. ositah/assets/sorttable.js +0 -499
  25. ositah/main.py +0 -449
  26. ositah/ositah.example.cfg +0 -215
  27. ositah/static/style.css +0 -54
  28. ositah/templates/base.html +0 -22
  29. ositah/templates/bootstrap_login.html +0 -38
  30. ositah/templates/login_form.html +0 -27
  31. ositah/utils/__init__.py +0 -0
  32. ositah/utils/agents.py +0 -117
  33. ositah/utils/authentication.py +0 -287
  34. ositah/utils/cache.py +0 -19
  35. ositah/utils/core.py +0 -13
  36. ositah/utils/exceptions.py +0 -64
  37. ositah/utils/hito_db.py +0 -51
  38. ositah/utils/hito_db_model.py +0 -245
  39. ositah/utils/menus.py +0 -339
  40. ositah/utils/period.py +0 -135
  41. ositah/utils/projects.py +0 -1175
  42. ositah/utils/teams.py +0 -42
  43. ositah/utils/utils.py +0 -459
  44. ositah-24.4.dev1.dist-info/RECORD +0 -46
  45. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/LICENSE +0 -0
  46. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/entry_points.txt +0 -0
  47. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/top_level.txt +0 -0
@@ -1,917 +0,0 @@
1
- """
2
- Dash callbacks for Configuration applications
3
- """
4
-
5
- from datetime import datetime
6
- from typing import Any, List
7
-
8
- import dash_bootstrap_components as dbc
9
- import numpy as np
10
- from dash import callback_context, html
11
- from dash.dependencies import Input, Output, State
12
- from dash.exceptions import PreventUpdate
13
-
14
- from ositah.app import app
15
- from ositah.apps.configuration.parameters import *
16
- from ositah.apps.configuration.tools import (
17
- get_masterprojects_items,
18
- get_projects_items,
19
- list_box_empty,
20
- )
21
- from ositah.utils.exceptions import InvalidCallbackInput
22
- from ositah.utils.menus import TABLE_TYPE_TABLE, GlobalParams
23
- from ositah.utils.period import get_declaration_periods
24
- from ositah.utils.projects import (
25
- MASTERPROJECT_DELETED_ACTIVITY,
26
- NSIP_CLASS_OTHER_ACTIVITY,
27
- NSIP_CLASS_PROJECT,
28
- add_activity,
29
- add_activity_teams,
30
- get_all_hito_activities,
31
- get_hito_nsip_activities,
32
- get_nsip_activities,
33
- nsip_activity_name_id,
34
- ositah2hito_project_name,
35
- reenable_activity,
36
- remove_activity,
37
- remove_activity_teams,
38
- update_activity_name,
39
- )
40
- from ositah.utils.teams import get_project_team_ids
41
-
42
-
43
- def check_activity_changes(project_activity: bool):
44
- """
45
- Retrieve the list of activity changes in NSIP compared to the Hito projects
46
-
47
- :param project_activity: true for projects, false for other activities
48
- :return: dataframe with changed activities
49
- """
50
-
51
- nsip_activities = get_nsip_activities(project_activity)
52
- if nsip_activities is None or nsip_activities.empty:
53
- raise PreventUpdate
54
-
55
- hito_projects = get_hito_nsip_activities(project_activity)
56
-
57
- if project_activity:
58
- merge_left_attr = "nsip_project_id"
59
- else:
60
- merge_left_attr = "nsip_reference_id"
61
- activity_changes = hito_projects.merge(
62
- nsip_activities,
63
- how="outer",
64
- left_on=merge_left_attr,
65
- right_on="id",
66
- indicator=True,
67
- suffixes=[None, "_nsip"],
68
- )
69
- activity_changes["status"] = np.NaN
70
- activity_changes.loc[activity_changes._merge == "left_only", "status"] = PROJECT_CHANGE_REMOVED
71
- activity_changes.loc[activity_changes._merge == "right_only", "status"] = PROJECT_CHANGE_ADDED
72
- activity_changes.loc[
73
- (activity_changes._merge == "both")
74
- & (
75
- (activity_changes.nsip_project != activity_changes.ositah_name)
76
- | (activity_changes["master_project.name"] != activity_changes.nsip_master)
77
- ),
78
- "status",
79
- ] = PROJECT_CHANGE_CHANGED
80
- activity_changes.drop(activity_changes[activity_changes.status.isna()].index, inplace=True)
81
-
82
- # Set NaN to 0 in referentiel_id and id_nsip as np.NaN is a float and prevent casting to int.
83
- activity_changes.loc[activity_changes["referentiel_id"].isna(), "referentiel_id"] = 0
84
- activity_changes.loc[activity_changes["id_nsip"].isna(), "id_nsip"] = 0
85
- activity_changes["referentiel_id"] = activity_changes.referentiel_id.astype(int)
86
- activity_changes["id_nsip"] = activity_changes.id_nsip.astype(int)
87
-
88
- return activity_changes
89
-
90
-
91
- @app.callback(
92
- Output(TAB_ID_ACTIVITY_TEAMS, "children"),
93
- Output(TAB_ID_NSIP_PROJECT_SYNC, "children"),
94
- Output(TAB_ID_DECLARATION_PERIODS, "children"),
95
- Output(TAB_ID_PROJECT_MGT, "children"),
96
- Input(CONFIGURATION_TAB_MENU_ID, "active_tab"),
97
- )
98
- def display_tab_content(active_tab):
99
- from ositah.apps.configuration.main import (
100
- declaration_periods_layout,
101
- nsip_sync_layout,
102
- project_mgt_layout,
103
- project_teams_layout,
104
- )
105
-
106
- if active_tab == TAB_ID_ACTIVITY_TEAMS:
107
- return project_teams_layout(), "", "", ""
108
- elif active_tab == TAB_ID_NSIP_PROJECT_SYNC:
109
- return "", nsip_sync_layout(), "", ""
110
- elif active_tab == TAB_ID_DECLARATION_PERIODS:
111
- return "", "", declaration_periods_layout(), ""
112
- elif active_tab == TAB_ID_PROJECT_MGT:
113
- return "", "", "", project_mgt_layout()
114
- else:
115
- return "", "", "", ""
116
-
117
-
118
- @app.callback(
119
- Output(NSIP_SYNC_CONTENT_ID, "children"),
120
- Output(NSIP_SYNC_APPLY_DIFF_ID, "disabled"),
121
- Input(NSIP_SYNC_SHOW_DIFF_ID, "n_clicks"),
122
- Input(NSIP_SYNC_APPLY_DIFF_ID, "n_clicks"),
123
- State(NSIP_SYNC_ACTIVITY_TYPE_ID, "value"),
124
- prevent_initial_call=True,
125
- )
126
- def show_nsip_activity_differences(_, __, activity_type):
127
- """
128
- Handle buttons from the NSIP synchronisation tab. Values of input parameters are not used.
129
- Do nothing if no button was clicked.
130
-
131
- :param activity_type: type of NSIP activity (integer)
132
- :return: contents of NSIP_SYNC_CONTENT_ID components
133
- """
134
-
135
- global_params = GlobalParams()
136
-
137
- if not callback_context.triggered:
138
- return PreventUpdate
139
- else:
140
- button_id = callback_context.triggered[0]["prop_id"].split(".")[0]
141
-
142
- if activity_type == NSIP_SYNC_ACTIVITY_TYPE_PROJECT:
143
- project_activity = True
144
- else:
145
- project_activity = False
146
-
147
- activity_changes = check_activity_changes(project_activity)
148
-
149
- if button_id == NSIP_SYNC_SHOW_DIFF_ID:
150
- add_num = len(activity_changes[activity_changes.status == PROJECT_CHANGE_ADDED])
151
- change_num = len(activity_changes[activity_changes.status == PROJECT_CHANGE_CHANGED])
152
- remove_num = len(activity_changes[activity_changes.status == PROJECT_CHANGE_REMOVED])
153
- total_changes = add_num + change_num + remove_num
154
-
155
- if add_num or change_num or remove_num:
156
- data_columns = {
157
- # TO BE FIXED: Column names (nsip_master, nsip_project, ositah_name)
158
- # are misleading...
159
- "Masterprojet OSITAH": "nsip_master",
160
- "Projet OSITAH": "nsip_project",
161
- "ID OSITAH": "id",
162
- "ID Référentiel": "referentiel_id",
163
- "Masterprojet NSIP": "master_project.name",
164
- "Projet NSIP": "ositah_name",
165
- "ID NSIP": "id_nsip",
166
- "Statut": "status",
167
- }
168
- theader = [html.Thead(html.Tr([html.Th(c) for c in data_columns.keys()]))]
169
-
170
- tbody = [
171
- html.Tbody(
172
- [
173
- html.Tr([html.Td(row[c]) for c in data_columns.values()])
174
- for i, row in activity_changes.sort_values(
175
- by=["nsip_master", "nsip_project"]
176
- ).iterrows()
177
- ]
178
- )
179
- ]
180
-
181
- table = dbc.Table(
182
- theader + tbody,
183
- id={"type": TABLE_TYPE_TABLE, "id": TABLE_NSIP_PROJECT_SYNC_ID},
184
- bordered=True,
185
- hover=True,
186
- striped=True,
187
- class_name="sortable",
188
- )
189
-
190
- summary_msg = html.B(
191
- (
192
- f"{total_changes} changements NSIP / OSITAH : {add_num} additions,"
193
- f" {remove_num} suppressions, {change_num} modifications"
194
- )
195
- )
196
-
197
- apply_button_disabled = False
198
-
199
- else:
200
- table = html.Div()
201
- if project_activity:
202
- summary_msg = "Les projets NSIP et OSITAH sont synchronisés"
203
- else:
204
- summary_msg = "Les activités NSIP et OSITAH sont synchronisées"
205
- apply_button_disabled = True
206
-
207
- return (
208
- html.Div(
209
- [
210
- dbc.Alert(summary_msg),
211
- html.P(),
212
- table,
213
- ]
214
- ),
215
- apply_button_disabled,
216
- )
217
-
218
- elif button_id == NSIP_SYNC_APPLY_DIFF_ID:
219
- add_failed = {}
220
- change_failed = {}
221
- delete_failed = {}
222
-
223
- for _, activity in activity_changes[
224
- activity_changes.status == PROJECT_CHANGE_CHANGED
225
- ].iterrows():
226
- status, error_msg = update_activity_name(
227
- activity.id,
228
- activity.referentiel_id,
229
- activity.id_nsip,
230
- activity["master_project.name"],
231
- activity.ositah_name,
232
- )
233
- if status:
234
- change_failed[f"{activity['nsip_name_id']}"] = error_msg
235
-
236
- for _, activity in activity_changes[
237
- activity_changes.status == PROJECT_CHANGE_ADDED
238
- ].iterrows():
239
- activity_full_name = ositah2hito_project_name(
240
- activity["master_project.name"], activity.ositah_name
241
- )
242
- activity_teams = get_project_team_ids(global_params.project_teams, activity_full_name)
243
- status, error_msg = add_activity(
244
- activity.id_nsip,
245
- activity["master_project.name"],
246
- activity.ositah_name,
247
- activity_teams,
248
- project_activity,
249
- )
250
- if status:
251
- add_failed[
252
- (
253
- f"{activity['master_project.name']} / {activity.ositah_name}"
254
- f" (NSIP ID: {activity.id_nsip})"
255
- )
256
- ] = error_msg
257
-
258
- for _, activity in activity_changes[
259
- activity_changes.status == PROJECT_CHANGE_REMOVED
260
- ].iterrows():
261
- _, _, project_id, reference_id = nsip_activity_name_id(
262
- activity.nsip_name_id,
263
- NSIP_CLASS_PROJECT if project_activity else NSIP_CLASS_OTHER_ACTIVITY,
264
- )
265
- if project_activity:
266
- nsip_id = project_id
267
- else:
268
- nsip_id = reference_id
269
- status, error_msg = remove_activity(
270
- activity.id, activity.referentiel_id, nsip_id, project_activity
271
- )
272
- if status:
273
- delete_failed[f"{activity['nsip_name_id']}"] = error_msg
274
-
275
- if (
276
- len(change_failed.keys()) == 0
277
- and len(add_failed.keys()) == 0
278
- and len(delete_failed) == 0
279
- ):
280
- return dbc.Alert("All changes applied successfully to Hito"), True
281
- else:
282
- alert_msg = []
283
- if len(change_failed.keys()) > 0:
284
- alert_msg.append(
285
- html.P(html.B("Erreur durant la mise à jour des activités suivantes :"))
286
- )
287
- alert_msg.append(html.Ul([html.Li(f"{k}: {v}") for k, v in change_failed.items()]))
288
- if len(add_failed.keys()) > 0:
289
- alert_msg.append(html.P(html.B("Erreur durant l'ajout des activités suivantes :")))
290
- alert_msg.append(html.Ul([html.Li(f"{k}: {v}") for k, v in add_failed.items()]))
291
- if len(delete_failed.keys()) > 0:
292
- alert_msg.append(
293
- html.P(html.B("Erreur durant la suppression des activités suivantes :"))
294
- )
295
- alert_msg.append(html.Ul([html.Li(f"{k}: {v}") for k, v in delete_failed.items()]))
296
- return dbc.Alert(alert_msg), True
297
-
298
- else:
299
- return dbc.Alert("Bouton invalide", color="danger"), True
300
-
301
-
302
- @app.callback(
303
- Output(ACTIVITY_TEAMS_PROJECTS_ID, "options"),
304
- Output(ACTIVITY_TEAMS_PROJECTS_ID, "value"),
305
- Input(ACTIVITY_TEAMS_MASTERPROJECTS_ID, "value"),
306
- State(ACTIVITY_TEAMS_PROJECT_ACTIVITY_ID, "data"),
307
- prevent_initial_call=True,
308
- )
309
- def activity_team_projects(masterproject, project_activity):
310
- """
311
- Display the list of projects associated with the selected masterproject in the project
312
- list box. Also reset the project selected value.
313
-
314
- :param masterproject: selected master project
315
- :param project_activity: True if it is a project rather than a Hito activity
316
- :return: list box options
317
- """
318
-
319
- items = get_projects_items(masterproject, project_activity, None)
320
- if len(items):
321
- return items, None
322
- else:
323
- return [], None
324
-
325
-
326
- @app.callback(
327
- Output(PROJECT_MGT_MASTERPROJECT_LIST_ID, "options"),
328
- Output(PROJECT_MGT_MASTERPROJECT_LIST_ID, "value"),
329
- Input(PROJECT_MGT_PROJECT_TYPE_ID, "value"),
330
- State(PROJECT_MGT_PROJECT_ACTIVITY_ID, "data"),
331
- prevent_initial_call=True,
332
- )
333
- def project_mgt_masterprojects(category, project_activity):
334
- """
335
- Display the list of projects associated with the selected masterproject in the project
336
- list box. Also reset the masterproject selected value.
337
-
338
- :param category: a number indicating if NSIP, local or disabled projects must be displayed
339
- :param project_activity: True if it is a project rather than a Hito activity
340
- :return: list box options, selected value reset to an empty string
341
- """
342
-
343
- if category is None:
344
- return [], ""
345
- else:
346
- items = get_masterprojects_items(project_activity, category)
347
- return items, ""
348
-
349
-
350
- @app.callback(
351
- Output(PROJECT_MGT_PROJECT_LIST_ID, "options"),
352
- Output(PROJECT_MGT_PROJECT_LIST_ID, "value"),
353
- Input(PROJECT_MGT_MASTERPROJECT_LIST_ID, "value"),
354
- Input(PROJECT_MGT_PROJECT_TYPE_ID, "value"),
355
- State(PROJECT_MGT_PROJECT_ACTIVITY_ID, "data"),
356
- prevent_initial_call=True,
357
- )
358
- def project_mgt_projects(masterproject, category, project_activity):
359
- """
360
- Display the list of projects associated with the selected masterproject in the project
361
- list box. If local projects are selected, the masterproject value is empty.
362
- Also reset the project selected value.
363
-
364
- :param masterproject: selected master project
365
- :param category: a number indicating if NSIP, local or disabled projects must be displayed
366
- :param project_activity: True if it is a project rather than a Hito activity
367
- :return: list box options
368
- """
369
-
370
- if masterproject or category == PROJECT_MGT_PROJECT_TYPE_LOCAL:
371
- items = get_projects_items(masterproject, project_activity, category)
372
- return items, ""
373
- else:
374
- return [], ""
375
-
376
-
377
- @app.callback(
378
- Output(PROJECT_MGT_ACTION_BUTTON_ID, "children"),
379
- Output(PROJECT_MGT_ACTION_BUTTON_ID, "style"),
380
- Output(PROJECT_MGT_ACTION_BUTTON_ID, "disabled"),
381
- Input(PROJECT_MGT_PROJECT_LIST_ID, "value"),
382
- State(PROJECT_MGT_PROJECT_TYPE_ID, "value"),
383
- prevent_initial_call=True,
384
- )
385
- def project_mgt_enable_action(activity, category):
386
- """
387
- If a project has been selected, enable the button with the appropriate action depending on
388
- activity type. If no project is elected, hide and disable the action button.
389
-
390
- :param activity: name of the selected activity
391
- :param category: whether the project is a local project, a NSIP project or a disabled one
392
- """
393
-
394
- if activity:
395
- if category == PROJECT_MGT_PROJECT_TYPE_DISABLED:
396
- action = PROJECT_MGT_ACTION_BUTTON_ENABLE
397
- else:
398
- action = PROJECT_MGT_ACTION_BUTTON_DISABLE
399
- return action, {"visibility": "visible"}, False
400
- else:
401
- return "", {"visibility": "hidden"}, True
402
-
403
-
404
- @app.callback(
405
- Output(PROJECT_MGT_STATUS_ID, "is_open"),
406
- Output(PROJECT_MGT_STATUS_ID, "children"),
407
- Output(PROJECT_MGT_STATUS_ID, "color"),
408
- Input(PROJECT_MGT_ACTION_BUTTON_ID, "n_clicks"),
409
- State(PROJECT_MGT_ACTION_BUTTON_ID, "children"),
410
- State(PROJECT_MGT_PROJECT_LIST_ID, "value"),
411
- State(PROJECT_MGT_PROJECT_ACTIVITY_ID, "data"),
412
- prevent_initial_call=True,
413
- )
414
- def project_mgt_execute_action(n_clicks, action, activity, is_project):
415
- """
416
- Execute action associated with the button. Actual action is retrieved from the
417
- button label.
418
-
419
- :param n_clicks: number of clicks, ignored
420
- :param action: the button label
421
- :param activity: name of the project/activity
422
- :param is_project: true if a project, false if is an Hito activity
423
- :return: status message and color
424
- """
425
-
426
- if action == PROJECT_MGT_ACTION_BUTTON_DISABLE:
427
- return True, "Désactivation projet : pas encore implémentée", "warning"
428
- elif action == PROJECT_MGT_ACTION_BUTTON_ENABLE:
429
- status, status_msg = reenable_activity(activity, is_project, MASTERPROJECT_DELETED_ACTIVITY)
430
- if status == 0:
431
- return True, f"Projet {activity} réactivé avec succès", "success"
432
- else:
433
- alert_msg = html.Div(
434
- html.P(f"Erreur durant la réactivation du projet {activity}"), html.P(error_msg)
435
- )
436
- return True, alert_msg, "danger"
437
- else:
438
- return True, f"Internal error: invalid action ({action})", "danger"
439
-
440
-
441
- @app.callback(
442
- Output(ACTIVITY_TEAMS_LAB_TEAMS_ID, "options"),
443
- Input(ACTIVITY_TEAMS_PROJECTS_ID, "value"),
444
- State(ACTIVITY_TEAMS_MASTERPROJECTS_ID, "value"),
445
- prevent_initial_call=True,
446
- )
447
- def display_teams(project, masterproject):
448
- """
449
- Display the Hito teams not yet associated with the selected project in the lab team
450
- list box
451
-
452
- :param project: selected project
453
- :param masterproject: selected master project
454
- :return: list box options for lab teams
455
- """
456
-
457
- global_params = GlobalParams()
458
- session_data = global_params.session_data
459
-
460
- if masterproject and project:
461
- lab_team_items = []
462
- for team in sorted(session_data.agent_teams):
463
- lab_team_items.append({"label": team, "value": team})
464
- return lab_team_items
465
-
466
- else:
467
- return []
468
-
469
-
470
- @app.callback(
471
- Output(ACTIVITY_TEAMS_BUTTON_ADD_ID, "disabled"),
472
- Input(ACTIVITY_TEAMS_LAB_TEAMS_ID, "value"),
473
- )
474
- def enable_team_update_buttons(selected_team):
475
- """
476
- Enable the button allowing to add the selected team in lab teams into the project teams
477
-
478
- :param selected_team: selected lab team
479
- :return: boolean
480
- """
481
- if selected_team:
482
- return False
483
- else:
484
- return True
485
-
486
-
487
- @app.callback(
488
- Output(ACTIVITY_TEAMS_BUTTON_REMOVE_ID, "disabled"),
489
- Input(ACTIVITY_TEAMS_SELECTED_TEAMS_ID, "value"),
490
- )
491
- def enable_team_remove_buttons(selected_team):
492
- """
493
- Enable the button allowing to remove a team from the project teams
494
-
495
- :param selected_team: selected lab team
496
- :return: boolean
497
- """
498
- if selected_team:
499
- return False
500
- else:
501
- return True
502
-
503
-
504
- @app.callback(
505
- Output(ACTIVITY_TEAMS_SELECTED_TEAMS_ID, "options"),
506
- Output(ACTIVITY_TEAMS_ADDED_TEAMS_ID, "data"),
507
- Output(ACTIVITY_TEAMS_REMOVED_TEAMS_ID, "data"),
508
- Output(ACTIVITY_TEAMS_BUTTON_UPDATE_ID, "disabled"),
509
- Output(ACTIVITY_TEAMS_BUTTON_CANCEL_ID, "disabled"),
510
- Output(ACTIVITY_TEAMS_LAB_TEAMS_ID, "value"),
511
- Output(ACTIVITY_TEAMS_SELECTED_TEAMS_ID, "value"),
512
- Input(ACTIVITY_TEAMS_BUTTON_ADD_ID, "n_clicks"),
513
- Input(ACTIVITY_TEAMS_BUTTON_REMOVE_ID, "n_clicks"),
514
- Input(ACTIVITY_TEAMS_PROJECTS_ID, "value"),
515
- State(ACTIVITY_TEAMS_MASTERPROJECTS_ID, "value"),
516
- State(ACTIVITY_TEAMS_LAB_TEAMS_ID, "value"),
517
- State(ACTIVITY_TEAMS_SELECTED_TEAMS_ID, "options"),
518
- State(ACTIVITY_TEAMS_SELECTED_TEAMS_ID, "value"),
519
- State(ACTIVITY_TEAMS_PROJECT_ACTIVITY_ID, "data"),
520
- State(ACTIVITY_TEAMS_ADDED_TEAMS_ID, "data"),
521
- State(ACTIVITY_TEAMS_REMOVED_TEAMS_ID, "data"),
522
- prevent_initial_call=True,
523
- )
524
- def update_selected_teams(
525
- update_n_click: int,
526
- cancel_n_click: int,
527
- project,
528
- masterproject,
529
- selected_lab_team,
530
- team_list_items,
531
- selected_activity_team,
532
- project_activity,
533
- added_teams,
534
- removed_teams,
535
- ):
536
- """
537
- Update the project teams list box after a project selection change or a lab team
538
- selection.
539
-
540
- :param update_n_click: clicks (ignored) for update button
541
- :param cancel_n_click: clicks (ignored) for cancel button
542
- :param project: selected project
543
- :param masterproject: selected masterproject
544
- :param selected_lab_team: selected team in lab teams (for addition)
545
- :param team_list_items: current contents of project teams list box
546
- :param selected_lab_team: selected team in project teams (for removal)
547
- :param project_activity: True if it is a project rather than a Hito activity
548
- :param added_teams: list of teams to be added
549
- :param removed_teams: list of teams to be removed
550
- :return: list box options for project teams and update/cancel buttons
551
- """
552
-
553
- ctx = callback_context
554
- if not ctx.triggered:
555
- raise PreventUpdate
556
- else:
557
- active_input = ctx.triggered[0]["prop_id"].split(".")[0]
558
-
559
- # Selected project changed: reset the list to project teams
560
- if active_input == ACTIVITY_TEAMS_PROJECTS_ID:
561
- global_params = GlobalParams()
562
- session_data = global_params.session_data
563
- activities = get_all_hito_activities(project_activity)
564
- activity_teams = activities[
565
- (activities.masterproject == masterproject) & (activities.project == project)
566
- ]["team_name"]
567
- team_list_items = []
568
- for team in sorted(activity_teams):
569
- team_disabled = False if team in session_data.agent_teams else True
570
- team_list_items.append({"label": team, "value": team, "disabled": team_disabled})
571
- return team_list_items, [], [], True, True, None, None
572
-
573
- # A team being added to the project
574
- elif active_input == ACTIVITY_TEAMS_BUTTON_ADD_ID:
575
- team_present = False
576
- if team_list_items and not list_box_empty(team_list_items):
577
- for item in team_list_items:
578
- if item["value"] == selected_lab_team:
579
- team_present = True
580
- break
581
- else:
582
- team_list_items = []
583
- if team_present:
584
- raise PreventUpdate
585
- else:
586
- if selected_lab_team in removed_teams:
587
- removed_teams.remove(selected_lab_team)
588
- added_teams.append(selected_lab_team)
589
- team_list_items.append(
590
- {
591
- "label": selected_lab_team,
592
- "value": selected_lab_team,
593
- "disabled": False,
594
- }
595
- )
596
- return (
597
- sorted(team_list_items, key=lambda x: x["label"]),
598
- added_teams,
599
- removed_teams,
600
- False,
601
- False,
602
- None,
603
- None,
604
- )
605
-
606
- elif active_input == ACTIVITY_TEAMS_BUTTON_REMOVE_ID:
607
- if team_list_items and not list_box_empty(team_list_items):
608
- team_list_items.remove(
609
- {
610
- "label": selected_activity_team,
611
- "value": selected_activity_team,
612
- "disabled": False,
613
- }
614
- )
615
- removed_teams.append(selected_activity_team)
616
- if selected_activity_team in added_teams:
617
- added_teams.remove(selected_activity_team)
618
- return (
619
- sorted(team_list_items, key=lambda x: x["label"]),
620
- added_teams,
621
- removed_teams,
622
- False,
623
- False,
624
- None,
625
- None,
626
- )
627
-
628
- # Should not happen...
629
- else:
630
- raise PreventUpdate
631
-
632
- else:
633
- raise InvalidCallbackInput(active_input)
634
-
635
-
636
- @app.callback(
637
- Output(ACTIVITY_TEAMS_STATUS_ID, "children"),
638
- Output(ACTIVITY_TEAMS_STATUS_ID, "is_open"),
639
- Output(ACTIVITY_TEAMS_STATUS_ID, "color"),
640
- Output(ACTIVITY_TEAMS_RESET_INDICATOR_ID, "data"),
641
- Input(ACTIVITY_TEAMS_BUTTON_UPDATE_ID, "n_clicks"),
642
- Input(ACTIVITY_TEAMS_BUTTON_CANCEL_ID, "n_clicks"),
643
- State(ACTIVITY_TEAMS_MASTERPROJECTS_ID, "value"),
644
- State(ACTIVITY_TEAMS_PROJECTS_ID, "value"),
645
- State(ACTIVITY_TEAMS_ADDED_TEAMS_ID, "data"),
646
- State(ACTIVITY_TEAMS_REMOVED_TEAMS_ID, "data"),
647
- State(ACTIVITY_TEAMS_PROJECT_ACTIVITY_ID, "data"),
648
- State(ACTIVITY_TEAMS_RESET_INDICATOR_ID, "data"),
649
- prevent_initial_call=True,
650
- )
651
- def update_activity_teams(
652
- update_n_click: int,
653
- cancel_n_click: int,
654
- masterproject: str,
655
- project: str,
656
- added_teams: List[str],
657
- removed_teams: List[str],
658
- project_activity: bool,
659
- reset_indicator: int,
660
- ):
661
- """
662
- Update the team list associated with the selected activity and if successful, reset the
663
- page elements (increment the reset indicator). Also clear the activity cache to force
664
- updating it from the database. This callback is also used to cancel the current
665
- modifications if the cancel button is clicked.
666
-
667
- :param update_n_click: clicks (ignored) for update button
668
- :param cancel_n_click: clicks (ignored) for cancel button
669
- :param masterproject: selected masterproject
670
- :param project: selected project
671
- :param added_teams: list of teams to add to the selected project
672
- :param removed_teams: list of teams to remove from the selected project
673
- :param project_activity: if true, an Hito project else an Hito activity
674
- :return: status msg, its color and the flag to reset the page elements
675
- """
676
-
677
- ctx = callback_context
678
- if not ctx.triggered:
679
- raise PreventUpdate
680
- else:
681
- active_input = ctx.triggered[0]["prop_id"].split(".")[0]
682
-
683
- global_params = GlobalParams()
684
- session_data = global_params.session_data
685
-
686
- if active_input == ACTIVITY_TEAMS_BUTTON_CANCEL_ID:
687
- return "", False, "success", reset_indicator + 1
688
- else:
689
- status_msg = []
690
- status = 0
691
-
692
- if len(added_teams):
693
- add_status, add_status_msg = add_activity_teams(
694
- masterproject, project, added_teams, project_activity
695
- )
696
- status += add_status
697
- if add_status == 0:
698
- status_msg.append(
699
- html.Div(
700
- f"'{masterproject}/{project}' team list updated:"
701
- f" '{', '.join(added_teams)}' added"
702
- )
703
- )
704
- else:
705
- status_msg.append(
706
- html.Div(
707
- f"Failed to add '{', '.join(added_teams)}' to '{masterproject}/{project}'"
708
- f" team list: {add_status_msg}",
709
- )
710
- )
711
-
712
- if len(removed_teams):
713
- remove_status, remove_status_msg = remove_activity_teams(
714
- masterproject, project, removed_teams, project_activity
715
- )
716
- status += remove_status
717
- if remove_status == 0:
718
- status_msg.append(
719
- html.Div(
720
- f"'{masterproject}/{project}' team list updated:"
721
- f" '{', '.join(removed_teams)}' removed"
722
- )
723
- )
724
- else:
725
- status_msg.append(
726
- html.Div(
727
- f"Failed to remove '{', '.join(added_teams)}' from "
728
- f"'{masterproject}/{project}' team list: {remove_status_msg}",
729
- )
730
- )
731
-
732
- session_data.set_hito_activities(None, project_activity)
733
-
734
- if status == 0:
735
- status_color = "success"
736
- else:
737
- status_color = "danger"
738
-
739
- return (
740
- status_msg,
741
- True,
742
- status_color,
743
- reset_indicator + 1,
744
- )
745
-
746
-
747
- @app.callback(
748
- Output(ACTIVITY_TEAMS_MASTERPROJECTS_ID, "value"),
749
- Input(ACTIVITY_TEAMS_RESET_INDICATOR_ID, "data"),
750
- prevent_initial_call=True,
751
- )
752
- def reset_mastproject_selection(_):
753
- """
754
- Reset the masterprojet selection to None after the teams for the currently selected
755
- project has been successfully updated.
756
-
757
- :param _: reset indicator (value not used)
758
- :return: masterproject selection
759
- """
760
- return None
761
-
762
-
763
- @app.callback(
764
- Output(DECLARATION_PERIOD_NAME_ID, "value"),
765
- Output(DECLARATION_PERIOD_NAME_ID, "readonly"),
766
- Output(DECLARATION_PERIOD_START_DATE_ID, "date"),
767
- Output(DECLARATION_PERIOD_START_DATE_ID, "disabled"),
768
- Output(DECLARATION_PERIOD_END_DATE_ID, "date"),
769
- Output(DECLARATION_PERIOD_END_DATE_ID, "disabled"),
770
- Output(DECLARATION_PERIOD_VALIDATION_DATE_ID, "date"),
771
- Output(DECLARATION_PERIOD_VALIDATION_DATE_ID, "disabled"),
772
- Output(DECLARATION_PERIOD_PARAMS_ID, "style"),
773
- Output(DECLARATION_PERIODS_CREATE_CLICK_ID, "data"),
774
- Output(DECLARATION_PERIODS_SAVE_NEW_ID, "disabled"),
775
- Output(DECLARATION_PERIODS_STATUS_HIDDEN_ID, "data"),
776
- Input(DECLARATION_PERIODS_ID, "value"),
777
- Input(DECLARATION_PERIODS_CREATE_NEW_ID, "n_clicks"),
778
- State(DECLARATION_PERIODS_CREATE_CLICK_ID, "data"),
779
- State(DECLARATION_PERIODS_STATUS_HIDDEN_ID, "data"),
780
- State(DECLARATION_PERIODS_STATUS_VISIBLE_ID, "data"),
781
- prevent_initial_call=True,
782
- )
783
- def display_period_params(
784
- period_index: str,
785
- num_clicks: int,
786
- num_clicks_previous: int,
787
- status_hidden: int,
788
- status_visible: int,
789
- ) -> (str, bool, str, bool, str, bool, str, bool, str, int, int):
790
- """
791
- This callback displays the parameters of a selected period. It can be called either by selecting
792
- an existing period or creating a new one.
793
- """
794
- periods = get_declaration_periods(descending=False)
795
-
796
- # Hide the status alert after this callback (both variables must be equal)
797
- if status_hidden != status_visible:
798
- status_hidden = status_visible
799
-
800
- if num_clicks is not None and num_clicks != num_clicks_previous:
801
- today = datetime.now()
802
- if today.month < 7:
803
- start_month = 1
804
- end_month = 6
805
- end_day = 30
806
- semester = 1
807
- else:
808
- start_month = 7
809
- end_month = 12
810
- end_day = 31
811
- semester = 2
812
- period_name = f"{today.year}-S{semester}"
813
- start_date = datetime(today.year, start_month, 1)
814
- end_date = datetime(today.year, end_month, end_day, 23, 59)
815
- validation_date = datetime(today.year, end_month, 15)
816
- saved_clicks = num_clicks
817
- period_save_disabled = False
818
-
819
- else:
820
- try:
821
- period_index = int(period_index)
822
- if period_index < len(periods):
823
- period_name = periods[period_index].name
824
- start_date = periods[period_index].start_date
825
- end_date = periods[period_index].end_date
826
- validation_date = periods[period_index].validation_date
827
- saved_clicks = num_clicks_previous
828
- period_save_disabled = True
829
- else:
830
- raise Exception(
831
- f"internal error: period_index has an invalid value ({period_index})"
832
- )
833
- except Exception as e:
834
- raise e
835
-
836
- return (
837
- period_name,
838
- True,
839
- start_date,
840
- True,
841
- end_date,
842
- True,
843
- validation_date,
844
- True,
845
- {"visibility": "visible"},
846
- saved_clicks,
847
- period_save_disabled,
848
- status_hidden,
849
- )
850
-
851
-
852
- @app.callback(
853
- Output(DECLARATION_PERIODS_STATUS_ID, "children"),
854
- Output(DECLARATION_PERIODS_STATUS_ID, "color"),
855
- Output(DECLARATION_PERIODS_STATUS_VISIBLE_ID, "data"),
856
- Input(DECLARATION_PERIODS_SAVE_NEW_ID, "n_clicks"),
857
- State(DECLARATION_PERIOD_NAME_ID, "value"),
858
- State(DECLARATION_PERIOD_START_DATE_ID, "date"),
859
- State(DECLARATION_PERIOD_END_DATE_ID, "date"),
860
- State(DECLARATION_PERIOD_VALIDATION_DATE_ID, "date"),
861
- State(DECLARATION_PERIODS_STATUS_HIDDEN_ID, "data"),
862
- State(DECLARATION_PERIODS_STATUS_VISIBLE_ID, "data"),
863
- prevent_initial_call=True,
864
- )
865
- def save_new_period(
866
- n_clicks: int,
867
- name: str,
868
- start_date: str,
869
- end_date: str,
870
- validation_date: str,
871
- status_hidden: int,
872
- status_visible: int,
873
- ) -> (Any, str, int):
874
- """
875
- Save new period in database
876
- """
877
- from ositah.utils.hito_db import get_db
878
- from ositah.utils.hito_db_model import OSITAHValidationPeriod
879
-
880
- db = get_db()
881
-
882
- # Display the status alert after this callback (status_visible must be greater
883
- # than status_hidden)
884
- if status_visible <= status_hidden:
885
- status_visible = status_hidden + 1
886
-
887
- period = OSITAHValidationPeriod(
888
- name=name,
889
- start_date=start_date,
890
- end_date=end_date,
891
- validation_date=validation_date,
892
- )
893
- try:
894
- db.session.add(period)
895
- db.session.commit()
896
- except Exception as e:
897
- return (html.Div(repr(e)), "danger", status_visible)
898
-
899
- return (
900
- html.Div([f"Nouvelle période {name} ajoutée. ", html.A("Recharger", href="")]),
901
- "success",
902
- status_visible,
903
- )
904
-
905
-
906
- @app.callback(
907
- Output(DECLARATION_PERIODS_STATUS_ID, "is_open"),
908
- Input(DECLARATION_PERIODS_STATUS_HIDDEN_ID, "data"),
909
- Input(DECLARATION_PERIODS_STATUS_VISIBLE_ID, "data"),
910
- prevent_initial_call=True,
911
- )
912
- def display_declaration_periods_status(status_hidden: int, status_visible: int) -> bool:
913
- """
914
- Callback to control whether the declaration periods status must be displayed or hidden.
915
- If status_visible > status_hidden, it must be displayed, else it must be hidden.
916
- """
917
- return status_visible > status_hidden