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