Fix: scodoc_dept : API

This commit is contained in:
Emmanuel Viennet 2022-07-26 09:00:48 +02:00
parent 10e0aa28b2
commit 6632fce008
9 changed files with 86 additions and 39 deletions

View File

@ -250,6 +250,7 @@ def create_app(config_class=DevConfig):
from app.views import users_bp from app.views import users_bp
from app.views import absences_bp from app.views import absences_bp
from app.api import bp as api_bp from app.api import bp as api_bp
from app.api import api_web_bp as api_web_bp
# https://scodoc.fr/ScoDoc # https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp) app.register_blueprint(scodoc_bp)
@ -264,6 +265,8 @@ def create_app(config_class=DevConfig):
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences" absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
) )
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/apiweb")
scodoc_log_formatter = LogRequestFormatter( scodoc_log_formatter = LogRequestFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s: %(message)s" "%(levelname)s: %(message)s"
@ -351,7 +354,7 @@ def create_app(config_class=DevConfig):
return app return app
def set_sco_dept(scodoc_dept: str): def set_sco_dept(scodoc_dept: str, open_cnx=True):
"""Set global g object to given dept and open db connection if needed""" """Set global g object to given dept and open db connection if needed"""
# Check that dept exists # Check that dept exists
try: try:
@ -362,7 +365,7 @@ def set_sco_dept(scodoc_dept: str):
raise ScoValueError(f"Invalid dept: {scodoc_dept}") raise ScoValueError(f"Invalid dept: {scodoc_dept}")
g.scodoc_dept = scodoc_dept # l'acronyme g.scodoc_dept = scodoc_dept # l'acronyme
g.scodoc_dept_id = dept.id # l'id g.scodoc_dept_id = dept.id # l'id
if not hasattr(g, "db_conn"): if open_cnx and not hasattr(g, "db_conn"):
ndb.open_db_connection() ndb.open_db_connection()
if not hasattr(g, "stored_get_formsemestre"): if not hasattr(g, "stored_get_formsemestre"):
g.stored_get_formsemestre = {} g.stored_get_formsemestre = {}

View File

@ -5,6 +5,7 @@ from flask import Blueprint
from flask import request from flask import request
bp = Blueprint("api", __name__) bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__)
def requested_format(default_format="json", allowed_formats=None): def requested_format(default_format="json", allowed_formats=None):

View File

@ -33,7 +33,9 @@ from flask_login import current_user
from app import log from app import log
from app.auth.models import User from app.auth.models import User
from app.api import bp, api_web_bp
from app.api.errors import error_response from app.api.errors import error_response
from app.decorators import scodoc, permission_required
basic_auth = HTTPBasicAuth() basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth() token_auth = HTTPTokenAuth()
@ -42,7 +44,7 @@ token_auth = HTTPTokenAuth()
@basic_auth.verify_password @basic_auth.verify_password
def verify_password(username, password): def verify_password(username, password):
"Verify password for this user" "Verify password for this user"
user = User.query.filter_by(user_name=username).first() user: User = User.query.filter_by(user_name=username).first()
if user and user.check_password(password): if user and user.check_password(password):
g.current_user = user g.current_user = user
# note: est aussi basic_auth.current_user() # note: est aussi basic_auth.current_user()
@ -85,7 +87,6 @@ def token_permission_required(permission):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
# token_auth.login_required()
current_user = basic_auth.current_user() current_user = basic_auth.current_user()
if not current_user or not current_user.has_permission(permission, None): if not current_user or not current_user.has_permission(permission, None):
if current_user: if current_user:
@ -95,6 +96,8 @@ def token_permission_required(permission):
log(message) log(message)
# raise werkzeug.exceptions.Forbidden(description=message) # raise werkzeug.exceptions.Forbidden(description=message)
return error_response(403, message=None) return error_response(403, message=None)
if not hasattr(g, "scodoc_dept"):
g.scodoc_dept = None
return f(*args, **kwargs) return f(*args, **kwargs)
# return decorated_function(token_auth.login_required()) # return decorated_function(token_auth.login_required())
@ -125,3 +128,21 @@ def permission_required_api(permission_web, permission_api):
return decorated_function return decorated_function
return decorator return decorator
def web_publish(route, function, permission, methods=("GET",)):
"""Declare a route for a python function protected by permission
using web http cookie
"""
return api_web_bp.route(route, methods=methods)(
scodoc(permission_required(permission)(function))
)
def api_publish(route, function, permission, methods=("GET",)):
"""Declare a route for a python function protected by permission
using API token
"""
return bp.route(route, methods=methods)(
token_auth.login_required(token_permission_required(permission)(function))
)

View File

@ -15,8 +15,9 @@ from sqlalchemy import or_
import app import app
from app.api import bp from app.api import bp
from app.api.errors import error_response from app.api.errors import error_response
from app.api.auth import permission_required_api from app.api.auth import permission_required_api, api_publish, web_publish
from app.api import tools from app.api import tools
from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.models import Departement, FormSemestreInscription, FormSemestre, Identite
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -24,6 +25,17 @@ from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
def api_function(arg: int):
"""Une fonction quelconque de l'API"""
# u = current_user
# dept = g.scodoc_dept # peut être None si accès API
return jsonify({"current_user": current_user.to_dict(), "dept": g.scodoc_dept})
api_publish("/api_function/<int:arg>", api_function, Permission.APIView)
web_publish("/api_function/<int:arg>", api_function, Permission.ScoView)
@bp.route("/etudiants/courants", defaults={"long": False}) @bp.route("/etudiants/courants", defaults={"long": False})
@bp.route("/etudiants/courants/long", defaults={"long": True}) @bp.route("/etudiants/courants/long", defaults={"long": True})
@permission_required_api(Permission.ScoView, Permission.APIView) @permission_required_api(Permission.ScoView, Permission.APIView)
@ -164,6 +176,9 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.). été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
""" """
allowed_depts = current_user.get_depts_with_permission(
Permission.APIView | Permission.ScoView
)
if etudid is not None: if etudid is not None:
query = Identite.query.filter_by(id=etudid) query = Identite.query.filter_by(id=etudid)
elif nip is not None: elif nip is not None:
@ -175,7 +190,11 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
404, 404,
message="parametre manquant", message="parametre manquant",
) )
if not None in allowed_depts:
# restreint aux départements autorisés:
etuds = etuds.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
return jsonify([etud.to_dict_bul(include_urls=False) for etud in query]) return jsonify([etud.to_dict_bul(include_urls=False) for etud in query])

View File

@ -182,6 +182,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
for g in groups: for g in groups:
g.etuds.remove(etud) g.etuds.remove(etud)
db.session.commit() db.session.commit()
app.set_sco_dept(partition.formsemestre.departement.acronym)
sco_cache.invalidate_formsemestre(partition.formsemestre_id) sco_cache.invalidate_formsemestre(partition.formsemestre_id)
return jsonify({"partition_id": partition_id, "etudid": etudid}) return jsonify({"partition_id": partition_id, "etudid": etudid})
@ -319,6 +320,7 @@ def formsemestre_order_partitions(formsemestre_id: int):
p.numero = numero p.numero = numero
db.session.add(p) db.session.add(p)
db.session.commit() db.session.commit()
app.set_sco_dept(formsemestre.departement.acronym)
sco_cache.invalidate_formsemestre(formsemestre_id) sco_cache.invalidate_formsemestre(formsemestre_id)
return jsonify(formsemestre.to_dict()) return jsonify(formsemestre.to_dict())
@ -343,6 +345,7 @@ def partition_order_groups(partition_id: int):
group.numero = numero group.numero = numero
db.session.add(group) db.session.add(group)
db.session.commit() db.session.commit()
app.set_sco_dept(partition.formsemestre.departement.acronym)
sco_cache.invalidate_formsemestre(partition.formsemestre_id) sco_cache.invalidate_formsemestre(partition.formsemestre_id)
return jsonify(partition.to_dict(with_groups=True)) return jsonify(partition.to_dict(with_groups=True))

View File

@ -6,11 +6,13 @@
"""ScoDoc 9 API : outils """ScoDoc 9 API : outils
""" """
from sqlalchemy import desc from flask_login import current_user
from sqlalchemy import desc, or_
from app import models from app import models
from app.api.errors import error_response from app.api.errors import error_response
from app.models import Identite, Admission from app.models import Departement, Identite, Admission
from app.scodoc.sco_permissions import Permission
def get_etud(etudid=None, nip=None, ine=None) -> models.Identite: def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
@ -24,8 +26,15 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
Return None si étudiant inexistant. Return None si étudiant inexistant.
""" """
allowed_depts = current_user.get_depts_with_permission(
Permission.APIView | Permission.ScoView
)
if etudid is not None: if etudid is not None:
return Identite.query.get(etudid) etud: Identite = Identite.query.get(etudid)
if (None in allowed_depts) or etud.departement.acronym in allowed_depts:
return etud
return None # accès interdit => pas d'étudiant
if nip is not None: if nip is not None:
query = Identite.query.filter_by(code_nip=nip) query = Identite.query.filter_by(code_nip=nip)
@ -36,4 +45,9 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
404, 404,
message="parametre manquant", message="parametre manquant",
) )
if None not in allowed_depts:
# restreint aux départements autorisés:
etuds = etuds.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
return query.join(Admission).order_by(desc(Admission.annee)).first() return query.join(Admission).order_by(desc(Admission.annee)).first()

View File

@ -53,16 +53,18 @@ import traceback
from flask import g from flask import g
import app
from app import log from app import log
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException
CACHE = None # set in app.__init__.py CACHE = None # set in app.__init__.py
class ScoDocCache: class ScoDocCache:
"""Cache for ScoDoc objects. """Cache for ScoDoc objects.
keys are prefixed by the current departement. keys are prefixed by the current departement: g.scodoc_dept MUST be set.
""" """
timeout = None # ttl, infinite by default timeout = None # ttl, infinite by default
@ -240,6 +242,15 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if getattr(g, "defer_cache_invalidation", 0) > 0: if getattr(g, "defer_cache_invalidation", 0) > 0:
g.sem_to_invalidate.add(formsemestre_id) g.sem_to_invalidate.add(formsemestre_id)
return return
if getattr(g, "scodoc_dept") is None:
# appel via API ou tests sans dept:
formsemestre = None
if formsemestre_id:
formsemestre = FormSemestre.query.get(formsemestre_id)
if formsemestre is None:
raise ScoException("invalidate_formsemestre: departement must be set")
app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False)
if formsemestre_id is None: if formsemestre_id is None:
# clear all caches # clear all caches
log( log(

View File

@ -153,34 +153,6 @@ def sco_publish(route, function, permission, methods=("GET",)):
) )
# --------------------- Quelques essais élémentaires:
# @bp.route("/essai")
# @scodoc
# @permission_required(Permission.ScoView)
# @scodoc7func
# def essai():
# return essai_()
# def essai_():
# return "<html><body><h2>essai !</h2><p>%s</p></body></html>" % ()
# def essai2():
# err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
# <p class="help">Il faut d'abord supprimer le semestre. Mais il est peut être préférable de
# laisser ce programme intact et d'en créer une nouvelle version pour la modifier.
# </p>
# <a href="url_for('notes.ue_table', scodoc-dept=g.scodoc_dept, formation_id='XXX')">reprendre</a>
# """
# raise ScoGenError(err_page)
# # raise ScoGenError("une erreur banale")
# return essai_("sans request")
# sco_publish("/essai2", essai2, Permission.ScoImplement)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# #
# Notes/ methods # Notes/ methods

View File

@ -169,6 +169,7 @@ pp(partitions)
POST_JSON(f"/group/5559/delete") POST_JSON(f"/group/5559/delete")
POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}) POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"})
# --------- XXX à passer en dans les tests unitaires
# 1- Crée une partition, puis la change de nom # 1- Crée une partition, puis la change de nom
js = POST_JSON( js = POST_JSON(
f"/formsemestre/{formsemestre_id}/partition/create", f"/formsemestre/{formsemestre_id}/partition/create",
@ -192,8 +193,10 @@ POST_JSON(f"/group/{group_id}/set_etudiant/{etudid}")
# 4- retire du groupe # 4- retire du groupe
POST_JSON(f"/group/{group_id}/remove_etudiant/{etudid}") POST_JSON(f"/group/{group_id}/remove_etudiant/{etudid}")
# Suppression # 5- Suppression
POST_JSON(f"/partition/{partition_id}/delete") POST_JSON(f"/partition/{partition_id}/delete")
# ------
# #
POST_JSON( POST_JSON(