WIP: migration de ZNotes, decorateurs, etc.

This commit is contained in:
Emmanuel Viennet 2021-05-31 00:14:15 +02:00
parent 4864fa5040
commit 369b45a8c4
17 changed files with 3886 additions and 3465 deletions

View File

@ -33,6 +33,13 @@ pour régénerer ce fichier:
pip freeze > requirements.txt pip freeze > requirements.txt
### Bidouilles temporaires
Installer le bon vieux `pyExcelerator` dans l'environnement:
(cd /tmp; tar xfz /opt/scodoc/Products/ScoDoc/config/softs/pyExcelerator-0.6.3a.patched.tgz )
(cd /tmp/pyExcelerator-0.6.3a.patched/; python setup.py install)
## Lancement serveur (développement, sur VM Linux) ## Lancement serveur (développement, sur VM Linux)
export FLASK_APP=scodoc.py export FLASK_APP=scodoc.py

17
app/__init__.py Executable file → Normal file
View File

@ -43,9 +43,22 @@ def create_app(config_class=Config):
app.register_blueprint(auth_bp, url_prefix="/auth") app.register_blueprint(auth_bp, url_prefix="/auth")
from app.views import notes_bp from app.views import essais_bp
app.register_blueprint(notes_bp, url_prefix="/ScoDoc") app.register_blueprint(essais_bp, url_prefix="/Essais")
from app.views import scolar_bp
from app.views import notes_bp
from app.views import absences_bp
# https://scodoc.fr/ScoDoc/RT/Scolarite/...
app.register_blueprint(scolar_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite")
# https://scodoc.fr/ScoDoc/RT/Scolarite/Notes/...
app.register_blueprint(notes_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Notes")
# https://scodoc.fr/ScoDoc/RT/Scolarite/Absences/...
app.register_blueprint(
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
)
from app.main import bp as main_bp from app.main import bp as main_bp

View File

@ -16,26 +16,6 @@ from werkzeug.exceptions import BadRequest
from app.auth.models import Permission from app.auth.models import Permission
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
current_app.logger.info(
"permission_required: %s in %s" % (permission, g.scodoc_dept)
)
if not current_user.has_permission(permission, g.scodoc_dept):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
return permission_required(Permission.ScoSuperAdmin)(f)
class ZUser(object): class ZUser(object):
"Emulating Zope User" "Emulating Zope User"
@ -99,90 +79,118 @@ class ZResponse(object):
self.headers[header.tolower()] = value self.headers[header.tolower()] = value
def scodoc7func(func): def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "scodoc_dept" in kwargs:
g.scodoc_dept = kwargs["scodoc_dept"]
del kwargs["scodoc_dept"]
current_app.logger.info(
"permission_required: %s in %s" % (permission, g.scodoc_dept)
)
if not current_user.has_permission(permission, g.scodoc_dept):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
return permission_required(Permission.ScoSuperAdmin)(f)
def scodoc7func(context):
"""Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7. """Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7.
Si on a un kwarg `scodoc_dept`(venant de la route), le stocke dans `g.scodoc_dept`. Si on a un kwarg `scodoc_dept`(venant de la route), le stocke dans `g.scodoc_dept`.
Ajoute l'argument REQUEST s'il est dans la signature de la fonction. Ajoute l'argument REQUEST s'il est dans la signature de la fonction.
Les paramètres de la query string deviennent des (keywords) paramètres de la fonction. Les paramètres de la query string deviennent des (keywords) paramètres de la fonction.
""" """
@wraps(func) def s7_decorator(func):
def scodoc7func_decorator(*args, **kwargs): @wraps(func)
"""Decorator allowing legacy Zope published methods to be called via Flask def scodoc7func_decorator(*args, **kwargs):
routes without modification. """Decorator allowing legacy Zope published methods to be called via Flask
routes without modification.
There are two cases: the function can be called There are two cases: the function can be called
1. via a Flask route ("top level call") 1. via a Flask route ("top level call")
2. or be called directly from Python. 2. or be called directly from Python.
If called via a route, this decorator setups a REQUEST object (emulating Zope2 REQUEST) If called via a route, this decorator setups a REQUEST object (emulating Zope2 REQUEST)
and `g.scodoc_dept` if present in the argument (for routes like `/<scodoc_dept>/Scolarite/sco_exemple`). and `g.scodoc_dept` if present in the argument (for routes like `/<scodoc_dept>/Scolarite/sco_exemple`).
""" """
assert not args assert not args
if hasattr(g, "zrequest"): # Détermine si on est appelé via une route ("toplevel")
top_level = False # ou par un appel de fonction python normal.
else: top_level = not hasattr(g, "zrequest")
g.zrequest = None if top_level:
top_level = True g.zrequest = None
# #
if "scodoc_dept" in kwargs: if "scodoc_dept" in kwargs:
g.scodoc_dept = kwargs["scodoc_dept"] g.scodoc_dept = kwargs["scodoc_dept"]
del kwargs["scodoc_dept"] del kwargs["scodoc_dept"]
elif not hasattr(g, "scodoc_dept"): # if toplevel call elif not hasattr(g, "scodoc_dept"): # if toplevel call
g.scodoc_dept = None g.scodoc_dept = None
# --- Emulate Zope's REQUEST # --- Emulate Zope's REQUEST
REQUEST = ZRequest() REQUEST = ZRequest()
g.zrequest = REQUEST g.zrequest = REQUEST
req_args = REQUEST.form # args from query string (get) or form (post) req_args = REQUEST.form # args from query string (get) or form (post)
# --- Add positional arguments # --- Add positional arguments
pos_arg_values = [] pos_arg_values = []
# PY3 à remplacer par inspect.getfullargspec en py3: # PY3 à remplacer par inspect.getfullargspec en py3:
argspec = inspect.getargspec(func) argspec = inspect.getargspec(func)
current_app.logger.info("argspec=%s" % str(argspec)) current_app.logger.info("argspec=%s" % str(argspec))
nb_default_args = len(argspec.defaults) if argspec.defaults else 0 nb_default_args = len(argspec.defaults) if argspec.defaults else 0
if nb_default_args: if nb_default_args:
arg_names = argspec.args[:-nb_default_args] arg_names = argspec.args[:-nb_default_args]
else:
arg_names = argspec.args
for arg_name in arg_names:
if arg_name == "REQUEST": # special case
pos_arg_values.append(REQUEST)
else: else:
pos_arg_values.append(req_args[arg_name]) arg_names = argspec.args
current_app.logger.info("pos_arg_values=%s" % pos_arg_values) for arg_name in arg_names:
# Add keyword arguments
if nb_default_args:
for arg_name in argspec.args[-nb_default_args:]:
if arg_name == "REQUEST": # special case if arg_name == "REQUEST": # special case
kwargs[arg_name] = REQUEST pos_arg_values.append(REQUEST)
elif arg_name in req_args: elif arg_name == "context":
# set argument kw optionnel pos_arg_values.append(context)
kwargs[arg_name] = req_args[arg_name] else:
current_app.logger.info( pos_arg_values.append(req_args[arg_name])
"scodoc7func_decorator: top_level=%s, pos_arg_values=%s, kwargs=%s" current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
% (top_level, pos_arg_values, kwargs) # Add keyword arguments
) if nb_default_args:
value = func(*pos_arg_values, **kwargs) for arg_name in argspec.args[-nb_default_args:]:
if arg_name == "REQUEST": # special case
kwargs[arg_name] = REQUEST
elif arg_name in req_args:
# set argument kw optionnel
kwargs[arg_name] = req_args[arg_name]
current_app.logger.info(
"scodoc7func_decorator: top_level=%s, pos_arg_values=%s, kwargs=%s"
% (top_level, pos_arg_values, kwargs)
)
value = func(*pos_arg_values, **kwargs)
if not top_level: if not top_level:
return value return value
else: else:
# Build response, adding collected http headers: # Build response, adding collected http headers:
headers = [] headers = []
kw = {"response": value, "status": 200} kw = {"response": value, "status": 200}
if g.zrequest: if g.zrequest:
headers = g.zrequest.RESPONSE.headers headers = g.zrequest.RESPONSE.headers
if not headers: if not headers:
# no customized header, speedup: # no customized header, speedup:
return value return value
if "content-type" in headers: if "content-type" in headers:
kw["mimetype"] = headers["content-type"] kw["mimetype"] = headers["content-type"]
r = flask.Response(**kw) r = flask.Response(**kw)
for h in headers: for h in headers:
r.headers[h] = headers[h] r.headers[h] = headers[h]
return r return r
return scodoc7func_decorator return scodoc7func_decorator
return s7_decorator
# Le "context" de ScoDoc7 # Le "context" de ScoDoc7
@ -193,3 +201,6 @@ class ScoDoc7Context(object):
def __init__(self, globals_dict): def __init__(self, globals_dict):
self.__dict__ = globals_dict self.__dict__ = globals_dict
def __repr__(self):
return "ScoDoc7Context()"

View File

@ -14,6 +14,8 @@ from app.main import bp
from app.decorators import scodoc7func, admin_required from app.decorators import scodoc7func, admin_required
context = None
@bp.route("/") @bp.route("/")
@bp.route("/index") @bp.route("/index")
@ -47,7 +49,7 @@ D = {"count": 0}
@bp.route("/zopefunction", methods=["POST", "GET"]) @bp.route("/zopefunction", methods=["POST", "GET"])
@login_required @login_required
@scodoc7func @scodoc7func(context)
def a_zope_function(y, x="defaut", REQUEST=None): def a_zope_function(y, x="defaut", REQUEST=None):
"""Une fonction typique de ScoDoc7""" """Une fonction typique de ScoDoc7"""
H = get_request_infos() + [ H = get_request_infos() + [
@ -64,7 +66,7 @@ def a_zope_function(y, x="defaut", REQUEST=None):
@bp.route("/zopeform_get") @bp.route("/zopeform_get")
@scodoc7func @scodoc7func(context)
def a_zope_form_get(REQUEST=None): def a_zope_form_get(REQUEST=None):
H = [ H = [
"""<h2>Formulaire GET</h2> """<h2>Formulaire GET</h2>
@ -81,7 +83,7 @@ def a_zope_form_get(REQUEST=None):
@bp.route("/zopeform_post") @bp.route("/zopeform_post")
@scodoc7func @scodoc7func(context)
def a_zope_form_post(REQUEST=None): def a_zope_form_post(REQUEST=None):
H = [ H = [
"""<h2>Formulaire POST</h2> """<h2>Formulaire POST</h2>
@ -98,7 +100,7 @@ def a_zope_form_post(REQUEST=None):
@bp.route("/ScoDoc/<dept_id>/Scolarite/Notes/formsemestre_status") @bp.route("/ScoDoc/<dept_id>/Scolarite/Notes/formsemestre_status")
@scodoc7func @scodoc7func(context)
def formsemestre_status(dept_id=None, formsemestre_id=None, REQUEST=None): def formsemestre_status(dept_id=None, formsemestre_id=None, REQUEST=None):
"""Essai méthode de département """Essai méthode de département
Le contrôle d'accès doit vérifier les bons rôles : ici Ens<dept_id> Le contrôle d'accès doit vérifier les bons rôles : ici Ens<dept_id>

File diff suppressed because it is too large Load Diff

View File

@ -416,11 +416,6 @@ REQUEST.URL0=%s<br/>
# GESTION DE LA BD # GESTION DE LA BD
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
security.declareProtected(ScoSuperAdmin, "GetDBConnexionString")
def GetDBConnexionString(self):
# should not be published (but used from contained classes via acquisition)
return self._db_cnx_string
security.declareProtected(ScoSuperAdmin, "GetDBConnexion") security.declareProtected(ScoSuperAdmin, "GetDBConnexion")
GetDBConnexion = ndb.GetDBConnexion GetDBConnexion = ndb.GetDBConnexion
@ -780,7 +775,9 @@ REQUEST.URL0=%s<br/>
# -------------------------- INFOS SUR ETUDIANTS -------------------------- # -------------------------- INFOS SUR ETUDIANTS --------------------------
security.declareProtected(ScoView, "getEtudInfo") security.declareProtected(ScoView, "getEtudInfo")
def getEtudInfo(self, etudid=False, code_nip=False, filled=False, REQUEST=None, format=None): def getEtudInfo(
self, etudid=False, code_nip=False, filled=False, REQUEST=None, format=None
):
"""infos sur un etudiant pour utilisation en Zope DTML """infos sur un etudiant pour utilisation en Zope DTML
On peut specifier etudid On peut specifier etudid
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine

View File

@ -66,6 +66,12 @@ class FormatError(ScoValueError):
pass pass
class ScoConfigurationError(ScoValueError):
"""Configuration invalid"""
pass
class ScoLockedFormError(ScoException): class ScoLockedFormError(ScoException):
def __init__(self, msg="", REQUEST=None): def __init__(self, msg="", REQUEST=None):
msg = ( msg = (

View File

@ -49,6 +49,8 @@ class Permission:
def init_permissions(): def init_permissions():
for (perm, symbol, description) in _SCO_PERMISSIONS: for (perm, symbol, description) in _SCO_PERMISSIONS:
setattr(Permission, symbol, perm) setattr(Permission, symbol, perm)
# Crée aussi les attributs dans le module (ScoDoc7 compat)
globals()[symbol] = perm
Permission.description[symbol] = description Permission.description[symbol] = description
Permission.NBITS = len(_SCO_PERMISSIONS) Permission.NBITS = len(_SCO_PERMISSIONS)

View File

@ -2,7 +2,7 @@
{% import 'bootstrap/wtf.html' as wtf %} {% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %} {% block app_content %}
<h1>Essais Flask pour ScoDoc 8: accueil</h1> <h1>Protoype ScoDoc 8: accueil</h1>
<div class="row"> <div class="row">
<h2>Avec login requis</h2> <h2>Avec login requis</h2>
<ul> <ul>

View File

@ -3,6 +3,9 @@
""" """
from flask import Blueprint from flask import Blueprint
scolar_bp = Blueprint("scolar", __name__)
notes_bp = Blueprint("notes", __name__) notes_bp = Blueprint("notes", __name__)
absences_bp = Blueprint("absences", __name__)
essais_bp = Blueprint("essais", __name__)
from app.views import notes from app.views import notes, scolar, absences

37
app/views/absences.py Normal file
View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
"""
Module absences: issu de ScoDoc7 / ZAbsences.py
Emmanuel Viennet, 2021
"""
from flask import g
from flask import current_app
from app.decorators import (
scodoc7func,
ScoDoc7Context,
permission_required,
admin_required,
login_required,
)
from app.auth.models import Permission
from app.views import absences_bp as bp
context = ScoDoc7Context(globals())
@bp.route("/")
@scodoc7func(context)
def index_html():
"""Un exemple de fonction ScoDoc 7 dans ZAbsences"""
return """<html>
<body><h1>ScoDoc 8 ZAbsences !</h1>
<p>ZAbsences ScoDoc 8</p>
<p>g.scodoc_dept=%(scodoc_dept)s</p>
</body>
</html>
""" % {
"scodoc_dept": g.scodoc_dept,
}

65
app/views/essais.py Normal file
View File

@ -0,0 +1,65 @@
# -*- coding: UTF-8 -*
"""
Module Essais: divers essais pour la migration vers Flask
Emmanuel Viennet, 2021
"""
from flask import g
from flask import current_app
from app.decorators import (
scodoc7func,
ScoDoc7Context,
permission_required,
admin_required,
login_required,
)
from app.auth.models import Permission
from app.views import notes_bp as bp
# import sco_core deviendra:
from app.ScoDoc import sco_core
context = ScoDoc7Context(globals())
@bp.route("/<scodoc_dept>/Scolarite/sco_exemple")
@scodoc7func(context)
def sco_exemple(etudid="NON"):
"""Un exemple de fonction ScoDoc 7"""
return """<html>
<body><h1>ScoDoc 7 rules !</h1>
<p>etudid=%(etudid)s</p>
<p>g.scodoc_dept=%(scodoc_dept)s</p>
</body>
</html>
""" % {
"etudid": etudid,
"scodoc_dept": g.scodoc_dept,
}
# En ScoDoc 7, on a souvent des vues qui en appellent d'autres
# avec context.sco_exemple( etudid="E12" )
@bp.route("/<scodoc_dept>/Scolarite/sco_exemple2")
@login_required
@scodoc7func(context)
def sco_exemple2():
return "Exemple 2" + context.sco_exemple(etudid="deux")
# Test avec un seul argument REQUEST positionnel
@bp.route("/<scodoc_dept>/Scolarite/sco_get_version")
@scodoc7func(context)
def sco_get_version(REQUEST):
return sco_core.sco_get_version(REQUEST)
# Fonction ressemblant à une méthode Zope protégée
@bp.route("/<scodoc_dept>/Scolarite/sco_test_view")
@scodoc7func(context)
@permission_required(Permission.ScoView)
def sco_test_view(REQUEST=None):
return """Vous avez vu sco_test_view !"""

File diff suppressed because it is too large Load Diff

37
app/views/scolar.py Normal file
View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
"""
Module scolar: issu de ScoDoc7 / ZScolar.py
Emmanuel Viennet, 2021
"""
from flask import g
from flask import current_app
from app.decorators import (
scodoc7func,
ScoDoc7Context,
permission_required,
admin_required,
login_required,
)
from app.auth.models import Permission
from app.views import scolar_bp as bp
context = ScoDoc7Context(globals())
@bp.route("/about")
@scodoc7func(context)
def about():
"""Un exemple de fonction ScoDoc 7 dans ZScolar"""
return """<html>
<body><h1>ScoDoc 7 ZScolar !</h1>
<p>ZScolar ScoDoc 8</p>
<p>g.scodoc_dept=%(scodoc_dept)s</p>
</body>
</html>
""" % {
"scodoc_dept": g.scodoc_dept,
}

18
config.py Normal file → Executable file
View File

@ -7,12 +7,13 @@ BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env")) load_dotenv(os.path.join(BASEDIR, ".env"))
class Config(object): class ConfigClass(object):
"""General configution. Mostly loaded from environment via .env""" """General configuration. Mostly loaded from environment via .env"""
SECRET_KEY = os.environ.get("SECRET_KEY") or "un-grand-secret-introuvable" SECRET_KEY = os.environ.get("SECRET_KEY") or "un-grand-secret-introuvable"
SQLALCHEMY_DATABASE_URI = ( SQLALCHEMY_DATABASE_URI = (
os.environ.get("DATABASE_URL") or "postgresql://scodoc@localhost:5432/SCO8USERS" os.environ.get("USERS_DATABASE_URI")
or "postgresql://scodoc@localhost:5432/SCO8USERS"
) )
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT") LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT")
@ -29,3 +30,14 @@ class Config(object):
BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL") BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
# for ScoDoc 7 compat (à changer) # for ScoDoc 7 compat (à changer)
INSTANCE_HOME = os.environ.get("INSTANCE_HOME", "/opt/scodoc") INSTANCE_HOME = os.environ.get("INSTANCE_HOME", "/opt/scodoc")
# For legacy ScoDoc7 installs: postgresql user
SCODOC7_SQL_USER = os.environ.get("SCODOC7_SQL_USER", "www-data")
DEFAULT_SQL_PORT = os.environ.get("DEFAULT_SQL_PORT", "5432")
def __init__(self):
"""Used to build some config variable at startup time"""
self.SCODOC_VAR_DIR = os.path.join(self.INSTANCE_HOME, "var", "scodoc")
Config = ConfigClass()

View File

@ -15,6 +15,7 @@ Flask-Migrate==2.7.0
Flask-Moment==0.11.0 Flask-Moment==0.11.0
Flask-SQLAlchemy==2.4.4 Flask-SQLAlchemy==2.4.4
Flask-WTF==0.14.3 Flask-WTF==0.14.3
icalendar==4.0.7
idna==2.10 idna==2.10
itsdangerous==1.1.0 itsdangerous==1.1.0
jaxml==3.2 jaxml==3.2
@ -24,13 +25,17 @@ MarkupSafe==1.1.1
Pillow==6.2.2 Pillow==6.2.2
pkg-resources==0.0.0 pkg-resources==0.0.0
psycopg2==2.8.6 psycopg2==2.8.6
pyExcelerator==0.6.3a0
PyJWT==1.7.1 PyJWT==1.7.1
PyRSS2Gen==1.1
python-dateutil==2.8.1 python-dateutil==2.8.1
python-dotenv==0.15.0 python-dotenv==0.15.0
python-editor==1.0.4 python-editor==1.0.4
pytz==2021.1 pytz==2021.1
reportlab==3.5.59
six==1.15.0 six==1.15.0
SQLAlchemy==1.3.23 SQLAlchemy==1.3.23
stripogram==1.5
typing==3.7.4.3 typing==3.7.4.3
visitor==0.1.3 visitor==0.1.3
Werkzeug==1.0.1 Werkzeug==1.0.1

70
scodoc_manager.py Normal file
View File

@ -0,0 +1,70 @@
# -*- coding: UTF-8 -*
"""
Manage departments, databases.
Each departement `X` has its own database, `SCOX`
and a small configuration file `.../config/depts/X.cfg`
containing the database URI
Old ScoDoc7 installs config files contained `dbname=SCOX`
which translates as
"postgresql://<user>@localhost:5432/<dbname>"
<user> being given by `SCODOC7_SQL_USER` env variable.
"""
import os
import re
import glob
from config import Config
from app.scodoc.sco_exceptions import ScoConfigurationError
class ScoDeptDescription:
def __init__(self, filename):
"""Read dept description from dept file"""
if os.path.split(filename)[1][-4:] != ".cfg":
raise ScoConfigurationError("Invalid dept config filename: %s" % filename)
self.dept_id = os.path.split(filename)[1][:-4]
if not self.dept_id:
raise ScoConfigurationError("Invalid dept config filename: %s" % filename)
try:
db_uri = open(filename).read().strip()
except:
raise ScoConfigurationError("Department config file missing: %s" % filename)
m = re.match(r"dbname=SCO([a-zA-Z0-9]+$)", db_uri)
if m:
# ScoDoc7 backward compat
dept = m.group(1) # unused in ScoDoc7
db_name = "SCO" + self.dept_id.upper()
db_uri = "postgresql://%(db_user)s@localhost:%(db_port)s/%(db_name)s" % {
"db_user": Config.SCODOC7_SQL_USER,
"db_name": db_name,
"db_port": Config.DEFAULT_SQL_PORT,
}
self.db_uri = db_uri
class ScoDocManager:
def __init__(self):
filenames = glob.glob(Config.SCODOC_VAR_DIR + "/config/depts/*.cfg")
descr_list = [ScoDeptDescription(f) for f in filenames]
self.dept_descriptions = {d.dept_id: d for d in descr_list}
def get_dept_db_uri(self, dept_id):
"DB URI for this dept id"
return self.dept_descriptions[dept_id].db_uri
def get_dept_ids(self):
"get (unsorted) dept ids"
return [d.dept_id for d in descr_list]
def get_db_uri(self):
"""
Returns DB URI for the "current" departement.
Replaces ScoDoc7 GetDBConnexionString()
"""
return self.get_dept_db_uri(g.scodoc_dept)
sco_mgr = ScoDocManager()