ScoDoc/app/api/formations.py

578 lines
18 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
ScoDoc 9 API : accès aux 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)
"""
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 ]
"""
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/<int:formation_id>")
@api_web_bp.route("/formation/<int:formation_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formation_by_id(formation_id: int):
"""
La formation d'id donné
formation_id : l'id d'une formation
Exemple de résultat :
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 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/<int:formation_id>/export",
defaults={"export_ids": False},
)
@bp.route(
"/formation/<int:formation_id>/export_with_ids",
defaults={"export_ids": True},
)
@api_web_bp.route(
"/formation/<int:formation_id>/export",
defaults={"export_ids": False},
)
@api_web_bp.route(
"/formation/<int:formation_id>/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
formation_id : l'id d'une formation
export_ids : True ou False, si l'on veut ou non exporter les ids
Exemple de résultat :
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 1,
"ue": [
{
"acronyme": "RT1.1",
"numero": 1,
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"type": 0,
"ue_code": "UCOD11",
"ects": 12.0,
"is_external": false,
"code_apogee": "",
"coefficient": 0.0,
"semestre_idx": 1,
"color": "#B80004",
"reference": 1,
"matiere": [
{
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"numero": 1,
"module": [
{
"titre": "Initiation aux r\u00e9seaux informatiques",
"abbrev": "Init aux r\u00e9seaux informatiques",
"code": "R101",
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"coefficient": 1.0,
"ects": "",
"semestre_id": 1,
"numero": 10,
"code_apogee": "",
"module_type": 2,
"coefficients": [
{
"ue_reference": "1",
"coef": "12.0"
},
{
"ue_reference": "2",
"coef": "4.0"
},
{
"ue_reference": "3",
"coef": "4.0"
}
]
},
{
"titre": "Se sensibiliser \u00e0 l&apos;hygi\u00e8ne informatique...",
"abbrev": "Hygi\u00e8ne informatique",
"code": "SAE11",
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"coefficient": 1.0,
"ects": "",
"semestre_id": 1,
"numero": 10,
"code_apogee": "",
"module_type": 3,
"coefficients": [
{
"ue_reference": "1",
"coef": "16.0"
}
]
},
...
]
},
...
]
},
]
}
"""
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/<int:formation_id>/referentiel_competences")
@api_web_bp.route("/formation/<int:formation_id>/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
formation_id : l'id d'une formation
return null si pas de référentiel associé.
"""
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/<int:ue_id>/set_parcours", methods=["POST"])
@api_web_bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def set_ue_parcours(ue_id: int):
"""Associe UE et parcours BUT.
La liste des ids de parcours est passée en argument JSON.
JSON arg: [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"set_ue_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/<int:ue_id>/assoc_niveau/<int:niveau_id>",
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def assoc_ue_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/<int:ue_id>/desassoc_niveau",
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/desassoc_niveau",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def desassoc_ue_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/<int:ue_id>", methods=["GET"])
@api_web_bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
def get_ue(ue_id: int):
"""Renvoie l'UE"""
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/<int:module_id>", methods=["GET"])
@api_web_bp.route("/formation/module/<int:module_id>", methods=["GET"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
def get_module(module_id: int):
"""Renvoie le module"""
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("/ue/set_code_apogee", methods=["POST"])
@bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
)
@bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/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 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/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
methods=["POST"],
)
@bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
defaults={"code_apogee": ""},
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/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 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/<int:module_id>/set_code_apogee/<string:code_apogee>",
methods=["POST"],
)
@api_web_bp.route(
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
methods=["POST"],
)
@bp.route(
"/formation/module/<int:module_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
)
@api_web_bp.route(
"/formation/module/<int:module_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
def 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 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 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/<int:module_id>/edit",
methods=["POST"],
)
@api_web_bp.route(
"/formation/module/<int:module_id>/edit",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def 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/<int:ue_id>/edit",
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/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