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/utils/utils.py
CHANGED
|
@@ -1,474 +1,474 @@
|
|
|
1
|
-
# Convenience objects for OSITAH application
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timedelta
|
|
4
|
-
from typing import List
|
|
5
|
-
|
|
6
|
-
import dash_bootstrap_components as dbc
|
|
7
|
-
from dash import html
|
|
8
|
-
from flask import session
|
|
9
|
-
from flask_sqlalchemy import SQLAlchemy
|
|
10
|
-
from hito_tools.exceptions import ConfigFileEmpty, ConfigMissingParam
|
|
11
|
-
from hito_tools.nsip import nsip_session_init
|
|
12
|
-
from hito_tools.utils import load_config_file
|
|
13
|
-
|
|
14
|
-
from ositah.app import app
|
|
15
|
-
|
|
16
|
-
from .core import singleton
|
|
17
|
-
from .exceptions import SessionDataMissing
|
|
18
|
-
|
|
19
|
-
CONFIG_DEFAULT_PORT = "8888"
|
|
20
|
-
|
|
21
|
-
# Define the dataframe column name to use for each kind of information
|
|
22
|
-
# The key is the kind of information, the value is the column name and must be lowercase
|
|
23
|
-
COLUMN_NAMES = {
|
|
24
|
-
"agent_id": "id",
|
|
25
|
-
"activity": "project_fullname",
|
|
26
|
-
"activity_id": "projet_id",
|
|
27
|
-
"category": "category",
|
|
28
|
-
"cem": "cem",
|
|
29
|
-
"declarations_number": "declarations_number",
|
|
30
|
-
"email": "email",
|
|
31
|
-
"email_auth": "email_auth",
|
|
32
|
-
"firstname": "prenom",
|
|
33
|
-
"fullname": "fullname",
|
|
34
|
-
"hours": "nbHeures",
|
|
35
|
-
"lastname": "nom",
|
|
36
|
-
"masterproject": "masterprojet",
|
|
37
|
-
"missings_number": "missings_number",
|
|
38
|
-
"percent": "pourcent",
|
|
39
|
-
"project": "projet",
|
|
40
|
-
"quotite": "quotite",
|
|
41
|
-
"statut": "statut",
|
|
42
|
-
"team": "team",
|
|
43
|
-
"team_id": "team_id",
|
|
44
|
-
"weeks": "weeks",
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# Define the column names in the NSIP export and the matching column name in the validated
|
|
48
|
-
# declarations dataframe
|
|
49
|
-
NSIP_COLUMN_NAMES = {
|
|
50
|
-
"email_auth": "reseda_eamil",
|
|
51
|
-
"nsip_project_id": "projet_id",
|
|
52
|
-
"nsip_reference_id": "reference_id",
|
|
53
|
-
"nsip_master": "masterprojet",
|
|
54
|
-
"nsip_project": "projet",
|
|
55
|
-
"time": "time",
|
|
56
|
-
"time_unit": "volume",
|
|
57
|
-
"validation_time": "timestamp",
|
|
58
|
-
"id_declaration": "NSIP declaration ID",
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
TIME_UNIT_HOURS = "h"
|
|
62
|
-
TIME_UNIT_HOURS_EN = "hours"
|
|
63
|
-
TIME_UNIT_HOURS_FR = "heures"
|
|
64
|
-
TIME_UNIT_WEEKS = "w"
|
|
65
|
-
TIME_UNIT_WEEKS_EN = "weeks"
|
|
66
|
-
TIME_UNIT_WEEKS_FR = "semaines"
|
|
67
|
-
TIME_UNIT_DEFAULT = TIME_UNIT_HOURS
|
|
68
|
-
|
|
69
|
-
# Hours per days and per week
|
|
70
|
-
DAY_HOURS = 7.7
|
|
71
|
-
WEEK_HOURS = 5 * DAY_HOURS
|
|
72
|
-
# Semester: assume no week of holidays
|
|
73
|
-
SEMESTER_WEEKS = 26
|
|
74
|
-
SEMESTER_HOURS = WEEK_HOURS * SEMESTER_WEEKS
|
|
75
|
-
|
|
76
|
-
TEAM_LIST_ALL_AGENTS = "Tous les agents"
|
|
77
|
-
|
|
78
|
-
HITO_ROLE_PROJECT_MGR = "ROLE_PROJECT_MANAGER"
|
|
79
|
-
HITO_ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN"
|
|
80
|
-
HITO_ROLE_TEAM_MGR = "ROLE_RESP"
|
|
81
|
-
# Must be in role power reverse order
|
|
82
|
-
AUTHORIZED_ROLES = [HITO_ROLE_SUPER_ADMIN, HITO_ROLE_PROJECT_MGR, HITO_ROLE_TEAM_MGR]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class OSITAHSessionData:
|
|
86
|
-
def __init__(self):
|
|
87
|
-
self._cache_initialisation_date = None
|
|
88
|
-
self._category_declarations = None
|
|
89
|
-
self._project_declarations = None
|
|
90
|
-
self._nsip_declarations = None
|
|
91
|
-
self._projects_data = None
|
|
92
|
-
self._project_declarations_source = None
|
|
93
|
-
self._hito_activities = None
|
|
94
|
-
self._hito_projects = None
|
|
95
|
-
self._agent_list = None
|
|
96
|
-
self._declaration_periods = None
|
|
97
|
-
# Use a list for agent_teams to preserve the order
|
|
98
|
-
self._agent_teams = []
|
|
99
|
-
self._role = None
|
|
100
|
-
|
|
101
|
-
@property
|
|
102
|
-
def agent_teams(self):
|
|
103
|
-
return self._agent_teams
|
|
104
|
-
|
|
105
|
-
def add_teams(self, teams: List[str], sort_list=True) -> None:
|
|
106
|
-
"""
|
|
107
|
-
Add a list of teams to agent_teams, without duplicates. The list is then sorted except
|
|
108
|
-
is sort_list=False.
|
|
109
|
-
|
|
110
|
-
:param teams: a list of team names
|
|
111
|
-
:param sort: if true sort the resulting team list
|
|
112
|
-
:return: None
|
|
113
|
-
"""
|
|
114
|
-
seen = set(self.agent_teams)
|
|
115
|
-
# Ensure that that there is no duplicate in the list
|
|
116
|
-
self._agent_teams.extend([x for x in teams if not (x in seen or seen.add(x))])
|
|
117
|
-
if sort_list:
|
|
118
|
-
self._agent_teams.sort()
|
|
119
|
-
|
|
120
|
-
@property
|
|
121
|
-
def agent_list(self):
|
|
122
|
-
return self._agent_list
|
|
123
|
-
|
|
124
|
-
@agent_list.setter
|
|
125
|
-
def agent_list(self, agent_list):
|
|
126
|
-
self._agent_list = agent_list
|
|
127
|
-
|
|
128
|
-
@property
|
|
129
|
-
def cache_date(self):
|
|
130
|
-
return self._cache_initialisation_date
|
|
131
|
-
|
|
132
|
-
@property
|
|
133
|
-
def category_declarations(self):
|
|
134
|
-
return self._category_declarations
|
|
135
|
-
|
|
136
|
-
@category_declarations.setter
|
|
137
|
-
def category_declarations(self, declarations):
|
|
138
|
-
self._category_declarations = declarations
|
|
139
|
-
|
|
140
|
-
@property
|
|
141
|
-
def declaration_periods(self):
|
|
142
|
-
return self._declaration_periods
|
|
143
|
-
|
|
144
|
-
@declaration_periods.setter
|
|
145
|
-
def declaration_periods(self, periods):
|
|
146
|
-
self._declaration_periods = periods
|
|
147
|
-
|
|
148
|
-
def get_hito_activities(self, project_activity: bool):
|
|
149
|
-
if project_activity:
|
|
150
|
-
return self._hito_projects
|
|
151
|
-
else:
|
|
152
|
-
return self._hito_activities
|
|
153
|
-
|
|
154
|
-
@property
|
|
155
|
-
def nsip_declarations(self):
|
|
156
|
-
return self._nsip_declarations
|
|
157
|
-
|
|
158
|
-
@nsip_declarations.setter
|
|
159
|
-
def nsip_declarations(self, declarations):
|
|
160
|
-
self._nsip_declarations = declarations
|
|
161
|
-
if self._cache_initialisation_date is None:
|
|
162
|
-
# Define only if the cache has not yet been initialized
|
|
163
|
-
self._cache_initialisation_date = datetime.now()
|
|
164
|
-
|
|
165
|
-
@property
|
|
166
|
-
def projects_data(self):
|
|
167
|
-
return self._projects_data
|
|
168
|
-
|
|
169
|
-
@projects_data.setter
|
|
170
|
-
def projects_data(self, projects_data):
|
|
171
|
-
self._projects_data = projects_data
|
|
172
|
-
|
|
173
|
-
@property
|
|
174
|
-
def project_declarations(self):
|
|
175
|
-
return self._project_declarations
|
|
176
|
-
|
|
177
|
-
@property
|
|
178
|
-
def project_declarations_source(self):
|
|
179
|
-
return self._project_declarations_source
|
|
180
|
-
|
|
181
|
-
@property
|
|
182
|
-
def role(self):
|
|
183
|
-
return self._role
|
|
184
|
-
|
|
185
|
-
@role.setter
|
|
186
|
-
def role(self, role):
|
|
187
|
-
self._role = role
|
|
188
|
-
|
|
189
|
-
@property
|
|
190
|
-
def total_declarations_num(self):
|
|
191
|
-
return len(self._project_declarations)
|
|
192
|
-
|
|
193
|
-
def reset_caches(self):
|
|
194
|
-
self._category_declarations = None
|
|
195
|
-
self._project_declarations = None
|
|
196
|
-
self._nsip_declarations = None
|
|
197
|
-
self._projects_data = None
|
|
198
|
-
self._hito_activities = None
|
|
199
|
-
self._hito_projects = None
|
|
200
|
-
self._project_declarations_source = None
|
|
201
|
-
self._cache_initialisation_date = None
|
|
202
|
-
self._agent_list = None
|
|
203
|
-
self._validation_data = None
|
|
204
|
-
self._total_declarations_num = 0
|
|
205
|
-
|
|
206
|
-
def reset_validated_declarations_cache(self):
|
|
207
|
-
self._nsip_declarations = None
|
|
208
|
-
|
|
209
|
-
def set_hito_activities(self, activities, project_activity: bool):
|
|
210
|
-
if project_activity:
|
|
211
|
-
self._hito_projects = activities
|
|
212
|
-
else:
|
|
213
|
-
self._hito_activities = activities
|
|
214
|
-
|
|
215
|
-
def set_project_declarations(self, declarations, source):
|
|
216
|
-
self._project_declarations = declarations
|
|
217
|
-
self._project_declarations_source = source
|
|
218
|
-
if self._cache_initialisation_date is None:
|
|
219
|
-
# Define only if the cache has not yet been initialized
|
|
220
|
-
self._cache_initialisation_date = datetime.now()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
@singleton
|
|
224
|
-
class GlobalParams:
|
|
225
|
-
def __init__(self):
|
|
226
|
-
self.agent_query = None
|
|
227
|
-
self.analysis_params = None
|
|
228
|
-
self.category_patterns = {}
|
|
229
|
-
self.columns = COLUMN_NAMES
|
|
230
|
-
self.column_titles = None
|
|
231
|
-
self.declaration_options = None
|
|
232
|
-
self.project_categories = None
|
|
233
|
-
self.reference_masterprojects = {}
|
|
234
|
-
self.roles = {}
|
|
235
|
-
self.time_unit = None
|
|
236
|
-
self.ldap = None
|
|
237
|
-
self.nsip = None
|
|
238
|
-
self.project_teams = {}
|
|
239
|
-
self.teaching_ratio = None
|
|
240
|
-
self.port = CONFIG_DEFAULT_PORT
|
|
241
|
-
self.validation_params = None
|
|
242
|
-
self._hito_db = None
|
|
243
|
-
self._session_data = {}
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
def hito_db(self):
|
|
247
|
-
if not self._hito_db:
|
|
248
|
-
self._hito_db = SQLAlchemy(app.server)
|
|
249
|
-
return self._hito_db
|
|
250
|
-
|
|
251
|
-
@property
|
|
252
|
-
def session_data(self):
|
|
253
|
-
"""
|
|
254
|
-
Returns the session data for the current session. Must not be called if the session UID
|
|
255
|
-
is not defined or will raise SessionDataMissing exception.
|
|
256
|
-
|
|
257
|
-
:return: session data for the current session
|
|
258
|
-
"""
|
|
259
|
-
|
|
260
|
-
if "uid" in session:
|
|
261
|
-
# If 'uid' is defined, it means the user was successfully authenticated.
|
|
262
|
-
if session["uid"] not in self._session_data:
|
|
263
|
-
# session_data may not exist if a multi-worker configuration is used and the
|
|
264
|
-
# authentication (done when moving from one subapp to another one) has been
|
|
265
|
-
# done on another worker.
|
|
266
|
-
self._session_data[session["uid"]] = OSITAHSessionData()
|
|
267
|
-
return self._session_data[session["uid"]]
|
|
268
|
-
|
|
269
|
-
else:
|
|
270
|
-
raise SessionDataMissing()
|
|
271
|
-
|
|
272
|
-
@session_data.deleter
|
|
273
|
-
def session_data(self):
|
|
274
|
-
if "uid" in session:
|
|
275
|
-
if session["uid"] in self._session_data:
|
|
276
|
-
del self._session_data[session["uid"]]
|
|
277
|
-
else:
|
|
278
|
-
print(
|
|
279
|
-
(
|
|
280
|
-
f"WARNING: attempt to delete non-existing session data for session"
|
|
281
|
-
f" {session['uid']} (user={session['user_id']})"
|
|
282
|
-
)
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
else:
|
|
286
|
-
raise SessionDataMissing()
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def define_config_params(file):
|
|
290
|
-
"""
|
|
291
|
-
Validate configuration and define appropriate defaults. Also define global parameters
|
|
292
|
-
from configuration.
|
|
293
|
-
|
|
294
|
-
:param file: configuration file
|
|
295
|
-
:return: updated configuration hash
|
|
296
|
-
"""
|
|
297
|
-
|
|
298
|
-
global_params = GlobalParams()
|
|
299
|
-
|
|
300
|
-
config = load_config_file(file, required=True)
|
|
301
|
-
if not config:
|
|
302
|
-
raise ConfigFileEmpty(file)
|
|
303
|
-
|
|
304
|
-
if "server" not in config or not config["server"]:
|
|
305
|
-
config["server"] = {}
|
|
306
|
-
if "port" in config["server"]:
|
|
307
|
-
global_params.port = config["server"]["port"]
|
|
308
|
-
if "authentication" in config["server"]:
|
|
309
|
-
if "ldap" in config["server"]["authentication"]:
|
|
310
|
-
global_params.ldap = config["server"]["authentication"]["ldap"]
|
|
311
|
-
for param in ["uri", "base_dn", "bind_dn", "password"]:
|
|
312
|
-
if param not in global_params.ldap:
|
|
313
|
-
raise ConfigMissingParam(f"server/authentication/ldap/{param}", file)
|
|
314
|
-
else:
|
|
315
|
-
raise ConfigMissingParam("server/authentication/ldap", file)
|
|
316
|
-
if "provider_name" not in config["server"]["authentication"]:
|
|
317
|
-
raise ConfigMissingParam("server/authentication/provider_name", file)
|
|
318
|
-
else:
|
|
319
|
-
raise ConfigMissingParam("server/authentication", file)
|
|
320
|
-
|
|
321
|
-
if "hito" not in config:
|
|
322
|
-
raise ConfigMissingParam("hito", file)
|
|
323
|
-
if "db" not in config["hito"]:
|
|
324
|
-
raise ConfigMissingParam("hito/db", file)
|
|
325
|
-
if "type" not in config["hito"]["db"]:
|
|
326
|
-
raise ConfigMissingParam("hito/db/type", file)
|
|
327
|
-
if "location" not in config["hito"]["db"]:
|
|
328
|
-
raise ConfigMissingParam("hito/db/location", file)
|
|
329
|
-
if "agent_query" not in config["hito"]["db"]:
|
|
330
|
-
raise ConfigMissingParam("hito/db/agent_query", file)
|
|
331
|
-
if config["hito"]["db"]["type"] == "sqlite":
|
|
332
|
-
app.server.config["SQLALCHEMY_DATABASE_URI"] = (
|
|
333
|
-
f"sqlite:///{config['hito']['db']['location']}"
|
|
334
|
-
)
|
|
335
|
-
elif config["hito"]["db"]["type"] == "mysql":
|
|
336
|
-
if "user" not in config["hito"]["db"]:
|
|
337
|
-
raise ConfigMissingParam("hito/db/user", file)
|
|
338
|
-
if "password" not in config["hito"]["db"]:
|
|
339
|
-
raise ConfigMissingParam("hito/db/passowrd", file)
|
|
340
|
-
app.server.config["SQLALCHEMY_DATABASE_URI"] = (
|
|
341
|
-
f"mysql+pymysql://{config['hito']['db']['user']}:{config['hito']['db']['password']}"
|
|
342
|
-
f"@{config['hito']['db']['location']}"
|
|
343
|
-
)
|
|
344
|
-
else:
|
|
345
|
-
raise Exception(f"Support for {config['hito']['db']['type']} not yet implemented...")
|
|
346
|
-
if "inactivity_timeout" in config["hito"]["db"]:
|
|
347
|
-
app.server.config["SQLALCHEMY_POOL_RECYCLE"] = config["hito"]["db"]["inactivity_timeout"]
|
|
348
|
-
global_params.agent_query = contextualize_sql_query(
|
|
349
|
-
config["hito"]["db"]["agent_query"], config["hito"]["db"]["type"]
|
|
350
|
-
)
|
|
351
|
-
app.server.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
352
|
-
|
|
353
|
-
if "categories" in config["hito"]:
|
|
354
|
-
global_params.project_categories = config["hito"]["categories"]
|
|
355
|
-
global_params.category_patterns = {
|
|
356
|
-
v: k for k, v in global_params.project_categories.items()
|
|
357
|
-
}
|
|
358
|
-
else:
|
|
359
|
-
raise ConfigMissingParam("hito/categories", file)
|
|
360
|
-
|
|
361
|
-
if "time_unit" in config["hito"]:
|
|
362
|
-
global_params.time_unit = config["hito"]["time_unit"]
|
|
363
|
-
else:
|
|
364
|
-
global_params.time_unit = {}
|
|
365
|
-
for category in global_params.project_categories:
|
|
366
|
-
if category not in global_params.time_unit:
|
|
367
|
-
global_params.time_unit[category] = TIME_UNIT_DEFAULT
|
|
368
|
-
|
|
369
|
-
if "titles" in config["hito"]:
|
|
370
|
-
global_params.column_titles = config["hito"]["titles"]
|
|
371
|
-
else:
|
|
372
|
-
raise ConfigMissingParam("hito/titles", file)
|
|
373
|
-
|
|
374
|
-
if "declaration" in config:
|
|
375
|
-
if "optional_statutes" not in config["declaration"]:
|
|
376
|
-
config["declaration"]["optional_statutes"] = []
|
|
377
|
-
if "optional_teams" not in config["declaration"]:
|
|
378
|
-
config["declaration"]["optional_teams"] = []
|
|
379
|
-
else:
|
|
380
|
-
config["declaration"] = {}
|
|
381
|
-
if "max_hours" not in config["declaration"]:
|
|
382
|
-
# Set a very high value
|
|
383
|
-
config["declaration"]["max_hours"] = 99999
|
|
384
|
-
missing_params = []
|
|
385
|
-
for semester in ["s1", "s2"]:
|
|
386
|
-
for k in ["low", "suspect", "good"]:
|
|
387
|
-
if (
|
|
388
|
-
"thresholds" not in config["declaration"]
|
|
389
|
-
or semester not in config["declaration"]["thresholds"]
|
|
390
|
-
or k not in config["declaration"]["thresholds"][semester]
|
|
391
|
-
):
|
|
392
|
-
missing_params.append(f"declaration/thresholds/{semester}/{k}")
|
|
393
|
-
if len(missing_params) > 0:
|
|
394
|
-
raise ConfigMissingParam(", ".join(missing_params), file)
|
|
395
|
-
# Default declaration period date defaults to current day if not explicitly defined
|
|
396
|
-
if "default_date" not in config["declaration"]:
|
|
397
|
-
if "period_change_delay" in config["declaration"]:
|
|
398
|
-
change_delay = config["declaration"]["period_change_delay"]
|
|
399
|
-
else:
|
|
400
|
-
change_delay = 0
|
|
401
|
-
config["declaration"]["default_date"] = datetime.now() - timedelta(days=change_delay)
|
|
402
|
-
global_params.declaration_options = config["declaration"]
|
|
403
|
-
|
|
404
|
-
if "analysis" not in config:
|
|
405
|
-
config["analysis"] = {}
|
|
406
|
-
if "contributions_sorted_by_name" not in config["analysis"]:
|
|
407
|
-
config["analysis"]["contributions_sorted_by_name"] = True
|
|
408
|
-
global_params.analysis_params = config["analysis"]
|
|
409
|
-
|
|
410
|
-
if "validation" not in config:
|
|
411
|
-
config["validation"] = {}
|
|
412
|
-
if "override_period" not in config["validation"]:
|
|
413
|
-
config["validation"]["override_period"] = []
|
|
414
|
-
global_params.validation_params = config["validation"]
|
|
415
|
-
|
|
416
|
-
if "roles" in config:
|
|
417
|
-
global_params.roles = config["roles"]
|
|
418
|
-
if "read-only" not in global_params.roles:
|
|
419
|
-
global_params.roles["read-only"] = []
|
|
420
|
-
|
|
421
|
-
if "project_teams" in config:
|
|
422
|
-
global_params.project_teams = config["project_teams"]
|
|
423
|
-
|
|
424
|
-
if "nsip" in config:
|
|
425
|
-
global_params.nsip = nsip_session_init(config["nsip"])
|
|
426
|
-
if "reference_masterprojects" in config["nsip"]:
|
|
427
|
-
global_params.reference_masterprojects = config["nsip"]["reference_masterprojects"]
|
|
428
|
-
if "teaching" in config["nsip"]:
|
|
429
|
-
global_params.teaching_ratio = config["nsip"]["teaching"]
|
|
430
|
-
if "ratio" not in global_params.teaching_ratio:
|
|
431
|
-
raise ConfigMissingParam("nsip/teeaching/ratio", file)
|
|
432
|
-
if "masterproject" not in global_params.teaching_ratio:
|
|
433
|
-
global_params.teaching_ratio["masterproject"] = "Enseignement Supérieur"
|
|
434
|
-
if "cem" not in global_params.teaching_ratio:
|
|
435
|
-
global_params.teaching_ratio["cem"] = None
|
|
436
|
-
|
|
437
|
-
return config
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def contextualize_sql_query(query: str, db_type: str) -> str:
|
|
441
|
-
"""
|
|
442
|
-
Function to replace placeholders in the query by the function/statement appropriate for
|
|
443
|
-
the actual DB back-end used.
|
|
444
|
-
|
|
445
|
-
:param query: the query with the placeholders
|
|
446
|
-
:param db_type: the DB back-end type
|
|
447
|
-
:return: the query for the selected back-end
|
|
448
|
-
"""
|
|
449
|
-
|
|
450
|
-
if db_type == "sqlite":
|
|
451
|
-
query = query.replace("$$TODAY$$", 'DATE("now")')
|
|
452
|
-
elif db_type == "mysql":
|
|
453
|
-
query = query.replace("$$TODAY$$", "CURRENT_DATE()")
|
|
454
|
-
# pymysql uses the query string as a formatter string: % characters must be esacped
|
|
455
|
-
query = query.replace("%", "%%")
|
|
456
|
-
else:
|
|
457
|
-
raise Exception(f"Support for {db_type} not yet implemented...")
|
|
458
|
-
|
|
459
|
-
return query
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def no_session_id_jumbotron(session_id=None):
|
|
463
|
-
return html.Div([dbc.Jumbotron(html.P(["Internal error: session ID invalid or undefined"]))])
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
def general_error_jumbotron(error):
|
|
467
|
-
"""
|
|
468
|
-
Print an error message that can be any representable object
|
|
469
|
-
|
|
470
|
-
:param error: error object, e.g. an exception
|
|
471
|
-
:return: a jumbotron
|
|
472
|
-
"""
|
|
473
|
-
|
|
474
|
-
return html.Div([dbc.Jumbotron(html.P([repr(error)]))])
|
|
1
|
+
# Convenience objects for OSITAH application
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import dash_bootstrap_components as dbc
|
|
7
|
+
from dash import html
|
|
8
|
+
from flask import session
|
|
9
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
10
|
+
from hito_tools.exceptions import ConfigFileEmpty, ConfigMissingParam
|
|
11
|
+
from hito_tools.nsip import nsip_session_init
|
|
12
|
+
from hito_tools.utils import load_config_file
|
|
13
|
+
|
|
14
|
+
from ositah.app import app
|
|
15
|
+
|
|
16
|
+
from .core import singleton
|
|
17
|
+
from .exceptions import SessionDataMissing
|
|
18
|
+
|
|
19
|
+
CONFIG_DEFAULT_PORT = "8888"
|
|
20
|
+
|
|
21
|
+
# Define the dataframe column name to use for each kind of information
|
|
22
|
+
# The key is the kind of information, the value is the column name and must be lowercase
|
|
23
|
+
COLUMN_NAMES = {
|
|
24
|
+
"agent_id": "id",
|
|
25
|
+
"activity": "project_fullname",
|
|
26
|
+
"activity_id": "projet_id",
|
|
27
|
+
"category": "category",
|
|
28
|
+
"cem": "cem",
|
|
29
|
+
"declarations_number": "declarations_number",
|
|
30
|
+
"email": "email",
|
|
31
|
+
"email_auth": "email_auth",
|
|
32
|
+
"firstname": "prenom",
|
|
33
|
+
"fullname": "fullname",
|
|
34
|
+
"hours": "nbHeures",
|
|
35
|
+
"lastname": "nom",
|
|
36
|
+
"masterproject": "masterprojet",
|
|
37
|
+
"missings_number": "missings_number",
|
|
38
|
+
"percent": "pourcent",
|
|
39
|
+
"project": "projet",
|
|
40
|
+
"quotite": "quotite",
|
|
41
|
+
"statut": "statut",
|
|
42
|
+
"team": "team",
|
|
43
|
+
"team_id": "team_id",
|
|
44
|
+
"weeks": "weeks",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Define the column names in the NSIP export and the matching column name in the validated
|
|
48
|
+
# declarations dataframe
|
|
49
|
+
NSIP_COLUMN_NAMES = {
|
|
50
|
+
"email_auth": "reseda_eamil",
|
|
51
|
+
"nsip_project_id": "projet_id",
|
|
52
|
+
"nsip_reference_id": "reference_id",
|
|
53
|
+
"nsip_master": "masterprojet",
|
|
54
|
+
"nsip_project": "projet",
|
|
55
|
+
"time": "time",
|
|
56
|
+
"time_unit": "volume",
|
|
57
|
+
"validation_time": "timestamp",
|
|
58
|
+
"id_declaration": "NSIP declaration ID",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
TIME_UNIT_HOURS = "h"
|
|
62
|
+
TIME_UNIT_HOURS_EN = "hours"
|
|
63
|
+
TIME_UNIT_HOURS_FR = "heures"
|
|
64
|
+
TIME_UNIT_WEEKS = "w"
|
|
65
|
+
TIME_UNIT_WEEKS_EN = "weeks"
|
|
66
|
+
TIME_UNIT_WEEKS_FR = "semaines"
|
|
67
|
+
TIME_UNIT_DEFAULT = TIME_UNIT_HOURS
|
|
68
|
+
|
|
69
|
+
# Hours per days and per week
|
|
70
|
+
DAY_HOURS = 7.7
|
|
71
|
+
WEEK_HOURS = 5 * DAY_HOURS
|
|
72
|
+
# Semester: assume no week of holidays
|
|
73
|
+
SEMESTER_WEEKS = 26
|
|
74
|
+
SEMESTER_HOURS = WEEK_HOURS * SEMESTER_WEEKS
|
|
75
|
+
|
|
76
|
+
TEAM_LIST_ALL_AGENTS = "Tous les agents"
|
|
77
|
+
|
|
78
|
+
HITO_ROLE_PROJECT_MGR = "ROLE_PROJECT_MANAGER"
|
|
79
|
+
HITO_ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN"
|
|
80
|
+
HITO_ROLE_TEAM_MGR = "ROLE_RESP"
|
|
81
|
+
# Must be in role power reverse order
|
|
82
|
+
AUTHORIZED_ROLES = [HITO_ROLE_SUPER_ADMIN, HITO_ROLE_PROJECT_MGR, HITO_ROLE_TEAM_MGR]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OSITAHSessionData:
|
|
86
|
+
def __init__(self):
|
|
87
|
+
self._cache_initialisation_date = None
|
|
88
|
+
self._category_declarations = None
|
|
89
|
+
self._project_declarations = None
|
|
90
|
+
self._nsip_declarations = None
|
|
91
|
+
self._projects_data = None
|
|
92
|
+
self._project_declarations_source = None
|
|
93
|
+
self._hito_activities = None
|
|
94
|
+
self._hito_projects = None
|
|
95
|
+
self._agent_list = None
|
|
96
|
+
self._declaration_periods = None
|
|
97
|
+
# Use a list for agent_teams to preserve the order
|
|
98
|
+
self._agent_teams = []
|
|
99
|
+
self._role = None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def agent_teams(self):
|
|
103
|
+
return self._agent_teams
|
|
104
|
+
|
|
105
|
+
def add_teams(self, teams: List[str], sort_list=True) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Add a list of teams to agent_teams, without duplicates. The list is then sorted except
|
|
108
|
+
is sort_list=False.
|
|
109
|
+
|
|
110
|
+
:param teams: a list of team names
|
|
111
|
+
:param sort: if true sort the resulting team list
|
|
112
|
+
:return: None
|
|
113
|
+
"""
|
|
114
|
+
seen = set(self.agent_teams)
|
|
115
|
+
# Ensure that that there is no duplicate in the list
|
|
116
|
+
self._agent_teams.extend([x for x in teams if not (x in seen or seen.add(x))])
|
|
117
|
+
if sort_list:
|
|
118
|
+
self._agent_teams.sort()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def agent_list(self):
|
|
122
|
+
return self._agent_list
|
|
123
|
+
|
|
124
|
+
@agent_list.setter
|
|
125
|
+
def agent_list(self, agent_list):
|
|
126
|
+
self._agent_list = agent_list
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def cache_date(self):
|
|
130
|
+
return self._cache_initialisation_date
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def category_declarations(self):
|
|
134
|
+
return self._category_declarations
|
|
135
|
+
|
|
136
|
+
@category_declarations.setter
|
|
137
|
+
def category_declarations(self, declarations):
|
|
138
|
+
self._category_declarations = declarations
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def declaration_periods(self):
|
|
142
|
+
return self._declaration_periods
|
|
143
|
+
|
|
144
|
+
@declaration_periods.setter
|
|
145
|
+
def declaration_periods(self, periods):
|
|
146
|
+
self._declaration_periods = periods
|
|
147
|
+
|
|
148
|
+
def get_hito_activities(self, project_activity: bool):
|
|
149
|
+
if project_activity:
|
|
150
|
+
return self._hito_projects
|
|
151
|
+
else:
|
|
152
|
+
return self._hito_activities
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def nsip_declarations(self):
|
|
156
|
+
return self._nsip_declarations
|
|
157
|
+
|
|
158
|
+
@nsip_declarations.setter
|
|
159
|
+
def nsip_declarations(self, declarations):
|
|
160
|
+
self._nsip_declarations = declarations
|
|
161
|
+
if self._cache_initialisation_date is None:
|
|
162
|
+
# Define only if the cache has not yet been initialized
|
|
163
|
+
self._cache_initialisation_date = datetime.now()
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def projects_data(self):
|
|
167
|
+
return self._projects_data
|
|
168
|
+
|
|
169
|
+
@projects_data.setter
|
|
170
|
+
def projects_data(self, projects_data):
|
|
171
|
+
self._projects_data = projects_data
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def project_declarations(self):
|
|
175
|
+
return self._project_declarations
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def project_declarations_source(self):
|
|
179
|
+
return self._project_declarations_source
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def role(self):
|
|
183
|
+
return self._role
|
|
184
|
+
|
|
185
|
+
@role.setter
|
|
186
|
+
def role(self, role):
|
|
187
|
+
self._role = role
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def total_declarations_num(self):
|
|
191
|
+
return len(self._project_declarations)
|
|
192
|
+
|
|
193
|
+
def reset_caches(self):
|
|
194
|
+
self._category_declarations = None
|
|
195
|
+
self._project_declarations = None
|
|
196
|
+
self._nsip_declarations = None
|
|
197
|
+
self._projects_data = None
|
|
198
|
+
self._hito_activities = None
|
|
199
|
+
self._hito_projects = None
|
|
200
|
+
self._project_declarations_source = None
|
|
201
|
+
self._cache_initialisation_date = None
|
|
202
|
+
self._agent_list = None
|
|
203
|
+
self._validation_data = None
|
|
204
|
+
self._total_declarations_num = 0
|
|
205
|
+
|
|
206
|
+
def reset_validated_declarations_cache(self):
|
|
207
|
+
self._nsip_declarations = None
|
|
208
|
+
|
|
209
|
+
def set_hito_activities(self, activities, project_activity: bool):
|
|
210
|
+
if project_activity:
|
|
211
|
+
self._hito_projects = activities
|
|
212
|
+
else:
|
|
213
|
+
self._hito_activities = activities
|
|
214
|
+
|
|
215
|
+
def set_project_declarations(self, declarations, source):
|
|
216
|
+
self._project_declarations = declarations
|
|
217
|
+
self._project_declarations_source = source
|
|
218
|
+
if self._cache_initialisation_date is None:
|
|
219
|
+
# Define only if the cache has not yet been initialized
|
|
220
|
+
self._cache_initialisation_date = datetime.now()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@singleton
|
|
224
|
+
class GlobalParams:
|
|
225
|
+
def __init__(self):
|
|
226
|
+
self.agent_query = None
|
|
227
|
+
self.analysis_params = None
|
|
228
|
+
self.category_patterns = {}
|
|
229
|
+
self.columns = COLUMN_NAMES
|
|
230
|
+
self.column_titles = None
|
|
231
|
+
self.declaration_options = None
|
|
232
|
+
self.project_categories = None
|
|
233
|
+
self.reference_masterprojects = {}
|
|
234
|
+
self.roles = {}
|
|
235
|
+
self.time_unit = None
|
|
236
|
+
self.ldap = None
|
|
237
|
+
self.nsip = None
|
|
238
|
+
self.project_teams = {}
|
|
239
|
+
self.teaching_ratio = None
|
|
240
|
+
self.port = CONFIG_DEFAULT_PORT
|
|
241
|
+
self.validation_params = None
|
|
242
|
+
self._hito_db = None
|
|
243
|
+
self._session_data = {}
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def hito_db(self):
|
|
247
|
+
if not self._hito_db:
|
|
248
|
+
self._hito_db = SQLAlchemy(app.server)
|
|
249
|
+
return self._hito_db
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def session_data(self):
|
|
253
|
+
"""
|
|
254
|
+
Returns the session data for the current session. Must not be called if the session UID
|
|
255
|
+
is not defined or will raise SessionDataMissing exception.
|
|
256
|
+
|
|
257
|
+
:return: session data for the current session
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
if "uid" in session:
|
|
261
|
+
# If 'uid' is defined, it means the user was successfully authenticated.
|
|
262
|
+
if session["uid"] not in self._session_data:
|
|
263
|
+
# session_data may not exist if a multi-worker configuration is used and the
|
|
264
|
+
# authentication (done when moving from one subapp to another one) has been
|
|
265
|
+
# done on another worker.
|
|
266
|
+
self._session_data[session["uid"]] = OSITAHSessionData()
|
|
267
|
+
return self._session_data[session["uid"]]
|
|
268
|
+
|
|
269
|
+
else:
|
|
270
|
+
raise SessionDataMissing()
|
|
271
|
+
|
|
272
|
+
@session_data.deleter
|
|
273
|
+
def session_data(self):
|
|
274
|
+
if "uid" in session:
|
|
275
|
+
if session["uid"] in self._session_data:
|
|
276
|
+
del self._session_data[session["uid"]]
|
|
277
|
+
else:
|
|
278
|
+
print(
|
|
279
|
+
(
|
|
280
|
+
f"WARNING: attempt to delete non-existing session data for session"
|
|
281
|
+
f" {session['uid']} (user={session['user_id']})"
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
raise SessionDataMissing()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def define_config_params(file):
|
|
290
|
+
"""
|
|
291
|
+
Validate configuration and define appropriate defaults. Also define global parameters
|
|
292
|
+
from configuration.
|
|
293
|
+
|
|
294
|
+
:param file: configuration file
|
|
295
|
+
:return: updated configuration hash
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
global_params = GlobalParams()
|
|
299
|
+
|
|
300
|
+
config = load_config_file(file, required=True)
|
|
301
|
+
if not config:
|
|
302
|
+
raise ConfigFileEmpty(file)
|
|
303
|
+
|
|
304
|
+
if "server" not in config or not config["server"]:
|
|
305
|
+
config["server"] = {}
|
|
306
|
+
if "port" in config["server"]:
|
|
307
|
+
global_params.port = config["server"]["port"]
|
|
308
|
+
if "authentication" in config["server"]:
|
|
309
|
+
if "ldap" in config["server"]["authentication"]:
|
|
310
|
+
global_params.ldap = config["server"]["authentication"]["ldap"]
|
|
311
|
+
for param in ["uri", "base_dn", "bind_dn", "password"]:
|
|
312
|
+
if param not in global_params.ldap:
|
|
313
|
+
raise ConfigMissingParam(f"server/authentication/ldap/{param}", file)
|
|
314
|
+
else:
|
|
315
|
+
raise ConfigMissingParam("server/authentication/ldap", file)
|
|
316
|
+
if "provider_name" not in config["server"]["authentication"]:
|
|
317
|
+
raise ConfigMissingParam("server/authentication/provider_name", file)
|
|
318
|
+
else:
|
|
319
|
+
raise ConfigMissingParam("server/authentication", file)
|
|
320
|
+
|
|
321
|
+
if "hito" not in config:
|
|
322
|
+
raise ConfigMissingParam("hito", file)
|
|
323
|
+
if "db" not in config["hito"]:
|
|
324
|
+
raise ConfigMissingParam("hito/db", file)
|
|
325
|
+
if "type" not in config["hito"]["db"]:
|
|
326
|
+
raise ConfigMissingParam("hito/db/type", file)
|
|
327
|
+
if "location" not in config["hito"]["db"]:
|
|
328
|
+
raise ConfigMissingParam("hito/db/location", file)
|
|
329
|
+
if "agent_query" not in config["hito"]["db"]:
|
|
330
|
+
raise ConfigMissingParam("hito/db/agent_query", file)
|
|
331
|
+
if config["hito"]["db"]["type"] == "sqlite":
|
|
332
|
+
app.server.config["SQLALCHEMY_DATABASE_URI"] = (
|
|
333
|
+
f"sqlite:///{config['hito']['db']['location']}"
|
|
334
|
+
)
|
|
335
|
+
elif config["hito"]["db"]["type"] == "mysql":
|
|
336
|
+
if "user" not in config["hito"]["db"]:
|
|
337
|
+
raise ConfigMissingParam("hito/db/user", file)
|
|
338
|
+
if "password" not in config["hito"]["db"]:
|
|
339
|
+
raise ConfigMissingParam("hito/db/passowrd", file)
|
|
340
|
+
app.server.config["SQLALCHEMY_DATABASE_URI"] = (
|
|
341
|
+
f"mysql+pymysql://{config['hito']['db']['user']}:{config['hito']['db']['password']}"
|
|
342
|
+
f"@{config['hito']['db']['location']}"
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
raise Exception(f"Support for {config['hito']['db']['type']} not yet implemented...")
|
|
346
|
+
if "inactivity_timeout" in config["hito"]["db"]:
|
|
347
|
+
app.server.config["SQLALCHEMY_POOL_RECYCLE"] = config["hito"]["db"]["inactivity_timeout"]
|
|
348
|
+
global_params.agent_query = contextualize_sql_query(
|
|
349
|
+
config["hito"]["db"]["agent_query"], config["hito"]["db"]["type"]
|
|
350
|
+
)
|
|
351
|
+
app.server.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
352
|
+
|
|
353
|
+
if "categories" in config["hito"]:
|
|
354
|
+
global_params.project_categories = config["hito"]["categories"]
|
|
355
|
+
global_params.category_patterns = {
|
|
356
|
+
v: k for k, v in global_params.project_categories.items()
|
|
357
|
+
}
|
|
358
|
+
else:
|
|
359
|
+
raise ConfigMissingParam("hito/categories", file)
|
|
360
|
+
|
|
361
|
+
if "time_unit" in config["hito"]:
|
|
362
|
+
global_params.time_unit = config["hito"]["time_unit"]
|
|
363
|
+
else:
|
|
364
|
+
global_params.time_unit = {}
|
|
365
|
+
for category in global_params.project_categories:
|
|
366
|
+
if category not in global_params.time_unit:
|
|
367
|
+
global_params.time_unit[category] = TIME_UNIT_DEFAULT
|
|
368
|
+
|
|
369
|
+
if "titles" in config["hito"]:
|
|
370
|
+
global_params.column_titles = config["hito"]["titles"]
|
|
371
|
+
else:
|
|
372
|
+
raise ConfigMissingParam("hito/titles", file)
|
|
373
|
+
|
|
374
|
+
if "declaration" in config:
|
|
375
|
+
if "optional_statutes" not in config["declaration"]:
|
|
376
|
+
config["declaration"]["optional_statutes"] = []
|
|
377
|
+
if "optional_teams" not in config["declaration"]:
|
|
378
|
+
config["declaration"]["optional_teams"] = []
|
|
379
|
+
else:
|
|
380
|
+
config["declaration"] = {}
|
|
381
|
+
if "max_hours" not in config["declaration"]:
|
|
382
|
+
# Set a very high value
|
|
383
|
+
config["declaration"]["max_hours"] = 99999
|
|
384
|
+
missing_params = []
|
|
385
|
+
for semester in ["s1", "s2"]:
|
|
386
|
+
for k in ["low", "suspect", "good"]:
|
|
387
|
+
if (
|
|
388
|
+
"thresholds" not in config["declaration"]
|
|
389
|
+
or semester not in config["declaration"]["thresholds"]
|
|
390
|
+
or k not in config["declaration"]["thresholds"][semester]
|
|
391
|
+
):
|
|
392
|
+
missing_params.append(f"declaration/thresholds/{semester}/{k}")
|
|
393
|
+
if len(missing_params) > 0:
|
|
394
|
+
raise ConfigMissingParam(", ".join(missing_params), file)
|
|
395
|
+
# Default declaration period date defaults to current day if not explicitly defined
|
|
396
|
+
if "default_date" not in config["declaration"]:
|
|
397
|
+
if "period_change_delay" in config["declaration"]:
|
|
398
|
+
change_delay = config["declaration"]["period_change_delay"]
|
|
399
|
+
else:
|
|
400
|
+
change_delay = 0
|
|
401
|
+
config["declaration"]["default_date"] = datetime.now() - timedelta(days=change_delay)
|
|
402
|
+
global_params.declaration_options = config["declaration"]
|
|
403
|
+
|
|
404
|
+
if "analysis" not in config:
|
|
405
|
+
config["analysis"] = {}
|
|
406
|
+
if "contributions_sorted_by_name" not in config["analysis"]:
|
|
407
|
+
config["analysis"]["contributions_sorted_by_name"] = True
|
|
408
|
+
global_params.analysis_params = config["analysis"]
|
|
409
|
+
|
|
410
|
+
if "validation" not in config:
|
|
411
|
+
config["validation"] = {}
|
|
412
|
+
if "override_period" not in config["validation"]:
|
|
413
|
+
config["validation"]["override_period"] = []
|
|
414
|
+
global_params.validation_params = config["validation"]
|
|
415
|
+
|
|
416
|
+
if "roles" in config:
|
|
417
|
+
global_params.roles = config["roles"]
|
|
418
|
+
if "read-only" not in global_params.roles:
|
|
419
|
+
global_params.roles["read-only"] = []
|
|
420
|
+
|
|
421
|
+
if "project_teams" in config:
|
|
422
|
+
global_params.project_teams = config["project_teams"]
|
|
423
|
+
|
|
424
|
+
if "nsip" in config:
|
|
425
|
+
global_params.nsip = nsip_session_init(config["nsip"])
|
|
426
|
+
if "reference_masterprojects" in config["nsip"]:
|
|
427
|
+
global_params.reference_masterprojects = config["nsip"]["reference_masterprojects"]
|
|
428
|
+
if "teaching" in config["nsip"]:
|
|
429
|
+
global_params.teaching_ratio = config["nsip"]["teaching"]
|
|
430
|
+
if "ratio" not in global_params.teaching_ratio:
|
|
431
|
+
raise ConfigMissingParam("nsip/teeaching/ratio", file)
|
|
432
|
+
if "masterproject" not in global_params.teaching_ratio:
|
|
433
|
+
global_params.teaching_ratio["masterproject"] = "Enseignement Supérieur"
|
|
434
|
+
if "cem" not in global_params.teaching_ratio:
|
|
435
|
+
global_params.teaching_ratio["cem"] = None
|
|
436
|
+
|
|
437
|
+
return config
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def contextualize_sql_query(query: str, db_type: str) -> str:
|
|
441
|
+
"""
|
|
442
|
+
Function to replace placeholders in the query by the function/statement appropriate for
|
|
443
|
+
the actual DB back-end used.
|
|
444
|
+
|
|
445
|
+
:param query: the query with the placeholders
|
|
446
|
+
:param db_type: the DB back-end type
|
|
447
|
+
:return: the query for the selected back-end
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
if db_type == "sqlite":
|
|
451
|
+
query = query.replace("$$TODAY$$", 'DATE("now")')
|
|
452
|
+
elif db_type == "mysql":
|
|
453
|
+
query = query.replace("$$TODAY$$", "CURRENT_DATE()")
|
|
454
|
+
# pymysql uses the query string as a formatter string: % characters must be esacped
|
|
455
|
+
query = query.replace("%", "%%")
|
|
456
|
+
else:
|
|
457
|
+
raise Exception(f"Support for {db_type} not yet implemented...")
|
|
458
|
+
|
|
459
|
+
return query
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def no_session_id_jumbotron(session_id=None):
|
|
463
|
+
return html.Div([dbc.Jumbotron(html.P(["Internal error: session ID invalid or undefined"]))])
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def general_error_jumbotron(error):
|
|
467
|
+
"""
|
|
468
|
+
Print an error message that can be any representable object
|
|
469
|
+
|
|
470
|
+
:param error: error object, e.g. an exception
|
|
471
|
+
:return: a jumbotron
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
return html.Div([dbc.Jumbotron(html.P([repr(error)]))])
|