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.
- 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 +1209 -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 +1179 -1178
- ositah/utils/teams.py +42 -42
- ositah/utils/utils.py +474 -474
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/METADATA +149 -150
- ositah-25.9.dev2.dist-info/RECORD +46 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/licenses/LICENSE +29 -29
- ositah-25.6.dev1.dist-info/RECORD +0 -46
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/WHEEL +0 -0
- {ositah-25.6.dev1.dist-info → ositah-25.9.dev2.dist-info}/entry_points.txt +0 -0
- {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())
|