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,13 +79,37 @@ 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.
""" """
def s7_decorator(func):
@wraps(func) @wraps(func)
def scodoc7func_decorator(*args, **kwargs): def scodoc7func_decorator(*args, **kwargs):
"""Decorator allowing legacy Zope published methods to be called via Flask """Decorator allowing legacy Zope published methods to be called via Flask
@ -119,11 +123,11 @@ def scodoc7func(func):
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")
if top_level:
g.zrequest = None g.zrequest = None
top_level = True
# #
if "scodoc_dept" in kwargs: if "scodoc_dept" in kwargs:
g.scodoc_dept = kwargs["scodoc_dept"] g.scodoc_dept = kwargs["scodoc_dept"]
@ -147,6 +151,8 @@ def scodoc7func(func):
for arg_name in arg_names: for arg_name in arg_names:
if arg_name == "REQUEST": # special case if arg_name == "REQUEST": # special case
pos_arg_values.append(REQUEST) pos_arg_values.append(REQUEST)
elif arg_name == "context":
pos_arg_values.append(context)
else: else:
pos_arg_values.append(req_args[arg_name]) pos_arg_values.append(req_args[arg_name])
current_app.logger.info("pos_arg_values=%s" % pos_arg_values) current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
@ -184,6 +190,8 @@ def scodoc7func(func):
return scodoc7func_decorator return scodoc7func_decorator
return s7_decorator
# Le "context" de ScoDoc7 # Le "context" de ScoDoc7
class ScoDoc7Context(object): class ScoDoc7Context(object):
@ -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()