forked from ScoDoc/ScoDoc
405 lines
15 KiB
Python
405 lines
15 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
from collections import defaultdict, Counter
|
|
from functools import cached_property
|
|
import numpy as np
|
|
import pandas as pd
|
|
from app.comp.aux_stats import StatsMoyenne
|
|
from app.comp.moy_mod import ModuleImplResults
|
|
from app.models import FormSemestre, Identite, ModuleImpl
|
|
from app.models.ues import UniteEns
|
|
from app.scodoc import sco_utils as scu
|
|
from app.scodoc.sco_cache import ResultatsSemestreCache
|
|
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
|
|
|
|
# Il faut bien distinguer
|
|
# - ce qui est caché de façon persistente (via redis):
|
|
# ce sont les attributs listés dans `_cached_attrs`
|
|
# le stockage et l'invalidation sont gérés dans sco_cache.py
|
|
#
|
|
# - les valeurs cachées durant le temps d'une requête
|
|
# (durée de vie de l'instance de ResultatsSemestre)
|
|
# qui sont notamment les attributs décorés par `@cached_property``
|
|
#
|
|
class ResultatsSemestre:
|
|
_cached_attrs = (
|
|
"etud_moy_gen_ranks",
|
|
"etud_moy_gen",
|
|
"etud_moy_ue",
|
|
"modimpl_inscr_df",
|
|
"modimpls_results",
|
|
"etud_coef_ue_df",
|
|
)
|
|
|
|
def __init__(self, formsemestre: FormSemestre):
|
|
self.formsemestre: FormSemestre = formsemestre
|
|
# BUT ou standard ? (apc == "approche par compétences")
|
|
self.is_apc = formsemestre.formation.is_apc()
|
|
# Attributs "virtuels", définis dans les sous-classes
|
|
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
|
self.etud_moy_ue = {}
|
|
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
|
self.etud_moy_gen = {}
|
|
self.etud_moy_gen_ranks = {}
|
|
self.modimpls_results: ModuleImplResults = None
|
|
self.etud_coef_ue_df = None
|
|
"""coefs d'UE effectifs pour chaque etudiant (pour form. classiques)"""
|
|
|
|
# TODO ?
|
|
|
|
def load_cached(self) -> bool:
|
|
"Load cached dataframes, returns False si pas en cache"
|
|
data = ResultatsSemestreCache.get(self.formsemestre.id)
|
|
if not data:
|
|
return False
|
|
for attr in self._cached_attrs:
|
|
setattr(self, attr, data[attr])
|
|
return True
|
|
|
|
def store(self):
|
|
"Cache our data"
|
|
ResultatsSemestreCache.set(
|
|
self.formsemestre.id,
|
|
{attr: getattr(self, attr) for attr in self._cached_attrs},
|
|
)
|
|
|
|
def compute(self):
|
|
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
|
# voir ce qui est chargé / calculé ici et dans les sous-classes
|
|
raise NotImplementedError()
|
|
|
|
def get_inscriptions_counts(self) -> Counter:
|
|
"""Nombre d'inscrits, défaillants, démissionnaires.
|
|
|
|
Exemple: res.get_inscriptions_counts()[scu.INSCRIT]
|
|
|
|
Result: a collections.Counter instance
|
|
"""
|
|
return Counter(ins.etat for ins in self.formsemestre.inscriptions)
|
|
|
|
@cached_property
|
|
def etuds(self) -> list[Identite]:
|
|
"Liste des inscrits au semestre, avec les démissionnaires et les défaillants"
|
|
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
|
|
return self.formsemestre.get_inscrits(include_demdef=True)
|
|
|
|
@cached_property
|
|
def etud_index(self) -> dict[int, int]:
|
|
"dict { etudid : indice dans les inscrits }"
|
|
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
|
|
|
@cached_property
|
|
def etuds_dict(self) -> dict[int, Identite]:
|
|
"""dict { etudid : Identite } inscrits au semestre,
|
|
avec les démissionnaires et defs."""
|
|
return {etud.id: etud for etud in self.etuds}
|
|
|
|
@cached_property
|
|
def ues(self) -> list[UniteEns]:
|
|
"""Liste des UEs du semestre (avec les UE bonus sport)
|
|
(indices des DataFrames)
|
|
"""
|
|
return self.formsemestre.query_ues(with_sport=True).all()
|
|
|
|
@cached_property
|
|
def ressources(self):
|
|
"Liste des ressources du semestre, triées par numéro de module"
|
|
return [
|
|
m
|
|
for m in self.formsemestre.modimpls_sorted
|
|
if m.module.module_type == scu.ModuleType.RESSOURCE
|
|
]
|
|
|
|
@cached_property
|
|
def saes(self):
|
|
"Liste des SAÉs du semestre, triées par numéro de module"
|
|
return [
|
|
m
|
|
for m in self.formsemestre.modimpls_sorted
|
|
if m.module.module_type == scu.ModuleType.SAE
|
|
]
|
|
|
|
@cached_property
|
|
def ue_validables(self) -> list:
|
|
"""Liste des UE du semestre qui doivent être validées
|
|
(toutes sauf le sport)
|
|
"""
|
|
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
|
|
|
|
@cached_property
|
|
def ue_au_dessus(self, seuil=10.0) -> pd.DataFrame:
|
|
"""DataFrame columns UE, rows etudid, valeurs: bool
|
|
Par exemple, pour avoir le nombre d'UE au dessus de 10 pour l'étudiant etudid
|
|
nb_ues_ok = sum(res.ue_au_dessus().loc[etudid])
|
|
"""
|
|
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
|
|
|
|
|
|
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
|
class NotesTableCompat(ResultatsSemestre):
|
|
"""Implementation partielle de NotesTable WIP TODO
|
|
|
|
Les méthodes définies dans cette classe sont là
|
|
pour conserver la compatibilité abvec les codes anciens et
|
|
il n'est pas recommandé de les utiliser dans de nouveaux
|
|
développements (API malcommode et peu efficace).
|
|
"""
|
|
|
|
_cached_attrs = ResultatsSemestre._cached_attrs + (
|
|
"bonus",
|
|
"bonus_ues",
|
|
)
|
|
|
|
def __init__(self, formsemestre: FormSemestre):
|
|
super().__init__(formsemestre)
|
|
|
|
nb_etuds = len(self.etuds)
|
|
self.bonus = None # virtuel
|
|
self.bonus_ues = None # virtuel
|
|
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
|
|
self.mod_rangs = {
|
|
m.id: (None, nb_etuds) for m in self.formsemestre.modimpls_sorted
|
|
}
|
|
self.moy_min = "NA"
|
|
self.moy_max = "NA"
|
|
self.moy_moy = "NA"
|
|
self.expr_diagnostics = ""
|
|
self.parcours = self.formsemestre.formation.get_parcours()
|
|
|
|
def get_etudids(self, sorted=False) -> list[int]:
|
|
"""Liste des etudids inscrits, incluant les démissionnaires.
|
|
Si sorted, triée par moy. générale décroissante
|
|
Sinon, triée par ordre alphabetique de NOM
|
|
"""
|
|
# Note: pour avoir les inscrits non triés,
|
|
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
|
|
if sorted:
|
|
# Tri par moy. generale décroissante
|
|
return [x[-1] for x in self.T]
|
|
|
|
return [x["etudid"] for x in self.inscrlist]
|
|
|
|
@cached_property
|
|
def sem(self) -> dict:
|
|
"""le formsemestre, comme un dict (nt.sem)"""
|
|
return self.formsemestre.to_dict()
|
|
|
|
@cached_property
|
|
def inscrlist(self) -> list[dict]: # utilisé par PE seulement
|
|
"""Liste des inscrits au semestre (avec DEM et DEF),
|
|
sous forme de dict etud,
|
|
classée dans l'ordre alphabétique de noms.
|
|
"""
|
|
etuds = self.formsemestre.get_inscrits(include_demdef=True)
|
|
etuds.sort(key=lambda e: e.sort_key)
|
|
return [e.to_dict_scodoc7() for e in etuds]
|
|
|
|
@cached_property
|
|
def stats_moy_gen(self):
|
|
"""Stats (moy/min/max) sur la moyenne générale"""
|
|
return StatsMoyenne(self.etud_moy_gen)
|
|
|
|
def get_ues_stat_dict(self, filter_sport=False): # was get_ues()
|
|
"""Liste des UEs, ordonnée par numero.
|
|
Si filter_sport, retire les UE de type SPORT.
|
|
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
|
"""
|
|
ues = []
|
|
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
|
|
d = ue.to_dict()
|
|
if ue.type != UE_SPORT:
|
|
moys = self.etud_moy_ue[ue.id]
|
|
else:
|
|
moys = None
|
|
d.update(StatsMoyenne(moys).to_dict())
|
|
ues.append(d)
|
|
return ues
|
|
|
|
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
|
|
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
|
triés par numéros (selon le type de formation)
|
|
"""
|
|
modimpls_dict = []
|
|
for modimpl in self.formsemestre.modimpls_sorted:
|
|
if ue_id == None or modimpl.module.ue.id == ue_id:
|
|
d = modimpl.to_dict()
|
|
# compat ScoDoc < 9.2: ajoute matières
|
|
d["mat"] = modimpl.module.matiere.to_dict()
|
|
modimpls_dict.append(d)
|
|
return modimpls_dict
|
|
|
|
def get_etud_decision_sem(self, etudid: int) -> dict:
|
|
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
|
|
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
|
Si état défaillant, force le code a DEF
|
|
"""
|
|
if self.get_etud_etat(etudid) == DEF:
|
|
return {
|
|
"code": DEF,
|
|
"assidu": False,
|
|
"event_date": "",
|
|
"compense_formsemestre_id": None,
|
|
}
|
|
else:
|
|
return {
|
|
"code": ATT, # XXX TODO
|
|
"assidu": True, # XXX TODO
|
|
"event_date": "",
|
|
"compense_formsemestre_id": None,
|
|
}
|
|
|
|
def get_etud_etat(self, etudid: int) -> str:
|
|
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
|
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
|
if ins is None:
|
|
return ""
|
|
return ins.etat
|
|
|
|
def get_etud_mat_moy(self, matiere_id, etudid):
|
|
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
|
# non supporté en 9.2
|
|
return "na"
|
|
|
|
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
|
"""La moyenne de l'étudiant dans le moduleimpl
|
|
En APC, il s'agira d'une moyenne indicative sans valeur.
|
|
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
|
"""
|
|
raise NotImplementedError() # virtual method
|
|
|
|
def get_etud_moy_gen(self, etudid): # -> float | str
|
|
"""Moyenne générale de cet etudiant dans ce semestre.
|
|
Prend(ra) en compte les UE capitalisées. (TODO) XXX
|
|
Si apc, moyenne indicative.
|
|
Si pas de notes: 'NA'
|
|
"""
|
|
return self.etud_moy_gen[etudid]
|
|
|
|
def get_etud_ue_status(self, etudid: int, ue_id: int):
|
|
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
|
|
return {
|
|
"cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
|
|
"moy": self.etud_moy_ue[ue_id][etudid],
|
|
"is_capitalized": False, # XXX TODO
|
|
"coef_ue": coef_ue, # XXX TODO
|
|
}
|
|
|
|
def get_etud_rang(self, etudid: int):
|
|
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
|
|
|
|
def get_etud_rang_group(self, etudid: int, group_id: int):
|
|
return (None, 0) # XXX unimplemented TODO
|
|
|
|
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
|
"""Liste d'informations (compat NotesTable) sur évaluations completes
|
|
de ce module.
|
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
|
"""
|
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
|
evals_results = []
|
|
for e in modimpl.evaluations:
|
|
if self.modimpls_results[moduleimpl_id].evaluations_completes_dict[e.id]:
|
|
d = e.to_dict()
|
|
moduleimpl_results = self.modimpls_results[e.moduleimpl_id]
|
|
d["heure_debut"] = e.heure_debut # datetime.time
|
|
d["heure_fin"] = e.heure_fin
|
|
d["jour"] = e.jour # datetime
|
|
d["notes"] = {
|
|
etud.id: {
|
|
"etudid": etud.id,
|
|
"value": moduleimpl_results.evals_notes[e.id][etud.id],
|
|
}
|
|
for etud in self.etuds
|
|
}
|
|
d["etat"] = {
|
|
"evalattente": moduleimpl_results.evaluations_etat[e.id].nb_attente,
|
|
}
|
|
evals_results.append(d)
|
|
return evals_results
|
|
|
|
def get_moduleimpls_attente(self):
|
|
return [] # XXX TODO
|
|
|
|
def get_mod_stats(self, moduleimpl_id: int) -> dict:
|
|
"""Stats sur les notes obtenues dans un modimpl
|
|
Vide en APC
|
|
"""
|
|
return {
|
|
"moy": "-",
|
|
"max": "-",
|
|
"min": "-",
|
|
"nb_notes": "-",
|
|
"nb_missing": "-",
|
|
"nb_valid_evals": "-",
|
|
}
|
|
|
|
def get_nom_short(self, etudid):
|
|
"formatte nom d'un etud (pour table recap)"
|
|
etud = self.identdict[etudid]
|
|
return (
|
|
(etud["nom_usuel"] or etud["nom"]).upper()
|
|
+ " "
|
|
+ etud["prenom"].capitalize()[:2]
|
|
+ "."
|
|
)
|
|
|
|
@cached_property
|
|
def T(self):
|
|
return self.get_table_moyennes_triees()
|
|
|
|
def get_table_moyennes_triees(self) -> list:
|
|
"""Result: liste de tuples
|
|
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
|
"""
|
|
table_moyennes = []
|
|
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
|
|
|
for etudid in etuds_inscriptions:
|
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
|
if moy_gen is False:
|
|
# pas de moyenne: démissionnaire ou def
|
|
t = (
|
|
["-"]
|
|
+ ["0.00"] * len(self.ues)
|
|
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
|
|
)
|
|
else:
|
|
moy_ues = self.etud_moy_ue.loc[etudid]
|
|
t = [moy_gen] + list(moy_ues)
|
|
# TODO UE capitalisées: ne pas afficher moyennes modules
|
|
for modimpl in self.formsemestre.modimpls_sorted:
|
|
val = self.get_etud_mod_moy(modimpl.id, etudid)
|
|
t.append(val)
|
|
t.append(etudid)
|
|
table_moyennes.append(t)
|
|
# tri par moyennes décroissantes,
|
|
# en laissant les démissionnaires à la fin, par ordre alphabetique
|
|
etuds = [ins.etud for ins in etuds_inscriptions.values()]
|
|
etuds.sort(key=lambda e: e.sort_key)
|
|
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
|
|
table_moyennes.sort(key=self._row_key)
|
|
return table_moyennes
|
|
|
|
def _row_key(self, x):
|
|
"""clé de tri par moyennes décroissantes,
|
|
en laissant les demissionnaires à la fin, par ordre alphabetique.
|
|
(moy_gen, rang_alpha)
|
|
"""
|
|
try:
|
|
moy = -float(x[0])
|
|
except (ValueError, TypeError):
|
|
moy = 1000.0
|
|
return (moy, self._rang_alpha[x[-1]])
|
|
|
|
@cached_property
|
|
def identdict(self) -> dict:
|
|
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
|
|
return {
|
|
ins.etud.id: ins.etud.to_dict_scodoc7()
|
|
for ins in self.formsemestre.inscriptions
|
|
}
|