ositah 25.6.dev1__py3-none-any.whl → 25.9.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ositah might be problematic. Click here for more details.
- ositah/app.py +17 -17
- ositah/apps/analysis.py +785 -785
- ositah/apps/configuration/callbacks.py +916 -916
- ositah/apps/configuration/main.py +546 -546
- ositah/apps/configuration/parameters.py +74 -74
- ositah/apps/configuration/tools.py +112 -112
- ositah/apps/export.py +1208 -1191
- ositah/apps/validation/callbacks.py +240 -240
- ositah/apps/validation/main.py +89 -89
- ositah/apps/validation/parameters.py +25 -25
- ositah/apps/validation/tables.py +646 -646
- ositah/apps/validation/tools.py +552 -552
- ositah/assets/arrow_down_up.svg +3 -3
- ositah/assets/ositah.css +53 -53
- ositah/assets/sort_ascending.svg +4 -4
- ositah/assets/sort_descending.svg +5 -5
- ositah/assets/sorttable.js +499 -499
- ositah/main.py +449 -449
- ositah/ositah.example.cfg +229 -229
- ositah/static/style.css +53 -53
- ositah/templates/base.html +22 -22
- ositah/templates/bootstrap_login.html +38 -38
- ositah/templates/login_form.html +26 -26
- ositah/utils/agents.py +124 -124
- ositah/utils/authentication.py +287 -287
- ositah/utils/cache.py +19 -19
- ositah/utils/core.py +13 -13
- ositah/utils/exceptions.py +64 -64
- ositah/utils/hito_db.py +51 -51
- ositah/utils/hito_db_model.py +253 -253
- ositah/utils/menus.py +339 -339
- ositah/utils/period.py +139 -139
- ositah/utils/projects.py +1178 -1178
- ositah/utils/teams.py +42 -42
- ositah/utils/utils.py +474 -474
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/METADATA +149 -150
- ositah-25.9.dev1.dist-info/RECORD +46 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/licenses/LICENSE +29 -29
- ositah-25.6.dev1.dist-info/RECORD +0 -46
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/WHEEL +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/entry_points.txt +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev1.dist-info}/top_level.txt +0 -0
ositah/apps/analysis.py
CHANGED
|
@@ -1,785 +1,785 @@
|
|
|
1
|
-
# OSITAH sub-application to analyse data to NSIP
|
|
2
|
-
from typing import Dict
|
|
3
|
-
|
|
4
|
-
import dash
|
|
5
|
-
import dash_bootstrap_components as dbc
|
|
6
|
-
import numpy as np
|
|
7
|
-
import plotly.express as px
|
|
8
|
-
from dash import dcc, html
|
|
9
|
-
from dash.dependencies import Input, Output, State
|
|
10
|
-
from dash.exceptions import PreventUpdate
|
|
11
|
-
|
|
12
|
-
from ositah.app import app
|
|
13
|
-
from ositah.utils.cache import clear_cached_data
|
|
14
|
-
from ositah.utils.menus import (
|
|
15
|
-
DATA_SELECTED_SOURCE_ID,
|
|
16
|
-
DATA_SELECTION_SOURCE_ID,
|
|
17
|
-
TABLE_TYPE_DUMMY_STORE,
|
|
18
|
-
TABLE_TYPE_TABLE,
|
|
19
|
-
TEAM_SELECTED_VALUE_ID,
|
|
20
|
-
TEAM_SELECTION_DATE_ID,
|
|
21
|
-
VALIDATION_PERIOD_SELECTED_ID,
|
|
22
|
-
build_accordion,
|
|
23
|
-
create_progress_bar,
|
|
24
|
-
team_list_dropdown,
|
|
25
|
-
)
|
|
26
|
-
from ositah.utils.period import get_validation_period_dates
|
|
27
|
-
from ositah.utils.projects import (
|
|
28
|
-
DATA_SOURCE_HITO,
|
|
29
|
-
DATA_SOURCE_OSITAH,
|
|
30
|
-
build_projects_data,
|
|
31
|
-
get_team_projects,
|
|
32
|
-
)
|
|
33
|
-
from ositah.utils.utils import WEEK_HOURS, GlobalParams, general_error_jumbotron
|
|
34
|
-
|
|
35
|
-
ANALYSIS_TAB_MENU_ID = "report-tabs"
|
|
36
|
-
TAB_ID_ANALYSIS_GRAPHICS = "graphics-page"
|
|
37
|
-
TAB_ID_ANALYSIS_IJCLAB = "project-report-page"
|
|
38
|
-
|
|
39
|
-
TAB_MENU_ANALYSIS_GRAPHICS = "Graphiques"
|
|
40
|
-
TAB_MENU_ANALYSIS_IJCLAB = "Rapports"
|
|
41
|
-
|
|
42
|
-
TABLE_TEAM_PROJECTS_ID = "analysis-ijclab"
|
|
43
|
-
|
|
44
|
-
ANALYSIS_LOAD_INDICATOR_ID = "analysis-others-data-load-indicator"
|
|
45
|
-
ANALYSIS_SAVED_INDICATOR_ID = "analysis-others-saved-data-load-indicator"
|
|
46
|
-
ANALYSIS_TRIGGER_INTERVAL_ID = "analysis-others-display-callback-interval"
|
|
47
|
-
ANALYSIS_PROGRESS_BAR_MAX_DURATION = 8 # seconds
|
|
48
|
-
ANALYSIS_SAVED_ACTIVE_TAB_ID = "analysis-saved-active-tab"
|
|
49
|
-
|
|
50
|
-
GRAPHICS_DROPDOWN_ID = "graphics-type-selection"
|
|
51
|
-
GRAPHICS_DROPDOWN_MENU = "Types de graphique"
|
|
52
|
-
GRAPHICS_DM_CATEGORY_TIME_ID = "graphics-cateogry-time"
|
|
53
|
-
GRAPHICS_DM_CATEGORY_TIME_MENU = "Catégorie d'activités"
|
|
54
|
-
GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID = "graphics-local-projects-time"
|
|
55
|
-
GRAPHICS_DM_LOCAL_PROJECTS_TIME_MENU = "Projets locaux"
|
|
56
|
-
GRAPHICS_DM_NSIP_PROJECTS_TIME_ID = "graphics-nsip-projects-time"
|
|
57
|
-
GRAPHICS_DM_NSIP_PROJECTS_TIME_MENU = "Projets NSIP"
|
|
58
|
-
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID = "graphics-teaching-activities-time"
|
|
59
|
-
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_MENU = "Enseignement"
|
|
60
|
-
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID = "graphics-consultancy-activities-time"
|
|
61
|
-
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_MENU = "Consultance & Expertise"
|
|
62
|
-
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID = "graphics-support-activities-time"
|
|
63
|
-
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_MENU = "Service & Support"
|
|
64
|
-
GRAPHICS_AREA_DIV_ID = "graphics-area"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def define_exported_column_names() -> Dict[str, str]:
|
|
68
|
-
"""
|
|
69
|
-
Function to build the EXPORT_COLUMN_NAMES dict from colum names defined in global parameters
|
|
70
|
-
|
|
71
|
-
:return: dict
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
global_params = GlobalParams()
|
|
75
|
-
columns = global_params.columns
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
columns["category"]: "Type d'activité",
|
|
79
|
-
columns["fullname"]: "Agent",
|
|
80
|
-
columns["hours"]: "Nombre d'heures",
|
|
81
|
-
columns["masterproject"]: "Masterprojet",
|
|
82
|
-
columns["team"]: "Equipe",
|
|
83
|
-
columns["project"]: "Projet",
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# Maps column names from queries to displayed column names in table/CSV
|
|
88
|
-
EXPORT_COLUMN_NAMES = define_exported_column_names()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def ijclab_team_export_table(team, team_selection_date, period_date: str, source):
|
|
92
|
-
"""
|
|
93
|
-
Build the project list contributed by the selected team and the related time declarations and
|
|
94
|
-
return a table.
|
|
95
|
-
|
|
96
|
-
:param team: selected team
|
|
97
|
-
:param team_selection_date: last time the team selection was changed
|
|
98
|
-
:param period_date: a date that must be inside the declaration period
|
|
99
|
-
:param source: whether to use Hito (non validated) or OSITAH (validated) as a data source
|
|
100
|
-
:return: dbc.Table
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
if team is None:
|
|
104
|
-
return html.Div("")
|
|
105
|
-
|
|
106
|
-
global_params = GlobalParams()
|
|
107
|
-
columns = global_params.columns
|
|
108
|
-
|
|
109
|
-
start_date, end_date = get_validation_period_dates(period_date)
|
|
110
|
-
|
|
111
|
-
projects_data, declaration_list = build_projects_data(
|
|
112
|
-
team, team_selection_date, period_date, source
|
|
113
|
-
)
|
|
114
|
-
if projects_data is None or declaration_list is None:
|
|
115
|
-
if source == DATA_SOURCE_HITO:
|
|
116
|
-
msg = f"L'équipe '{team}' ne contribue à aucun projet"
|
|
117
|
-
else:
|
|
118
|
-
msg = f"Aucune données validées n'existe pour l'équipe '{team}'"
|
|
119
|
-
msg += (
|
|
120
|
-
f" pour la période du {start_date.strftime('%Y-%m-%d')} au"
|
|
121
|
-
f" {end_date.strftime('%Y-%m-%d')}"
|
|
122
|
-
)
|
|
123
|
-
return html.Div([dbc.Alert(msg, color="warning"), add_source_selection_switch(source)])
|
|
124
|
-
|
|
125
|
-
table_columns = [columns["masterproject"], columns["project"], columns["hours"]]
|
|
126
|
-
|
|
127
|
-
table_header = [
|
|
128
|
-
html.Thead(
|
|
129
|
-
html.Tr(
|
|
130
|
-
[
|
|
131
|
-
*[
|
|
132
|
-
html.Th(
|
|
133
|
-
[
|
|
134
|
-
html.I(f"{EXPORT_COLUMN_NAMES[c]} "),
|
|
135
|
-
html.I(className="fas fa-sort mr-3"),
|
|
136
|
-
],
|
|
137
|
-
className="text-center",
|
|
138
|
-
)
|
|
139
|
-
for c in table_columns
|
|
140
|
-
],
|
|
141
|
-
]
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
table_body = [
|
|
147
|
-
html.Tbody(
|
|
148
|
-
[
|
|
149
|
-
html.Tr(
|
|
150
|
-
[
|
|
151
|
-
html.Td(
|
|
152
|
-
projects_data.iloc[i - 1][columns["masterproject"]],
|
|
153
|
-
className="align-middle",
|
|
154
|
-
key=f"analysis-table-cell-{i}-masterproject",
|
|
155
|
-
),
|
|
156
|
-
html.Td(
|
|
157
|
-
projects_data.iloc[i - 1][columns["project"]],
|
|
158
|
-
className="align-middle",
|
|
159
|
-
key=f"analysis-table-cell-{i}-project",
|
|
160
|
-
),
|
|
161
|
-
html.Td(
|
|
162
|
-
build_accordion(
|
|
163
|
-
i,
|
|
164
|
-
projects_data.iloc[i - 1][columns["hours"]],
|
|
165
|
-
project_agents_time(
|
|
166
|
-
declaration_list,
|
|
167
|
-
projects_data.iloc[i - 1][columns["activity"]],
|
|
168
|
-
),
|
|
169
|
-
f"{projects_data.iloc[i-1][columns['weeks']]} semaines",
|
|
170
|
-
),
|
|
171
|
-
className="accordion",
|
|
172
|
-
key=f"analysis-table-cell-{i}-time",
|
|
173
|
-
),
|
|
174
|
-
]
|
|
175
|
-
)
|
|
176
|
-
for i in range(1, len(projects_data) + 1)
|
|
177
|
-
]
|
|
178
|
-
)
|
|
179
|
-
]
|
|
180
|
-
|
|
181
|
-
if source == DATA_SOURCE_OSITAH:
|
|
182
|
-
page_title = f"Contributions par projet validées de '{team}'"
|
|
183
|
-
else:
|
|
184
|
-
page_title = f"Contributions par projet déclarées (non validées) de '{team}'"
|
|
185
|
-
page_title += f" du {start_date.strftime('%Y-%m-%d')} au {end_date.strftime('%Y-%m-%d')}"
|
|
186
|
-
|
|
187
|
-
return html.Div(
|
|
188
|
-
[
|
|
189
|
-
html.Div(
|
|
190
|
-
[
|
|
191
|
-
dbc.Row(
|
|
192
|
-
[
|
|
193
|
-
dbc.Col(dbc.Alert(page_title), width=8),
|
|
194
|
-
dbc.Col(
|
|
195
|
-
[
|
|
196
|
-
dbc.Button("Export CSV", id="ijclab-export-file-button"),
|
|
197
|
-
dcc.Download(id="ijclab-export-file-download"),
|
|
198
|
-
],
|
|
199
|
-
width={"size": 2, "offset": 2},
|
|
200
|
-
),
|
|
201
|
-
]
|
|
202
|
-
),
|
|
203
|
-
add_source_selection_switch(source),
|
|
204
|
-
]
|
|
205
|
-
),
|
|
206
|
-
html.P(""),
|
|
207
|
-
dbc.Table(
|
|
208
|
-
table_header + table_body,
|
|
209
|
-
id={"type": TABLE_TYPE_TABLE, "id": TABLE_TEAM_PROJECTS_ID},
|
|
210
|
-
bordered=True,
|
|
211
|
-
hover=True,
|
|
212
|
-
striped=True,
|
|
213
|
-
class_name="sortable",
|
|
214
|
-
),
|
|
215
|
-
]
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def ijclab_graphics(team, team_selection_date, period_date: str, source):
|
|
220
|
-
"""
|
|
221
|
-
Build various graphics from declarations. This function just creates the basic structure of
|
|
222
|
-
the graphic page and read the data. The actual graphic will be displayed by the callback
|
|
223
|
-
associated with the dropdown menu used to select the graphics type.
|
|
224
|
-
|
|
225
|
-
:param team: selected team
|
|
226
|
-
:param team_selection_date: last time the team selection was changed
|
|
227
|
-
:param period_date: a date that must be inside the declaration period
|
|
228
|
-
:param source: whether to use Hito (non validated) or OSITAH (validated) as a data source
|
|
229
|
-
:return: graphics and associated menus
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
if team is None:
|
|
233
|
-
return html.Div("")
|
|
234
|
-
|
|
235
|
-
start_date, end_date = get_validation_period_dates(period_date)
|
|
236
|
-
|
|
237
|
-
projects_data, declaration_list = build_projects_data(
|
|
238
|
-
team, team_selection_date, period_date, source
|
|
239
|
-
)
|
|
240
|
-
if projects_data is None or declaration_list is None:
|
|
241
|
-
if source == DATA_SOURCE_HITO:
|
|
242
|
-
msg = f"L'équipe '{team}' ne contribue à aucun projet"
|
|
243
|
-
else:
|
|
244
|
-
msg = f"Aucune données validées n'existe pour l'équipe '{team}'"
|
|
245
|
-
msg += (
|
|
246
|
-
f" pour la période du {start_date.strftime('%Y-%m-%d')} au"
|
|
247
|
-
f" {end_date.strftime('%Y-%m-%d')}"
|
|
248
|
-
)
|
|
249
|
-
return html.Div([dbc.Alert(msg, color="warning"), add_source_selection_switch(source)])
|
|
250
|
-
|
|
251
|
-
return html.Div(
|
|
252
|
-
[
|
|
253
|
-
dbc.Row(
|
|
254
|
-
[
|
|
255
|
-
dbc.Col(add_source_selection_switch(source), width=8),
|
|
256
|
-
dbc.Col(graphics_dropdown_menu(), width={"size": 3, "offset": 1}),
|
|
257
|
-
]
|
|
258
|
-
),
|
|
259
|
-
html.Div(id=GRAPHICS_AREA_DIV_ID),
|
|
260
|
-
]
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def add_source_selection_switch(current_source):
|
|
265
|
-
"""
|
|
266
|
-
Add a dbc.RadioItems to select the data source.
|
|
267
|
-
|
|
268
|
-
:param current_source: currently selected source
|
|
269
|
-
:return: dbc.RadioItems
|
|
270
|
-
"""
|
|
271
|
-
|
|
272
|
-
return dbc.Row(
|
|
273
|
-
[
|
|
274
|
-
dbc.RadioItems(
|
|
275
|
-
options=[
|
|
276
|
-
{"label": "Toutes les déclarations", "value": DATA_SOURCE_HITO},
|
|
277
|
-
{
|
|
278
|
-
"label": "Déclarations validées uniquement",
|
|
279
|
-
"value": DATA_SOURCE_OSITAH,
|
|
280
|
-
},
|
|
281
|
-
],
|
|
282
|
-
value=current_source,
|
|
283
|
-
id=DATA_SELECTION_SOURCE_ID,
|
|
284
|
-
inline=True,
|
|
285
|
-
),
|
|
286
|
-
],
|
|
287
|
-
justify="center",
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def project_agents_time(declarations, project):
|
|
292
|
-
"""
|
|
293
|
-
Return a HTML Div with the list of agents who contributed to the project and their
|
|
294
|
-
declared time.
|
|
295
|
-
|
|
296
|
-
:param declarations: dataframe with the contribution of each agent to each project
|
|
297
|
-
:param project: project fullname
|
|
298
|
-
:return:
|
|
299
|
-
"""
|
|
300
|
-
|
|
301
|
-
global_params = GlobalParams()
|
|
302
|
-
columns = global_params.columns
|
|
303
|
-
|
|
304
|
-
project_agents = declarations[declarations[columns["activity"]] == project]
|
|
305
|
-
project_agents.loc[:, columns["hours"]] = np.round(project_agents[columns["hours"]]).astype(
|
|
306
|
-
"int"
|
|
307
|
-
)
|
|
308
|
-
project_agents.loc[:, columns["weeks"]] = np.round(
|
|
309
|
-
project_agents.loc[:, columns["hours"]] / WEEK_HOURS, 1
|
|
310
|
-
)
|
|
311
|
-
if global_params.analysis_params["contributions_sorted_by_name"]:
|
|
312
|
-
sort_by = ["nom", columns["hours"]]
|
|
313
|
-
sort_ascending = True
|
|
314
|
-
else:
|
|
315
|
-
sort_by = [columns["hours"], "nom"]
|
|
316
|
-
sort_ascending = False
|
|
317
|
-
project_agents.sort_values(
|
|
318
|
-
by=sort_by, ascending=sort_ascending, ignore_index=True, inplace=True
|
|
319
|
-
)
|
|
320
|
-
return html.Div(
|
|
321
|
-
[
|
|
322
|
-
html.Div(
|
|
323
|
-
(
|
|
324
|
-
f"{project_agents.iloc[i]['fullname']}:"
|
|
325
|
-
f" {project_agents.iloc[i][columns['hours']]}"
|
|
326
|
-
f" ({project_agents.iloc[i][columns['weeks']]} sem.)"
|
|
327
|
-
)
|
|
328
|
-
)
|
|
329
|
-
for i in range(len(project_agents))
|
|
330
|
-
]
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def graphics_dropdown_menu():
|
|
335
|
-
"""
|
|
336
|
-
Build the dropdown menu to select the graphics type
|
|
337
|
-
|
|
338
|
-
:return: dropdown menu
|
|
339
|
-
"""
|
|
340
|
-
|
|
341
|
-
return dbc.DropdownMenu(
|
|
342
|
-
[
|
|
343
|
-
dbc.DropdownMenuItem(
|
|
344
|
-
GRAPHICS_DM_CATEGORY_TIME_MENU,
|
|
345
|
-
id=GRAPHICS_DM_CATEGORY_TIME_ID,
|
|
346
|
-
n_clicks=0,
|
|
347
|
-
),
|
|
348
|
-
dbc.DropdownMenuItem(divider=True),
|
|
349
|
-
dbc.DropdownMenuItem(
|
|
350
|
-
GRAPHICS_DM_NSIP_PROJECTS_TIME_MENU,
|
|
351
|
-
id=GRAPHICS_DM_NSIP_PROJECTS_TIME_ID,
|
|
352
|
-
n_clicks=0,
|
|
353
|
-
),
|
|
354
|
-
dbc.DropdownMenuItem(
|
|
355
|
-
GRAPHICS_DM_LOCAL_PROJECTS_TIME_MENU,
|
|
356
|
-
id=GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID,
|
|
357
|
-
n_clicks=0,
|
|
358
|
-
),
|
|
359
|
-
dbc.DropdownMenuItem(divider=True),
|
|
360
|
-
dbc.DropdownMenuItem(
|
|
361
|
-
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_MENU,
|
|
362
|
-
id=GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID,
|
|
363
|
-
n_clicks=0,
|
|
364
|
-
),
|
|
365
|
-
dbc.DropdownMenuItem(
|
|
366
|
-
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_MENU,
|
|
367
|
-
id=GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID,
|
|
368
|
-
n_clicks=0,
|
|
369
|
-
),
|
|
370
|
-
dbc.DropdownMenuItem(
|
|
371
|
-
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_MENU,
|
|
372
|
-
id=GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID,
|
|
373
|
-
n_clicks=0,
|
|
374
|
-
),
|
|
375
|
-
],
|
|
376
|
-
id=GRAPHICS_DROPDOWN_ID,
|
|
377
|
-
label=GRAPHICS_DROPDOWN_MENU,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def analysis_submenus():
|
|
382
|
-
"""
|
|
383
|
-
Build the tabs menus of the export subapplication
|
|
384
|
-
|
|
385
|
-
:return: DBC Tabs
|
|
386
|
-
"""
|
|
387
|
-
|
|
388
|
-
return dbc.Tabs(
|
|
389
|
-
[
|
|
390
|
-
dbc.Tab(
|
|
391
|
-
id=TAB_ID_ANALYSIS_IJCLAB,
|
|
392
|
-
tab_id=TAB_ID_ANALYSIS_IJCLAB,
|
|
393
|
-
label=TAB_MENU_ANALYSIS_IJCLAB,
|
|
394
|
-
),
|
|
395
|
-
dbc.Tab(
|
|
396
|
-
id=TAB_ID_ANALYSIS_GRAPHICS,
|
|
397
|
-
tab_id=TAB_ID_ANALYSIS_GRAPHICS,
|
|
398
|
-
label=TAB_MENU_ANALYSIS_GRAPHICS,
|
|
399
|
-
),
|
|
400
|
-
],
|
|
401
|
-
id=ANALYSIS_TAB_MENU_ID,
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def analysis_layout():
|
|
406
|
-
"""
|
|
407
|
-
Build the layout for this application, after reading the data if necessary.
|
|
408
|
-
|
|
409
|
-
:return: application layout
|
|
410
|
-
"""
|
|
411
|
-
|
|
412
|
-
return html.Div(
|
|
413
|
-
[
|
|
414
|
-
html.H1("Analyse des déclarations"),
|
|
415
|
-
team_list_dropdown(),
|
|
416
|
-
# The following dcc.Store is used to ensure that the the ijclab_export input exists
|
|
417
|
-
# before the export page is created
|
|
418
|
-
dcc.Store(id=DATA_SELECTED_SOURCE_ID, data=DATA_SOURCE_HITO),
|
|
419
|
-
html.Div(analysis_submenus(), id="analysis-submenus", style={"marginTop": "3em"}),
|
|
420
|
-
dcc.Store(id=ANALYSIS_LOAD_INDICATOR_ID, data=0),
|
|
421
|
-
dcc.Store(id=ANALYSIS_SAVED_INDICATOR_ID, data=0),
|
|
422
|
-
dcc.Store(id=ANALYSIS_SAVED_ACTIVE_TAB_ID, data=""),
|
|
423
|
-
dcc.Store(
|
|
424
|
-
id={"type": TABLE_TYPE_DUMMY_STORE, "id": TABLE_TEAM_PROJECTS_ID},
|
|
425
|
-
data=0,
|
|
426
|
-
),
|
|
427
|
-
]
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
@app.callback(
|
|
432
|
-
Output(DATA_SELECTED_SOURCE_ID, "data"),
|
|
433
|
-
Input(DATA_SELECTION_SOURCE_ID, "value"),
|
|
434
|
-
State(DATA_SELECTED_SOURCE_ID, "data"),
|
|
435
|
-
prevent_initial_call=True,
|
|
436
|
-
)
|
|
437
|
-
def select_data_source(new_source, previous_source):
|
|
438
|
-
"""
|
|
439
|
-
This callback is used to forward to the export callback the selected source through a
|
|
440
|
-
dcc.Store that exists before the page is created. It also clears the data cache if
|
|
441
|
-
the source has been changed.
|
|
442
|
-
|
|
443
|
-
:param new_source: value to forward to the dcc.Store
|
|
444
|
-
:param previous_source: previous value of the selection
|
|
445
|
-
:return: new_source value
|
|
446
|
-
"""
|
|
447
|
-
|
|
448
|
-
if new_source != previous_source:
|
|
449
|
-
clear_cached_data()
|
|
450
|
-
|
|
451
|
-
return new_source
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
@app.callback(
|
|
455
|
-
[
|
|
456
|
-
Output(TAB_ID_ANALYSIS_IJCLAB, "children"),
|
|
457
|
-
Output(TAB_ID_ANALYSIS_GRAPHICS, "children"),
|
|
458
|
-
Output(ANALYSIS_SAVED_INDICATOR_ID, "data"),
|
|
459
|
-
Output(ANALYSIS_SAVED_ACTIVE_TAB_ID, "data"),
|
|
460
|
-
],
|
|
461
|
-
[
|
|
462
|
-
Input(ANALYSIS_LOAD_INDICATOR_ID, "data"),
|
|
463
|
-
Input(ANALYSIS_TAB_MENU_ID, "active_tab"),
|
|
464
|
-
Input(TEAM_SELECTED_VALUE_ID, "data"),
|
|
465
|
-
Input(DATA_SELECTED_SOURCE_ID, "data"),
|
|
466
|
-
],
|
|
467
|
-
[
|
|
468
|
-
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
469
|
-
State(ANALYSIS_SAVED_INDICATOR_ID, "data"),
|
|
470
|
-
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
471
|
-
State(ANALYSIS_SAVED_ACTIVE_TAB_ID, "data"),
|
|
472
|
-
],
|
|
473
|
-
prevent_initial_call=True,
|
|
474
|
-
)
|
|
475
|
-
def display_analysis_tables(
|
|
476
|
-
load_in_progress,
|
|
477
|
-
active_tab,
|
|
478
|
-
team,
|
|
479
|
-
data_source,
|
|
480
|
-
team_selection_date,
|
|
481
|
-
previous_load_in_progress,
|
|
482
|
-
period_date: str,
|
|
483
|
-
previous_active_tab,
|
|
484
|
-
):
|
|
485
|
-
"""
|
|
486
|
-
Display active tab contents after a team or an active tab change. Exact action depends on the
|
|
487
|
-
value of the load in progress indicator. If it is equal to the previous value, it means this
|
|
488
|
-
is the start of the update process: progress bar is displayed and a dcc.Interval is created
|
|
489
|
-
to schedule again this callback after incrementing the load in progress indicator. This causes
|
|
490
|
-
the callback to be reentered and this time it triggers the real processing for the tab
|
|
491
|
-
resulting in the final update of the active tab contents. An empty content is returned for
|
|
492
|
-
inactive tabs.
|
|
493
|
-
|
|
494
|
-
:param load_in_progress: load in progress indicator
|
|
495
|
-
:param tab: tab name
|
|
496
|
-
:param team: selected team
|
|
497
|
-
:param data_source: Hito (non-validated declarations) or OSITAH (validated declarations)
|
|
498
|
-
:param team_selection_date: last time the team selection was changed
|
|
499
|
-
:param previous_load_in_progress: previous value of the load_in_progress indicator
|
|
500
|
-
:param period_date: a date that must be inside the declaration period
|
|
501
|
-
:param previous_active_tab: previously active tab
|
|
502
|
-
:return: tab content
|
|
503
|
-
"""
|
|
504
|
-
|
|
505
|
-
tab_contents = []
|
|
506
|
-
|
|
507
|
-
# Be sure to fill the return values in the same order as Output are declared
|
|
508
|
-
tab_list = [TAB_ID_ANALYSIS_IJCLAB, TAB_ID_ANALYSIS_GRAPHICS]
|
|
509
|
-
for tab in tab_list:
|
|
510
|
-
if team and len(team) > 0 and tab == active_tab:
|
|
511
|
-
if load_in_progress > previous_load_in_progress and active_tab == previous_active_tab:
|
|
512
|
-
if tab == TAB_ID_ANALYSIS_IJCLAB:
|
|
513
|
-
tab_contents.append(
|
|
514
|
-
ijclab_team_export_table(
|
|
515
|
-
team, team_selection_date, period_date, data_source
|
|
516
|
-
)
|
|
517
|
-
)
|
|
518
|
-
elif tab == TAB_ID_ANALYSIS_GRAPHICS:
|
|
519
|
-
tab_contents.append(
|
|
520
|
-
ijclab_graphics(team, team_selection_date, period_date, data_source)
|
|
521
|
-
)
|
|
522
|
-
else:
|
|
523
|
-
tab_contents.append(
|
|
524
|
-
dbc.Alert("Erreur interne: tab non supporté"), color="warning"
|
|
525
|
-
)
|
|
526
|
-
previous_load_in_progress += 1
|
|
527
|
-
else:
|
|
528
|
-
component = html.Div(
|
|
529
|
-
[
|
|
530
|
-
create_progress_bar(team, duration=ANALYSIS_PROGRESS_BAR_MAX_DURATION),
|
|
531
|
-
dcc.Interval(
|
|
532
|
-
id=ANALYSIS_TRIGGER_INTERVAL_ID,
|
|
533
|
-
n_intervals=0,
|
|
534
|
-
max_intervals=1,
|
|
535
|
-
interval=500,
|
|
536
|
-
),
|
|
537
|
-
]
|
|
538
|
-
)
|
|
539
|
-
tab_contents.append(component)
|
|
540
|
-
else:
|
|
541
|
-
tab_contents.append("")
|
|
542
|
-
|
|
543
|
-
tab_contents.extend([previous_load_in_progress, active_tab])
|
|
544
|
-
|
|
545
|
-
return tab_contents
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
@app.callback(
|
|
549
|
-
Output(ANALYSIS_LOAD_INDICATOR_ID, "data"),
|
|
550
|
-
Input(ANALYSIS_TRIGGER_INTERVAL_ID, "n_intervals"),
|
|
551
|
-
State(ANALYSIS_SAVED_INDICATOR_ID, "data"),
|
|
552
|
-
prevent_initial_call=True,
|
|
553
|
-
)
|
|
554
|
-
def display_tables_trigger(n, previous_load_indicator):
|
|
555
|
-
"""
|
|
556
|
-
Increment (change) of the input of display_tables_trigger callback to get it fired a
|
|
557
|
-
second time after displaying the progress bar. The output component must be updated each
|
|
558
|
-
time the callback is entered to trigger the execution of the other callback, thus the
|
|
559
|
-
choice of incrementing it at each call.
|
|
560
|
-
|
|
561
|
-
:param n: n_interval property of the dcc.Interval (0 or 1)
|
|
562
|
-
:return: 1 increment to previous value
|
|
563
|
-
"""
|
|
564
|
-
|
|
565
|
-
return previous_load_indicator + 1
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
@app.callback(
|
|
569
|
-
Output("ijclab-export-file-download", "data"),
|
|
570
|
-
Input("ijclab-export-file-button", "n_clicks"),
|
|
571
|
-
[
|
|
572
|
-
State(TEAM_SELECTED_VALUE_ID, "data"),
|
|
573
|
-
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
574
|
-
State(DATA_SELECTED_SOURCE_ID, "data"),
|
|
575
|
-
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
576
|
-
],
|
|
577
|
-
prevent_initial_call=True,
|
|
578
|
-
)
|
|
579
|
-
def ijclab_export_to_csv(_, team, team_selection_date, source, period_date):
|
|
580
|
-
"""
|
|
581
|
-
Generate a CSV file for the selected team, using the appropriate data source.
|
|
582
|
-
|
|
583
|
-
:param _: unused, just an input to trigger the callback
|
|
584
|
-
:param team: selected team
|
|
585
|
-
:param team_selection_date: timestamp of the last change in team selection
|
|
586
|
-
:param period_date: a date that must be inside the declaration period
|
|
587
|
-
:return: None
|
|
588
|
-
"""
|
|
589
|
-
|
|
590
|
-
global_params = GlobalParams()
|
|
591
|
-
columns = global_params.columns
|
|
592
|
-
|
|
593
|
-
declaration_list = get_team_projects(team, team_selection_date, period_date, source)
|
|
594
|
-
if declaration_list is None:
|
|
595
|
-
return dbc.Alert(
|
|
596
|
-
f"L'équipe '{team}' ne contribue à aucun projet actuellement",
|
|
597
|
-
color="warning",
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
exported_data = declaration_list[
|
|
601
|
-
[
|
|
602
|
-
columns["masterproject"],
|
|
603
|
-
columns["project"],
|
|
604
|
-
columns["category"],
|
|
605
|
-
columns["fullname"],
|
|
606
|
-
columns["team"],
|
|
607
|
-
columns["hours"],
|
|
608
|
-
]
|
|
609
|
-
]
|
|
610
|
-
exported_data.loc[:, columns["hours"]] = np.round(exported_data[columns["hours"]]).astype("int")
|
|
611
|
-
column_renames = {}
|
|
612
|
-
for c in exported_data.columns.tolist():
|
|
613
|
-
if c in EXPORT_COLUMN_NAMES:
|
|
614
|
-
column_renames[c] = EXPORT_COLUMN_NAMES[c]
|
|
615
|
-
if len(column_renames.keys()) > 0:
|
|
616
|
-
exported_data.rename(columns=column_renames, inplace=True)
|
|
617
|
-
|
|
618
|
-
return dict(
|
|
619
|
-
content=exported_data.to_csv(index=False, sep=";"),
|
|
620
|
-
filename="project_contributions.csv",
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
@app.callback(
|
|
625
|
-
Output(GRAPHICS_AREA_DIV_ID, "children"),
|
|
626
|
-
Output(GRAPHICS_DROPDOWN_ID, "label"),
|
|
627
|
-
Output(GRAPHICS_AREA_DIV_ID, "style"),
|
|
628
|
-
Input(GRAPHICS_DM_CATEGORY_TIME_ID, "n_clicks"),
|
|
629
|
-
Input(GRAPHICS_DM_NSIP_PROJECTS_TIME_ID, "n_clicks"),
|
|
630
|
-
Input(GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID, "n_clicks"),
|
|
631
|
-
Input(GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID, "n_clicks"),
|
|
632
|
-
Input(GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID, "n_clicks"),
|
|
633
|
-
Input(GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID, "n_clicks"),
|
|
634
|
-
State(TEAM_SELECTED_VALUE_ID, "data"),
|
|
635
|
-
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
636
|
-
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
637
|
-
State(DATA_SELECTED_SOURCE_ID, "data"),
|
|
638
|
-
State(GRAPHICS_DROPDOWN_ID, "label"),
|
|
639
|
-
prevent_initial_call=True,
|
|
640
|
-
)
|
|
641
|
-
def display_graphics(
|
|
642
|
-
_1,
|
|
643
|
-
_2,
|
|
644
|
-
_3,
|
|
645
|
-
_4,
|
|
646
|
-
_5,
|
|
647
|
-
_6,
|
|
648
|
-
team,
|
|
649
|
-
team_selection_date,
|
|
650
|
-
period_date,
|
|
651
|
-
data_source,
|
|
652
|
-
dropdown_label,
|
|
653
|
-
):
|
|
654
|
-
"""
|
|
655
|
-
Display the selected graphics type
|
|
656
|
-
|
|
657
|
-
:param _n: n_clicks property for each menu item used as input
|
|
658
|
-
:param team: selected team
|
|
659
|
-
:param team_selection_date: last time the team selection was changed
|
|
660
|
-
:param period_date: a date that must be inside the declaration period
|
|
661
|
-
:param data_source: Hito (non-validated declarations) or OSITAH (validated declarations)
|
|
662
|
-
:param dropdown_label: Dropdown menu label
|
|
663
|
-
:return: dcc.Graph
|
|
664
|
-
"""
|
|
665
|
-
|
|
666
|
-
global_params = GlobalParams()
|
|
667
|
-
columns = global_params.columns
|
|
668
|
-
|
|
669
|
-
ctx = dash.callback_context
|
|
670
|
-
if not ctx.triggered:
|
|
671
|
-
raise PreventUpdate
|
|
672
|
-
else:
|
|
673
|
-
selected_item = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
674
|
-
|
|
675
|
-
projects_data, _ = build_projects_data(team, team_selection_date, period_date, data_source)
|
|
676
|
-
|
|
677
|
-
if selected_item in [
|
|
678
|
-
GRAPHICS_DM_NSIP_PROJECTS_TIME_ID,
|
|
679
|
-
GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID,
|
|
680
|
-
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID,
|
|
681
|
-
]:
|
|
682
|
-
if selected_item == GRAPHICS_DM_NSIP_PROJECTS_TIME_ID:
|
|
683
|
-
activity_data = projects_data.loc[projects_data[columns["category"]] == "nsip_project"]
|
|
684
|
-
fig_title = "Temps par masterprojet et projet NSIP"
|
|
685
|
-
y_column = columns["masterproject"]
|
|
686
|
-
new_dropdown_label = GRAPHICS_DM_NSIP_PROJECTS_TIME_MENU
|
|
687
|
-
elif selected_item == GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID:
|
|
688
|
-
activity_data = projects_data.loc[projects_data[columns["category"]] == "local_project"]
|
|
689
|
-
fig_title = "Temps par projet local"
|
|
690
|
-
y_column = "project_short"
|
|
691
|
-
new_dropdown_label = GRAPHICS_DM_LOCAL_PROJECTS_TIME_MENU
|
|
692
|
-
elif selected_item == GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID:
|
|
693
|
-
activity_data = projects_data.loc[projects_data[columns["category"]] == "service"]
|
|
694
|
-
fig_title = "Activités de Service & Support"
|
|
695
|
-
y_column = "project_short"
|
|
696
|
-
new_dropdown_label = GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_MENU
|
|
697
|
-
else:
|
|
698
|
-
return general_error_jumbotron(
|
|
699
|
-
f"Erreur interne : '{selected_item}' non supporté pour un graphique en barre"
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
bar_num = len(activity_data[columns["project"]].unique())
|
|
703
|
-
fig_height = "calc(100vh - 300px)"
|
|
704
|
-
|
|
705
|
-
if activity_data.empty:
|
|
706
|
-
fig = None
|
|
707
|
-
fig_area_style = None
|
|
708
|
-
else:
|
|
709
|
-
fig = px.bar(
|
|
710
|
-
activity_data,
|
|
711
|
-
x=columns["hours"],
|
|
712
|
-
y=y_column,
|
|
713
|
-
color=columns["project"],
|
|
714
|
-
orientation="h",
|
|
715
|
-
height=200 + (30 * bar_num),
|
|
716
|
-
title=fig_title,
|
|
717
|
-
)
|
|
718
|
-
fig.update_layout(
|
|
719
|
-
showlegend=False,
|
|
720
|
-
yaxis={"categoryorder": "category descending"},
|
|
721
|
-
)
|
|
722
|
-
fig_area_style = {
|
|
723
|
-
"max-height": fig_height,
|
|
724
|
-
"overflow-y": "scroll",
|
|
725
|
-
"position": "relative",
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
elif selected_item in [
|
|
729
|
-
GRAPHICS_DM_CATEGORY_TIME_ID,
|
|
730
|
-
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID,
|
|
731
|
-
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID,
|
|
732
|
-
]:
|
|
733
|
-
if selected_item == GRAPHICS_DM_CATEGORY_TIME_ID:
|
|
734
|
-
activity_data = projects_data
|
|
735
|
-
fig_title = "Temps par catégorie d'activités"
|
|
736
|
-
y_column = columns["category"]
|
|
737
|
-
new_dropdown_label = GRAPHICS_DM_CATEGORY_TIME_MENU
|
|
738
|
-
elif selected_item == GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID:
|
|
739
|
-
activity_data = projects_data.loc[projects_data[columns["category"]] == "enseignement"]
|
|
740
|
-
fig_title = "Activités d'enseignement"
|
|
741
|
-
y_column = columns["project"]
|
|
742
|
-
new_dropdown_label = GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_MENU
|
|
743
|
-
elif selected_item == GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID:
|
|
744
|
-
activity_data = projects_data.loc[projects_data[columns["category"]] == "consultance"]
|
|
745
|
-
fig_title = "Activités de Consultance et Expertise"
|
|
746
|
-
y_column = columns["project"]
|
|
747
|
-
new_dropdown_label = GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_MENU
|
|
748
|
-
else:
|
|
749
|
-
return general_error_jumbotron(
|
|
750
|
-
f"Erreur interne : '{selected_item}' non supporté pour un graphique en barre"
|
|
751
|
-
)
|
|
752
|
-
|
|
753
|
-
fig_area_style = None
|
|
754
|
-
fig_height = 400
|
|
755
|
-
|
|
756
|
-
if activity_data.empty:
|
|
757
|
-
fig = None
|
|
758
|
-
else:
|
|
759
|
-
fig = px.pie(
|
|
760
|
-
activity_data,
|
|
761
|
-
values=columns["hours"],
|
|
762
|
-
names=y_column,
|
|
763
|
-
title=fig_title,
|
|
764
|
-
height=fig_height,
|
|
765
|
-
)
|
|
766
|
-
|
|
767
|
-
else:
|
|
768
|
-
return (
|
|
769
|
-
general_error_jumbotron(f"Graphics type '{selected_item}' not yet implemented"),
|
|
770
|
-
dropdown_label,
|
|
771
|
-
None,
|
|
772
|
-
)
|
|
773
|
-
|
|
774
|
-
if fig is None:
|
|
775
|
-
return (
|
|
776
|
-
dbc.Alert(f"Aucune activité correspondant à {new_dropdown_label}", color="warning"),
|
|
777
|
-
dropdown_label,
|
|
778
|
-
fig_area_style,
|
|
779
|
-
)
|
|
780
|
-
else:
|
|
781
|
-
return (
|
|
782
|
-
dcc.Graph("graphics-figure", figure=fig),
|
|
783
|
-
new_dropdown_label,
|
|
784
|
-
fig_area_style,
|
|
785
|
-
)
|
|
1
|
+
# OSITAH sub-application to analyse data to NSIP
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
import dash
|
|
5
|
+
import dash_bootstrap_components as dbc
|
|
6
|
+
import numpy as np
|
|
7
|
+
import plotly.express as px
|
|
8
|
+
from dash import dcc, html
|
|
9
|
+
from dash.dependencies import Input, Output, State
|
|
10
|
+
from dash.exceptions import PreventUpdate
|
|
11
|
+
|
|
12
|
+
from ositah.app import app
|
|
13
|
+
from ositah.utils.cache import clear_cached_data
|
|
14
|
+
from ositah.utils.menus import (
|
|
15
|
+
DATA_SELECTED_SOURCE_ID,
|
|
16
|
+
DATA_SELECTION_SOURCE_ID,
|
|
17
|
+
TABLE_TYPE_DUMMY_STORE,
|
|
18
|
+
TABLE_TYPE_TABLE,
|
|
19
|
+
TEAM_SELECTED_VALUE_ID,
|
|
20
|
+
TEAM_SELECTION_DATE_ID,
|
|
21
|
+
VALIDATION_PERIOD_SELECTED_ID,
|
|
22
|
+
build_accordion,
|
|
23
|
+
create_progress_bar,
|
|
24
|
+
team_list_dropdown,
|
|
25
|
+
)
|
|
26
|
+
from ositah.utils.period import get_validation_period_dates
|
|
27
|
+
from ositah.utils.projects import (
|
|
28
|
+
DATA_SOURCE_HITO,
|
|
29
|
+
DATA_SOURCE_OSITAH,
|
|
30
|
+
build_projects_data,
|
|
31
|
+
get_team_projects,
|
|
32
|
+
)
|
|
33
|
+
from ositah.utils.utils import WEEK_HOURS, GlobalParams, general_error_jumbotron
|
|
34
|
+
|
|
35
|
+
ANALYSIS_TAB_MENU_ID = "report-tabs"
|
|
36
|
+
TAB_ID_ANALYSIS_GRAPHICS = "graphics-page"
|
|
37
|
+
TAB_ID_ANALYSIS_IJCLAB = "project-report-page"
|
|
38
|
+
|
|
39
|
+
TAB_MENU_ANALYSIS_GRAPHICS = "Graphiques"
|
|
40
|
+
TAB_MENU_ANALYSIS_IJCLAB = "Rapports"
|
|
41
|
+
|
|
42
|
+
TABLE_TEAM_PROJECTS_ID = "analysis-ijclab"
|
|
43
|
+
|
|
44
|
+
ANALYSIS_LOAD_INDICATOR_ID = "analysis-others-data-load-indicator"
|
|
45
|
+
ANALYSIS_SAVED_INDICATOR_ID = "analysis-others-saved-data-load-indicator"
|
|
46
|
+
ANALYSIS_TRIGGER_INTERVAL_ID = "analysis-others-display-callback-interval"
|
|
47
|
+
ANALYSIS_PROGRESS_BAR_MAX_DURATION = 8 # seconds
|
|
48
|
+
ANALYSIS_SAVED_ACTIVE_TAB_ID = "analysis-saved-active-tab"
|
|
49
|
+
|
|
50
|
+
GRAPHICS_DROPDOWN_ID = "graphics-type-selection"
|
|
51
|
+
GRAPHICS_DROPDOWN_MENU = "Types de graphique"
|
|
52
|
+
GRAPHICS_DM_CATEGORY_TIME_ID = "graphics-cateogry-time"
|
|
53
|
+
GRAPHICS_DM_CATEGORY_TIME_MENU = "Catégorie d'activités"
|
|
54
|
+
GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID = "graphics-local-projects-time"
|
|
55
|
+
GRAPHICS_DM_LOCAL_PROJECTS_TIME_MENU = "Projets locaux"
|
|
56
|
+
GRAPHICS_DM_NSIP_PROJECTS_TIME_ID = "graphics-nsip-projects-time"
|
|
57
|
+
GRAPHICS_DM_NSIP_PROJECTS_TIME_MENU = "Projets NSIP"
|
|
58
|
+
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID = "graphics-teaching-activities-time"
|
|
59
|
+
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_MENU = "Enseignement"
|
|
60
|
+
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID = "graphics-consultancy-activities-time"
|
|
61
|
+
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_MENU = "Consultance & Expertise"
|
|
62
|
+
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID = "graphics-support-activities-time"
|
|
63
|
+
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_MENU = "Service & Support"
|
|
64
|
+
GRAPHICS_AREA_DIV_ID = "graphics-area"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def define_exported_column_names() -> Dict[str, str]:
|
|
68
|
+
"""
|
|
69
|
+
Function to build the EXPORT_COLUMN_NAMES dict from colum names defined in global parameters
|
|
70
|
+
|
|
71
|
+
:return: dict
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
global_params = GlobalParams()
|
|
75
|
+
columns = global_params.columns
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
columns["category"]: "Type d'activité",
|
|
79
|
+
columns["fullname"]: "Agent",
|
|
80
|
+
columns["hours"]: "Nombre d'heures",
|
|
81
|
+
columns["masterproject"]: "Masterprojet",
|
|
82
|
+
columns["team"]: "Equipe",
|
|
83
|
+
columns["project"]: "Projet",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Maps column names from queries to displayed column names in table/CSV
|
|
88
|
+
EXPORT_COLUMN_NAMES = define_exported_column_names()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def ijclab_team_export_table(team, team_selection_date, period_date: str, source):
|
|
92
|
+
"""
|
|
93
|
+
Build the project list contributed by the selected team and the related time declarations and
|
|
94
|
+
return a table.
|
|
95
|
+
|
|
96
|
+
:param team: selected team
|
|
97
|
+
:param team_selection_date: last time the team selection was changed
|
|
98
|
+
:param period_date: a date that must be inside the declaration period
|
|
99
|
+
:param source: whether to use Hito (non validated) or OSITAH (validated) as a data source
|
|
100
|
+
:return: dbc.Table
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
if team is None:
|
|
104
|
+
return html.Div("")
|
|
105
|
+
|
|
106
|
+
global_params = GlobalParams()
|
|
107
|
+
columns = global_params.columns
|
|
108
|
+
|
|
109
|
+
start_date, end_date = get_validation_period_dates(period_date)
|
|
110
|
+
|
|
111
|
+
projects_data, declaration_list = build_projects_data(
|
|
112
|
+
team, team_selection_date, period_date, source
|
|
113
|
+
)
|
|
114
|
+
if projects_data is None or declaration_list is None:
|
|
115
|
+
if source == DATA_SOURCE_HITO:
|
|
116
|
+
msg = f"L'équipe '{team}' ne contribue à aucun projet"
|
|
117
|
+
else:
|
|
118
|
+
msg = f"Aucune données validées n'existe pour l'équipe '{team}'"
|
|
119
|
+
msg += (
|
|
120
|
+
f" pour la période du {start_date.strftime('%Y-%m-%d')} au"
|
|
121
|
+
f" {end_date.strftime('%Y-%m-%d')}"
|
|
122
|
+
)
|
|
123
|
+
return html.Div([dbc.Alert(msg, color="warning"), add_source_selection_switch(source)])
|
|
124
|
+
|
|
125
|
+
table_columns = [columns["masterproject"], columns["project"], columns["hours"]]
|
|
126
|
+
|
|
127
|
+
table_header = [
|
|
128
|
+
html.Thead(
|
|
129
|
+
html.Tr(
|
|
130
|
+
[
|
|
131
|
+
*[
|
|
132
|
+
html.Th(
|
|
133
|
+
[
|
|
134
|
+
html.I(f"{EXPORT_COLUMN_NAMES[c]} "),
|
|
135
|
+
html.I(className="fas fa-sort mr-3"),
|
|
136
|
+
],
|
|
137
|
+
className="text-center",
|
|
138
|
+
)
|
|
139
|
+
for c in table_columns
|
|
140
|
+
],
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
table_body = [
|
|
147
|
+
html.Tbody(
|
|
148
|
+
[
|
|
149
|
+
html.Tr(
|
|
150
|
+
[
|
|
151
|
+
html.Td(
|
|
152
|
+
projects_data.iloc[i - 1][columns["masterproject"]],
|
|
153
|
+
className="align-middle",
|
|
154
|
+
key=f"analysis-table-cell-{i}-masterproject",
|
|
155
|
+
),
|
|
156
|
+
html.Td(
|
|
157
|
+
projects_data.iloc[i - 1][columns["project"]],
|
|
158
|
+
className="align-middle",
|
|
159
|
+
key=f"analysis-table-cell-{i}-project",
|
|
160
|
+
),
|
|
161
|
+
html.Td(
|
|
162
|
+
build_accordion(
|
|
163
|
+
i,
|
|
164
|
+
projects_data.iloc[i - 1][columns["hours"]],
|
|
165
|
+
project_agents_time(
|
|
166
|
+
declaration_list,
|
|
167
|
+
projects_data.iloc[i - 1][columns["activity"]],
|
|
168
|
+
),
|
|
169
|
+
f"{projects_data.iloc[i-1][columns['weeks']]} semaines",
|
|
170
|
+
),
|
|
171
|
+
className="accordion",
|
|
172
|
+
key=f"analysis-table-cell-{i}-time",
|
|
173
|
+
),
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
for i in range(1, len(projects_data) + 1)
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if source == DATA_SOURCE_OSITAH:
|
|
182
|
+
page_title = f"Contributions par projet validées de '{team}'"
|
|
183
|
+
else:
|
|
184
|
+
page_title = f"Contributions par projet déclarées (non validées) de '{team}'"
|
|
185
|
+
page_title += f" du {start_date.strftime('%Y-%m-%d')} au {end_date.strftime('%Y-%m-%d')}"
|
|
186
|
+
|
|
187
|
+
return html.Div(
|
|
188
|
+
[
|
|
189
|
+
html.Div(
|
|
190
|
+
[
|
|
191
|
+
dbc.Row(
|
|
192
|
+
[
|
|
193
|
+
dbc.Col(dbc.Alert(page_title), width=8),
|
|
194
|
+
dbc.Col(
|
|
195
|
+
[
|
|
196
|
+
dbc.Button("Export CSV", id="ijclab-export-file-button"),
|
|
197
|
+
dcc.Download(id="ijclab-export-file-download"),
|
|
198
|
+
],
|
|
199
|
+
width={"size": 2, "offset": 2},
|
|
200
|
+
),
|
|
201
|
+
]
|
|
202
|
+
),
|
|
203
|
+
add_source_selection_switch(source),
|
|
204
|
+
]
|
|
205
|
+
),
|
|
206
|
+
html.P(""),
|
|
207
|
+
dbc.Table(
|
|
208
|
+
table_header + table_body,
|
|
209
|
+
id={"type": TABLE_TYPE_TABLE, "id": TABLE_TEAM_PROJECTS_ID},
|
|
210
|
+
bordered=True,
|
|
211
|
+
hover=True,
|
|
212
|
+
striped=True,
|
|
213
|
+
class_name="sortable",
|
|
214
|
+
),
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def ijclab_graphics(team, team_selection_date, period_date: str, source):
|
|
220
|
+
"""
|
|
221
|
+
Build various graphics from declarations. This function just creates the basic structure of
|
|
222
|
+
the graphic page and read the data. The actual graphic will be displayed by the callback
|
|
223
|
+
associated with the dropdown menu used to select the graphics type.
|
|
224
|
+
|
|
225
|
+
:param team: selected team
|
|
226
|
+
:param team_selection_date: last time the team selection was changed
|
|
227
|
+
:param period_date: a date that must be inside the declaration period
|
|
228
|
+
:param source: whether to use Hito (non validated) or OSITAH (validated) as a data source
|
|
229
|
+
:return: graphics and associated menus
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
if team is None:
|
|
233
|
+
return html.Div("")
|
|
234
|
+
|
|
235
|
+
start_date, end_date = get_validation_period_dates(period_date)
|
|
236
|
+
|
|
237
|
+
projects_data, declaration_list = build_projects_data(
|
|
238
|
+
team, team_selection_date, period_date, source
|
|
239
|
+
)
|
|
240
|
+
if projects_data is None or declaration_list is None:
|
|
241
|
+
if source == DATA_SOURCE_HITO:
|
|
242
|
+
msg = f"L'équipe '{team}' ne contribue à aucun projet"
|
|
243
|
+
else:
|
|
244
|
+
msg = f"Aucune données validées n'existe pour l'équipe '{team}'"
|
|
245
|
+
msg += (
|
|
246
|
+
f" pour la période du {start_date.strftime('%Y-%m-%d')} au"
|
|
247
|
+
f" {end_date.strftime('%Y-%m-%d')}"
|
|
248
|
+
)
|
|
249
|
+
return html.Div([dbc.Alert(msg, color="warning"), add_source_selection_switch(source)])
|
|
250
|
+
|
|
251
|
+
return html.Div(
|
|
252
|
+
[
|
|
253
|
+
dbc.Row(
|
|
254
|
+
[
|
|
255
|
+
dbc.Col(add_source_selection_switch(source), width=8),
|
|
256
|
+
dbc.Col(graphics_dropdown_menu(), width={"size": 3, "offset": 1}),
|
|
257
|
+
]
|
|
258
|
+
),
|
|
259
|
+
html.Div(id=GRAPHICS_AREA_DIV_ID),
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def add_source_selection_switch(current_source):
|
|
265
|
+
"""
|
|
266
|
+
Add a dbc.RadioItems to select the data source.
|
|
267
|
+
|
|
268
|
+
:param current_source: currently selected source
|
|
269
|
+
:return: dbc.RadioItems
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
return dbc.Row(
|
|
273
|
+
[
|
|
274
|
+
dbc.RadioItems(
|
|
275
|
+
options=[
|
|
276
|
+
{"label": "Toutes les déclarations", "value": DATA_SOURCE_HITO},
|
|
277
|
+
{
|
|
278
|
+
"label": "Déclarations validées uniquement",
|
|
279
|
+
"value": DATA_SOURCE_OSITAH,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
value=current_source,
|
|
283
|
+
id=DATA_SELECTION_SOURCE_ID,
|
|
284
|
+
inline=True,
|
|
285
|
+
),
|
|
286
|
+
],
|
|
287
|
+
justify="center",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def project_agents_time(declarations, project):
|
|
292
|
+
"""
|
|
293
|
+
Return a HTML Div with the list of agents who contributed to the project and their
|
|
294
|
+
declared time.
|
|
295
|
+
|
|
296
|
+
:param declarations: dataframe with the contribution of each agent to each project
|
|
297
|
+
:param project: project fullname
|
|
298
|
+
:return:
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
global_params = GlobalParams()
|
|
302
|
+
columns = global_params.columns
|
|
303
|
+
|
|
304
|
+
project_agents = declarations[declarations[columns["activity"]] == project]
|
|
305
|
+
project_agents.loc[:, columns["hours"]] = np.round(project_agents[columns["hours"]]).astype(
|
|
306
|
+
"int"
|
|
307
|
+
)
|
|
308
|
+
project_agents.loc[:, columns["weeks"]] = np.round(
|
|
309
|
+
project_agents.loc[:, columns["hours"]] / WEEK_HOURS, 1
|
|
310
|
+
)
|
|
311
|
+
if global_params.analysis_params["contributions_sorted_by_name"]:
|
|
312
|
+
sort_by = ["nom", columns["hours"]]
|
|
313
|
+
sort_ascending = True
|
|
314
|
+
else:
|
|
315
|
+
sort_by = [columns["hours"], "nom"]
|
|
316
|
+
sort_ascending = False
|
|
317
|
+
project_agents.sort_values(
|
|
318
|
+
by=sort_by, ascending=sort_ascending, ignore_index=True, inplace=True
|
|
319
|
+
)
|
|
320
|
+
return html.Div(
|
|
321
|
+
[
|
|
322
|
+
html.Div(
|
|
323
|
+
(
|
|
324
|
+
f"{project_agents.iloc[i]['fullname']}:"
|
|
325
|
+
f" {project_agents.iloc[i][columns['hours']]}"
|
|
326
|
+
f" ({project_agents.iloc[i][columns['weeks']]} sem.)"
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
for i in range(len(project_agents))
|
|
330
|
+
]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def graphics_dropdown_menu():
|
|
335
|
+
"""
|
|
336
|
+
Build the dropdown menu to select the graphics type
|
|
337
|
+
|
|
338
|
+
:return: dropdown menu
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
return dbc.DropdownMenu(
|
|
342
|
+
[
|
|
343
|
+
dbc.DropdownMenuItem(
|
|
344
|
+
GRAPHICS_DM_CATEGORY_TIME_MENU,
|
|
345
|
+
id=GRAPHICS_DM_CATEGORY_TIME_ID,
|
|
346
|
+
n_clicks=0,
|
|
347
|
+
),
|
|
348
|
+
dbc.DropdownMenuItem(divider=True),
|
|
349
|
+
dbc.DropdownMenuItem(
|
|
350
|
+
GRAPHICS_DM_NSIP_PROJECTS_TIME_MENU,
|
|
351
|
+
id=GRAPHICS_DM_NSIP_PROJECTS_TIME_ID,
|
|
352
|
+
n_clicks=0,
|
|
353
|
+
),
|
|
354
|
+
dbc.DropdownMenuItem(
|
|
355
|
+
GRAPHICS_DM_LOCAL_PROJECTS_TIME_MENU,
|
|
356
|
+
id=GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID,
|
|
357
|
+
n_clicks=0,
|
|
358
|
+
),
|
|
359
|
+
dbc.DropdownMenuItem(divider=True),
|
|
360
|
+
dbc.DropdownMenuItem(
|
|
361
|
+
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_MENU,
|
|
362
|
+
id=GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID,
|
|
363
|
+
n_clicks=0,
|
|
364
|
+
),
|
|
365
|
+
dbc.DropdownMenuItem(
|
|
366
|
+
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_MENU,
|
|
367
|
+
id=GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID,
|
|
368
|
+
n_clicks=0,
|
|
369
|
+
),
|
|
370
|
+
dbc.DropdownMenuItem(
|
|
371
|
+
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_MENU,
|
|
372
|
+
id=GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID,
|
|
373
|
+
n_clicks=0,
|
|
374
|
+
),
|
|
375
|
+
],
|
|
376
|
+
id=GRAPHICS_DROPDOWN_ID,
|
|
377
|
+
label=GRAPHICS_DROPDOWN_MENU,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def analysis_submenus():
|
|
382
|
+
"""
|
|
383
|
+
Build the tabs menus of the export subapplication
|
|
384
|
+
|
|
385
|
+
:return: DBC Tabs
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
return dbc.Tabs(
|
|
389
|
+
[
|
|
390
|
+
dbc.Tab(
|
|
391
|
+
id=TAB_ID_ANALYSIS_IJCLAB,
|
|
392
|
+
tab_id=TAB_ID_ANALYSIS_IJCLAB,
|
|
393
|
+
label=TAB_MENU_ANALYSIS_IJCLAB,
|
|
394
|
+
),
|
|
395
|
+
dbc.Tab(
|
|
396
|
+
id=TAB_ID_ANALYSIS_GRAPHICS,
|
|
397
|
+
tab_id=TAB_ID_ANALYSIS_GRAPHICS,
|
|
398
|
+
label=TAB_MENU_ANALYSIS_GRAPHICS,
|
|
399
|
+
),
|
|
400
|
+
],
|
|
401
|
+
id=ANALYSIS_TAB_MENU_ID,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def analysis_layout():
|
|
406
|
+
"""
|
|
407
|
+
Build the layout for this application, after reading the data if necessary.
|
|
408
|
+
|
|
409
|
+
:return: application layout
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
return html.Div(
|
|
413
|
+
[
|
|
414
|
+
html.H1("Analyse des déclarations"),
|
|
415
|
+
team_list_dropdown(),
|
|
416
|
+
# The following dcc.Store is used to ensure that the the ijclab_export input exists
|
|
417
|
+
# before the export page is created
|
|
418
|
+
dcc.Store(id=DATA_SELECTED_SOURCE_ID, data=DATA_SOURCE_HITO),
|
|
419
|
+
html.Div(analysis_submenus(), id="analysis-submenus", style={"marginTop": "3em"}),
|
|
420
|
+
dcc.Store(id=ANALYSIS_LOAD_INDICATOR_ID, data=0),
|
|
421
|
+
dcc.Store(id=ANALYSIS_SAVED_INDICATOR_ID, data=0),
|
|
422
|
+
dcc.Store(id=ANALYSIS_SAVED_ACTIVE_TAB_ID, data=""),
|
|
423
|
+
dcc.Store(
|
|
424
|
+
id={"type": TABLE_TYPE_DUMMY_STORE, "id": TABLE_TEAM_PROJECTS_ID},
|
|
425
|
+
data=0,
|
|
426
|
+
),
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@app.callback(
|
|
432
|
+
Output(DATA_SELECTED_SOURCE_ID, "data"),
|
|
433
|
+
Input(DATA_SELECTION_SOURCE_ID, "value"),
|
|
434
|
+
State(DATA_SELECTED_SOURCE_ID, "data"),
|
|
435
|
+
prevent_initial_call=True,
|
|
436
|
+
)
|
|
437
|
+
def select_data_source(new_source, previous_source):
|
|
438
|
+
"""
|
|
439
|
+
This callback is used to forward to the export callback the selected source through a
|
|
440
|
+
dcc.Store that exists before the page is created. It also clears the data cache if
|
|
441
|
+
the source has been changed.
|
|
442
|
+
|
|
443
|
+
:param new_source: value to forward to the dcc.Store
|
|
444
|
+
:param previous_source: previous value of the selection
|
|
445
|
+
:return: new_source value
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
if new_source != previous_source:
|
|
449
|
+
clear_cached_data()
|
|
450
|
+
|
|
451
|
+
return new_source
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@app.callback(
|
|
455
|
+
[
|
|
456
|
+
Output(TAB_ID_ANALYSIS_IJCLAB, "children"),
|
|
457
|
+
Output(TAB_ID_ANALYSIS_GRAPHICS, "children"),
|
|
458
|
+
Output(ANALYSIS_SAVED_INDICATOR_ID, "data"),
|
|
459
|
+
Output(ANALYSIS_SAVED_ACTIVE_TAB_ID, "data"),
|
|
460
|
+
],
|
|
461
|
+
[
|
|
462
|
+
Input(ANALYSIS_LOAD_INDICATOR_ID, "data"),
|
|
463
|
+
Input(ANALYSIS_TAB_MENU_ID, "active_tab"),
|
|
464
|
+
Input(TEAM_SELECTED_VALUE_ID, "data"),
|
|
465
|
+
Input(DATA_SELECTED_SOURCE_ID, "data"),
|
|
466
|
+
],
|
|
467
|
+
[
|
|
468
|
+
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
469
|
+
State(ANALYSIS_SAVED_INDICATOR_ID, "data"),
|
|
470
|
+
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
471
|
+
State(ANALYSIS_SAVED_ACTIVE_TAB_ID, "data"),
|
|
472
|
+
],
|
|
473
|
+
prevent_initial_call=True,
|
|
474
|
+
)
|
|
475
|
+
def display_analysis_tables(
|
|
476
|
+
load_in_progress,
|
|
477
|
+
active_tab,
|
|
478
|
+
team,
|
|
479
|
+
data_source,
|
|
480
|
+
team_selection_date,
|
|
481
|
+
previous_load_in_progress,
|
|
482
|
+
period_date: str,
|
|
483
|
+
previous_active_tab,
|
|
484
|
+
):
|
|
485
|
+
"""
|
|
486
|
+
Display active tab contents after a team or an active tab change. Exact action depends on the
|
|
487
|
+
value of the load in progress indicator. If it is equal to the previous value, it means this
|
|
488
|
+
is the start of the update process: progress bar is displayed and a dcc.Interval is created
|
|
489
|
+
to schedule again this callback after incrementing the load in progress indicator. This causes
|
|
490
|
+
the callback to be reentered and this time it triggers the real processing for the tab
|
|
491
|
+
resulting in the final update of the active tab contents. An empty content is returned for
|
|
492
|
+
inactive tabs.
|
|
493
|
+
|
|
494
|
+
:param load_in_progress: load in progress indicator
|
|
495
|
+
:param tab: tab name
|
|
496
|
+
:param team: selected team
|
|
497
|
+
:param data_source: Hito (non-validated declarations) or OSITAH (validated declarations)
|
|
498
|
+
:param team_selection_date: last time the team selection was changed
|
|
499
|
+
:param previous_load_in_progress: previous value of the load_in_progress indicator
|
|
500
|
+
:param period_date: a date that must be inside the declaration period
|
|
501
|
+
:param previous_active_tab: previously active tab
|
|
502
|
+
:return: tab content
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
tab_contents = []
|
|
506
|
+
|
|
507
|
+
# Be sure to fill the return values in the same order as Output are declared
|
|
508
|
+
tab_list = [TAB_ID_ANALYSIS_IJCLAB, TAB_ID_ANALYSIS_GRAPHICS]
|
|
509
|
+
for tab in tab_list:
|
|
510
|
+
if team and len(team) > 0 and tab == active_tab:
|
|
511
|
+
if load_in_progress > previous_load_in_progress and active_tab == previous_active_tab:
|
|
512
|
+
if tab == TAB_ID_ANALYSIS_IJCLAB:
|
|
513
|
+
tab_contents.append(
|
|
514
|
+
ijclab_team_export_table(
|
|
515
|
+
team, team_selection_date, period_date, data_source
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
elif tab == TAB_ID_ANALYSIS_GRAPHICS:
|
|
519
|
+
tab_contents.append(
|
|
520
|
+
ijclab_graphics(team, team_selection_date, period_date, data_source)
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
tab_contents.append(
|
|
524
|
+
dbc.Alert("Erreur interne: tab non supporté"), color="warning"
|
|
525
|
+
)
|
|
526
|
+
previous_load_in_progress += 1
|
|
527
|
+
else:
|
|
528
|
+
component = html.Div(
|
|
529
|
+
[
|
|
530
|
+
create_progress_bar(team, duration=ANALYSIS_PROGRESS_BAR_MAX_DURATION),
|
|
531
|
+
dcc.Interval(
|
|
532
|
+
id=ANALYSIS_TRIGGER_INTERVAL_ID,
|
|
533
|
+
n_intervals=0,
|
|
534
|
+
max_intervals=1,
|
|
535
|
+
interval=500,
|
|
536
|
+
),
|
|
537
|
+
]
|
|
538
|
+
)
|
|
539
|
+
tab_contents.append(component)
|
|
540
|
+
else:
|
|
541
|
+
tab_contents.append("")
|
|
542
|
+
|
|
543
|
+
tab_contents.extend([previous_load_in_progress, active_tab])
|
|
544
|
+
|
|
545
|
+
return tab_contents
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@app.callback(
|
|
549
|
+
Output(ANALYSIS_LOAD_INDICATOR_ID, "data"),
|
|
550
|
+
Input(ANALYSIS_TRIGGER_INTERVAL_ID, "n_intervals"),
|
|
551
|
+
State(ANALYSIS_SAVED_INDICATOR_ID, "data"),
|
|
552
|
+
prevent_initial_call=True,
|
|
553
|
+
)
|
|
554
|
+
def display_tables_trigger(n, previous_load_indicator):
|
|
555
|
+
"""
|
|
556
|
+
Increment (change) of the input of display_tables_trigger callback to get it fired a
|
|
557
|
+
second time after displaying the progress bar. The output component must be updated each
|
|
558
|
+
time the callback is entered to trigger the execution of the other callback, thus the
|
|
559
|
+
choice of incrementing it at each call.
|
|
560
|
+
|
|
561
|
+
:param n: n_interval property of the dcc.Interval (0 or 1)
|
|
562
|
+
:return: 1 increment to previous value
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
return previous_load_indicator + 1
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@app.callback(
|
|
569
|
+
Output("ijclab-export-file-download", "data"),
|
|
570
|
+
Input("ijclab-export-file-button", "n_clicks"),
|
|
571
|
+
[
|
|
572
|
+
State(TEAM_SELECTED_VALUE_ID, "data"),
|
|
573
|
+
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
574
|
+
State(DATA_SELECTED_SOURCE_ID, "data"),
|
|
575
|
+
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
576
|
+
],
|
|
577
|
+
prevent_initial_call=True,
|
|
578
|
+
)
|
|
579
|
+
def ijclab_export_to_csv(_, team, team_selection_date, source, period_date):
|
|
580
|
+
"""
|
|
581
|
+
Generate a CSV file for the selected team, using the appropriate data source.
|
|
582
|
+
|
|
583
|
+
:param _: unused, just an input to trigger the callback
|
|
584
|
+
:param team: selected team
|
|
585
|
+
:param team_selection_date: timestamp of the last change in team selection
|
|
586
|
+
:param period_date: a date that must be inside the declaration period
|
|
587
|
+
:return: None
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
global_params = GlobalParams()
|
|
591
|
+
columns = global_params.columns
|
|
592
|
+
|
|
593
|
+
declaration_list = get_team_projects(team, team_selection_date, period_date, source)
|
|
594
|
+
if declaration_list is None:
|
|
595
|
+
return dbc.Alert(
|
|
596
|
+
f"L'équipe '{team}' ne contribue à aucun projet actuellement",
|
|
597
|
+
color="warning",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
exported_data = declaration_list[
|
|
601
|
+
[
|
|
602
|
+
columns["masterproject"],
|
|
603
|
+
columns["project"],
|
|
604
|
+
columns["category"],
|
|
605
|
+
columns["fullname"],
|
|
606
|
+
columns["team"],
|
|
607
|
+
columns["hours"],
|
|
608
|
+
]
|
|
609
|
+
]
|
|
610
|
+
exported_data.loc[:, columns["hours"]] = np.round(exported_data[columns["hours"]]).astype("int")
|
|
611
|
+
column_renames = {}
|
|
612
|
+
for c in exported_data.columns.tolist():
|
|
613
|
+
if c in EXPORT_COLUMN_NAMES:
|
|
614
|
+
column_renames[c] = EXPORT_COLUMN_NAMES[c]
|
|
615
|
+
if len(column_renames.keys()) > 0:
|
|
616
|
+
exported_data.rename(columns=column_renames, inplace=True)
|
|
617
|
+
|
|
618
|
+
return dict(
|
|
619
|
+
content=exported_data.to_csv(index=False, sep=";"),
|
|
620
|
+
filename="project_contributions.csv",
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@app.callback(
|
|
625
|
+
Output(GRAPHICS_AREA_DIV_ID, "children"),
|
|
626
|
+
Output(GRAPHICS_DROPDOWN_ID, "label"),
|
|
627
|
+
Output(GRAPHICS_AREA_DIV_ID, "style"),
|
|
628
|
+
Input(GRAPHICS_DM_CATEGORY_TIME_ID, "n_clicks"),
|
|
629
|
+
Input(GRAPHICS_DM_NSIP_PROJECTS_TIME_ID, "n_clicks"),
|
|
630
|
+
Input(GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID, "n_clicks"),
|
|
631
|
+
Input(GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID, "n_clicks"),
|
|
632
|
+
Input(GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID, "n_clicks"),
|
|
633
|
+
Input(GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID, "n_clicks"),
|
|
634
|
+
State(TEAM_SELECTED_VALUE_ID, "data"),
|
|
635
|
+
State(TEAM_SELECTION_DATE_ID, "data"),
|
|
636
|
+
State(VALIDATION_PERIOD_SELECTED_ID, "data"),
|
|
637
|
+
State(DATA_SELECTED_SOURCE_ID, "data"),
|
|
638
|
+
State(GRAPHICS_DROPDOWN_ID, "label"),
|
|
639
|
+
prevent_initial_call=True,
|
|
640
|
+
)
|
|
641
|
+
def display_graphics(
|
|
642
|
+
_1,
|
|
643
|
+
_2,
|
|
644
|
+
_3,
|
|
645
|
+
_4,
|
|
646
|
+
_5,
|
|
647
|
+
_6,
|
|
648
|
+
team,
|
|
649
|
+
team_selection_date,
|
|
650
|
+
period_date,
|
|
651
|
+
data_source,
|
|
652
|
+
dropdown_label,
|
|
653
|
+
):
|
|
654
|
+
"""
|
|
655
|
+
Display the selected graphics type
|
|
656
|
+
|
|
657
|
+
:param _n: n_clicks property for each menu item used as input
|
|
658
|
+
:param team: selected team
|
|
659
|
+
:param team_selection_date: last time the team selection was changed
|
|
660
|
+
:param period_date: a date that must be inside the declaration period
|
|
661
|
+
:param data_source: Hito (non-validated declarations) or OSITAH (validated declarations)
|
|
662
|
+
:param dropdown_label: Dropdown menu label
|
|
663
|
+
:return: dcc.Graph
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
global_params = GlobalParams()
|
|
667
|
+
columns = global_params.columns
|
|
668
|
+
|
|
669
|
+
ctx = dash.callback_context
|
|
670
|
+
if not ctx.triggered:
|
|
671
|
+
raise PreventUpdate
|
|
672
|
+
else:
|
|
673
|
+
selected_item = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
674
|
+
|
|
675
|
+
projects_data, _ = build_projects_data(team, team_selection_date, period_date, data_source)
|
|
676
|
+
|
|
677
|
+
if selected_item in [
|
|
678
|
+
GRAPHICS_DM_NSIP_PROJECTS_TIME_ID,
|
|
679
|
+
GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID,
|
|
680
|
+
GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID,
|
|
681
|
+
]:
|
|
682
|
+
if selected_item == GRAPHICS_DM_NSIP_PROJECTS_TIME_ID:
|
|
683
|
+
activity_data = projects_data.loc[projects_data[columns["category"]] == "nsip_project"]
|
|
684
|
+
fig_title = "Temps par masterprojet et projet NSIP"
|
|
685
|
+
y_column = columns["masterproject"]
|
|
686
|
+
new_dropdown_label = GRAPHICS_DM_NSIP_PROJECTS_TIME_MENU
|
|
687
|
+
elif selected_item == GRAPHICS_DM_LOCAL_PROJECTS_TIME_ID:
|
|
688
|
+
activity_data = projects_data.loc[projects_data[columns["category"]] == "local_project"]
|
|
689
|
+
fig_title = "Temps par projet local"
|
|
690
|
+
y_column = "project_short"
|
|
691
|
+
new_dropdown_label = GRAPHICS_DM_LOCAL_PROJECTS_TIME_MENU
|
|
692
|
+
elif selected_item == GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_ID:
|
|
693
|
+
activity_data = projects_data.loc[projects_data[columns["category"]] == "service"]
|
|
694
|
+
fig_title = "Activités de Service & Support"
|
|
695
|
+
y_column = "project_short"
|
|
696
|
+
new_dropdown_label = GRAPHICS_DM_SUPPORT_ACTIVITIES_TIME_MENU
|
|
697
|
+
else:
|
|
698
|
+
return general_error_jumbotron(
|
|
699
|
+
f"Erreur interne : '{selected_item}' non supporté pour un graphique en barre"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
bar_num = len(activity_data[columns["project"]].unique())
|
|
703
|
+
fig_height = "calc(100vh - 300px)"
|
|
704
|
+
|
|
705
|
+
if activity_data.empty:
|
|
706
|
+
fig = None
|
|
707
|
+
fig_area_style = None
|
|
708
|
+
else:
|
|
709
|
+
fig = px.bar(
|
|
710
|
+
activity_data,
|
|
711
|
+
x=columns["hours"],
|
|
712
|
+
y=y_column,
|
|
713
|
+
color=columns["project"],
|
|
714
|
+
orientation="h",
|
|
715
|
+
height=200 + (30 * bar_num),
|
|
716
|
+
title=fig_title,
|
|
717
|
+
)
|
|
718
|
+
fig.update_layout(
|
|
719
|
+
showlegend=False,
|
|
720
|
+
yaxis={"categoryorder": "category descending"},
|
|
721
|
+
)
|
|
722
|
+
fig_area_style = {
|
|
723
|
+
"max-height": fig_height,
|
|
724
|
+
"overflow-y": "scroll",
|
|
725
|
+
"position": "relative",
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
elif selected_item in [
|
|
729
|
+
GRAPHICS_DM_CATEGORY_TIME_ID,
|
|
730
|
+
GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID,
|
|
731
|
+
GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID,
|
|
732
|
+
]:
|
|
733
|
+
if selected_item == GRAPHICS_DM_CATEGORY_TIME_ID:
|
|
734
|
+
activity_data = projects_data
|
|
735
|
+
fig_title = "Temps par catégorie d'activités"
|
|
736
|
+
y_column = columns["category"]
|
|
737
|
+
new_dropdown_label = GRAPHICS_DM_CATEGORY_TIME_MENU
|
|
738
|
+
elif selected_item == GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_ID:
|
|
739
|
+
activity_data = projects_data.loc[projects_data[columns["category"]] == "enseignement"]
|
|
740
|
+
fig_title = "Activités d'enseignement"
|
|
741
|
+
y_column = columns["project"]
|
|
742
|
+
new_dropdown_label = GRAPHICS_DM_TEACHING_ACTIVITIES_TIME_MENU
|
|
743
|
+
elif selected_item == GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_ID:
|
|
744
|
+
activity_data = projects_data.loc[projects_data[columns["category"]] == "consultance"]
|
|
745
|
+
fig_title = "Activités de Consultance et Expertise"
|
|
746
|
+
y_column = columns["project"]
|
|
747
|
+
new_dropdown_label = GRAPHICS_DM_CONSULTANCY_ACTIVITIES_TIME_MENU
|
|
748
|
+
else:
|
|
749
|
+
return general_error_jumbotron(
|
|
750
|
+
f"Erreur interne : '{selected_item}' non supporté pour un graphique en barre"
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
fig_area_style = None
|
|
754
|
+
fig_height = 400
|
|
755
|
+
|
|
756
|
+
if activity_data.empty:
|
|
757
|
+
fig = None
|
|
758
|
+
else:
|
|
759
|
+
fig = px.pie(
|
|
760
|
+
activity_data,
|
|
761
|
+
values=columns["hours"],
|
|
762
|
+
names=y_column,
|
|
763
|
+
title=fig_title,
|
|
764
|
+
height=fig_height,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
else:
|
|
768
|
+
return (
|
|
769
|
+
general_error_jumbotron(f"Graphics type '{selected_item}' not yet implemented"),
|
|
770
|
+
dropdown_label,
|
|
771
|
+
None,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
if fig is None:
|
|
775
|
+
return (
|
|
776
|
+
dbc.Alert(f"Aucune activité correspondant à {new_dropdown_label}", color="warning"),
|
|
777
|
+
dropdown_label,
|
|
778
|
+
fig_area_style,
|
|
779
|
+
)
|
|
780
|
+
else:
|
|
781
|
+
return (
|
|
782
|
+
dcc.Graph("graphics-figure", figure=fig),
|
|
783
|
+
new_dropdown_label,
|
|
784
|
+
fig_area_style,
|
|
785
|
+
)
|