ScoDoc/app/scodoc/sco_bulletins_json.py

520 lines
19 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Génération du bulletin en format JSON (formations classiques)
"""
import datetime
import json
from flask import abort
from app import db, ScoDocJSONEncoder
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import but_validations
from app.models import BulAppreciations, Evaluation, Matiere, UniteEns
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import sco_assiduites
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
from app.scodoc.sco_preferences import SemPreferences
from app.scodoc.sco_xml import quote_xml_attr
# -------- Bulletin en JSON
def make_json_formsemestre_bulletinetud(
formsemestre_id: int,
etudid: int,
xml_with_decisions=False,
version="long",
force_publishing=False, # force publication meme si semestre non publie sur "portail"
) -> str:
"""Renvoie bulletin en chaine JSON"""
d = formsemestre_bulletinetud_published_dict(
formsemestre_id,
etudid,
force_publishing=force_publishing,
xml_with_decisions=xml_with_decisions,
version=version,
)
return json.dumps(d, cls=ScoDocJSONEncoder)
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
# pour simplifier le code, mais attention a la maintenance !)
#
def formsemestre_bulletinetud_published_dict(
formsemestre_id,
etudid,
force_publishing=False,
xml_nodate=False,
xml_with_decisions=False, # inclure les décisions même si non publiées
version="long",
) -> dict:
"""Dictionnaire representant les informations _publiees_ du bulletin de notes
Utilisé pour JSON des formations classiques (mais pas pour le XML, qui est deprecated).
version:
short (sans les évaluations)
long (avec les évaluations)
# non implémenté: short_mat (sans évaluations, et structuration en matières)
# long_mat (avec évaluations, et structuration en matières)
"""
from app.scodoc import sco_bulletins
with_matieres = False
if version.endswith("_mat"):
version = version[:-4] # enlève le "_mat"
with_matieres = True
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id)
etud = Identite.get_etud(etudid)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if etudid not in nt.identdict:
abort(404, "etudiant non inscrit dans ce semestre")
d = {"type": "classic", "version": "0"}
published = (not formsemestre.bul_hide_xml) or force_publishing
if xml_nodate:
docdate = ""
else:
docdate = datetime.datetime.now().isoformat()
el = {
"etudid": etudid,
"formsemestre_id": formsemestre_id,
"date": docdate,
"publie": published,
"etapes": sem["etapes"],
}
# backward compat:
if sem["etapes"]:
el["etape_apo"] = sem["etapes"][0] or ""
n = 2
for et in sem["etapes"][1:]:
el["etape_apo" + str(n)] = et or ""
n += 1
d.update(**el)
# Infos sur l'etudiant
etudinfo = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
d["etudiant"] = dict(
etudid=etudid,
code_nip=etudinfo["code_nip"],
code_ine=etudinfo["code_ine"],
nom=quote_xml_attr(etudinfo["nom"]),
prenom=quote_xml_attr(etudinfo["prenom"]),
civilite=quote_xml_attr(etudinfo["civilite_str"]),
photo_url=quote_xml_attr(sco_photos.etud_photo_url(etudinfo, fast=True)),
email=quote_xml_attr(etudinfo["email"]),
emailperso=quote_xml_attr(etudinfo["emailperso"]),
)
d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients
# Disponible pour publication ?
d["publie"] = published
if not published:
return d # stop !
etat_inscription = etud.inscription_etat(formsemestre.id)
if etat_inscription != scu.INSCRIT:
d.update(dict_decision_jury(etud, formsemestre, with_decisions=True))
return d
# Groupes:
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
partitions_etud_groups = {} # { partition_id : { etudid : group } }
for partition in partitions:
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
# Il serait préférable de factoriser et d'avoir la même section
# "semestre" que celle des bulletins BUT.
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
d["semestre"] = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": formsemestre.annee_scolaire_str(),
"numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [group.to_dict() for group in etud_groups],
}
ues_stat = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks)
moy_gen = scu.fmt_note(nt.get_etud_moy_gen(etudid))
if nt.get_moduleimpls_attente() or not prefs["bul_show_rangs"]:
# n'affiche pas le rang sur le bulletin s'il y a des
# notes en attente dans ce semestre
rang = ""
rang_gr = {}
ninscrits_gr = {}
else:
rang = str(nt.get_etud_rang(etudid))
rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups(
etudid, partitions, partitions_etud_groups, nt
)
d["note"] = dict(
value=moy_gen,
min=scu.fmt_note(nt.moy_min),
max=scu.fmt_note(nt.moy_max),
moy=scu.fmt_note(nt.moy_moy),
)
d["rang"] = dict(value=rang, ninscrits=nbetuds)
d["rang_group"] = []
if rang_gr:
for partition in partitions:
d["rang_group"].append(
dict(
group_type=partition["partition_name"],
group_name=gr_name[partition["partition_id"]],
value=rang_gr[partition["partition_id"]],
ninscrits=ninscrits_gr[partition["partition_id"]],
)
)
d["note_max"] = dict(value=20) # notes toujours sur 20
d["bonus_sport_culture"] = dict(
value=nt.bonus[etudid] if nt.bonus is not None else 0.0
)
# Liste les UE / modules /evals
d["ue"] = []
d["ue_capitalisee"] = []
for ue_st in ues_stat:
ue_id = ue_st["ue_id"]
ue_status = nt.get_etud_ue_status(etudid, ue_id)
if ue_st["ects"] is None:
ects_txt = ""
else:
ects_txt = f"{ue_st['ects']:2.3g}"
rang, effectif = nt.get_etud_ue_rang(ue_id, etudid)
u = dict(
id=ue_id,
numero=quote_xml_attr(ue_st["numero"]),
acronyme=quote_xml_attr(ue_st["acronyme"]),
titre=quote_xml_attr(ue_st["titre"]),
note=dict(
value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""),
min=scu.fmt_note(ue_st["min"]),
max=scu.fmt_note(ue_st["max"]),
moy=scu.fmt_note(ue_st["moy"]),
),
rang=rang,
effectif=effectif,
ects=ects_txt,
code_apogee=quote_xml_attr(ue_st["code_apogee"]),
)
d["ue"].append(u)
if with_matieres:
u["module"] = []
# Structure UE/Matière/Module
# Recodé en 2022
ue = db.session.get(UniteEns, ue_id)
u["matiere"] = [
{
"matiere_id": mat.id,
"note": scu.fmt_note(nt.get_etud_mat_moy(mat.id, etudid)),
"titre": mat.titre,
"module": _list_modimpls(
nt,
etudid,
[
mod
for mod in modimpls
if mod["module"]["matiere_id"] == mat.id
],
prefs,
version,
),
}
for mat in ue.matieres.order_by(Matiere.numero)
]
else:
# Liste les modules de l'UE
u["module"] = _list_modimpls(
nt,
etudid,
[mod for mod in modimpls if mod["module"]["ue_id"] == ue_id],
prefs,
version,
)
# UE capitalisée (listée seulement si meilleure que l'UE courante)
if ue_status["is_capitalized"]:
try:
ects_txt = str(int(ue_status["ue"].get("ects", "") or 0.0))
except ValueError:
ects_txt = ""
d["ue_capitalisee"].append(
dict(
id=ue_id,
numero=quote_xml_attr(ue_st["numero"]),
acronyme=quote_xml_attr(ue_st["acronyme"]),
titre=quote_xml_attr(ue_st["titre"]),
note=scu.fmt_note(ue_status["moy"]),
coefficient_ue=scu.fmt_note(ue_status["coef_ue"]),
date_capitalisation=ndb.DateDMYtoISO(ue_status["event_date"]),
ects=ects_txt,
)
)
# --- Absences
if prefs["bul_show_abs"]:
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
# --- Décision Jury
d.update(dict_decision_jury(etud, formsemestre, with_decisions=xml_with_decisions))
# --- Appréciations
appreciations = BulAppreciations.get_appreciations_list(formsemestre.id, etudid)
d["appreciation"] = [
{
"comment": quote_xml_attr(appreciation.comment),
"date": appreciation.date.isoformat() if appreciation.date else "",
}
for appreciation in appreciations
]
#
return d
def _list_modimpls(
nt: NotesTableCompat,
etudid: int,
modimpls: list[dict],
prefs: SemPreferences,
version: str,
) -> list[dict]:
modules_dict = []
for modimpl in modimpls:
mod_moy = scu.fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid))
if mod_moy == "NI": # ne mentionne pas les modules ou n'est pas inscrit
continue
modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"])
mod = modimpl["module"]
# if mod['ects'] is None:
# ects = ''
# else:
# ects = str(mod['ects'])
modstat = nt.get_mod_stats(modimpl["moduleimpl_id"])
mod_dict = dict(
id=modimpl["moduleimpl_id"],
code=mod["code"],
coefficient=mod["coefficient"],
numero=mod["numero"],
titre=quote_xml_attr(mod["titre"]),
abbrev=quote_xml_attr(mod["abbrev"]),
# ects=ects, ects des modules maintenant inutilisés
note=dict(value=mod_moy),
code_apogee=quote_xml_attr(mod["code_apogee"]),
matiere_id=mod["matiere_id"],
)
mod_dict["note"].update(modstat)
for k in ("min", "max", "moy"): # formatte toutes les notes
mod_dict["note"][k] = scu.fmt_note(mod_dict["note"][k])
if prefs["bul_show_mod_rangs"] and nt.mod_rangs is not None:
mod_dict["rang"] = dict(
value=nt.mod_rangs[modimpl["moduleimpl_id"]][0][etudid]
)
mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
# --- notes de chaque eval:
evaluations_completes = nt.get_modimpl_evaluations_completes(
modimpl["moduleimpl_id"]
)
mod_dict["evaluation"] = []
if version != "short":
for e in evaluations_completes:
if e.visibulletin or version == "long":
# Note à l'évaluation:
val = modimpl_results.evals_notes[e.id].get(etudid, "NP")
# nb: val est NA si etud démissionnaire
e_dict = e.to_dict_bul()
e_dict["note"] = scu.fmt_note(val, note_max=e.note_max)
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
# XXX à revoir pour utiliser modimplresult
etat = sco_evaluations.do_evaluation_etat(e.id)
if prefs["bul_show_minmax_eval"]:
e_dict["min"] = etat["mini"] # chaine, sur 20
e_dict["max"] = etat["maxi"]
if prefs["bul_show_moypromo"]:
e_dict["moy"] = etat["moy"]
mod_dict["evaluation"].append(e_dict)
# Evaluations incomplètes ou futures:
complete_eval_ids = {e.id for e in evaluations_completes}
if prefs["bul_show_all_evals"]:
evaluations: list[Evaluation] = Evaluation.query.filter_by(
moduleimpl_id=modimpl["moduleimpl_id"]
).order_by(Evaluation.date_debut)
# plus ancienne d'abord
for e in evaluations:
if e.id not in complete_eval_ids:
e_dict = e.to_dict_bul()
e_dict["incomplete"] = 1
mod_dict["evaluation"].append(e_dict)
modules_dict.append(mod_dict)
return modules_dict
def dict_decision_jury(
etud: Identite, formsemestre: FormSemestre, with_decisions: bool = False
) -> dict:
"""dict avec decision pour bulletins json
- autorisation_inscription
- decision : décision semestre
- decision_annee : annee BUT
- decision_ue : list des décisions UE
- situation
with_decision donne les décision même si bul_show_decision est faux.
Si formation APC, indique aussi validations année et RCUEs
Exemple:
{
'autorisation_inscription': [{'semestre_id': 4}],
'decision': {'code': 'ADM',
'compense_formsemestre_id': None,
'date': '2022-01-21',
'etat': 'I'},
'decision_ue': [
{
'acronyme': 'UE31',
'code': 'ADM',
'ects': 16.0,
'numero': 23,
'titre': 'Approfondissement métiers',
'ue_id': 1787
},
...
],
'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: '
'UE31, UE32. Diplôme obtenu.',
'diplomation' : 'Diplôme obtenu.' # (ou vide)
}
"""
from app.scodoc import sco_bulletins
prefs = sco_preferences.SemPreferences(formsemestre.id)
d = {}
if prefs["bul_show_decision"] or with_decisions:
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etud.id,
formsemestre,
show_uevalid=prefs["bul_show_uevalid"],
)
d["situation"] = infos["situation"]
d["diplomation"] = infos["diplomation"]
if dpv:
decision = dpv["decisions"][0]
etat = decision["etat"]
if decision["decision_sem"]:
code = decision["decision_sem"]["code"]
date = ndb.DateDMYtoISO(
dpv["decisions"][0]["decision_sem"]["event_date"]
)
else:
code = ""
date = ""
d["decision"] = dict(
code=code,
etat=etat,
date=date,
)
if (
decision["decision_sem"]
and "compense_formsemestre_id" in decision["decision_sem"]
):
d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][
"compense_formsemestre_id"
]
d["decision_ue"] = []
if decision[
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
d["decision_ue"].append(
dict(
ue_id=ue["ue_id"],
numero=ue["numero"],
acronyme=ue["acronyme"],
titre=ue["titre"],
code=decision["decisions_ue"][ue_id]["code"],
ects=ue["ects"] or "",
)
)
d["autorisation_inscription"] = []
for aut in decision["autorisations"]:
d["autorisation_inscription"].append(
dict(
semestre_id=aut["semestre_id"],
date=aut["date"].isoformat() if aut["date"] else None,
)
)
else:
d["decision"] = dict(code="", etat="DEM")
# Ajout jury BUT:
if formsemestre.formation.is_apc():
d.update(but_validations.dict_decision_jury(etud, formsemestre))
return d