ositah 25.6.dev1__py3-none-any.whl → 25.9.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ositah might be problematic. Click here for more details.

Files changed (42) hide show
  1. ositah/app.py +17 -17
  2. ositah/apps/analysis.py +785 -785
  3. ositah/apps/configuration/callbacks.py +916 -916
  4. ositah/apps/configuration/main.py +546 -546
  5. ositah/apps/configuration/parameters.py +74 -74
  6. ositah/apps/configuration/tools.py +112 -112
  7. ositah/apps/export.py +1209 -1191
  8. ositah/apps/validation/callbacks.py +240 -240
  9. ositah/apps/validation/main.py +89 -89
  10. ositah/apps/validation/parameters.py +25 -25
  11. ositah/apps/validation/tables.py +646 -646
  12. ositah/apps/validation/tools.py +552 -552
  13. ositah/assets/arrow_down_up.svg +3 -3
  14. ositah/assets/ositah.css +53 -53
  15. ositah/assets/sort_ascending.svg +4 -4
  16. ositah/assets/sort_descending.svg +5 -5
  17. ositah/assets/sorttable.js +499 -499
  18. ositah/main.py +449 -449
  19. ositah/ositah.example.cfg +229 -229
  20. ositah/static/style.css +53 -53
  21. ositah/templates/base.html +22 -22
  22. ositah/templates/bootstrap_login.html +38 -38
  23. ositah/templates/login_form.html +26 -26
  24. ositah/utils/agents.py +124 -124
  25. ositah/utils/authentication.py +287 -287
  26. ositah/utils/cache.py +19 -19
  27. ositah/utils/core.py +13 -13
  28. ositah/utils/exceptions.py +64 -64
  29. ositah/utils/hito_db.py +51 -51
  30. ositah/utils/hito_db_model.py +253 -253
  31. ositah/utils/menus.py +339 -339
  32. ositah/utils/period.py +139 -139
  33. ositah/utils/projects.py +1179 -1178
  34. ositah/utils/teams.py +42 -42
  35. ositah/utils/utils.py +474 -474
  36. {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/METADATA +149 -150
  37. ositah-25.9.dev2.dist-info/RECORD +46 -0
  38. {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/licenses/LICENSE +29 -29
  39. ositah-25.6.dev1.dist-info/RECORD +0 -46
  40. {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/WHEEL +0 -0
  41. {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/entry_points.txt +0 -0
  42. {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/top_level.txt +0 -0
ositah/main.py CHANGED
@@ -1,449 +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
- "backgroundColor": "#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
- "marginLeft": f"{SIDEBAR_WIDTH+2}rem",
93
- "marginRight": "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()
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=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())
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
+ "backgroundColor": "#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
+ "marginLeft": f"{SIDEBAR_WIDTH+2}rem",
93
+ "marginRight": "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()
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=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())