ositah 24.4.dev1__py3-none-any.whl → 24.7.dev1__py3-none-any.whl

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