ScoDoc/app/api/formsemestres.py

585 lines
20 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
ScoDoc 9 API : accès aux formsemestres
"""
from operator import attrgetter, itemgetter
from flask import g, make_response, request
from flask_json import as_json
from flask_login import current_user, login_required
import app
from app import db
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
from app.decorators import scodoc, permission_required
from app.scodoc.sco_utils import json_error
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_compat import NotesTableCompat
from app.models import (
Departement,
Evaluation,
FormSemestre,
FormSemestreEtape,
FormSemestreInscription,
Identite,
ModuleImpl,
NotesNotes,
)
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
from app.scodoc import sco_edt_cal
from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import ModuleType
import app.scodoc.sco_utils as scu
from app.tables.recap import TableRecap
@bp.route("/formsemestre/<int:formsemestre_id>")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_infos(formsemestre_id: int):
"""
Information sur le formsemestre indiqué.
formsemestre_id : l'id du formsemestre
Exemple de résultat :
{
"block_moyennes": false,
"bul_bgcolor": "white",
"bul_hide_xml": false,
"date_debut_iso": "2021-09-01",
"date_debut": "01/09/2021",
"date_fin_iso": "2022-08-31",
"date_fin": "31/08/2022",
"dept_id": 1,
"elt_annee_apo": null,
"elt_sem_apo": null,
"ens_can_edit_eval": false,
"etat": true,
"formation_id": 1,
"formsemestre_id": 1,
"gestion_compensation": false,
"gestion_semestrielle": false,
"id": 1,
"modalite": "FI",
"resp_can_change_ens": true,
"resp_can_edit": false,
"responsables": [1, 99], // uids
"scodoc7_id": null,
"semestre_id": 1,
"titre_formation" : "BUT GEA",
"titre_num": "BUT GEA semestre 1",
"titre": "BUT GEA",
}
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
return formsemestre.to_dict_api()
@bp.route("/formsemestres/query")
@api_web_bp.route("/formsemestres/query")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestres_query():
"""
Retourne les formsemestres filtrés par
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
etape_apo : un code étape apogée
annee_scolaire : année de début de l'année scolaire
dept_acronym : acronyme du département (eg "RT")
dept_id : id du département
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
etat: 0 si verrouillé, 1 sinon
"""
etape_apo = request.args.get("etape_apo")
annee_scolaire = request.args.get("annee_scolaire")
dept_acronym = request.args.get("dept_acronym")
dept_id = request.args.get("dept_id")
etat = request.args.get("etat")
nip = request.args.get("nip")
ine = request.args.get("ine")
formsemestres = FormSemestre.query
if g.scodoc_dept:
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
if annee_scolaire is not None:
try:
annee_scolaire_int = int(annee_scolaire)
except ValueError:
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
debut_annee = scu.date_debut_annee_scolaire(annee_scolaire_int)
fin_annee = scu.date_fin_annee_scolaire(annee_scolaire_int)
formsemestres = formsemestres.filter(
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
)
if etat is not None:
try:
etat = bool(int(etat))
except ValueError:
return json_error(404, "invalid etat: integer expected")
formsemestres = formsemestres.filter_by(etat=etat)
if dept_acronym is not None:
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
if dept_id is not None:
try:
dept_id = int(dept_id)
except ValueError:
return json_error(404, "invalid dept_id: integer expected")
formsemestres = formsemestres.filter_by(dept_id=dept_id)
if etape_apo is not None:
formsemestres = formsemestres.join(FormSemestreEtape).filter(
FormSemestreEtape.etape_apo == etape_apo
)
inscr_joined = False
if nip is not None:
formsemestres = (
formsemestres.join(FormSemestreInscription)
.join(Identite)
.filter_by(code_nip=nip)
)
inscr_joined = True
if ine is not None:
if not inscr_joined:
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
formsemestres = formsemestres.filter_by(code_ine=ine)
return [
formsemestre.to_dict_api()
for formsemestre in formsemestres.order_by(
FormSemestre.date_debut.desc(),
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
)
]
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def bulletins(formsemestre_id: int, version: str = "long"):
"""
Retourne les bulletins d'un formsemestre donné
formsemestre_id : l'id d'un formesemestre
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first()
if formsemestre is None:
return json_error(404, "formsemestre non trouve")
app.set_sco_dept(formsemestre.departement.acronym)
data = []
for etu in formsemestre.etuds:
bul_etu = get_formsemestre_bulletin_etud_json(
formsemestre, etu, version=version
)
data.append(bul_etu.json)
return data
@bp.route("/formsemestre/<int:formsemestre_id>/programme")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/programme")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_programme(formsemestre_id: int):
"""
Retourne la liste des UEs, ressources et SAEs d'un semestre
formsemestre_id : l'id d'un formsemestre
Exemple de résultat :
{
"ues": [
{
"type": 0,
"formation_id": 1,
"ue_code": "UCOD11",
"id": 1,
"ects": 12.0,
"acronyme": "RT1.1",
"is_external": false,
"numero": 1,
"code_apogee": "",
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"coefficient": 0.0,
"semestre_idx": 1,
"color": "#B80004",
"ue_id": 1
},
...
],
"ressources": [
{
"ens": [ 10, 18 ],
"formsemestre_id": 1,
"id": 15,
"module": {
"abbrev": "Programmer",
"code": "SAE15",
"code_apogee": "V7GOP",
"coefficient": 1.0,
"formation_id": 1,
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"id": 15,
"matiere_id": 3,
"module_id": 15,
"module_type": 3,
"numero": 50,
"semestre_id": 1,
"titre": "Programmer en Python",
"ue_id": 3
},
"module_id": 15,
"moduleimpl_id": 15,
"responsable_id": 2
},
...
],
"saes": [
{
...
},
...
],
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
}
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
ues = formsemestre.get_ues()
m_list = {
ModuleType.RESSOURCE: [],
ModuleType.SAE: [],
ModuleType.STANDARD: [],
ModuleType.MALUS: [],
}
for modimpl in formsemestre.modimpls_sorted:
d = modimpl.to_dict(convert_objects=True)
m_list[modimpl.module.module_type].append(d)
return {
"ues": [ue.to_dict(convert_objects=True) for ue in ues],
"ressources": m_list[ModuleType.RESSOURCE],
"saes": m_list[ModuleType.SAE],
"modules": m_list[ModuleType.STANDARD],
"malus": m_list[ModuleType.MALUS],
}
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants",
defaults={"with_query": False, "long": False},
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/long",
defaults={"with_query": False, "long": True},
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/query",
defaults={"with_query": True, "long": False},
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/long/query",
defaults={"with_query": True, "long": True},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants",
defaults={"with_query": False, "long": False},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/long",
defaults={"with_query": False, "long": True},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/query",
defaults={"with_query": True, "long": False},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/long/query",
defaults={"with_query": True, "long": True},
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_etudiants(
formsemestre_id: int, with_query: bool = False, long: bool = False
):
"""Étudiants d'un formsemestre."""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
if with_query:
etat = request.args.get("etat")
if etat is not None:
etat = {
"actifs": scu.INSCRIT,
"demissionnaires": scu.DEMISSION,
"defaillants": scu.DEF,
}.get(etat, etat)
inscriptions = [
ins for ins in formsemestre.inscriptions if ins.etat == etat
]
else:
inscriptions = formsemestre.inscriptions
else:
inscriptions = formsemestre.inscriptions
if long:
restrict = not current_user.has_permission(Permission.ViewEtudData)
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
else:
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
# Ajout des groupes de chaque étudiants
# XXX A REVOIR: trop inefficace !
for etud in etuds:
etud["groups"] = sco_groups.get_etud_groups(
etud["id"], formsemestre_id, exclude_default=True
)
return sorted(etuds, key=itemgetter("sort_key"))
# retrait (temporaire ? à discuter)
# @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
# @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
# @login_required
# @scodoc
# @permission_required(Permission.ScoView)
# @as_json
# def etat_evals(formsemestre_id: int):
# """
# Informations sur l'état des évaluations d'un formsemestre.
# formsemestre_id : l'id d'un semestre
# Exemple de résultat :
# [
# {
# "id": 1, // moduleimpl_id
# "titre": "Initiation aux réseaux informatiques",
# "evaluations": [
# {
# "id": 1,
# "description": null,
# "datetime_epreuve": null,
# "heure_fin": "09:00:00",
# "coefficient": "02.00"
# "is_complete": true,
# "nb_inscrits": 16,
# "nb_manquantes": 0,
# "ABS": 0,
# "ATT": 0,
# "EXC": 0,
# "saisie_notes": {
# "datetime_debut": "2021-09-11T00:00:00+02:00",
# "datetime_fin": "2022-08-25T00:00:00+02:00",
# "datetime_mediane": "2022-03-19T00:00:00+01:00"
# }
# },
# ...
# ]
# },
# ]
# """
# query = FormSemestre.query.filter_by(id=formsemestre_id)
# if g.scodoc_dept:
# query = query.filter_by(dept_id=g.scodoc_dept_id)
# formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
# app.set_sco_dept(formsemestre.departement.acronym)
# nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# result = []
# for modimpl_id in nt.modimpls_results:
# modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
# modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
# modimpl_dict = modimpl.to_dict(convert_objects=True)
# list_eval = []
# for evaluation_id in modimpl_results.evaluations_etat:
# eval_etat = modimpl_results.evaluations_etat[evaluation_id]
# evaluation = Evaluation.query.get_or_404(evaluation_id)
# eval_dict = evaluation.to_dict_api()
# eval_dict["etat"] = eval_etat.to_dict()
# eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
# eval_dict["nb_notes_manquantes"] = len(
# modimpl_results.evals_etudids_sans_note[evaluation.id]
# )
# eval_dict["nb_notes_abs"] = sum(
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
# )
# eval_dict["nb_notes_att"] = eval_etat.nb_attente
# eval_dict["nb_notes_exc"] = sum(
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
# )
# # Récupération de toutes les notes de l'évaluation
# # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
# notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
# date_debut = None
# date_fin = None
# date_mediane = None
# # Si il y a plus d'une note saisie pour l'évaluation
# if len(notes) >= 1:
# # Tri des notes en fonction de leurs dates
# notes_sorted = sorted(notes, key=attrgetter("date"))
# date_debut = notes_sorted[0].date
# date_fin = notes_sorted[-1].date
# # Note médiane
# date_mediane = notes_sorted[len(notes_sorted) // 2].date
# eval_dict["saisie_notes"] = {
# "datetime_debut": date_debut.isoformat()
# if date_debut is not None
# else None,
# "datetime_fin": date_fin.isoformat() if date_fin is not None else None,
# "datetime_mediane": date_mediane.isoformat()
# if date_mediane is not None
# else None,
# }
# list_eval.append(eval_dict)
# modimpl_dict["evaluations"] = list_eval
# result.append(modimpl_dict)
# return result
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/resultats")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_resultat(formsemestre_id: int):
"""Tableau récapitulatif des résultats
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
"""
format_spec = request.args.get("format", None)
if format_spec is not None and format_spec != "raw":
return json_error(API_CLIENT_ERROR, "invalid format specification")
convert_values = format_spec != "raw"
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table = TableRecap(
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
)
# Supprime les champs inutiles (mise en forme)
rows = table.to_list()
# Ajoute le groupe de chaque partition:
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
for row in rows:
row["partitions"] = etud_groups.get(row["etudid"], {})
return rows
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def get_groups_auto_assignment(formsemestre_id: int):
"""rend les données"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
response = make_response(formsemestre.groups_auto_assignment_data or b"")
response.headers["Content-Type"] = scu.JSON_MIMETYPE
return response
@bp.route(
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def save_groups_auto_assignment(formsemestre_id: int):
"""enregistre les données"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
return json_error(413, "data too large")
formsemestre.groups_auto_assignment_data = request.data
db.session.add(formsemestre)
db.session.commit()
@bp.route("/formsemestre/<int:formsemestre_id>/edt")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edt")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_edt(formsemestre_id: int):
"""l'emploi du temps du semestre.
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
group_ids permet de filtrer sur les groupes ScoDoc.
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
group_ids = request.args.getlist("group_ids", int)
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
return sco_edt_cal.formsemestre_edt_dict(
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
)