ScoDoc/app/api/formations.py

603 lines
18 KiB
Python
Raw Normal View History

##############################################################################
# ScoDoc
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
ScoDoc 9 API : accès aux formations
2024-07-24 17:34:30 +02:00
CATEGORY
--------
Formations
"""
from flask import flash, g, request
from flask_json import as_json
from flask_login import login_required
2022-05-03 13:35:17 +02:00
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,
)
2022-05-03 13:35:17 +02:00
from app.scodoc import sco_formations
2022-03-04 17:16:08 +01:00
from app.scodoc.sco_permissions import Permission
@bp.route("/formations")
@api_web_bp.route("/formations")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
2022-06-17 15:46:17 +02:00
def formations():
"""
2024-07-24 17:34:30 +02:00
Retourne la liste de toutes les formations (tous départements,
sauf si route départementale).
2022-06-17 15:46:17 +02:00
"""
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
2022-05-03 13:35:17 +02:00
def formations_ids():
"""
Retourne la liste de toutes les id de formations
(tous départements, ou du département indiqué dans la route)
2024-07-24 17:34:30 +02:00
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):
"""
2024-07-24 17:34:30 +02:00
La formation d'id donné.
Exemple de résultat :
2024-07-24 17:34:30 +02:00
```json
{
"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()
2022-05-03 13:35:17 +02:00
@bp.route(
"/formation/<int:formation_id>/export",
2022-05-03 13:35:17 +02:00
defaults={"export_ids": False},
)
@bp.route(
"/formation/<int:formation_id>/export_with_ids",
2022-05-03 13:35:17 +02:00
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
2024-07-24 17:34:30 +02:00
PARAMS
------
formation_id : l'id d'une formation
2024-07-24 17:34:30 +02:00
export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
Exemple de résultat :
2024-07-24 17:34:30 +02:00
```json
{
"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": [
{
2024-07-24 17:34:30 +02:00
"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": [
{
2024-07-24 17:34:30 +02:00
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"numero": 1,
"module": [
{
2024-07-24 17:34:30 +02:00
"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": [
{
2024-07-24 17:34:30 +02:00
"ue_reference": "1",
"coef": "12.0"
},
{
2024-07-24 17:34:30 +02:00
"ue_reference": "2",
"coef": "4.0"
},
2024-07-24 17:34:30 +02:00
{
"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"
}
]
},
...
2024-07-24 17:34:30 +02:00
]
},
2024-07-24 17:34:30 +02:00
...
]
},
]
}
```
"""
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:
2022-05-03 13:35:17 +02:00
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):
"""
2024-07-24 17:34:30 +02:00
Retourne le référentiel de compétences de la formation
ou 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
2024-07-17 14:58:49 +02:00
def ue_set_parcours(ue_id: int):
"""Associe UE et parcours BUT.
2024-07-24 17:34:30 +02:00
La liste des ids de parcours est passée en argument JSON.
2024-07-24 17:34:30 +02:00
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
]
2024-07-17 14:58:49 +02:00
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/<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
2024-07-17 14:58:49 +02:00
def ue_assoc_niveau(ue_id: int, niveau_id: int):
2024-07-24 17:34:30 +02:00
"""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
2024-07-17 14:58:49 +02:00
def ue_desassoc_niveau(ue_id: int):
"""Désassocie cette UE de son niveau de compétence
2024-07-24 17:34:30 +02:00
(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):
2024-07-24 17:34:30 +02:00
"""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)
2024-07-17 14:58:49 +02:00
def formation_module_get(module_id: int):
2024-07-24 17:34:30 +02:00
"""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"])
2024-07-17 14:58:49 +02:00
@api_web_bp.route("/formation/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.
2024-07-24 17:34:30 +02:00
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
2024-07-24 17:34:30 +02:00
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,
2024-07-24 17:34:30 +02:00
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.
2024-07-24 17:34:30 +02:00
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
2024-07-24 17:34:30 +02:00
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)
2024-07-17 14:58:49 +02:00
def formation_module_set_code_apogee(
module_id: int | None = None, code_apogee: str = ""
):
"""Change le code Apogée du module.
2024-07-24 17:34:30 +02:00
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
2024-07-24 17:34:30 +02:00
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
2024-07-18 16:54:48 +02:00
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/<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
2024-07-17 14:58:49 +02:00
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/<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