forked from ScoDoc/ScoDoc
535 lines
22 KiB
Python
535 lines
22 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""Génération bulletin BUT
|
|
"""
|
|
|
|
import collections
|
|
import datetime
|
|
import numpy as np
|
|
from flask import g, has_request_context, url_for
|
|
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
from app.models import Evaluation, FormSemestre, Identite
|
|
from app.models.groups import GroupDescr
|
|
from app.models.ues import UniteEns
|
|
from app.scodoc import sco_bulletins, sco_utils as scu
|
|
from app.scodoc import sco_bulletins_json
|
|
from app.scodoc import sco_bulletins_pdf
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
|
from app.scodoc.sco_utils import fmt_note
|
|
|
|
|
|
class BulletinBUT:
|
|
"""Génération du bulletin BUT.
|
|
Cette classe génère des dictionnaires avec toutes les informations
|
|
du bulletin, qui sont immédiatement traduisibles en JSON.
|
|
"""
|
|
|
|
def __init__(self, formsemestre: FormSemestre):
|
|
""" """
|
|
self.res = ResultatsSemestreBUT(formsemestre)
|
|
self.prefs = sco_preferences.SemPreferences(formsemestre.id)
|
|
|
|
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
|
|
"dict synthèse résultats dans l'UE pour les modules indiqués"
|
|
res = self.res
|
|
d = {}
|
|
etud_idx = res.etud_index[etud.id]
|
|
if ue.type != UE_SPORT:
|
|
ue_idx = res.modimpl_coefs_df.index.get_loc(ue.id)
|
|
etud_moy_module = res.sem_cube[etud_idx] # module x UE
|
|
for modimpl in modimpls:
|
|
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
|
if ue.type != UE_SPORT:
|
|
coef = res.modimpl_coefs_df[modimpl.id][ue.id]
|
|
if coef > 0:
|
|
d[modimpl.module.code] = {
|
|
"id": modimpl.id,
|
|
"coef": coef,
|
|
"moyenne": fmt_note(
|
|
etud_moy_module[
|
|
res.modimpl_coefs_df.columns.get_loc(modimpl.id)
|
|
][ue_idx]
|
|
),
|
|
}
|
|
# else: # modules dans UE bonus sport
|
|
# d[modimpl.module.code] = {
|
|
# "id": modimpl.id,
|
|
# "coef": "",
|
|
# "moyenne": "?x?",
|
|
# }
|
|
return d
|
|
|
|
def etud_ue_results(
|
|
self,
|
|
etud: Identite,
|
|
ue: UniteEns,
|
|
decision_ue: dict,
|
|
etud_groups: list[GroupDescr] = None,
|
|
) -> dict:
|
|
"""dict synthèse résultats UE
|
|
etud_groups : liste des groupes, pour affichage du rang.
|
|
Si UE sport et étudiant non inscrit, renvoie dict vide.
|
|
"""
|
|
res = self.res
|
|
|
|
if (etud.id, ue.id) in self.res.dispense_ues:
|
|
return {}
|
|
|
|
if ue.type == UE_SPORT:
|
|
modimpls_spo = [
|
|
modimpl
|
|
for modimpl in res.formsemestre.modimpls_sorted
|
|
if modimpl.module.ue.type == UE_SPORT
|
|
]
|
|
# L'étudiant est-il inscrit à l'un des modules de l'UE bonus ?
|
|
if not any(res.modimpl_inscr_df.loc[etud.id][[m.id for m in modimpls_spo]]):
|
|
return {}
|
|
|
|
d = {
|
|
"id": ue.id,
|
|
"titre": ue.titre,
|
|
"numero": ue.numero,
|
|
"type": ue.type,
|
|
"color": ue.color,
|
|
"competence": None, # XXX TODO lien avec référentiel
|
|
"moyenne": None,
|
|
# Le bonus sport appliqué sur cette UE
|
|
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
|
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
|
else fmt_note(0.0),
|
|
"malus": fmt_note(res.malus[ue.id][etud.id]),
|
|
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93
|
|
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
|
"saes": self.etud_ue_mod_results(etud, ue, res.saes),
|
|
}
|
|
if self.prefs["bul_show_ects"]:
|
|
d["ECTS"] = {
|
|
"acquis": decision_ue.get("ects", 0.0),
|
|
"total": ue.ects or 0.0, # float même si non renseigné
|
|
}
|
|
if ue.type != UE_SPORT:
|
|
if self.prefs["bul_show_ue_rangs"]:
|
|
rangs, effectif = res.ue_rangs[ue.id]
|
|
rang = rangs[etud.id]
|
|
else:
|
|
rang, effectif = "", 0
|
|
d["moyenne"] = {
|
|
"value": fmt_note(res.etud_moy_ue[ue.id][etud.id]),
|
|
"min": fmt_note(res.etud_moy_ue[ue.id].min()),
|
|
"max": fmt_note(res.etud_moy_ue[ue.id].max()),
|
|
"moy": fmt_note(res.etud_moy_ue[ue.id].mean()),
|
|
"rang": rang,
|
|
"total": effectif, # nb etud avec note dans cette UE
|
|
"groupes": {},
|
|
}
|
|
if self.prefs["bul_show_ue_rangs"]:
|
|
for group in etud_groups:
|
|
if group.partition.bul_show_rank:
|
|
rang, effectif = self.res.get_etud_ue_rang(
|
|
ue.id, etud.id, group.id
|
|
)
|
|
d["moyenne"]["groupes"][group.id] = {
|
|
"value": rang,
|
|
"total": effectif,
|
|
}
|
|
else: # UE BONUS
|
|
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
|
|
# ceci suppose que l'on a une seule UE bonus,
|
|
# en tous cas elles auront la même description
|
|
d["bonus_description"] = self.etud_bonus_description(etud.id)
|
|
|
|
return d
|
|
|
|
def etud_ues_capitalisees(self, etud: Identite) -> dict:
|
|
"""dict avec les UE capitalisees. la clé est l'acronyme d'UE, qui ne
|
|
peut donc être capitalisée qu'une seule fois (on prend la meilleure)"""
|
|
if not etud.id in self.res.validations.ue_capitalisees.index:
|
|
return {} # aucune capitalisation
|
|
d = {}
|
|
for _, ue_capitalisee in self.res.validations.ue_capitalisees.loc[
|
|
[etud.id]
|
|
].iterrows():
|
|
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
|
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
|
# déjà capitalisé ? montre la meilleure
|
|
if ue.acronyme in d:
|
|
moy_cap = d[ue.acronyme]["moyenne_num"] or 0.0
|
|
if (not isinstance(moy_cap, float)) or (
|
|
(ue_capitalisee.moy_ue or 0.0) < moy_cap
|
|
):
|
|
continue # skip this duplicate UE
|
|
|
|
d[ue.acronyme] = {
|
|
"id": ue.id,
|
|
"ue_code": ue_capitalisee.ue_code,
|
|
"titre": ue.titre,
|
|
"numero": ue.numero,
|
|
"type": ue.type,
|
|
"color": ue.color,
|
|
"moyenne": fmt_note(ue_capitalisee.moy_ue), # arrondi en str
|
|
"moyenne_num": fmt_note(ue_capitalisee.moy_ue, keep_numeric=True),
|
|
"is_external": ue_capitalisee.is_external,
|
|
"date_capitalisation": ue_capitalisee.event_date,
|
|
"formsemestre_id": ue_capitalisee.formsemestre_id,
|
|
"bul_orig_url": url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
etudid=etud.id,
|
|
formsemestre_id=ue_capitalisee.formsemestre_id,
|
|
)
|
|
if ue_capitalisee.formsemestre_id
|
|
else None,
|
|
"ressources": {}, # sans détail en BUT
|
|
"saes": {},
|
|
}
|
|
if self.prefs["bul_show_ects"]:
|
|
d[ue.acronyme]["ECTS"] = {
|
|
"acquis": ue.ects or 0.0, # toujours validée ici
|
|
"total": ue.ects or 0.0, # float même si non renseigné
|
|
}
|
|
return d
|
|
|
|
def etud_mods_results(self, etud, modimpls, version="long") -> dict:
|
|
"""dict synthèse résultats des modules indiqués,
|
|
avec évaluations de chacun (sauf si version == "short")
|
|
"""
|
|
res = self.res
|
|
d = {}
|
|
# etud_idx = self.etud_index[etud.id]
|
|
for modimpl in modimpls:
|
|
# mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
|
|
# # moyennes indicatives (moyennes de moyennes d'UE)
|
|
# try:
|
|
# moyennes_etuds = np.nan_to_num(
|
|
# np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
|
|
# copy=False,
|
|
# )
|
|
# except RuntimeWarning:
|
|
# # all nans in np.nanmean (sur certains etuds sans notes valides)
|
|
# pass
|
|
# try:
|
|
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
|
# except RuntimeWarning: # all nans in np.nanmean
|
|
# pass
|
|
modimpl_results = res.modimpls_results[modimpl.id]
|
|
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
|
d[modimpl.module.code] = {
|
|
"id": modimpl.id,
|
|
"titre": modimpl.module.titre,
|
|
"code_apogee": modimpl.module.code_apogee,
|
|
"url": url_for(
|
|
"notes.moduleimpl_status",
|
|
scodoc_dept=g.scodoc_dept,
|
|
moduleimpl_id=modimpl.id,
|
|
)
|
|
if has_request_context()
|
|
else "na",
|
|
"moyenne": {
|
|
# # moyenne indicative de module: moyenne des UE,
|
|
# # ignorant celles sans notes (nan)
|
|
# "value": fmt_note(moy_indicative_mod),
|
|
# "min": fmt_note(moyennes_etuds.min()),
|
|
# "max": fmt_note(moyennes_etuds.max()),
|
|
# "moy": fmt_note(moyennes_etuds.mean()),
|
|
},
|
|
"evaluations": [
|
|
self.etud_eval_results(etud, e)
|
|
for e in modimpl.evaluations
|
|
if (e.visibulletin or version == "long")
|
|
and (e.id in modimpl_results.evaluations_etat)
|
|
and (
|
|
modimpl_results.evaluations_etat[e.id].is_complete
|
|
or self.prefs["bul_show_all_evals"]
|
|
)
|
|
]
|
|
if version != "short"
|
|
else [],
|
|
}
|
|
return d
|
|
|
|
def etud_eval_results(self, etud, e: Evaluation) -> dict:
|
|
"dict resultats d'un étudiant à une évaluation"
|
|
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
|
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
|
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
|
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
|
try:
|
|
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
|
poids = {
|
|
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
|
for ue in self.res.ues
|
|
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
|
}
|
|
except KeyError:
|
|
poids = collections.defaultdict(lambda: 0.0)
|
|
d = {
|
|
"id": e.id,
|
|
"coef": fmt_note(e.coefficient)
|
|
if e.evaluation_type == scu.EVALUATION_NORMALE
|
|
else None,
|
|
"date": e.jour.isoformat() if e.jour else None,
|
|
"description": e.description,
|
|
"evaluation_type": e.evaluation_type,
|
|
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
|
|
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
|
"note": {
|
|
"value": fmt_note(
|
|
eval_notes[etud.id],
|
|
note_max=e.note_max,
|
|
),
|
|
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
|
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
|
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
|
},
|
|
"poids": poids,
|
|
"url": url_for(
|
|
"notes.evaluation_listenotes",
|
|
scodoc_dept=g.scodoc_dept,
|
|
evaluation_id=e.id,
|
|
)
|
|
if has_request_context()
|
|
else "na",
|
|
}
|
|
return d
|
|
|
|
def etud_bonus_description(self, etudid):
|
|
"""description du bonus affichée dans la section "UE bonus"."""
|
|
res = self.res
|
|
if res.bonus_ues is None or res.bonus_ues.shape[1] == 0:
|
|
return ""
|
|
|
|
bonus_vect = res.bonus_ues.loc[etudid]
|
|
if bonus_vect.nunique() > 1:
|
|
# détail UE par UE
|
|
details = [
|
|
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
|
|
for ue in res.ues
|
|
if ue.type != UE_SPORT
|
|
and res.modimpls_in_ue(ue, etudid)
|
|
and ue.id in res.bonus_ues
|
|
and bonus_vect[ue.id] > 0.0
|
|
]
|
|
if details:
|
|
return "Bonus de " + ", ".join(details)
|
|
else:
|
|
return "" # aucun bonus
|
|
else:
|
|
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
|
|
|
|
def bulletin_etud(
|
|
self,
|
|
etud: Identite,
|
|
formsemestre: FormSemestre,
|
|
force_publishing=False,
|
|
version="long",
|
|
) -> dict:
|
|
"""Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
|
|
- version:
|
|
"long", "selectedevals": toutes les infos (notes des évaluations)
|
|
"short" : ne descend pas plus bas que les modules.
|
|
|
|
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
|
(bulletins non publiés).
|
|
"""
|
|
res = self.res
|
|
etat_inscription = etud.inscription_etat(formsemestre.id)
|
|
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
|
published = (not formsemestre.bul_hide_xml) or force_publishing
|
|
if formsemestre.formation.referentiel_competence is None:
|
|
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
|
|
else:
|
|
etud_ues_ids = res.etud_ues_ids(etud.id)
|
|
|
|
d = {
|
|
"version": "0",
|
|
"type": "BUT",
|
|
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
|
"publie": not formsemestre.bul_hide_xml,
|
|
"etudiant": etud.to_dict_bul(),
|
|
"formation": {
|
|
"id": formsemestre.formation.id,
|
|
"acronyme": formsemestre.formation.acronyme,
|
|
"titre_officiel": formsemestre.formation.titre_officiel,
|
|
"titre": formsemestre.formation.titre,
|
|
},
|
|
"formsemestre_id": formsemestre.id,
|
|
"etat_inscription": etat_inscription,
|
|
"options": sco_preferences.bulletin_option_affichage(
|
|
formsemestre, self.prefs
|
|
),
|
|
}
|
|
if not published:
|
|
return d
|
|
|
|
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
|
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
|
etud, formsemestre, only_to_show=True
|
|
)
|
|
semestre_infos = {
|
|
"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],
|
|
}
|
|
if self.prefs["bul_show_abs"]:
|
|
semestre_infos["absences"] = {
|
|
"injustifie": nbabs - nbabsjust,
|
|
"total": nbabs,
|
|
"metrique": {
|
|
"H.": "Heure(s)",
|
|
"J.": "Journée(s)",
|
|
"1/2 J.": "1/2 Jour.",
|
|
}.get(sco_preferences.get_preference("assi_metrique")),
|
|
}
|
|
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
|
if self.prefs["bul_show_ects"]:
|
|
ects_tot = res.etud_ects_tot_sem(etud.id)
|
|
ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
|
|
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
|
|
if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
|
|
semestre_infos.update(
|
|
sco_bulletins_json.dict_decision_jury(etud, formsemestre)
|
|
)
|
|
if etat_inscription == scu.INSCRIT:
|
|
# moyenne des moyennes générales du semestre
|
|
semestre_infos["notes"] = {
|
|
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
|
"min": fmt_note(res.etud_moy_gen.min()),
|
|
"moy": fmt_note(res.etud_moy_gen.mean()),
|
|
"max": fmt_note(res.etud_moy_gen.max()),
|
|
}
|
|
if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]):
|
|
# classement wrt moyenne générale, indicatif
|
|
semestre_infos["rang"] = {
|
|
"value": res.etud_moy_gen_ranks[etud.id],
|
|
"total": nb_inscrits,
|
|
"groupes": {},
|
|
}
|
|
# Rangs par groupes
|
|
for group in etud_groups:
|
|
if group.partition.bul_show_rank:
|
|
rang, effectif = self.res.get_etud_rang_group(etud.id, group.id)
|
|
semestre_infos["rang"]["groupes"][group.id] = {
|
|
"value": rang,
|
|
"total": effectif,
|
|
}
|
|
else:
|
|
semestre_infos["rang"] = {
|
|
"value": "-",
|
|
"total": nb_inscrits,
|
|
"groupes": {},
|
|
}
|
|
d.update(
|
|
{
|
|
"ressources": self.etud_mods_results(
|
|
etud, res.ressources, version=version
|
|
),
|
|
"saes": self.etud_mods_results(etud, res.saes, version=version),
|
|
"ues_capitalisees": self.etud_ues_capitalisees(etud),
|
|
"semestre": semestre_infos,
|
|
},
|
|
)
|
|
d_ues = {}
|
|
for ue in res.ues:
|
|
# si l'UE comporte des modules auxquels on est inscrit:
|
|
if (ue.type == UE_SPORT) or ue.id in etud_ues_ids:
|
|
ue_r = self.etud_ue_results(
|
|
etud,
|
|
ue,
|
|
decision_ue=decisions_ues.get(ue.id, {}),
|
|
etud_groups=etud_groups,
|
|
)
|
|
if ue_r: # exclu UE sport sans inscriptions
|
|
d_ues[ue.acronyme] = ue_r
|
|
d["ues"] = d_ues
|
|
|
|
else:
|
|
semestre_infos.update(
|
|
{
|
|
"notes": {
|
|
"value": "DEM",
|
|
"min": "",
|
|
"moy": "",
|
|
"max": "",
|
|
},
|
|
"rang": {"value": "DEM", "total": nb_inscrits},
|
|
}
|
|
)
|
|
d.update(
|
|
{
|
|
"semestre": semestre_infos,
|
|
"ressources": {},
|
|
"saes": {},
|
|
"ues": {},
|
|
"ues_capitalisees": {},
|
|
}
|
|
)
|
|
|
|
return d
|
|
|
|
def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
|
|
"""Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
|
|
(pas utilisé pour json/html)
|
|
Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
|
|
"""
|
|
d = self.bulletin_etud(
|
|
etud, self.res.formsemestre, version=version, force_publishing=True
|
|
)
|
|
d["etudid"] = etud.id
|
|
d["etud"] = d["etudiant"]
|
|
d["etud"]["nomprenom"] = etud.nomprenom
|
|
d["etud"]["etat_civil"] = etud.etat_civil
|
|
d.update(self.res.sem)
|
|
etud_etat = self.res.get_etud_etat(etud.id)
|
|
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
|
etud_etat,
|
|
self.prefs,
|
|
decision_sem=d["semestre"].get("decision"),
|
|
)
|
|
if etud_etat == scu.DEMISSION:
|
|
d["demission"] = "(Démission)"
|
|
elif etud_etat == DEF:
|
|
d["demission"] = "(Défaillant)"
|
|
else:
|
|
d["demission"] = ""
|
|
|
|
# --- Absences
|
|
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
|
|
|
|
# --- Decision Jury
|
|
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
|
etud.id,
|
|
self.res.formsemestre,
|
|
format="html",
|
|
show_date_inscr=self.prefs["bul_show_date_inscr"],
|
|
show_decisions=self.prefs["bul_show_decision"],
|
|
show_uevalid=self.prefs["bul_show_uevalid"],
|
|
show_mention=self.prefs["bul_show_mention"],
|
|
)
|
|
|
|
d.update(infos)
|
|
# --- Rangs
|
|
d[
|
|
"rang_nt"
|
|
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
|
|
d["rang_txt"] = "Rang " + d["rang_nt"]
|
|
|
|
# --- Appréciations
|
|
d.update(
|
|
sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
|
|
)
|
|
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
|
|
|
|
return d
|