############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ ScoDoc 9 API : accès aux formations CATEGORY -------- Formations """ from flask import flash, g, request from flask_json import as_json from flask_login import login_required import app from app import db, log from app.api import api_bp as bp, api_web_bp from app.api import api_permission_required as permission_required from app.decorators import scodoc from app.models import APO_CODE_STR_LEN from app.scodoc.sco_utils import json_error from app.models import ( ApcNiveau, ApcParcours, Formation, Module, UniteEns, ) from app.scodoc import sco_formations from app.scodoc.sco_permissions import Permission @bp.route("/formations") @api_web_bp.route("/formations") @login_required @scodoc @permission_required(Permission.ScoView) @as_json def formations(): """ Retourne la liste de toutes les formations (tous départements, sauf si route départementale). SAMPLES ------- /formations; """ query = Formation.query if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) return [d.to_dict() for d in query] @bp.route("/formations_ids") @api_web_bp.route("/formations_ids") @login_required @scodoc @permission_required(Permission.ScoView) @as_json def formations_ids(): """ Retourne la liste de toutes les id de formations (tous départements, ou du département indiqué dans la route) Exemple de résultat : `[ 17, 99, 32 ]`. SAMPLES ------- /formations_ids; """ query = Formation.query if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) return [d.id for d in query] @bp.route("/formation/") @api_web_bp.route("/formation/") @login_required @scodoc @permission_required(Permission.ScoView) @as_json def formation_get(formation_id: int): """ La formation d'id donné. SAMPLES ------- /formation/1; """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) return query.first_or_404().to_dict() @bp.route( "/formation//export", defaults={"export_ids": False}, ) @bp.route( "/formation//export_with_ids", defaults={"export_ids": True}, ) @api_web_bp.route( "/formation//export", defaults={"export_ids": False}, ) @api_web_bp.route( "/formation//export_with_ids", defaults={"export_ids": True}, ) @login_required @scodoc @permission_required(Permission.ScoView) @as_json def formation_export_by_formation_id(formation_id: int, export_ids=False): """ Retourne la formation, avec UE, matières, modules PARAMS ------ formation_id : l'id d'une formation export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation. SAMPLES ------- /formation/1/export """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formation = query.first_or_404(formation_id) app.set_sco_dept(formation.departement.acronym) try: data = sco_formations.formation_export(formation_id, export_ids) except ValueError: return json_error(500, message="Erreur inconnue") return data @bp.route("/formation//referentiel_competences") @api_web_bp.route("/formation//referentiel_competences") @login_required @scodoc @permission_required(Permission.ScoView) @as_json def referentiel_competences(formation_id: int): """ Retourne le référentiel de compétences de la formation ou null si pas de référentiel associé. SAMPLES ------- /formation/1/referentiel_competences; """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formation = query.first_or_404(formation_id) if formation.referentiel_competence is None: return None return formation.referentiel_competence.to_dict() @bp.route("/formation/ue//set_parcours", methods=["POST"]) @api_web_bp.route("/formation/ue//set_parcours", methods=["POST"]) @login_required @scodoc @permission_required(Permission.EditFormation) @as_json def ue_set_parcours(ue_id: int): """Associe UE et parcours BUT. La liste des ids de parcours est passée en argument JSON. DATA ---- ```json [ parcour_id1, parcour_id2, ... ] ``` """ query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue: UniteEns = query.first_or_404() parcours_ids = request.get_json(force=True) or [] # may raise 400 Bad Request if parcours_ids == [""]: parcours = [] else: parcours = [ ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids ] log(f"ue_set_parcours: ue_id={ue.id} parcours_ids={parcours_ids}") ok, error_message = ue.set_parcours(parcours) if not ok: return json_error(404, error_message) return {"status": ok, "message": error_message} @bp.route( "/formation/ue//assoc_niveau/", methods=["POST"], ) @api_web_bp.route( "/formation/ue//assoc_niveau/", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) @as_json def ue_assoc_niveau(ue_id: int, niveau_id: int): """Associe l'UE au niveau de compétence.""" query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue: UniteEns = query.first_or_404() niveau: ApcNiveau = ApcNiveau.query.get_or_404(niveau_id) ok, error_message = ue.set_niveau_competence(niveau) if not ok: if g.scodoc_dept: # "usage web" flash(error_message, "error") return json_error(404, error_message) if g.scodoc_dept: # "usage web" flash(f"""{ue.acronyme} associée au niveau "{niveau.libelle}" """) return {"status": 0} @bp.route( "/formation/ue//desassoc_niveau", methods=["POST"], ) @api_web_bp.route( "/formation/ue//desassoc_niveau", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) @as_json def ue_desassoc_niveau(ue_id: int): """Désassocie cette UE de son niveau de compétence (si elle n'est pas associée, ne fait rien). """ query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue: UniteEns = query.first_or_404() ok, error_message = ue.set_niveau_competence(None) if not ok: if g.scodoc_dept: # "usage web" flash(error_message, "error") return json_error(404, error_message) if g.scodoc_dept: # "usage web" flash(f"UE {ue.acronyme} dé-associée") return {"status": 0} @bp.route("/formation/ue/", methods=["GET"]) @api_web_bp.route("/formation/ue/", methods=["GET"]) @login_required @scodoc @permission_required(Permission.ScoView) def get_ue(ue_id: int): """Renvoie l'UE. SAMPLES ------- /formation/ue/1; """ query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue: UniteEns = query.first_or_404() return ue.to_dict(convert_objects=True) @bp.route("/formation/module/", methods=["GET"]) @api_web_bp.route("/formation/module/", methods=["GET"]) @login_required @scodoc @permission_required(Permission.ScoView) def formation_module_get(module_id: int): """Renvoie le module. SAMPLES ------- /formation/module/1; """ query = Module.query.filter_by(id=module_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) module: Module = query.first_or_404() return module.to_dict(convert_objects=True) @bp.route("/formation/ue/set_code_apogee", methods=["POST"]) @api_web_bp.route("/formation/ue/set_code_apogee", methods=["POST"]) @bp.route( "/formation/ue//set_code_apogee/", methods=["POST"] ) @api_web_bp.route( "/formation/ue//set_code_apogee/", methods=["POST"] ) @bp.route( "/formation/ue//set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"], ) @api_web_bp.route( "/formation/ue//set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""): """Change le code Apogée de l'UE. Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. Ce changement peut être fait sur formation verrouillée. Si `ue_id` n'est pas spécifié, utilise l'argument oid du POST. Si `code_apogee` n'est pas spécifié ou vide, utilise l'argument value du POST. Le retour est une chaîne (le code enregistré), pas du json. """ if ue_id is None: ue_id = request.form.get("oid") if ue_id is None: return json_error(404, "argument oid manquant") if not code_apogee: code_apogee = request.form.get("value", "") query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue: UniteEns = query.first_or_404() code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque log(f"API ue_set_code_apogee: ue_id={ue.id} code_apogee={code_apogee}") ue.code_apogee = code_apogee db.session.add(ue) db.session.commit() return code_apogee or "" @bp.route( "/formation/ue//set_code_apogee_rcue/", methods=["POST"], ) @api_web_bp.route( "/formation/ue//set_code_apogee_rcue/", methods=["POST"], ) @bp.route( "/formation/ue//set_code_apogee_rcue", defaults={"code_apogee": ""}, methods=["POST"], ) @api_web_bp.route( "/formation/ue//set_code_apogee_rcue", defaults={"code_apogee": ""}, methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""): """Change le code Apogée du RCUE de l'UE. Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. Ce changement peut être fait sur formation verrouillée. Si code_apogee n'est pas spécifié ou vide, utilise l'argument value du POST (utilisé par `jinplace.js`) Le retour est une chaîne (le code enregistré), pas du json. """ if not code_apogee: code_apogee = request.form.get("value", "") query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue: UniteEns = query.first_or_404() code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque log(f"API ue_set_code_apogee_rcue: ue_id={ue.id} code_apogee={code_apogee}") ue.code_apogee_rcue = code_apogee db.session.add(ue) db.session.commit() return code_apogee or "" @bp.route("/formation/module/set_code_apogee", methods=["POST"]) @api_web_bp.route("/formation/module/set_code_apogee", methods=["POST"]) @bp.route( "/formation/module//set_code_apogee/", methods=["POST"], ) @api_web_bp.route( "/formation/module//set_code_apogee/", methods=["POST"], ) @bp.route( "/formation/module//set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"], ) @api_web_bp.route( "/formation/module//set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) def formation_module_set_code_apogee( module_id: int | None = None, code_apogee: str = "" ): """Change le code Apogée du module. Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. Ce changement peut être fait sur formation verrouillée. Si `module_id` n'est pas spécifié, utilise l'argument `oid` du POST. Si `code_apogee` n'est pas spécifié ou vide, utilise l'argument value du POST (utilisé par jinplace.js) Le retour est une chaîne (le code enregistré), pas du json. """ if module_id is None: module_id = request.form.get("oid") if module_id is None: return json_error(404, "argument oid manquant") if not code_apogee: code_apogee = request.form.get("value", "") query = Module.query.filter_by(id=module_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) module: Module = query.first_or_404() code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque log( f"API formation_module_set_code_apogee: module_id={module.id} code_apogee={code_apogee}" ) module.code_apogee = code_apogee db.session.add(module) db.session.commit() return code_apogee or "" @bp.route( "/formation/module//edit", methods=["POST"], ) @api_web_bp.route( "/formation/module//edit", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) @as_json def formation_module_edit(module_id: int): """Édition d'un module. Renvoie le module en json.""" query = Module.query.filter_by(id=module_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) module: Module = query.first_or_404() args = request.get_json(force=True) # may raise 400 Bad Request module.from_dict(args) db.session.commit() db.session.refresh(module) log(f"API module_edit: module_id={module.id} args={args}") r = module.to_dict(convert_objects=True, with_parcours_ids=True) return r @bp.route( "/formation/ue//edit", methods=["POST"], ) @api_web_bp.route( "/formation/ue//edit", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EditFormation) @as_json def ue_edit(ue_id: int): """Édition d'une UE. Renvoie l'UE en json.""" ue = UniteEns.get_ue(ue_id) args = request.get_json(force=True) # may raise 400 Bad Request ue.from_dict(args) db.session.commit() db.session.refresh(ue) log(f"API ue_edit: ue_id={ue.id} args={args}") r = ue.to_dict(convert_objects=True) return r