ositah 24.7.dev1__py3-none-any.whl → 24.7.dev3__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.
- ositah/__init__.py +0 -0
- ositah/app.py +17 -0
- ositah/apps/__init__.py +0 -0
- ositah/apps/analysis.py +774 -0
- ositah/apps/configuration/__init__.py +0 -0
- ositah/apps/configuration/callbacks.py +917 -0
- ositah/apps/configuration/main.py +546 -0
- ositah/apps/configuration/parameters.py +74 -0
- ositah/apps/configuration/tools.py +112 -0
- ositah/apps/export.py +1172 -0
- ositah/apps/validation/__init__.py +0 -0
- ositah/apps/validation/callbacks.py +240 -0
- ositah/apps/validation/main.py +89 -0
- ositah/apps/validation/parameters.py +25 -0
- ositah/apps/validation/tables.py +654 -0
- ositah/apps/validation/tools.py +533 -0
- ositah/assets/arrow_down_up.svg +4 -0
- ositah/assets/ositah.css +54 -0
- ositah/assets/sort_ascending.svg +5 -0
- ositah/assets/sort_descending.svg +6 -0
- ositah/assets/sorttable.js +499 -0
- ositah/main.py +449 -0
- ositah/ositah.example.cfg +215 -0
- ositah/static/style.css +54 -0
- ositah/templates/base.html +22 -0
- ositah/templates/bootstrap_login.html +38 -0
- ositah/templates/login_form.html +27 -0
- ositah/utils/__init__.py +0 -0
- ositah/utils/agents.py +117 -0
- ositah/utils/authentication.py +287 -0
- ositah/utils/cache.py +19 -0
- ositah/utils/core.py +13 -0
- ositah/utils/exceptions.py +64 -0
- ositah/utils/hito_db.py +51 -0
- ositah/utils/hito_db_model.py +253 -0
- ositah/utils/menus.py +339 -0
- ositah/utils/period.py +135 -0
- ositah/utils/projects.py +1175 -0
- ositah/utils/teams.py +42 -0
- ositah/utils/utils.py +458 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/METADATA +1 -1
- ositah-24.7.dev3.dist-info/RECORD +46 -0
- ositah-24.7.dev1.dist-info/RECORD +0 -6
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/LICENSE +0 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/WHEEL +0 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/entry_points.txt +0 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/top_level.txt +0 -0
ositah/main.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Application to display the declared time on projects by agents in Hito and to allow validation of
|
|
5
|
+
these declarations by the line managers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
|
|
12
|
+
import dash_bootstrap_components as dbc
|
|
13
|
+
from dash import dcc, html
|
|
14
|
+
from dash.dependencies import Input, Output, State
|
|
15
|
+
from flask import session
|
|
16
|
+
|
|
17
|
+
from ositah.app import app
|
|
18
|
+
from ositah.apps.analysis import analysis_layout
|
|
19
|
+
from ositah.apps.configuration.main import configuration_layout
|
|
20
|
+
from ositah.apps.export import export_layout
|
|
21
|
+
from ositah.apps.validation.main import validation_layout
|
|
22
|
+
from ositah.utils.agents import get_agent_by_email
|
|
23
|
+
from ositah.utils.authentication import (
|
|
24
|
+
LOGIN_URL,
|
|
25
|
+
LOGOUT_URL,
|
|
26
|
+
SESSION_MAX_DURATION,
|
|
27
|
+
configure_multipass_ldap,
|
|
28
|
+
identity_list,
|
|
29
|
+
multipass,
|
|
30
|
+
protect_views,
|
|
31
|
+
)
|
|
32
|
+
from ositah.utils.menus import (
|
|
33
|
+
TEAM_SELECTED_VALUE_ID,
|
|
34
|
+
TEAM_SELECTION_DATE_ID,
|
|
35
|
+
VALIDATION_PERIOD_SELECTED_ID,
|
|
36
|
+
ositah_jumbotron,
|
|
37
|
+
)
|
|
38
|
+
from ositah.utils.utils import (
|
|
39
|
+
AUTHORIZED_ROLES,
|
|
40
|
+
HITO_ROLE_TEAM_MGR,
|
|
41
|
+
TEAM_LIST_ALL_AGENTS,
|
|
42
|
+
GlobalParams,
|
|
43
|
+
define_config_params,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Minimum role to access the configuration page
|
|
47
|
+
CONFIGURATION_MIN_ROLE = HITO_ROLE_TEAM_MGR
|
|
48
|
+
|
|
49
|
+
CONFIG_FILE_NAME_DEFAULT = "ositah.cfg"
|
|
50
|
+
|
|
51
|
+
SIDEBAR_WIDTH = 16
|
|
52
|
+
SIDEBAR_HREF_HOME = "/"
|
|
53
|
+
SIDEBAR_HREF_ANALYSIS = "/analysis"
|
|
54
|
+
SIDEBAR_HREF_CONFIGURATION = "/configuration"
|
|
55
|
+
SIDEBAR_HREF_NSIP_EXPORT = "/export"
|
|
56
|
+
SIDEBAR_HREF_VALIDATION = "/validation"
|
|
57
|
+
# SIDEBAR_HREF_ALL entry order must match render_page_content callback
|
|
58
|
+
# output order
|
|
59
|
+
SIDEBAR_HREF_ALL = [
|
|
60
|
+
SIDEBAR_HREF_ANALYSIS,
|
|
61
|
+
SIDEBAR_HREF_CONFIGURATION,
|
|
62
|
+
SIDEBAR_HREF_NSIP_EXPORT,
|
|
63
|
+
SIDEBAR_HREF_VALIDATION,
|
|
64
|
+
LOGOUT_URL,
|
|
65
|
+
SIDEBAR_HREF_HOME,
|
|
66
|
+
LOGIN_URL,
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
MENU_ID_ANALYSIS = "analysis-menu"
|
|
70
|
+
MENU_ID_CONFIGURATION = "configuration-menu"
|
|
71
|
+
MENU_ID_EXPORT = "export-menu"
|
|
72
|
+
MENU_ID_HOME = "home-menu"
|
|
73
|
+
MENU_ID_LOGIN = "login-menu"
|
|
74
|
+
MENU_ID_LOGOUT = "logout-menu"
|
|
75
|
+
MENU_ID_VALIDATION = "validation-menu"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# the style arguments for the sidebar. We use position:fixed and a fixed width
|
|
79
|
+
SIDEBAR_STYLE = {
|
|
80
|
+
"position": "fixed",
|
|
81
|
+
"top": 0,
|
|
82
|
+
"left": 0,
|
|
83
|
+
"bottom": 0,
|
|
84
|
+
"width": f"{SIDEBAR_WIDTH}rem",
|
|
85
|
+
"padding": "2rem 1rem",
|
|
86
|
+
"background-color": "#f8f9fa",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# the styles for the main content position it to the right of the sidebar and
|
|
90
|
+
# add some padding.
|
|
91
|
+
CONTENT_STYLE = {
|
|
92
|
+
"margin-left": f"{SIDEBAR_WIDTH+2}rem",
|
|
93
|
+
"margin-right": "2rem",
|
|
94
|
+
"padding": "2rem 1rem",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Get default configuration file: look first in the current directory and if not
|
|
99
|
+
# found use the application directory.
|
|
100
|
+
def default_config_path() -> str:
|
|
101
|
+
config_file = f"{os.getcwd()}/{CONFIG_FILE_NAME_DEFAULT}"
|
|
102
|
+
if not os.path.exists(config_file):
|
|
103
|
+
config_file = f"{os.path.dirname(os.path.abspath(__file__))}/{CONFIG_FILE_NAME_DEFAULT}"
|
|
104
|
+
|
|
105
|
+
return config_file
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# URL not found jumbotron
|
|
109
|
+
def url_not_found(path):
|
|
110
|
+
return ositah_jumbotron(
|
|
111
|
+
"404: Not found",
|
|
112
|
+
f"URL {path} was not recognised...",
|
|
113
|
+
title_class="text-danger",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# valid role missing jumbotron
|
|
118
|
+
def valid_role_missing(msg):
|
|
119
|
+
return ositah_jumbotron(
|
|
120
|
+
"You don't have a valid Hito role to OSITAH",
|
|
121
|
+
msg,
|
|
122
|
+
title_class="text-warning",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.callback(
|
|
127
|
+
[
|
|
128
|
+
Output("page-content", "children"),
|
|
129
|
+
Output("login-info", "children"),
|
|
130
|
+
# Output order must match SIDEBAR_HREF_ALL entry order
|
|
131
|
+
Output(MENU_ID_ANALYSIS, "disabled"),
|
|
132
|
+
Output(MENU_ID_CONFIGURATION, "disabled"),
|
|
133
|
+
Output(MENU_ID_EXPORT, "disabled"),
|
|
134
|
+
Output(MENU_ID_VALIDATION, "disabled"),
|
|
135
|
+
Output(MENU_ID_LOGOUT, "disabled"),
|
|
136
|
+
],
|
|
137
|
+
Input("url", "pathname"),
|
|
138
|
+
State("login-info", "children"),
|
|
139
|
+
)
|
|
140
|
+
def render_page_content(pathname, login_menu):
|
|
141
|
+
"""
|
|
142
|
+
Function called to render the main page in OSITAH. It is also in charge of managing user
|
|
143
|
+
sessions.
|
|
144
|
+
|
|
145
|
+
:param pathname:
|
|
146
|
+
:param login_menu:
|
|
147
|
+
:return: callback output
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
from ositah.utils.authentication import remove_session
|
|
151
|
+
from ositah.utils.hito_db import get_db, new_uuid
|
|
152
|
+
from ositah.utils.hito_db_model import OSITAHSession, Team
|
|
153
|
+
|
|
154
|
+
global_params = GlobalParams()
|
|
155
|
+
logged_in_user = login_menu
|
|
156
|
+
menus_disabled = True
|
|
157
|
+
|
|
158
|
+
db = get_db()
|
|
159
|
+
OSITAHSession.__table__.create(db.session.bind, checkfirst=True)
|
|
160
|
+
|
|
161
|
+
user_authenticated = False
|
|
162
|
+
if "user_id" in session:
|
|
163
|
+
# 'user_id' is defined by Multipass if the login was successful
|
|
164
|
+
if "uid" in session and "user_email" in session:
|
|
165
|
+
# The user already logged in successfully once, check if the session is among the known
|
|
166
|
+
# valid sessions. As id and email columns have a unique constraint, the query can
|
|
167
|
+
# return only 0 or 1 value
|
|
168
|
+
saved_session = OSITAHSession.query.filter(
|
|
169
|
+
OSITAHSession.id == str(session["uid"]),
|
|
170
|
+
OSITAHSession.email == session["user_email"],
|
|
171
|
+
).first()
|
|
172
|
+
if saved_session:
|
|
173
|
+
current_time = datetime.now()
|
|
174
|
+
if current_time > saved_session.last_use + timedelta(hours=SESSION_MAX_DURATION):
|
|
175
|
+
remove_session()
|
|
176
|
+
else:
|
|
177
|
+
saved_session.last_use = current_time
|
|
178
|
+
db.session.commit()
|
|
179
|
+
user_authenticated = True
|
|
180
|
+
if not user_authenticated and session["user_id"] in identity_list:
|
|
181
|
+
# Session has been fully initialized yet: do it and save it
|
|
182
|
+
session["user_email"] = identity_list[session["user_id"]].email
|
|
183
|
+
session["uid"] = new_uuid()
|
|
184
|
+
this_session = OSITAHSession(
|
|
185
|
+
id=str(session["uid"]),
|
|
186
|
+
email=session["user_email"],
|
|
187
|
+
last_use=datetime.now(),
|
|
188
|
+
)
|
|
189
|
+
db.session.add(this_session)
|
|
190
|
+
db.session.commit()
|
|
191
|
+
user_authenticated = True
|
|
192
|
+
|
|
193
|
+
if user_authenticated:
|
|
194
|
+
user_session_data = global_params.session_data
|
|
195
|
+
user = get_agent_by_email(session["user_email"])
|
|
196
|
+
role_ok = False
|
|
197
|
+
user_roles = user.roles
|
|
198
|
+
for role in AUTHORIZED_ROLES:
|
|
199
|
+
if role in user_roles:
|
|
200
|
+
role_ok = True
|
|
201
|
+
user_session_data.role = role
|
|
202
|
+
break
|
|
203
|
+
if role_ok:
|
|
204
|
+
if not user_session_data.agent_teams:
|
|
205
|
+
if role == HITO_ROLE_TEAM_MGR:
|
|
206
|
+
# For a team manager, show only the teams he/she is a manager
|
|
207
|
+
teams = Team.query.filter(Team.managers.any(email=session["user_email"])).all()
|
|
208
|
+
if len(teams) == 0:
|
|
209
|
+
# A user with role ROLE_RESP but is not the manager of any team is degraded
|
|
210
|
+
# to ROLE_AGENT
|
|
211
|
+
role_ok = False
|
|
212
|
+
role_not_ok_msg = (
|
|
213
|
+
f"{user.prenom} {user.nom} n'est" " responsable d'aucune équipe"
|
|
214
|
+
)
|
|
215
|
+
team_list = []
|
|
216
|
+
else:
|
|
217
|
+
teams.extend(
|
|
218
|
+
Team.query.filter(
|
|
219
|
+
Team.children_managers.any(email_auth=session["user_email"])
|
|
220
|
+
).all()
|
|
221
|
+
)
|
|
222
|
+
team_list = sorted([t.nom for t in teams])
|
|
223
|
+
else:
|
|
224
|
+
# For other allowed roles, show all teams and add an entry for all agents
|
|
225
|
+
teams = Team.query.all()
|
|
226
|
+
team_list = sorted([t.nom for t in teams])
|
|
227
|
+
team_list.insert(0, TEAM_LIST_ALL_AGENTS)
|
|
228
|
+
user_session_data.add_teams(team_list, sort_list=False)
|
|
229
|
+
logged_in_user = f"logged in as {user.prenom} {user.nom}"
|
|
230
|
+
menus_disabled = False
|
|
231
|
+
else:
|
|
232
|
+
role_not_ok_msg = (
|
|
233
|
+
f"{user.prenom} {user.nom} n'a pas de role Hito approprié"
|
|
234
|
+
" ({', '.join(AUTHORIZED_ROLES)})"
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
logged_in_user = login_menu_link()
|
|
238
|
+
role_ok = True
|
|
239
|
+
|
|
240
|
+
return_values = [logged_in_user]
|
|
241
|
+
for i in range(len(SIDEBAR_HREF_ALL) - 2):
|
|
242
|
+
if (
|
|
243
|
+
SIDEBAR_HREF_ALL[i] == SIDEBAR_HREF_CONFIGURATION
|
|
244
|
+
and user_authenticated
|
|
245
|
+
and AUTHORIZED_ROLES.index(user_session_data.role)
|
|
246
|
+
> AUTHORIZED_ROLES.index(CONFIGURATION_MIN_ROLE)
|
|
247
|
+
):
|
|
248
|
+
disable_flag = True
|
|
249
|
+
else:
|
|
250
|
+
disable_flag = menus_disabled
|
|
251
|
+
return_values.append(disable_flag)
|
|
252
|
+
if not role_ok:
|
|
253
|
+
return_values.insert(0, valid_role_missing(role_not_ok_msg))
|
|
254
|
+
return return_values
|
|
255
|
+
|
|
256
|
+
if user_authenticated:
|
|
257
|
+
if pathname == SIDEBAR_HREF_VALIDATION:
|
|
258
|
+
return_values.insert(0, validation_layout())
|
|
259
|
+
return return_values
|
|
260
|
+
elif pathname == SIDEBAR_HREF_ANALYSIS:
|
|
261
|
+
return_values.insert(0, analysis_layout())
|
|
262
|
+
return return_values
|
|
263
|
+
elif pathname == SIDEBAR_HREF_NSIP_EXPORT:
|
|
264
|
+
return_values.insert(0, export_layout())
|
|
265
|
+
return return_values
|
|
266
|
+
elif pathname == SIDEBAR_HREF_CONFIGURATION:
|
|
267
|
+
return_values.insert(0, configuration_layout())
|
|
268
|
+
return return_values
|
|
269
|
+
|
|
270
|
+
# Display the same message based on the login state for all valid URLs if none matched
|
|
271
|
+
# previously
|
|
272
|
+
if pathname in SIDEBAR_HREF_ALL:
|
|
273
|
+
if user_authenticated:
|
|
274
|
+
return_values.insert(
|
|
275
|
+
0,
|
|
276
|
+
html.P("Sélectionner ce que vous souhaitez faire dans le menu principal"),
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
return_values.insert(0, html.P("Veuillez vous authentifier"))
|
|
280
|
+
return return_values
|
|
281
|
+
|
|
282
|
+
# If the user tries to reach a different page, return a 404 message
|
|
283
|
+
return_values.insert(0, url_not_found(pathname))
|
|
284
|
+
return return_values
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def login_menu_link():
|
|
288
|
+
"""
|
|
289
|
+
:return: graphic object for login menu
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
return dbc.NavLink("Login", id=MENU_ID_LOGIN, href=LOGIN_URL, external_link=True)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def make_app(environ=None, start_response=None, options=None):
|
|
296
|
+
"""
|
|
297
|
+
Function to create the application without running it. It is the main entry point when called
|
|
298
|
+
as a uWSGI application (from gunicorn or uwswgi for example). Must return the Flask application
|
|
299
|
+
server.
|
|
300
|
+
|
|
301
|
+
:param environ: environment variables received from the uWSGI server
|
|
302
|
+
:param start_response: function received from the uWSGI server (not used)
|
|
303
|
+
:param options: parser options (will be initialized to sensible values when called from a uSWGI
|
|
304
|
+
server)
|
|
305
|
+
:return: Flask application server
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
# Initialize options to sensible values: required when called from a uWSGI server
|
|
309
|
+
if options is None:
|
|
310
|
+
options = argparse.Namespace()
|
|
311
|
+
options.configuration_file = default_config_path()
|
|
312
|
+
options.debug = True
|
|
313
|
+
|
|
314
|
+
config = define_config_params(options.configuration_file)
|
|
315
|
+
|
|
316
|
+
configure_multipass_ldap(app.server, config["server"]["authentication"]["provider_name"])
|
|
317
|
+
multipass.init_app(app.server)
|
|
318
|
+
|
|
319
|
+
app.server.secret_key = "ositah-dashboard"
|
|
320
|
+
|
|
321
|
+
protect_views(app)
|
|
322
|
+
|
|
323
|
+
# Import from hito_db must not be done after configuration has been loaded
|
|
324
|
+
from ositah.utils.hito_db import get_db
|
|
325
|
+
|
|
326
|
+
# Initialize DB connection by calling get_db()
|
|
327
|
+
get_db(init_session=False)
|
|
328
|
+
|
|
329
|
+
sidebar = html.Div(
|
|
330
|
+
[
|
|
331
|
+
html.H2("OSITAH", className="display-4"),
|
|
332
|
+
html.Hr(),
|
|
333
|
+
html.P("Suivi des déclarations de temps", className="lead"),
|
|
334
|
+
dbc.Nav(
|
|
335
|
+
[
|
|
336
|
+
# external_link=True is required for the callback on dcc.Location to work
|
|
337
|
+
html.Div(login_menu_link(), id="login-info"),
|
|
338
|
+
dbc.NavLink(
|
|
339
|
+
"Logout",
|
|
340
|
+
id=MENU_ID_LOGOUT,
|
|
341
|
+
href=LOGOUT_URL,
|
|
342
|
+
disabled=True,
|
|
343
|
+
external_link=True,
|
|
344
|
+
),
|
|
345
|
+
],
|
|
346
|
+
vertical="md",
|
|
347
|
+
),
|
|
348
|
+
html.Hr(),
|
|
349
|
+
dbc.Nav(
|
|
350
|
+
[
|
|
351
|
+
dbc.NavLink(
|
|
352
|
+
"Home",
|
|
353
|
+
id=MENU_ID_HOME,
|
|
354
|
+
href=SIDEBAR_HREF_HOME,
|
|
355
|
+
active="exact",
|
|
356
|
+
),
|
|
357
|
+
dbc.NavLink(
|
|
358
|
+
"Suvi / Validation",
|
|
359
|
+
id=MENU_ID_VALIDATION,
|
|
360
|
+
href=SIDEBAR_HREF_VALIDATION,
|
|
361
|
+
active="exact",
|
|
362
|
+
disabled=True,
|
|
363
|
+
),
|
|
364
|
+
dbc.NavLink(
|
|
365
|
+
"Analyse",
|
|
366
|
+
id=MENU_ID_ANALYSIS,
|
|
367
|
+
href=SIDEBAR_HREF_ANALYSIS,
|
|
368
|
+
active="exact",
|
|
369
|
+
disabled=True,
|
|
370
|
+
),
|
|
371
|
+
dbc.NavLink(
|
|
372
|
+
"Export NSIP",
|
|
373
|
+
id=MENU_ID_EXPORT,
|
|
374
|
+
href=SIDEBAR_HREF_NSIP_EXPORT,
|
|
375
|
+
active="exact",
|
|
376
|
+
disabled=True,
|
|
377
|
+
),
|
|
378
|
+
dbc.NavLink(
|
|
379
|
+
"Configuration",
|
|
380
|
+
id=MENU_ID_CONFIGURATION,
|
|
381
|
+
href=SIDEBAR_HREF_CONFIGURATION,
|
|
382
|
+
active="exact",
|
|
383
|
+
disabled=True,
|
|
384
|
+
),
|
|
385
|
+
],
|
|
386
|
+
vertical="md",
|
|
387
|
+
pills=True,
|
|
388
|
+
),
|
|
389
|
+
dcc.Store(id=TEAM_SELECTED_VALUE_ID, data=""),
|
|
390
|
+
dcc.Store(id=TEAM_SELECTION_DATE_ID, data=""),
|
|
391
|
+
dcc.Store(id=VALIDATION_PERIOD_SELECTED_ID, data=""),
|
|
392
|
+
],
|
|
393
|
+
style=SIDEBAR_STYLE,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
content = html.Div(id="page-content", style=CONTENT_STYLE)
|
|
397
|
+
|
|
398
|
+
app.layout = html.Div([dcc.Location(id="url"), sidebar, content])
|
|
399
|
+
|
|
400
|
+
return app.server
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def main():
|
|
404
|
+
"""
|
|
405
|
+
Main entry point when executing the application from the command line
|
|
406
|
+
|
|
407
|
+
:return: exit status
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
global_params = GlobalParams()
|
|
411
|
+
|
|
412
|
+
DEBUG_DASH = "dash"
|
|
413
|
+
DEBUG_SQLALCHEMY = "db"
|
|
414
|
+
DEBUG_ALL = "all"
|
|
415
|
+
DEBUG_NONE = "none"
|
|
416
|
+
|
|
417
|
+
# parser must be run here to avoid messing up with gunicorn
|
|
418
|
+
parser = argparse.ArgumentParser()
|
|
419
|
+
parser.add_argument(
|
|
420
|
+
"--configuration-file",
|
|
421
|
+
default=default_config_path(),
|
|
422
|
+
help=f"Configuration file (D: {default_config_path()})",
|
|
423
|
+
)
|
|
424
|
+
parser.add_argument(
|
|
425
|
+
"--debug",
|
|
426
|
+
choices=[DEBUG_DASH, DEBUG_SQLALCHEMY, DEBUG_ALL, DEBUG_NONE],
|
|
427
|
+
default=DEBUG_NONE,
|
|
428
|
+
help="Enable debugging mode in Dash and/or SQLAlchemy (do not use in production)",
|
|
429
|
+
)
|
|
430
|
+
options = parser.parse_args()
|
|
431
|
+
|
|
432
|
+
dash_debug = False
|
|
433
|
+
sqlalchemy_debug = False
|
|
434
|
+
if options.debug == DEBUG_DASH or options.debug == DEBUG_ALL:
|
|
435
|
+
dash_debug = True
|
|
436
|
+
if options.debug == DEBUG_SQLALCHEMY or options.debug == DEBUG_ALL:
|
|
437
|
+
sqlalchemy_debug = "debug"
|
|
438
|
+
|
|
439
|
+
make_app(options=options)
|
|
440
|
+
|
|
441
|
+
# If --debug, enable SQLAlchemy verbose mode
|
|
442
|
+
if sqlalchemy_debug:
|
|
443
|
+
app.server.config["SQLALCHEMY_ECHO"] = True
|
|
444
|
+
|
|
445
|
+
app.run_server(debug=dash_debug, port=global_params.port)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
if __name__ == "__main__":
|
|
449
|
+
exit(main())
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Configuration file for OSITAH application
|
|
2
|
+
|
|
3
|
+
# Where to read the data
|
|
4
|
+
hito:
|
|
5
|
+
db:
|
|
6
|
+
# type muste be sqlite or mysql
|
|
7
|
+
#type: sqlite
|
|
8
|
+
type: mysql
|
|
9
|
+
# location must be a filename for sqlite or server/database for mysql
|
|
10
|
+
#location: 'c:\temp\hito-dev.sqlite'
|
|
11
|
+
location: 'localhost/hito'
|
|
12
|
+
user: hitousery
|
|
13
|
+
password: *verysecret*
|
|
14
|
+
# After this time (in seconds), DB connection is recycled
|
|
15
|
+
inactivity_timeout: 900
|
|
16
|
+
# For some unidentified reason, in SQLite, "stagiaire" carriere type is not found with an exact match...
|
|
17
|
+
agent_query: |
|
|
18
|
+
SELECT
|
|
19
|
+
DISTINCT(agent.id)
|
|
20
|
+
, agent.nom
|
|
21
|
+
, agent.prenom
|
|
22
|
+
, agent.team_id as team_id
|
|
23
|
+
, team.nom as team
|
|
24
|
+
, agent.statut as statut
|
|
25
|
+
, agent.email as email
|
|
26
|
+
, agent.corps_exercice_metier as cem
|
|
27
|
+
, CASE
|
|
28
|
+
WHEN agent.email_auth IS NULL THEN agent.email
|
|
29
|
+
ELSE agent.email_auth
|
|
30
|
+
END AS email_auth
|
|
31
|
+
FROM
|
|
32
|
+
agent
|
|
33
|
+
LEFT JOIN team on team.id = agent.team_id
|
|
34
|
+
JOIN carriere on carriere.agent_id = agent.id
|
|
35
|
+
WHERE
|
|
36
|
+
agent.archive = 0
|
|
37
|
+
AND
|
|
38
|
+
carriere.date_debut = (
|
|
39
|
+
SELECT MAX(c.date_debut)
|
|
40
|
+
FROM carriere AS c
|
|
41
|
+
WHERE
|
|
42
|
+
agent.id = c.agent_id
|
|
43
|
+
AND
|
|
44
|
+
(c.type IN ("apprenti", "cdd", "emeritat", "stagiaire", "visiteur") OR c.type LIKE "arrivee%" OR c.type LIKE "stagiaire%")
|
|
45
|
+
)
|
|
46
|
+
AND
|
|
47
|
+
carriere.date_debut <= $$TODAY$$
|
|
48
|
+
# Patterns to identify the different activity categories from the activity name
|
|
49
|
+
categories:
|
|
50
|
+
consultance: 'Consultances & Expertises'
|
|
51
|
+
enseignement: 'Enseignement Supérieur'
|
|
52
|
+
local_project: 'Local projects'
|
|
53
|
+
service: 'Services & Support'
|
|
54
|
+
# Time unit (default is hours for categories not listed). Supported values h(our), w(eek)
|
|
55
|
+
time_unit:
|
|
56
|
+
local_project: w
|
|
57
|
+
nsip_project: w
|
|
58
|
+
service: w
|
|
59
|
+
# Column titles to use when displying the information about categories and agents
|
|
60
|
+
titles:
|
|
61
|
+
consultance: 'Consultance'
|
|
62
|
+
declarations_number: 'Déclarations effectuées'
|
|
63
|
+
enseignement: 'Enseignement'
|
|
64
|
+
fullname: 'Agent'
|
|
65
|
+
local_project: 'projets locaux'
|
|
66
|
+
missings_number: 'Déclarations obligatoires manquantes'
|
|
67
|
+
nsip_project: 'projets NSIP'
|
|
68
|
+
percent_global: 'Temps déclarés (%)'
|
|
69
|
+
service: 'Service'
|
|
70
|
+
statut: 'Statut'
|
|
71
|
+
team: 'Equipe'
|
|
72
|
+
|
|
73
|
+
# Web server information
|
|
74
|
+
server:
|
|
75
|
+
port: 9999
|
|
76
|
+
authentication:
|
|
77
|
+
ldap:
|
|
78
|
+
# uri can be space-separated list
|
|
79
|
+
uri: 'ldaps://ijclabad1.ijclab.in2p3.fr ldaps://ijclabad2.ijclab.in2p3.fr ldaps://ccijclabad01.ijclab.in2p3.fr'
|
|
80
|
+
base_dn: 'ou=ijclab,dc=ijclab,dc=in2p3,dc=fr'
|
|
81
|
+
bind_dn: 'CN=Non-Interactive Queries,OU=SI,OU=Services techniques,OU=Laboratoire,DC=lal,DC=in2p3,DC=fr'
|
|
82
|
+
password: 'ldap_bind_password'
|
|
83
|
+
provider_name: "MyLab account"
|
|
84
|
+
|
|
85
|
+
# Options related to declarations
|
|
86
|
+
declaration:
|
|
87
|
+
# Default date must be a date included into the selected validation period. It is a default, if no other explicitly select
|
|
88
|
+
#default_date: 2021-07-01
|
|
89
|
+
# max_hours specifies the upper valid value for activities declared in hours
|
|
90
|
+
max_hours: 400
|
|
91
|
+
# Agent statut whose declaration is not mandatory
|
|
92
|
+
optional_statutes:
|
|
93
|
+
- benevole
|
|
94
|
+
- emerite
|
|
95
|
+
- stagiaire
|
|
96
|
+
- visiteur
|
|
97
|
+
# Team who are not required to declare: values must match the beginning of the team name
|
|
98
|
+
optional_teams:
|
|
99
|
+
- Administration
|
|
100
|
+
- Support
|
|
101
|
+
# Thresholds used to mark declarations as low, suspect or good
|
|
102
|
+
# The value is the upper bound for the corresonding class
|
|
103
|
+
thresholds:
|
|
104
|
+
low: 50
|
|
105
|
+
suspect: 80
|
|
106
|
+
good: 100
|
|
107
|
+
|
|
108
|
+
# Information related to validation
|
|
109
|
+
validation:
|
|
110
|
+
override_period:
|
|
111
|
+
- ROLE_SUPER_ADMIN
|
|
112
|
+
- ROLE_PROJECT_MANAGER
|
|
113
|
+
|
|
114
|
+
# Users with specific rights
|
|
115
|
+
roles:
|
|
116
|
+
# List users (with their connection email) who are given a read-only access to OSITAH (no validation right)
|
|
117
|
+
read-only:
|
|
118
|
+
- user@my.dom.ain
|
|
119
|
+
|
|
120
|
+
# NSIP related parameters
|
|
121
|
+
|
|
122
|
+
nsip:
|
|
123
|
+
# The following sections configures information about NSIP service
|
|
124
|
+
# For each part of the API, there must be a base URL and some operation sub-urls
|
|
125
|
+
agent_api:
|
|
126
|
+
base_url: /api_labo/agent
|
|
127
|
+
declaration_add: /declaration/create
|
|
128
|
+
declaration_delete: /declaration/delete
|
|
129
|
+
declaration_update: /declaration/update
|
|
130
|
+
institute_api:
|
|
131
|
+
base_url: /api_institut
|
|
132
|
+
declaration_period_list: /declaration_period/list
|
|
133
|
+
lab_api:
|
|
134
|
+
base_url: /api_labo
|
|
135
|
+
agent_list: /agent/list
|
|
136
|
+
declaration_list: /declaration/list
|
|
137
|
+
project_list: /project/list
|
|
138
|
+
reference_list: /reference/list
|
|
139
|
+
server_url: https://nsip.in2p3.fr
|
|
140
|
+
token: nsip_lab_token
|
|
141
|
+
|
|
142
|
+
# teaching dictionary allows to define a ratio to apply to teaching activities.
|
|
143
|
+
# It also allows to define (optionally) a list of CEM that this ratio must
|
|
144
|
+
# be applied to (instead of all agents).
|
|
145
|
+
# master allows to define the masterproject identifying teaching activities
|
|
146
|
+
# (default: Enseignement Supérieur)
|
|
147
|
+
teaching:
|
|
148
|
+
masterproject: Enseignement Supérieur
|
|
149
|
+
ratio: 4.4
|
|
150
|
+
cem:
|
|
151
|
+
- cem_enseignant_chercheur
|
|
152
|
+
|
|
153
|
+
# Match against NSIP reference types to define the OSITAH masterproject.
|
|
154
|
+
# A reference with no value will cause the entry to be ignored by OSITAH.
|
|
155
|
+
# Reference types can be regex.
|
|
156
|
+
reference_masterprojects:
|
|
157
|
+
consultancyexpertise: Consultances & Expertises
|
|
158
|
+
highereducation: Enseignement Supérieur
|
|
159
|
+
servicesupport: Services & Support
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# project_teams allow to define a list of teams for a project
|
|
163
|
+
# The project name is matched against the list of defined projects and must be a regex matching
|
|
164
|
+
# the beginning of the project name. A team can be specified as a regex.
|
|
165
|
+
# Example below is for IJCLab: update according to your needs.
|
|
166
|
+
project_teams:
|
|
167
|
+
Consultances & Expertises:
|
|
168
|
+
- .*
|
|
169
|
+
Enseignement Supérieur:
|
|
170
|
+
- Accélérateurs.*
|
|
171
|
+
- A2C.*
|
|
172
|
+
- Direction
|
|
173
|
+
- Energie.*
|
|
174
|
+
- Ingénierie.*
|
|
175
|
+
- Nucléaire.*
|
|
176
|
+
- PHE.*
|
|
177
|
+
- Plateforme.*
|
|
178
|
+
- Santé.*
|
|
179
|
+
- Théorie.*
|
|
180
|
+
Services & Support / Assurance-Qualité:
|
|
181
|
+
- Support - Projets
|
|
182
|
+
Services & Support / Budget-Finances:
|
|
183
|
+
- Administration - Division financière
|
|
184
|
+
Services & Support / Communication-Documentation:
|
|
185
|
+
- Support - Communication
|
|
186
|
+
#Services & Support / Environnement:
|
|
187
|
+
Services & Support / Formation:
|
|
188
|
+
- Support - Enseignement
|
|
189
|
+
#Services & Support / Hygiène-Sécurité:
|
|
190
|
+
Services & Support / Management:
|
|
191
|
+
- .*
|
|
192
|
+
Services & Support / Partenariat:
|
|
193
|
+
- Support - STIRI
|
|
194
|
+
Services & Support / Patrimoine:
|
|
195
|
+
- Support - Infrastructure
|
|
196
|
+
Services & Support / Radioprotection:
|
|
197
|
+
- Support - Service SPR
|
|
198
|
+
Services & Support / Ressources Humaines:
|
|
199
|
+
- Administration.* - RH.*
|
|
200
|
+
Services & Support / Secrétariat:
|
|
201
|
+
- Direction - Assistantes
|
|
202
|
+
#Services & Support / Services Généraux:
|
|
203
|
+
Services & Support / Support-Biologie:
|
|
204
|
+
- Santé.*
|
|
205
|
+
Services & Support / Support-Electronique:
|
|
206
|
+
- Ingénierie - Electronique.*
|
|
207
|
+
Services & Support / Support-Informatique:
|
|
208
|
+
- Ingénierie - Informatique
|
|
209
|
+
Services & Support / Support-Instrumentation:
|
|
210
|
+
- Ingénierie - Détecteurs.*
|
|
211
|
+
Services & Support / Support-Mécanique:
|
|
212
|
+
- Ingénierie - Mécanique
|
|
213
|
+
#Services & Support / Sûreté:
|
|
214
|
+
Services & Support / Valorisation:
|
|
215
|
+
- Support - STIRI
|