ositah 25.6.dev1__py3-none-any.whl → 25.9.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ositah might be problematic. Click here for more details.
- ositah/app.py +17 -17
- ositah/apps/analysis.py +785 -785
- ositah/apps/configuration/callbacks.py +916 -916
- ositah/apps/configuration/main.py +546 -546
- ositah/apps/configuration/parameters.py +74 -74
- ositah/apps/configuration/tools.py +112 -112
- ositah/apps/export.py +1209 -1191
- ositah/apps/validation/callbacks.py +240 -240
- ositah/apps/validation/main.py +89 -89
- ositah/apps/validation/parameters.py +25 -25
- ositah/apps/validation/tables.py +646 -646
- ositah/apps/validation/tools.py +552 -552
- ositah/assets/arrow_down_up.svg +3 -3
- ositah/assets/ositah.css +53 -53
- ositah/assets/sort_ascending.svg +4 -4
- ositah/assets/sort_descending.svg +5 -5
- ositah/assets/sorttable.js +499 -499
- ositah/main.py +449 -449
- ositah/ositah.example.cfg +229 -229
- ositah/static/style.css +53 -53
- ositah/templates/base.html +22 -22
- ositah/templates/bootstrap_login.html +38 -38
- ositah/templates/login_form.html +26 -26
- ositah/utils/agents.py +124 -124
- ositah/utils/authentication.py +287 -287
- ositah/utils/cache.py +19 -19
- ositah/utils/core.py +13 -13
- ositah/utils/exceptions.py +64 -64
- ositah/utils/hito_db.py +51 -51
- ositah/utils/hito_db_model.py +253 -253
- ositah/utils/menus.py +339 -339
- ositah/utils/period.py +139 -139
- ositah/utils/projects.py +1179 -1178
- ositah/utils/teams.py +42 -42
- ositah/utils/utils.py +474 -474
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/METADATA +149 -150
- ositah-25.9.dev2.dist-info/RECORD +46 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/licenses/LICENSE +29 -29
- ositah-25.6.dev1.dist-info/RECORD +0 -46
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/WHEEL +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/entry_points.txt +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/top_level.txt +0 -0
ositah/apps/validation/tools.py
CHANGED
|
@@ -1,552 +1,552 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Various functions used by Validation sub-application
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from datetime import date, datetime
|
|
6
|
-
|
|
7
|
-
import dash_bootstrap_components as dbc
|
|
8
|
-
import pandas as pd
|
|
9
|
-
from dash import html
|
|
10
|
-
|
|
11
|
-
from ositah.apps.validation.parameters import (
|
|
12
|
-
VALIDATION_DECLARATIONS_SELECT_ALL,
|
|
13
|
-
VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED,
|
|
14
|
-
VALIDATION_DECLARATIONS_SELECT_VALIDATED,
|
|
15
|
-
VALIDATION_DECLARATIONS_SWITCH_ID,
|
|
16
|
-
)
|
|
17
|
-
from ositah.utils.exceptions import SessionDataMissing
|
|
18
|
-
from ositah.utils.hito_db import get_db
|
|
19
|
-
from ositah.utils.period import get_validation_period_data
|
|
20
|
-
from ositah.utils.projects import (
|
|
21
|
-
CATEGORY_DEFAULT,
|
|
22
|
-
DATA_SOURCE_HITO,
|
|
23
|
-
get_team_projects,
|
|
24
|
-
project_time,
|
|
25
|
-
time_unit,
|
|
26
|
-
)
|
|
27
|
-
from ositah.utils.utils import (
|
|
28
|
-
SEMESTER_HOURS,
|
|
29
|
-
TIME_UNIT_HOURS,
|
|
30
|
-
TIME_UNIT_HOURS_EN,
|
|
31
|
-
TIME_UNIT_WEEKS,
|
|
32
|
-
WEEK_HOURS,
|
|
33
|
-
GlobalParams,
|
|
34
|
-
no_session_id_jumbotron,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def activity_time_cell(row, column, row_index):
|
|
39
|
-
"""
|
|
40
|
-
Build the cell content for an activity time, adding the appropriate class and in case of
|
|
41
|
-
inconsistencies between the Hito declared time and the validated time, add a tooltip
|
|
42
|
-
to display both values.
|
|
43
|
-
|
|
44
|
-
:param row: declaration row
|
|
45
|
-
:param column: column name for the cell to build
|
|
46
|
-
:param row_index: row index of the current row
|
|
47
|
-
:return: html.Td
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
global_params = GlobalParams()
|
|
51
|
-
|
|
52
|
-
classes = "text-center align-middle"
|
|
53
|
-
cell_value = int(round(row[column]))
|
|
54
|
-
cell_id = f"validation-table-value-{row_index}-{column}"
|
|
55
|
-
|
|
56
|
-
if column == "percent_global":
|
|
57
|
-
thresholds = global_params.declaration_options["thresholds"]["current"]
|
|
58
|
-
percent = round(row["percent_global"], 1)
|
|
59
|
-
if percent <= thresholds["low"]:
|
|
60
|
-
percent_class = "table-danger"
|
|
61
|
-
tooltip_txt = f"Percentage low (<={thresholds['low']}%)"
|
|
62
|
-
elif percent <= thresholds["suspect"]:
|
|
63
|
-
percent_class = "table-warning"
|
|
64
|
-
tooltip_txt = (
|
|
65
|
-
f"Percentage suspect (>{thresholds['low']}% and" f" <={thresholds['suspect']}%)"
|
|
66
|
-
)
|
|
67
|
-
elif percent > thresholds["good"]:
|
|
68
|
-
percent_class = "table-info"
|
|
69
|
-
tooltip_txt = f"Percentage too high (>{thresholds['good']}%)"
|
|
70
|
-
else:
|
|
71
|
-
percent_class = "table-success"
|
|
72
|
-
|
|
73
|
-
contents = [html.Div(percent, id=cell_id)]
|
|
74
|
-
if percent_class != "table-success":
|
|
75
|
-
contents.append(dbc.Tooltip(html.Div(tooltip_txt), target=cell_id))
|
|
76
|
-
|
|
77
|
-
classes += f" {percent_class}"
|
|
78
|
-
|
|
79
|
-
elif (
|
|
80
|
-
(column != "enseignement" or not global_params.teaching_ratio)
|
|
81
|
-
and time_unit(column) == TIME_UNIT_HOURS_EN
|
|
82
|
-
and row[column] > global_params.declaration_options["max_hours"]
|
|
83
|
-
):
|
|
84
|
-
contents = [
|
|
85
|
-
html.Div(cell_value, id=cell_id),
|
|
86
|
-
dbc.Tooltip(
|
|
87
|
-
html.Div(
|
|
88
|
-
(
|
|
89
|
-
f"Déclaration supérieure au maximum"
|
|
90
|
-
f" ({global_params.declaration_options['max_hours']} heures)"
|
|
91
|
-
)
|
|
92
|
-
),
|
|
93
|
-
target=cell_id,
|
|
94
|
-
),
|
|
95
|
-
]
|
|
96
|
-
classes += " table-danger"
|
|
97
|
-
|
|
98
|
-
elif row.hito_missing:
|
|
99
|
-
validated_column = f"{column}_val"
|
|
100
|
-
contents = [
|
|
101
|
-
html.Div(int(round(row[validated_column])), id=cell_id),
|
|
102
|
-
dbc.Tooltip(
|
|
103
|
-
[
|
|
104
|
-
html.Div(f"Déclaration validée: {round(row[validated_column], 3)}"),
|
|
105
|
-
html.Div("Déclaration Hito correspondante supprimée"),
|
|
106
|
-
],
|
|
107
|
-
target=cell_id,
|
|
108
|
-
),
|
|
109
|
-
]
|
|
110
|
-
classes += " validated_hito_missing"
|
|
111
|
-
|
|
112
|
-
elif row.validated and not row[f"{column}_time_ok"]:
|
|
113
|
-
validated_column = f"{column}_val"
|
|
114
|
-
contents = [
|
|
115
|
-
html.Div(cell_value, id=cell_id),
|
|
116
|
-
dbc.Tooltip(
|
|
117
|
-
[
|
|
118
|
-
html.Div(f"Dernière déclaration: {round(row[column], 3)}"),
|
|
119
|
-
html.Div(f"Déclaration validée: {round(row[validated_column], 3)}"),
|
|
120
|
-
],
|
|
121
|
-
target=cell_id,
|
|
122
|
-
),
|
|
123
|
-
]
|
|
124
|
-
classes += " table-warning"
|
|
125
|
-
|
|
126
|
-
else:
|
|
127
|
-
contents = [html.Div(cell_value, id=cell_id)]
|
|
128
|
-
if row.suspect:
|
|
129
|
-
contents.append(
|
|
130
|
-
dbc.Tooltip(
|
|
131
|
-
[
|
|
132
|
-
html.Div("Déclaration suspecte: vérifier les quotités déclarées"),
|
|
133
|
-
],
|
|
134
|
-
target=cell_id,
|
|
135
|
-
)
|
|
136
|
-
)
|
|
137
|
-
classes += " table-warning"
|
|
138
|
-
|
|
139
|
-
return html.Td(contents, className=classes, key=f"validation-table-cell-{row}-{column}")
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def agent_tooltip_txt(agent_data, data_columns):
|
|
143
|
-
"""
|
|
144
|
-
Build the tooltip text associated with an agent.
|
|
145
|
-
|
|
146
|
-
:param agent_data: the dataframe row corresponding to the agent
|
|
147
|
-
:param data_columns: the list of columns to use to compute the total time
|
|
148
|
-
:return: list of html elements
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
global_params = GlobalParams()
|
|
152
|
-
|
|
153
|
-
if agent_data["hito_missing"]:
|
|
154
|
-
tooltip_detail = "Déclarations validées supprimées de Hito"
|
|
155
|
-
else:
|
|
156
|
-
tooltip_detail = f"Total (semaines) : {total_time(agent_data, data_columns)}"
|
|
157
|
-
tooltip_txt = [
|
|
158
|
-
html.Div(
|
|
159
|
-
f"{global_params.column_titles['team']}: {agent_data[global_params.columns['team']]}"
|
|
160
|
-
),
|
|
161
|
-
html.Div(tooltip_detail),
|
|
162
|
-
]
|
|
163
|
-
|
|
164
|
-
return tooltip_txt
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def add_validation_declaration_selection_switch(current_set):
|
|
168
|
-
"""
|
|
169
|
-
Add a dbc.RadioItems to select whether to show all declaration or only a subset.
|
|
170
|
-
|
|
171
|
-
:param current_set: currently selected declaration set
|
|
172
|
-
:return: dbc.RadioItems
|
|
173
|
-
"""
|
|
174
|
-
|
|
175
|
-
return dbc.Row(
|
|
176
|
-
[
|
|
177
|
-
dbc.RadioItems(
|
|
178
|
-
options=[
|
|
179
|
-
{
|
|
180
|
-
"label": "Toutes les déclarations",
|
|
181
|
-
"value": VALIDATION_DECLARATIONS_SELECT_ALL,
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
"label": "Déclarations non validées uniquement",
|
|
185
|
-
"value": VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED,
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"label": "Déclarations validées uniquement",
|
|
189
|
-
"value": VALIDATION_DECLARATIONS_SELECT_VALIDATED,
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
value=current_set,
|
|
193
|
-
id=VALIDATION_DECLARATIONS_SWITCH_ID,
|
|
194
|
-
inline=True,
|
|
195
|
-
),
|
|
196
|
-
],
|
|
197
|
-
justify="center",
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def agent_list(dataframe: pd.DataFrame) -> pd.DataFrame:
|
|
202
|
-
global_params = GlobalParams()
|
|
203
|
-
fullname_df = pd.DataFrame()
|
|
204
|
-
fullname_df[global_params.columns["fullname"]] = dataframe[
|
|
205
|
-
global_params.columns["fullname"]
|
|
206
|
-
].drop_duplicates()
|
|
207
|
-
return fullname_df
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def total_time(row, categories, rounded=True) -> int:
|
|
211
|
-
"""
|
|
212
|
-
Compute total time declared by an agent in weeks
|
|
213
|
-
|
|
214
|
-
:param row: dataframe row for an agent
|
|
215
|
-
:param categories: list of categories to sum up
|
|
216
|
-
:param rounded: if true, returns the rounded value
|
|
217
|
-
:return: number of weeks declared
|
|
218
|
-
"""
|
|
219
|
-
|
|
220
|
-
global_params = GlobalParams()
|
|
221
|
-
|
|
222
|
-
weeks_number = 0
|
|
223
|
-
hours_number = 0
|
|
224
|
-
for category in categories:
|
|
225
|
-
if global_params.time_unit[category] == TIME_UNIT_WEEKS:
|
|
226
|
-
weeks_number += row[category]
|
|
227
|
-
elif global_params.time_unit[category] == TIME_UNIT_HOURS:
|
|
228
|
-
hours_number += row[category]
|
|
229
|
-
else:
|
|
230
|
-
raise Exception(
|
|
231
|
-
(
|
|
232
|
-
f"Unsupported time unit '{global_params.time_unit[category]}'"
|
|
233
|
-
f" for category {category}"
|
|
234
|
-
)
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
weeks_number += hours_number / WEEK_HOURS
|
|
238
|
-
|
|
239
|
-
if rounded:
|
|
240
|
-
return int(round(weeks_number))
|
|
241
|
-
else:
|
|
242
|
-
return weeks_number
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def category_time(dataframe, category) -> None:
|
|
246
|
-
"""
|
|
247
|
-
Convert the number of hours into the time unit of the category. Keep track of the
|
|
248
|
-
conversions done to prevent doing it twice.
|
|
249
|
-
|
|
250
|
-
:param dataframe: dataframe to update
|
|
251
|
-
:param category: category name
|
|
252
|
-
:return: none, dataframe updated
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
global_params = GlobalParams()
|
|
256
|
-
|
|
257
|
-
# Default time unit is hour
|
|
258
|
-
unit_hour = True
|
|
259
|
-
if category in global_params.time_unit:
|
|
260
|
-
if global_params.time_unit[category] == TIME_UNIT_WEEKS:
|
|
261
|
-
unit_hour = False
|
|
262
|
-
elif global_params.time_unit[category] != TIME_UNIT_HOURS:
|
|
263
|
-
raise Exception(
|
|
264
|
-
(
|
|
265
|
-
f"Unsupported time unit '{global_params.time_unit[category]}'"
|
|
266
|
-
f" for category {category}"
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
if not unit_hour:
|
|
271
|
-
dataframe[category] = dataframe[category] / WEEK_HOURS
|
|
272
|
-
|
|
273
|
-
return
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def agent_project_time(agent: str) -> html.Div:
|
|
277
|
-
"""
|
|
278
|
-
Return a HTML Div with the list of projects and the time spent on them
|
|
279
|
-
|
|
280
|
-
:param agent: agent fullname
|
|
281
|
-
:return: html.Div
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
global_params = GlobalParams()
|
|
285
|
-
columns = global_params.columns
|
|
286
|
-
|
|
287
|
-
try:
|
|
288
|
-
session_data = global_params.session_data
|
|
289
|
-
df = session_data.project_declarations[
|
|
290
|
-
session_data.project_declarations[global_params.columns["fullname"]] == agent
|
|
291
|
-
]
|
|
292
|
-
|
|
293
|
-
return html.Div(
|
|
294
|
-
[
|
|
295
|
-
html.P(
|
|
296
|
-
(
|
|
297
|
-
f"{df.iloc[i][global_params.columns['activity']]}:"
|
|
298
|
-
f" {' '.join(project_time(df.iloc[i][columns['activity']], df.iloc[i][columns['hours']]))}" # noqa: E501
|
|
299
|
-
)
|
|
300
|
-
)
|
|
301
|
-
for i in range(len(df))
|
|
302
|
-
]
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
except SessionDataMissing:
|
|
306
|
-
return no_session_id_jumbotron()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def category_declarations(
|
|
310
|
-
project_declarations: pd.DataFrame, use_cache: bool = True
|
|
311
|
-
) -> pd.DataFrame:
|
|
312
|
-
"""
|
|
313
|
-
Process the project declarations (time per project) and convert it into declarations by
|
|
314
|
-
category of projects.
|
|
315
|
-
|
|
316
|
-
:param project_declarations: project declarations to consolidate
|
|
317
|
-
:param use_cache: if True, use and update cache
|
|
318
|
-
:return: dataframe
|
|
319
|
-
"""
|
|
320
|
-
|
|
321
|
-
global_params = GlobalParams()
|
|
322
|
-
columns = global_params.columns
|
|
323
|
-
|
|
324
|
-
try:
|
|
325
|
-
session_data = global_params.session_data
|
|
326
|
-
except SessionDataMissing:
|
|
327
|
-
return no_session_id_jumbotron()
|
|
328
|
-
|
|
329
|
-
# Check if there is a cached version
|
|
330
|
-
if session_data.category_declarations is not None and use_cache:
|
|
331
|
-
return session_data.category_declarations
|
|
332
|
-
|
|
333
|
-
if project_declarations is not None:
|
|
334
|
-
category_declarations = project_declarations.copy()
|
|
335
|
-
categories = [CATEGORY_DEFAULT]
|
|
336
|
-
categories.extend(global_params.project_categories.keys())
|
|
337
|
-
for category in categories:
|
|
338
|
-
category_declarations[category] = category_declarations.loc[
|
|
339
|
-
category_declarations[columns["category"]] == category, columns["hours"]
|
|
340
|
-
]
|
|
341
|
-
category_declarations.drop(columns=columns["hours"], inplace=True)
|
|
342
|
-
category_declarations.fillna(0, inplace=True)
|
|
343
|
-
category_declarations = category_declarations.infer_objects(copy=False)
|
|
344
|
-
|
|
345
|
-
agg_functions = {c: "sum" for c in categories}
|
|
346
|
-
agg_functions["suspect"] = "any"
|
|
347
|
-
category_declarations_pt = pd.pivot_table(
|
|
348
|
-
category_declarations,
|
|
349
|
-
index=[
|
|
350
|
-
global_params.columns["fullname"],
|
|
351
|
-
global_params.columns["team"],
|
|
352
|
-
global_params.columns["agent_id"],
|
|
353
|
-
],
|
|
354
|
-
values=[*categories, "suspect"],
|
|
355
|
-
aggfunc=agg_functions,
|
|
356
|
-
)
|
|
357
|
-
category_declarations = pd.DataFrame(category_declarations_pt.to_records())
|
|
358
|
-
|
|
359
|
-
category_declarations["total_hours"] = category_declarations[categories].sum(axis=1)
|
|
360
|
-
category_declarations["percent_global"] = (
|
|
361
|
-
category_declarations["total_hours"] / SEMESTER_HOURS * 100
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
# Convert category time into the appropriate unit
|
|
365
|
-
for column_name in categories:
|
|
366
|
-
category_time(category_declarations, column_name)
|
|
367
|
-
|
|
368
|
-
else:
|
|
369
|
-
raise Exception("Project declarations are not defined")
|
|
370
|
-
|
|
371
|
-
if use_cache:
|
|
372
|
-
session_data.category_declarations = category_declarations
|
|
373
|
-
|
|
374
|
-
return category_declarations
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def define_declaration_thresholds(period_date: str):
|
|
378
|
-
"""
|
|
379
|
-
Define the declaration thresholds (low, suspect, normal) for the current period
|
|
380
|
-
|
|
381
|
-
:param period_date: a date that must be inside the declaration period
|
|
382
|
-
"""
|
|
383
|
-
global_params = GlobalParams()
|
|
384
|
-
|
|
385
|
-
period_datetime = date.fromisoformat(period_date)
|
|
386
|
-
if period_datetime.month >= 7:
|
|
387
|
-
global_params.declaration_options["thresholds"]["current"] = (
|
|
388
|
-
global_params.declaration_options["thresholds"]["s2"]
|
|
389
|
-
)
|
|
390
|
-
else:
|
|
391
|
-
global_params.declaration_options["thresholds"]["current"] = (
|
|
392
|
-
global_params.declaration_options["thresholds"]["s1"]
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def get_validation_data(agent_id, period_date: str, session=None):
|
|
397
|
-
"""
|
|
398
|
-
Return the validation data for an agent or None if there is no entry in the database for this
|
|
399
|
-
agent_id.
|
|
400
|
-
|
|
401
|
-
:param agent_id: agent_id of the agent to check
|
|
402
|
-
:param session: DB session to use (default one if None)
|
|
403
|
-
:param period_date: a date that must be inside the declaration period
|
|
404
|
-
:return: an OSITAHValidation object or None
|
|
405
|
-
"""
|
|
406
|
-
from ositah.utils.hito_db_model import OSITAHValidation
|
|
407
|
-
|
|
408
|
-
db = get_db()
|
|
409
|
-
if session is None:
|
|
410
|
-
session = db.session
|
|
411
|
-
|
|
412
|
-
validation_period = get_validation_period_data(period_date)
|
|
413
|
-
return (
|
|
414
|
-
session.query(OSITAHValidation)
|
|
415
|
-
.filter_by(agent_id=agent_id, period_id=validation_period.id)
|
|
416
|
-
.order_by(OSITAHValidation.timestamp.desc())
|
|
417
|
-
.first()
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
def get_validation_status(agent_id, period_date: str, session=None):
|
|
422
|
-
"""
|
|
423
|
-
Return True if the agent entry has been validated, False otherwise (including if there is no
|
|
424
|
-
validation entry for the agent)
|
|
425
|
-
|
|
426
|
-
:param agent_id: agent_id of the agent to check
|
|
427
|
-
:param session: DB session to use
|
|
428
|
-
:param period_date: a date that must be inside the declaration period
|
|
429
|
-
:return: boolean
|
|
430
|
-
"""
|
|
431
|
-
|
|
432
|
-
validation_data = get_validation_data(agent_id, period_date, session)
|
|
433
|
-
if validation_data is None:
|
|
434
|
-
return False
|
|
435
|
-
else:
|
|
436
|
-
return validation_data.validated
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def get_all_validation_status(period_date: str):
|
|
440
|
-
"""
|
|
441
|
-
Returns the list of agents whose declaration has been validated as a dataframe. It is intended
|
|
442
|
-
to be used to build the validation table but should not be used when updating the status, as
|
|
443
|
-
it may have been updated by somebody else.
|
|
444
|
-
|
|
445
|
-
:param period_date: a date that must be inside the declaration period
|
|
446
|
-
:return: dataframe
|
|
447
|
-
"""
|
|
448
|
-
|
|
449
|
-
from ositah.utils.hito_db_model import OSITAHValidation
|
|
450
|
-
|
|
451
|
-
db = get_db()
|
|
452
|
-
validation_period = get_validation_period_data(period_date)
|
|
453
|
-
# By design, only one entry in the declaration period can be with the status validated,
|
|
454
|
-
# except if the database has been messed up...
|
|
455
|
-
validation_query = OSITAHValidation.query.filter(
|
|
456
|
-
OSITAHValidation.period_id == validation_period.id,
|
|
457
|
-
OSITAHValidation.validated,
|
|
458
|
-
)
|
|
459
|
-
validation_data = pd.read_sql(validation_query.statement, con=db.engine)
|
|
460
|
-
if validation_data is None:
|
|
461
|
-
validation_data = pd.DataFrame()
|
|
462
|
-
else:
|
|
463
|
-
validation_data.set_index("agent_id", inplace=True)
|
|
464
|
-
|
|
465
|
-
return validation_data
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
def validation_started(period_date: str):
|
|
469
|
-
"""
|
|
470
|
-
Compare the current date with the validation start date (validation_date) and return True
|
|
471
|
-
if the validation has started, False otherwise.
|
|
472
|
-
|
|
473
|
-
:param period_date: date included in the declaration period
|
|
474
|
-
:return: boolean
|
|
475
|
-
"""
|
|
476
|
-
|
|
477
|
-
current_date = datetime.now()
|
|
478
|
-
period_params = get_validation_period_data(period_date)
|
|
479
|
-
if current_date >= period_params.validation_date:
|
|
480
|
-
return True
|
|
481
|
-
else:
|
|
482
|
-
return False
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def project_declaration_snapshot(
|
|
486
|
-
agent_id,
|
|
487
|
-
validation_id,
|
|
488
|
-
team,
|
|
489
|
-
team_selection_date,
|
|
490
|
-
period_date,
|
|
491
|
-
db_session=None,
|
|
492
|
-
commit=False,
|
|
493
|
-
):
|
|
494
|
-
"""
|
|
495
|
-
Save into table ositah_validation_project_declaration the validated project declarations
|
|
496
|
-
for an agent.
|
|
497
|
-
|
|
498
|
-
:param agent_id: agent (ID) whose project declarations must be saved
|
|
499
|
-
:param validation_id: validation ID associated with the declarations snapshot
|
|
500
|
-
:param db_session: session for the current transaction. If None, use the default one.
|
|
501
|
-
:param commit: if false, do not commit added rows
|
|
502
|
-
:return: None
|
|
503
|
-
"""
|
|
504
|
-
|
|
505
|
-
from ositah.utils.hito_db_model import OSITAHProjectDeclaration
|
|
506
|
-
|
|
507
|
-
global_params = GlobalParams()
|
|
508
|
-
columns = global_params.columns
|
|
509
|
-
try:
|
|
510
|
-
session_data = global_params.session_data
|
|
511
|
-
except SessionDataMissing:
|
|
512
|
-
return no_session_id_jumbotron()
|
|
513
|
-
|
|
514
|
-
db = get_db()
|
|
515
|
-
if db_session:
|
|
516
|
-
session = db_session
|
|
517
|
-
else:
|
|
518
|
-
session = db.session
|
|
519
|
-
|
|
520
|
-
# The cache cannot be used directly as the callback may run on a server where the session
|
|
521
|
-
# cache for the current user doesn't exist yet
|
|
522
|
-
project_declarations = get_team_projects(
|
|
523
|
-
team, team_selection_date, period_date, DATA_SOURCE_HITO
|
|
524
|
-
)
|
|
525
|
-
agent_projects = project_declarations[project_declarations[columns["agent_id"]] == agent_id]
|
|
526
|
-
if agent_projects is None:
|
|
527
|
-
print(f"ERROR: no declaration found for agent ID '{agent_id}' (internal error)")
|
|
528
|
-
else:
|
|
529
|
-
for _, project in agent_projects.iterrows():
|
|
530
|
-
declaration = OSITAHProjectDeclaration(
|
|
531
|
-
projet=project[columns["project"]],
|
|
532
|
-
masterprojet=project[columns["masterproject"]],
|
|
533
|
-
category=project[columns["category"]],
|
|
534
|
-
hours=project[columns["hours"]],
|
|
535
|
-
quotite=project[columns["quotite"]],
|
|
536
|
-
validation_id=validation_id,
|
|
537
|
-
hito_project_id=project[columns["activity_id"]],
|
|
538
|
-
)
|
|
539
|
-
try:
|
|
540
|
-
session.add(declaration)
|
|
541
|
-
except Exception:
|
|
542
|
-
# If the default session is used, let the caller process the exception and
|
|
543
|
-
# eventually do the rollback
|
|
544
|
-
if db_session:
|
|
545
|
-
session.rollback()
|
|
546
|
-
raise
|
|
547
|
-
|
|
548
|
-
# Reset the cache of validated declarations if a modification occured
|
|
549
|
-
session_data.reset_validated_declarations_cache()
|
|
550
|
-
|
|
551
|
-
if commit:
|
|
552
|
-
session.commit
|
|
1
|
+
"""
|
|
2
|
+
Various functions used by Validation sub-application
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
|
|
7
|
+
import dash_bootstrap_components as dbc
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from dash import html
|
|
10
|
+
|
|
11
|
+
from ositah.apps.validation.parameters import (
|
|
12
|
+
VALIDATION_DECLARATIONS_SELECT_ALL,
|
|
13
|
+
VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED,
|
|
14
|
+
VALIDATION_DECLARATIONS_SELECT_VALIDATED,
|
|
15
|
+
VALIDATION_DECLARATIONS_SWITCH_ID,
|
|
16
|
+
)
|
|
17
|
+
from ositah.utils.exceptions import SessionDataMissing
|
|
18
|
+
from ositah.utils.hito_db import get_db
|
|
19
|
+
from ositah.utils.period import get_validation_period_data
|
|
20
|
+
from ositah.utils.projects import (
|
|
21
|
+
CATEGORY_DEFAULT,
|
|
22
|
+
DATA_SOURCE_HITO,
|
|
23
|
+
get_team_projects,
|
|
24
|
+
project_time,
|
|
25
|
+
time_unit,
|
|
26
|
+
)
|
|
27
|
+
from ositah.utils.utils import (
|
|
28
|
+
SEMESTER_HOURS,
|
|
29
|
+
TIME_UNIT_HOURS,
|
|
30
|
+
TIME_UNIT_HOURS_EN,
|
|
31
|
+
TIME_UNIT_WEEKS,
|
|
32
|
+
WEEK_HOURS,
|
|
33
|
+
GlobalParams,
|
|
34
|
+
no_session_id_jumbotron,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def activity_time_cell(row, column, row_index):
|
|
39
|
+
"""
|
|
40
|
+
Build the cell content for an activity time, adding the appropriate class and in case of
|
|
41
|
+
inconsistencies between the Hito declared time and the validated time, add a tooltip
|
|
42
|
+
to display both values.
|
|
43
|
+
|
|
44
|
+
:param row: declaration row
|
|
45
|
+
:param column: column name for the cell to build
|
|
46
|
+
:param row_index: row index of the current row
|
|
47
|
+
:return: html.Td
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
global_params = GlobalParams()
|
|
51
|
+
|
|
52
|
+
classes = "text-center align-middle"
|
|
53
|
+
cell_value = int(round(row[column]))
|
|
54
|
+
cell_id = f"validation-table-value-{row_index}-{column}"
|
|
55
|
+
|
|
56
|
+
if column == "percent_global":
|
|
57
|
+
thresholds = global_params.declaration_options["thresholds"]["current"]
|
|
58
|
+
percent = round(row["percent_global"], 1)
|
|
59
|
+
if percent <= thresholds["low"]:
|
|
60
|
+
percent_class = "table-danger"
|
|
61
|
+
tooltip_txt = f"Percentage low (<={thresholds['low']}%)"
|
|
62
|
+
elif percent <= thresholds["suspect"]:
|
|
63
|
+
percent_class = "table-warning"
|
|
64
|
+
tooltip_txt = (
|
|
65
|
+
f"Percentage suspect (>{thresholds['low']}% and" f" <={thresholds['suspect']}%)"
|
|
66
|
+
)
|
|
67
|
+
elif percent > thresholds["good"]:
|
|
68
|
+
percent_class = "table-info"
|
|
69
|
+
tooltip_txt = f"Percentage too high (>{thresholds['good']}%)"
|
|
70
|
+
else:
|
|
71
|
+
percent_class = "table-success"
|
|
72
|
+
|
|
73
|
+
contents = [html.Div(percent, id=cell_id)]
|
|
74
|
+
if percent_class != "table-success":
|
|
75
|
+
contents.append(dbc.Tooltip(html.Div(tooltip_txt), target=cell_id))
|
|
76
|
+
|
|
77
|
+
classes += f" {percent_class}"
|
|
78
|
+
|
|
79
|
+
elif (
|
|
80
|
+
(column != "enseignement" or not global_params.teaching_ratio)
|
|
81
|
+
and time_unit(column) == TIME_UNIT_HOURS_EN
|
|
82
|
+
and row[column] > global_params.declaration_options["max_hours"]
|
|
83
|
+
):
|
|
84
|
+
contents = [
|
|
85
|
+
html.Div(cell_value, id=cell_id),
|
|
86
|
+
dbc.Tooltip(
|
|
87
|
+
html.Div(
|
|
88
|
+
(
|
|
89
|
+
f"Déclaration supérieure au maximum"
|
|
90
|
+
f" ({global_params.declaration_options['max_hours']} heures)"
|
|
91
|
+
)
|
|
92
|
+
),
|
|
93
|
+
target=cell_id,
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
classes += " table-danger"
|
|
97
|
+
|
|
98
|
+
elif row.hito_missing:
|
|
99
|
+
validated_column = f"{column}_val"
|
|
100
|
+
contents = [
|
|
101
|
+
html.Div(int(round(row[validated_column])), id=cell_id),
|
|
102
|
+
dbc.Tooltip(
|
|
103
|
+
[
|
|
104
|
+
html.Div(f"Déclaration validée: {round(row[validated_column], 3)}"),
|
|
105
|
+
html.Div("Déclaration Hito correspondante supprimée"),
|
|
106
|
+
],
|
|
107
|
+
target=cell_id,
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
classes += " validated_hito_missing"
|
|
111
|
+
|
|
112
|
+
elif row.validated and not row[f"{column}_time_ok"]:
|
|
113
|
+
validated_column = f"{column}_val"
|
|
114
|
+
contents = [
|
|
115
|
+
html.Div(cell_value, id=cell_id),
|
|
116
|
+
dbc.Tooltip(
|
|
117
|
+
[
|
|
118
|
+
html.Div(f"Dernière déclaration: {round(row[column], 3)}"),
|
|
119
|
+
html.Div(f"Déclaration validée: {round(row[validated_column], 3)}"),
|
|
120
|
+
],
|
|
121
|
+
target=cell_id,
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
classes += " table-warning"
|
|
125
|
+
|
|
126
|
+
else:
|
|
127
|
+
contents = [html.Div(cell_value, id=cell_id)]
|
|
128
|
+
if row.suspect:
|
|
129
|
+
contents.append(
|
|
130
|
+
dbc.Tooltip(
|
|
131
|
+
[
|
|
132
|
+
html.Div("Déclaration suspecte: vérifier les quotités déclarées"),
|
|
133
|
+
],
|
|
134
|
+
target=cell_id,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
classes += " table-warning"
|
|
138
|
+
|
|
139
|
+
return html.Td(contents, className=classes, key=f"validation-table-cell-{row}-{column}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def agent_tooltip_txt(agent_data, data_columns):
|
|
143
|
+
"""
|
|
144
|
+
Build the tooltip text associated with an agent.
|
|
145
|
+
|
|
146
|
+
:param agent_data: the dataframe row corresponding to the agent
|
|
147
|
+
:param data_columns: the list of columns to use to compute the total time
|
|
148
|
+
:return: list of html elements
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
global_params = GlobalParams()
|
|
152
|
+
|
|
153
|
+
if agent_data["hito_missing"]:
|
|
154
|
+
tooltip_detail = "Déclarations validées supprimées de Hito"
|
|
155
|
+
else:
|
|
156
|
+
tooltip_detail = f"Total (semaines) : {total_time(agent_data, data_columns)}"
|
|
157
|
+
tooltip_txt = [
|
|
158
|
+
html.Div(
|
|
159
|
+
f"{global_params.column_titles['team']}: {agent_data[global_params.columns['team']]}"
|
|
160
|
+
),
|
|
161
|
+
html.Div(tooltip_detail),
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
return tooltip_txt
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def add_validation_declaration_selection_switch(current_set):
|
|
168
|
+
"""
|
|
169
|
+
Add a dbc.RadioItems to select whether to show all declaration or only a subset.
|
|
170
|
+
|
|
171
|
+
:param current_set: currently selected declaration set
|
|
172
|
+
:return: dbc.RadioItems
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
return dbc.Row(
|
|
176
|
+
[
|
|
177
|
+
dbc.RadioItems(
|
|
178
|
+
options=[
|
|
179
|
+
{
|
|
180
|
+
"label": "Toutes les déclarations",
|
|
181
|
+
"value": VALIDATION_DECLARATIONS_SELECT_ALL,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"label": "Déclarations non validées uniquement",
|
|
185
|
+
"value": VALIDATION_DECLARATIONS_SELECT_NOT_VALIDATED,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"label": "Déclarations validées uniquement",
|
|
189
|
+
"value": VALIDATION_DECLARATIONS_SELECT_VALIDATED,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
value=current_set,
|
|
193
|
+
id=VALIDATION_DECLARATIONS_SWITCH_ID,
|
|
194
|
+
inline=True,
|
|
195
|
+
),
|
|
196
|
+
],
|
|
197
|
+
justify="center",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def agent_list(dataframe: pd.DataFrame) -> pd.DataFrame:
|
|
202
|
+
global_params = GlobalParams()
|
|
203
|
+
fullname_df = pd.DataFrame()
|
|
204
|
+
fullname_df[global_params.columns["fullname"]] = dataframe[
|
|
205
|
+
global_params.columns["fullname"]
|
|
206
|
+
].drop_duplicates()
|
|
207
|
+
return fullname_df
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def total_time(row, categories, rounded=True) -> int:
|
|
211
|
+
"""
|
|
212
|
+
Compute total time declared by an agent in weeks
|
|
213
|
+
|
|
214
|
+
:param row: dataframe row for an agent
|
|
215
|
+
:param categories: list of categories to sum up
|
|
216
|
+
:param rounded: if true, returns the rounded value
|
|
217
|
+
:return: number of weeks declared
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
global_params = GlobalParams()
|
|
221
|
+
|
|
222
|
+
weeks_number = 0
|
|
223
|
+
hours_number = 0
|
|
224
|
+
for category in categories:
|
|
225
|
+
if global_params.time_unit[category] == TIME_UNIT_WEEKS:
|
|
226
|
+
weeks_number += row[category]
|
|
227
|
+
elif global_params.time_unit[category] == TIME_UNIT_HOURS:
|
|
228
|
+
hours_number += row[category]
|
|
229
|
+
else:
|
|
230
|
+
raise Exception(
|
|
231
|
+
(
|
|
232
|
+
f"Unsupported time unit '{global_params.time_unit[category]}'"
|
|
233
|
+
f" for category {category}"
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
weeks_number += hours_number / WEEK_HOURS
|
|
238
|
+
|
|
239
|
+
if rounded:
|
|
240
|
+
return int(round(weeks_number))
|
|
241
|
+
else:
|
|
242
|
+
return weeks_number
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def category_time(dataframe, category) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Convert the number of hours into the time unit of the category. Keep track of the
|
|
248
|
+
conversions done to prevent doing it twice.
|
|
249
|
+
|
|
250
|
+
:param dataframe: dataframe to update
|
|
251
|
+
:param category: category name
|
|
252
|
+
:return: none, dataframe updated
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
global_params = GlobalParams()
|
|
256
|
+
|
|
257
|
+
# Default time unit is hour
|
|
258
|
+
unit_hour = True
|
|
259
|
+
if category in global_params.time_unit:
|
|
260
|
+
if global_params.time_unit[category] == TIME_UNIT_WEEKS:
|
|
261
|
+
unit_hour = False
|
|
262
|
+
elif global_params.time_unit[category] != TIME_UNIT_HOURS:
|
|
263
|
+
raise Exception(
|
|
264
|
+
(
|
|
265
|
+
f"Unsupported time unit '{global_params.time_unit[category]}'"
|
|
266
|
+
f" for category {category}"
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if not unit_hour:
|
|
271
|
+
dataframe[category] = dataframe[category] / WEEK_HOURS
|
|
272
|
+
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def agent_project_time(agent: str) -> html.Div:
|
|
277
|
+
"""
|
|
278
|
+
Return a HTML Div with the list of projects and the time spent on them
|
|
279
|
+
|
|
280
|
+
:param agent: agent fullname
|
|
281
|
+
:return: html.Div
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
global_params = GlobalParams()
|
|
285
|
+
columns = global_params.columns
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
session_data = global_params.session_data
|
|
289
|
+
df = session_data.project_declarations[
|
|
290
|
+
session_data.project_declarations[global_params.columns["fullname"]] == agent
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
return html.Div(
|
|
294
|
+
[
|
|
295
|
+
html.P(
|
|
296
|
+
(
|
|
297
|
+
f"{df.iloc[i][global_params.columns['activity']]}:"
|
|
298
|
+
f" {' '.join(project_time(df.iloc[i][columns['activity']], df.iloc[i][columns['hours']]))}" # noqa: E501
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
for i in range(len(df))
|
|
302
|
+
]
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
except SessionDataMissing:
|
|
306
|
+
return no_session_id_jumbotron()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def category_declarations(
|
|
310
|
+
project_declarations: pd.DataFrame, use_cache: bool = True
|
|
311
|
+
) -> pd.DataFrame:
|
|
312
|
+
"""
|
|
313
|
+
Process the project declarations (time per project) and convert it into declarations by
|
|
314
|
+
category of projects.
|
|
315
|
+
|
|
316
|
+
:param project_declarations: project declarations to consolidate
|
|
317
|
+
:param use_cache: if True, use and update cache
|
|
318
|
+
:return: dataframe
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
global_params = GlobalParams()
|
|
322
|
+
columns = global_params.columns
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
session_data = global_params.session_data
|
|
326
|
+
except SessionDataMissing:
|
|
327
|
+
return no_session_id_jumbotron()
|
|
328
|
+
|
|
329
|
+
# Check if there is a cached version
|
|
330
|
+
if session_data.category_declarations is not None and use_cache:
|
|
331
|
+
return session_data.category_declarations
|
|
332
|
+
|
|
333
|
+
if project_declarations is not None:
|
|
334
|
+
category_declarations = project_declarations.copy()
|
|
335
|
+
categories = [CATEGORY_DEFAULT]
|
|
336
|
+
categories.extend(global_params.project_categories.keys())
|
|
337
|
+
for category in categories:
|
|
338
|
+
category_declarations[category] = category_declarations.loc[
|
|
339
|
+
category_declarations[columns["category"]] == category, columns["hours"]
|
|
340
|
+
]
|
|
341
|
+
category_declarations.drop(columns=columns["hours"], inplace=True)
|
|
342
|
+
category_declarations.fillna(0, inplace=True)
|
|
343
|
+
category_declarations = category_declarations.infer_objects(copy=False)
|
|
344
|
+
|
|
345
|
+
agg_functions = {c: "sum" for c in categories}
|
|
346
|
+
agg_functions["suspect"] = "any"
|
|
347
|
+
category_declarations_pt = pd.pivot_table(
|
|
348
|
+
category_declarations,
|
|
349
|
+
index=[
|
|
350
|
+
global_params.columns["fullname"],
|
|
351
|
+
global_params.columns["team"],
|
|
352
|
+
global_params.columns["agent_id"],
|
|
353
|
+
],
|
|
354
|
+
values=[*categories, "suspect"],
|
|
355
|
+
aggfunc=agg_functions,
|
|
356
|
+
)
|
|
357
|
+
category_declarations = pd.DataFrame(category_declarations_pt.to_records())
|
|
358
|
+
|
|
359
|
+
category_declarations["total_hours"] = category_declarations[categories].sum(axis=1)
|
|
360
|
+
category_declarations["percent_global"] = (
|
|
361
|
+
category_declarations["total_hours"] / SEMESTER_HOURS * 100
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Convert category time into the appropriate unit
|
|
365
|
+
for column_name in categories:
|
|
366
|
+
category_time(category_declarations, column_name)
|
|
367
|
+
|
|
368
|
+
else:
|
|
369
|
+
raise Exception("Project declarations are not defined")
|
|
370
|
+
|
|
371
|
+
if use_cache:
|
|
372
|
+
session_data.category_declarations = category_declarations
|
|
373
|
+
|
|
374
|
+
return category_declarations
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def define_declaration_thresholds(period_date: str):
|
|
378
|
+
"""
|
|
379
|
+
Define the declaration thresholds (low, suspect, normal) for the current period
|
|
380
|
+
|
|
381
|
+
:param period_date: a date that must be inside the declaration period
|
|
382
|
+
"""
|
|
383
|
+
global_params = GlobalParams()
|
|
384
|
+
|
|
385
|
+
period_datetime = date.fromisoformat(period_date)
|
|
386
|
+
if period_datetime.month >= 7:
|
|
387
|
+
global_params.declaration_options["thresholds"]["current"] = (
|
|
388
|
+
global_params.declaration_options["thresholds"]["s2"]
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
global_params.declaration_options["thresholds"]["current"] = (
|
|
392
|
+
global_params.declaration_options["thresholds"]["s1"]
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def get_validation_data(agent_id, period_date: str, session=None):
|
|
397
|
+
"""
|
|
398
|
+
Return the validation data for an agent or None if there is no entry in the database for this
|
|
399
|
+
agent_id.
|
|
400
|
+
|
|
401
|
+
:param agent_id: agent_id of the agent to check
|
|
402
|
+
:param session: DB session to use (default one if None)
|
|
403
|
+
:param period_date: a date that must be inside the declaration period
|
|
404
|
+
:return: an OSITAHValidation object or None
|
|
405
|
+
"""
|
|
406
|
+
from ositah.utils.hito_db_model import OSITAHValidation
|
|
407
|
+
|
|
408
|
+
db = get_db()
|
|
409
|
+
if session is None:
|
|
410
|
+
session = db.session
|
|
411
|
+
|
|
412
|
+
validation_period = get_validation_period_data(period_date)
|
|
413
|
+
return (
|
|
414
|
+
session.query(OSITAHValidation)
|
|
415
|
+
.filter_by(agent_id=agent_id, period_id=validation_period.id)
|
|
416
|
+
.order_by(OSITAHValidation.timestamp.desc())
|
|
417
|
+
.first()
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def get_validation_status(agent_id, period_date: str, session=None):
|
|
422
|
+
"""
|
|
423
|
+
Return True if the agent entry has been validated, False otherwise (including if there is no
|
|
424
|
+
validation entry for the agent)
|
|
425
|
+
|
|
426
|
+
:param agent_id: agent_id of the agent to check
|
|
427
|
+
:param session: DB session to use
|
|
428
|
+
:param period_date: a date that must be inside the declaration period
|
|
429
|
+
:return: boolean
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
validation_data = get_validation_data(agent_id, period_date, session)
|
|
433
|
+
if validation_data is None:
|
|
434
|
+
return False
|
|
435
|
+
else:
|
|
436
|
+
return validation_data.validated
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def get_all_validation_status(period_date: str):
|
|
440
|
+
"""
|
|
441
|
+
Returns the list of agents whose declaration has been validated as a dataframe. It is intended
|
|
442
|
+
to be used to build the validation table but should not be used when updating the status, as
|
|
443
|
+
it may have been updated by somebody else.
|
|
444
|
+
|
|
445
|
+
:param period_date: a date that must be inside the declaration period
|
|
446
|
+
:return: dataframe
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
from ositah.utils.hito_db_model import OSITAHValidation
|
|
450
|
+
|
|
451
|
+
db = get_db()
|
|
452
|
+
validation_period = get_validation_period_data(period_date)
|
|
453
|
+
# By design, only one entry in the declaration period can be with the status validated,
|
|
454
|
+
# except if the database has been messed up...
|
|
455
|
+
validation_query = OSITAHValidation.query.filter(
|
|
456
|
+
OSITAHValidation.period_id == validation_period.id,
|
|
457
|
+
OSITAHValidation.validated,
|
|
458
|
+
)
|
|
459
|
+
validation_data = pd.read_sql(validation_query.statement, con=db.engine)
|
|
460
|
+
if validation_data is None:
|
|
461
|
+
validation_data = pd.DataFrame()
|
|
462
|
+
else:
|
|
463
|
+
validation_data.set_index("agent_id", inplace=True)
|
|
464
|
+
|
|
465
|
+
return validation_data
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def validation_started(period_date: str):
|
|
469
|
+
"""
|
|
470
|
+
Compare the current date with the validation start date (validation_date) and return True
|
|
471
|
+
if the validation has started, False otherwise.
|
|
472
|
+
|
|
473
|
+
:param period_date: date included in the declaration period
|
|
474
|
+
:return: boolean
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
current_date = datetime.now()
|
|
478
|
+
period_params = get_validation_period_data(period_date)
|
|
479
|
+
if current_date >= period_params.validation_date:
|
|
480
|
+
return True
|
|
481
|
+
else:
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def project_declaration_snapshot(
|
|
486
|
+
agent_id,
|
|
487
|
+
validation_id,
|
|
488
|
+
team,
|
|
489
|
+
team_selection_date,
|
|
490
|
+
period_date,
|
|
491
|
+
db_session=None,
|
|
492
|
+
commit=False,
|
|
493
|
+
):
|
|
494
|
+
"""
|
|
495
|
+
Save into table ositah_validation_project_declaration the validated project declarations
|
|
496
|
+
for an agent.
|
|
497
|
+
|
|
498
|
+
:param agent_id: agent (ID) whose project declarations must be saved
|
|
499
|
+
:param validation_id: validation ID associated with the declarations snapshot
|
|
500
|
+
:param db_session: session for the current transaction. If None, use the default one.
|
|
501
|
+
:param commit: if false, do not commit added rows
|
|
502
|
+
:return: None
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
from ositah.utils.hito_db_model import OSITAHProjectDeclaration
|
|
506
|
+
|
|
507
|
+
global_params = GlobalParams()
|
|
508
|
+
columns = global_params.columns
|
|
509
|
+
try:
|
|
510
|
+
session_data = global_params.session_data
|
|
511
|
+
except SessionDataMissing:
|
|
512
|
+
return no_session_id_jumbotron()
|
|
513
|
+
|
|
514
|
+
db = get_db()
|
|
515
|
+
if db_session:
|
|
516
|
+
session = db_session
|
|
517
|
+
else:
|
|
518
|
+
session = db.session
|
|
519
|
+
|
|
520
|
+
# The cache cannot be used directly as the callback may run on a server where the session
|
|
521
|
+
# cache for the current user doesn't exist yet
|
|
522
|
+
project_declarations = get_team_projects(
|
|
523
|
+
team, team_selection_date, period_date, DATA_SOURCE_HITO
|
|
524
|
+
)
|
|
525
|
+
agent_projects = project_declarations[project_declarations[columns["agent_id"]] == agent_id]
|
|
526
|
+
if agent_projects is None:
|
|
527
|
+
print(f"ERROR: no declaration found for agent ID '{agent_id}' (internal error)")
|
|
528
|
+
else:
|
|
529
|
+
for _, project in agent_projects.iterrows():
|
|
530
|
+
declaration = OSITAHProjectDeclaration(
|
|
531
|
+
projet=project[columns["project"]],
|
|
532
|
+
masterprojet=project[columns["masterproject"]],
|
|
533
|
+
category=project[columns["category"]],
|
|
534
|
+
hours=project[columns["hours"]],
|
|
535
|
+
quotite=project[columns["quotite"]],
|
|
536
|
+
validation_id=validation_id,
|
|
537
|
+
hito_project_id=project[columns["activity_id"]],
|
|
538
|
+
)
|
|
539
|
+
try:
|
|
540
|
+
session.add(declaration)
|
|
541
|
+
except Exception:
|
|
542
|
+
# If the default session is used, let the caller process the exception and
|
|
543
|
+
# eventually do the rollback
|
|
544
|
+
if db_session:
|
|
545
|
+
session.rollback()
|
|
546
|
+
raise
|
|
547
|
+
|
|
548
|
+
# Reset the cache of validated declarations if a modification occured
|
|
549
|
+
session_data.reset_validated_declarations_cache()
|
|
550
|
+
|
|
551
|
+
if commit:
|
|
552
|
+
session.commit
|