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
### 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)
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")
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

View File

@ -16,26 +16,6 @@ from werkzeug.exceptions import BadRequest
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):
"Emulating Zope User"
@ -99,90 +79,118 @@ class ZResponse(object):
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.
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.
Les paramètres de la query string deviennent des (keywords) paramètres de la fonction.
"""
@wraps(func)
def scodoc7func_decorator(*args, **kwargs):
"""Decorator allowing legacy Zope published methods to be called via Flask
routes without modification.
def s7_decorator(func):
@wraps(func)
def scodoc7func_decorator(*args, **kwargs):
"""Decorator allowing legacy Zope published methods to be called via Flask
routes without modification.
There are two cases: the function can be called
1. via a Flask route ("top level call")
2. or be called directly from Python.
There are two cases: the function can be called
1. via a Flask route ("top level call")
2. or be called directly from Python.
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`).
"""
assert not args
if hasattr(g, "zrequest"):
top_level = False
else:
g.zrequest = None
top_level = True
#
if "scodoc_dept" in kwargs:
g.scodoc_dept = kwargs["scodoc_dept"]
del kwargs["scodoc_dept"]
elif not hasattr(g, "scodoc_dept"): # if toplevel call
g.scodoc_dept = None
# --- Emulate Zope's REQUEST
REQUEST = ZRequest()
g.zrequest = REQUEST
req_args = REQUEST.form # args from query string (get) or form (post)
# --- Add positional arguments
pos_arg_values = []
# PY3 à remplacer par inspect.getfullargspec en py3:
argspec = inspect.getargspec(func)
current_app.logger.info("argspec=%s" % str(argspec))
nb_default_args = len(argspec.defaults) if argspec.defaults else 0
if 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)
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`).
"""
assert not args
# Détermine si on est appelé via une route ("toplevel")
# ou par un appel de fonction python normal.
top_level = not hasattr(g, "zrequest")
if top_level:
g.zrequest = None
#
if "scodoc_dept" in kwargs:
g.scodoc_dept = kwargs["scodoc_dept"]
del kwargs["scodoc_dept"]
elif not hasattr(g, "scodoc_dept"): # if toplevel call
g.scodoc_dept = None
# --- Emulate Zope's REQUEST
REQUEST = ZRequest()
g.zrequest = REQUEST
req_args = REQUEST.form # args from query string (get) or form (post)
# --- Add positional arguments
pos_arg_values = []
# PY3 à remplacer par inspect.getfullargspec en py3:
argspec = inspect.getargspec(func)
current_app.logger.info("argspec=%s" % str(argspec))
nb_default_args = len(argspec.defaults) if argspec.defaults else 0
if nb_default_args:
arg_names = argspec.args[:-nb_default_args]
else:
pos_arg_values.append(req_args[arg_name])
current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
# Add keyword arguments
if nb_default_args:
for arg_name in argspec.args[-nb_default_args:]:
arg_names = argspec.args
for arg_name in arg_names:
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)
pos_arg_values.append(REQUEST)
elif arg_name == "context":
pos_arg_values.append(context)
else:
pos_arg_values.append(req_args[arg_name])
current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
# Add keyword arguments
if nb_default_args:
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:
return value
else:
# Build response, adding collected http headers:
headers = []
kw = {"response": value, "status": 200}
if g.zrequest:
headers = g.zrequest.RESPONSE.headers
if not headers:
# no customized header, speedup:
return value
if "content-type" in headers:
kw["mimetype"] = headers["content-type"]
r = flask.Response(**kw)
for h in headers:
r.headers[h] = headers[h]
return r
if not top_level:
return value
else:
# Build response, adding collected http headers:
headers = []
kw = {"response": value, "status": 200}
if g.zrequest:
headers = g.zrequest.RESPONSE.headers
if not headers:
# no customized header, speedup:
return value
if "content-type" in headers:
kw["mimetype"] = headers["content-type"]
r = flask.Response(**kw)
for h in headers:
r.headers[h] = headers[h]
return r
return scodoc7func_decorator
return scodoc7func_decorator
return s7_decorator
# Le "context" de ScoDoc7
@ -193,3 +201,6 @@ class ScoDoc7Context(object):
def __init__(self, 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
context = None
@bp.route("/")
@bp.route("/index")
@ -47,7 +49,7 @@ D = {"count": 0}
@bp.route("/zopefunction", methods=["POST", "GET"])
@login_required
@scodoc7func
@scodoc7func(context)
def a_zope_function(y, x="defaut", REQUEST=None):
"""Une fonction typique de ScoDoc7"""
H = get_request_infos() + [
@ -64,7 +66,7 @@ def a_zope_function(y, x="defaut", REQUEST=None):
@bp.route("/zopeform_get")
@scodoc7func
@scodoc7func(context)
def a_zope_form_get(REQUEST=None):
H = [
"""<h2>Formulaire GET</h2>
@ -81,7 +83,7 @@ def a_zope_form_get(REQUEST=None):
@bp.route("/zopeform_post")
@scodoc7func
@scodoc7func(context)
def a_zope_form_post(REQUEST=None):
H = [
"""<h2>Formulaire POST</h2>
@ -98,7 +100,7 @@ def a_zope_form_post(REQUEST=None):
@bp.route("/ScoDoc/<dept_id>/Scolarite/Notes/formsemestre_status")
@scodoc7func
@scodoc7func(context)
def formsemestre_status(dept_id=None, formsemestre_id=None, REQUEST=None):
"""Essai méthode de département
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
#
# --------------------------------------------------------------------
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")
GetDBConnexion = ndb.GetDBConnexion
@ -780,7 +775,9 @@ REQUEST.URL0=%s<br/>
# -------------------------- INFOS SUR ETUDIANTS --------------------------
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
On peut specifier etudid
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine

View File

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

View File

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

View File

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

View File

@ -3,6 +3,9 @@
"""
from flask import Blueprint
scolar_bp = Blueprint("scolar", __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,
}

20
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"))
class Config(object):
"""General configution. Mostly loaded from environment via .env"""
class ConfigClass(object):
"""General configuration. Mostly loaded from environment via .env"""
SECRET_KEY = os.environ.get("SECRET_KEY") or "un-grand-secret-introuvable"
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
LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT")
@ -28,4 +29,15 @@ class Config(object):
SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL")
BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
# 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-SQLAlchemy==2.4.4
Flask-WTF==0.14.3
icalendar==4.0.7
idna==2.10
itsdangerous==1.1.0
jaxml==3.2
@ -24,13 +25,17 @@ MarkupSafe==1.1.1
Pillow==6.2.2
pkg-resources==0.0.0
psycopg2==2.8.6
pyExcelerator==0.6.3a0
PyJWT==1.7.1
PyRSS2Gen==1.1
python-dateutil==2.8.1
python-dotenv==0.15.0
python-editor==1.0.4
pytz==2021.1
reportlab==3.5.59
six==1.15.0
SQLAlchemy==1.3.23
stripogram==1.5
typing==3.7.4.3
visitor==0.1.3
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()