diff --git a/app/__init__.py b/app/__init__.py index 2ecd0d33a..eb9d1980a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -250,6 +250,7 @@ def create_app(config_class=DevConfig): from app.views import users_bp from app.views import absences_bp from app.api import bp as api_bp + from app.api import api_web_bp as api_web_bp # https://scodoc.fr/ScoDoc app.register_blueprint(scodoc_bp) @@ -264,6 +265,8 @@ def create_app(config_class=DevConfig): absences_bp, url_prefix="/ScoDoc//Scolarite/Absences" ) app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") + app.register_blueprint(api_web_bp, url_prefix="/ScoDoc//apiweb") + scodoc_log_formatter = LogRequestFormatter( "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" "%(levelname)s: %(message)s" @@ -351,7 +354,7 @@ def create_app(config_class=DevConfig): 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""" # Check that dept exists try: @@ -362,7 +365,7 @@ def set_sco_dept(scodoc_dept: str): raise ScoValueError(f"Invalid dept: {scodoc_dept}") g.scodoc_dept = scodoc_dept # l'acronyme 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() if not hasattr(g, "stored_get_formsemestre"): g.stored_get_formsemestre = {} diff --git a/app/api/__init__.py b/app/api/__init__.py index 9be4d3b69..45cad23ba 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -5,6 +5,7 @@ from flask import Blueprint from flask import request bp = Blueprint("api", __name__) +api_web_bp = Blueprint("apiweb", __name__) def requested_format(default_format="json", allowed_formats=None): diff --git a/app/api/auth.py b/app/api/auth.py index 8662312aa..22492620c 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -33,7 +33,9 @@ from flask_login import current_user from app import log from app.auth.models import User +from app.api import bp, api_web_bp from app.api.errors import error_response +from app.decorators import scodoc, permission_required basic_auth = HTTPBasicAuth() token_auth = HTTPTokenAuth() @@ -42,7 +44,7 @@ token_auth = HTTPTokenAuth() @basic_auth.verify_password def verify_password(username, password): "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): g.current_user = user # note: est aussi basic_auth.current_user() @@ -85,7 +87,6 @@ def token_permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): - # token_auth.login_required() current_user = basic_auth.current_user() if not current_user or not current_user.has_permission(permission, None): if current_user: @@ -95,6 +96,8 @@ def token_permission_required(permission): log(message) # raise werkzeug.exceptions.Forbidden(description=message) return error_response(403, message=None) + if not hasattr(g, "scodoc_dept"): + g.scodoc_dept = None return f(*args, **kwargs) # return decorated_function(token_auth.login_required()) @@ -125,3 +128,21 @@ def permission_required_api(permission_web, permission_api): return decorated_function 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)) + ) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index fc5de0f71..01b86b5e2 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -15,8 +15,9 @@ from sqlalchemy import or_ import app from app.api import bp 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.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins 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 +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/", api_function, Permission.APIView) +web_publish("/api_function/", api_function, Permission.ScoView) + + @bp.route("/etudiants/courants", defaults={"long": False}) @bp.route("/etudiants/courants/long", defaults={"long": True}) @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 é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: query = Identite.query.filter_by(id=etudid) elif nip is not None: @@ -175,7 +190,11 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None): 404, 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]) diff --git a/app/api/partitions.py b/app/api/partitions.py index 20b92ce30..c55ef9113 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -182,6 +182,7 @@ def partition_remove_etud(partition_id: int, etudid: int): for g in groups: g.etuds.remove(etud) db.session.commit() + app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) return jsonify({"partition_id": partition_id, "etudid": etudid}) @@ -319,6 +320,7 @@ def formsemestre_order_partitions(formsemestre_id: int): p.numero = numero db.session.add(p) db.session.commit() + app.set_sco_dept(formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) return jsonify(formsemestre.to_dict()) @@ -343,6 +345,7 @@ def partition_order_groups(partition_id: int): group.numero = numero db.session.add(group) db.session.commit() + app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) return jsonify(partition.to_dict(with_groups=True)) diff --git a/app/api/tools.py b/app/api/tools.py index d2a4d1d6c..9da42fabc 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -6,11 +6,13 @@ """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.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: @@ -24,8 +26,15 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite: Return None si étudiant inexistant. """ + allowed_depts = current_user.get_depts_with_permission( + Permission.APIView | Permission.ScoView + ) + 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: query = Identite.query.filter_by(code_nip=nip) @@ -36,4 +45,9 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite: 404, 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() diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 28e46cfc5..8cdeed439 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -53,16 +53,18 @@ import traceback from flask import g +import app from app import log from app.scodoc import notesdb as ndb from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoException CACHE = None # set in app.__init__.py class ScoDocCache: """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 @@ -240,6 +242,15 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa if getattr(g, "defer_cache_invalidation", 0) > 0: g.sem_to_invalidate.add(formsemestre_id) 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: # clear all caches log( diff --git a/app/views/notes.py b/app/views/notes.py index 111019d82..b1514ef5a 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -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 "

essai !

%s

" % () - - -# def essai2(): -# err_page = f"""

Destruction du module impossible car il est utilisé dans des semestres existants !

-#

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. -#

-# reprendre -# """ -# raise ScoGenError(err_page) -# # raise ScoGenError("une erreur banale") -# return essai_("sans request") - - -# sco_publish("/essai2", essai2, Permission.ScoImplement) - - # -------------------------------------------------------------------- # # Notes/ methods diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index b03b9ff64..c889909a3 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -169,6 +169,7 @@ pp(partitions) POST_JSON(f"/group/5559/delete") 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 js = POST_JSON( f"/formsemestre/{formsemestre_id}/partition/create", @@ -192,8 +193,10 @@ POST_JSON(f"/group/{group_id}/set_etudiant/{etudid}") # 4- retire du groupe POST_JSON(f"/group/{group_id}/remove_etudiant/{etudid}") -# Suppression +# 5- Suppression POST_JSON(f"/partition/{partition_id}/delete") +# ------ + # POST_JSON(