ScoDoc-PE/app/comp/res_common.py

400 lines
14 KiB
Python
Raw Normal View History

2021-12-24 00:08:25 +01:00
##############################################################################
# ScoDoc
2022-01-01 14:51:28 +01:00
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
2021-12-24 00:08:25 +01:00
# See LICENSE
##############################################################################
2022-01-16 23:47:52 +01:00
from collections import defaultdict, Counter
2021-12-24 00:08:25 +01:00
from functools import cached_property
import numpy as np
import pandas as pd
2021-12-26 19:15:47 +01:00
from app.comp.aux import StatsMoyenne
2022-01-07 10:37:48 +01:00
from app.comp.moy_mod import ModuleImplResults
2022-01-16 23:47:52 +01:00
from app.models import FormSemestre, Identite, ModuleImpl
2022-01-08 19:56:09 +01:00
from app.models.ues import UniteEns
2021-12-24 00:08:25 +01:00
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
2021-12-30 23:58:38 +01:00
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
2021-12-24 00:08:25 +01:00
# 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 = (
2021-12-30 23:58:38 +01:00
"etud_moy_gen_ranks",
"etud_moy_gen",
2021-12-24 00:08:25 +01:00
"etud_moy_ue",
2021-12-30 23:58:38 +01:00
"modimpl_inscr_df",
2021-12-26 19:15:47 +01:00
"modimpls_results",
2022-01-16 23:47:52 +01:00
"etud_coef_ue_df",
2021-12-24 00:08:25 +01:00
)
2021-12-30 23:58:38 +01:00
def __init__(self, formsemestre: FormSemestre):
2022-01-09 21:02:07 +01:00
self.formsemestre: FormSemestre = formsemestre
2021-12-26 19:15:47 +01:00
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
2022-01-07 10:37:48 +01:00
# Attributs "virtuels", définis dans les sous-classes
2022-01-08 19:56:09 +01:00
# ResultatsSemestreBUT ou ResultatsSemestreClassic
2021-12-26 19:15:47 +01:00
self.etud_moy_ue = {}
2022-01-08 19:56:09 +01:00
"etud_moy_ue: DataFrame columns UE, rows etudid"
2021-12-26 19:15:47 +01:00
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
2022-01-07 10:37:48 +01:00
self.modimpls_results: ModuleImplResults = None
2022-01-16 23:47:52 +01:00
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque etudiant (pour form. classiques)"""
# TODO ?
2021-12-24 00:08:25 +01:00
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
2021-12-26 19:15:47 +01:00
raise NotImplementedError()
2021-12-24 00:08:25 +01:00
2022-01-16 23:47:52 +01:00
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)
2021-12-24 00:08:25 +01:00
@cached_property
2022-01-16 23:47:52 +01:00
def etuds(self) -> list[Identite]:
"Liste des inscrits au semestre, avec les démissionnaires et les défaillants"
2021-12-30 23:58:38 +01:00
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
2022-01-16 23:47:52 +01:00
return self.formsemestre.get_inscrits(include_demdef=True)
2021-12-24 00:08:25 +01:00
@cached_property
2022-01-16 23:47:52 +01:00
def etud_index(self) -> dict[int, int]:
2021-12-24 00:08:25 +01:00
"dict { etudid : indice dans les inscrits }"
return {e.id: idx for idx, e in enumerate(self.etuds)}
2022-01-09 21:02:07 +01:00
@cached_property
2022-01-16 23:47:52 +01:00
def etuds_dict(self) -> dict[int, Identite]:
"""dict { etudid : Identite } inscrits au semestre,
avec les démissionnaires et defs."""
2022-01-09 21:02:07 +01:00
return {etud.id: etud for etud in self.etuds}
2021-12-24 00:08:25 +01:00
@cached_property
2022-01-16 23:47:52 +01:00
def ues(self) -> list[UniteEns]:
2022-01-25 10:45:13 +01:00
"""Liste des UEs du semestre (avec les UE bonus sport)
2022-01-09 21:02:07 +01:00
(indices des DataFrames)
"""
return self.formsemestre.query_ues(with_sport=True).all()
2021-12-24 00:08:25 +01:00
@cached_property
def ressources(self):
"Liste des ressources du semestre, triées par numéro de module"
return [
2022-01-25 10:45:13 +01:00
m
for m in self.formsemestre.modimpls_sorted
if m.module.module_type == scu.ModuleType.RESSOURCE
2021-12-24 00:08:25 +01:00
]
@cached_property
def saes(self):
"Liste des SAÉs du semestre, triées par numéro de module"
2022-01-25 10:45:13 +01:00
return [
m
for m in self.formsemestre.modimpls_sorted
if m.module.module_type == scu.ModuleType.SAE
]
2021-12-24 00:08:25 +01:00
2022-01-08 19:56:09 +01:00
@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)
2021-12-24 00:08:25 +01:00
# Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre):
"""Implementation partielle de NotesTable WIP TODO
2021-12-30 23:58:38 +01:00
Les méthodes définies dans cette classe sont
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).
2021-12-24 00:08:25 +01:00
"""
2022-01-25 10:45:13 +01:00
_cached_attrs = ResultatsSemestre._cached_attrs + (
"bonus",
"bonus_ues",
)
2021-12-24 00:08:25 +01:00
2021-12-30 23:58:38 +01:00
def __init__(self, formsemestre: FormSemestre):
2021-12-24 00:08:25 +01:00
super().__init__(formsemestre)
2022-01-16 23:47:52 +01:00
2021-12-24 00:08:25 +01:00
nb_etuds = len(self.etuds)
2022-01-25 10:45:13 +01:00
self.bonus = None # virtuel
self.bonus_ues = None # virtuel
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
2021-12-24 00:08:25 +01:00
self.mod_rangs = {
2022-01-25 10:45:13 +01:00
m.id: (None, nb_etuds) for m in self.formsemestre.modimpls_sorted
2021-12-24 00:08:25 +01:00
}
2021-12-30 23:58:38 +01:00
self.moy_min = "NA"
self.moy_max = "NA"
self.moy_moy = "NA"
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours()
2021-12-30 23:58:38 +01:00
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]
2022-01-16 23:47:52 +01:00
@cached_property
def sem(self) -> dict:
"""le formsemestre, comme un dict (nt.sem)"""
return self.formsemestre.to_dict()
2021-12-30 23:58:38 +01:00
@cached_property
def inscrlist(self) -> list[dict]: # utilisé par PE seulement
2022-01-16 23:47:52 +01:00
"""Liste des inscrits au semestre (avec DEM et DEF),
sous forme de dict etud,
2021-12-30 23:58:38 +01:00
classée dans l'ordre alphabétique de noms.
"""
2022-01-16 23:47:52 +01:00
etuds = self.formsemestre.get_inscrits(include_demdef=True)
2021-12-30 23:58:38 +01:00
etuds.sort(key=lambda e: e.sort_key)
return [e.to_dict_scodoc7() for e in etuds]
2021-12-24 00:08:25 +01:00
@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 = []
2022-01-09 21:02:07 +01:00
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
2021-12-24 00:08:25 +01:00
d = ue.to_dict()
2022-01-25 10:45:13 +01:00
if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id]
else:
moys = None
d.update(StatsMoyenne(moys).to_dict())
2021-12-24 00:08:25 +01:00
ues.append(d)
return ues
2021-12-26 19:15:47 +01:00
def get_modimpls_dict(self, ue_id=None):
"""Liste des modules pour une UE (ou toutes si ue_id==None),
triés par numéros (selon le type de formation)
"""
if ue_id is None:
2022-01-25 10:45:13 +01:00
return [m.to_dict() for m in self.formsemestre.modimpls_sorted]
2021-12-26 19:15:47 +01:00
else:
2022-01-25 10:45:13 +01:00
return [
m.to_dict()
for m in self.formsemestre.modimpls_sorted
if m.module.ue.id == ue_id
]
2021-12-26 19:15:47 +01:00
2021-12-30 23:58:38 +01:00
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
2021-12-26 19:15:47 +01:00
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne générale de cet etudiant dans ce semestre.
2022-01-07 10:37:48 +01:00
Prend(ra) en compte les UE capitalisées. (TODO) XXX
2021-12-26 19:15:47 +01:00
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
2021-12-24 00:08:25 +01:00
2021-12-30 23:58:38 +01:00
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
2021-12-24 00:08:25 +01:00
2021-12-30 23:58:38 +01:00
def get_etud_ue_status(self, etudid: int, ue_id: int):
2022-01-16 23:47:52 +01:00
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
2021-12-24 00:08:25 +01:00
return {
2022-01-03 12:31:20 +01:00
"cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
2022-01-16 23:47:52 +01:00
"moy": self.etud_moy_ue[ue_id][etudid],
2021-12-24 00:08:25 +01:00
"is_capitalized": False, # XXX TODO
2022-01-16 23:47:52 +01:00
"coef_ue": coef_ue, # XXX TODO
2021-12-24 00:08:25 +01:00
}
2021-12-30 23:58:38 +01:00
def get_etud_rang(self, etudid: int):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
2021-12-24 00:08:25 +01:00
2021-12-30 23:58:38 +01:00
def get_etud_rang_group(self, etudid: int, group_id: int):
return (None, 0) # XXX unimplemented TODO
2021-12-24 00:08:25 +01:00
2021-12-30 23:58:38 +01:00
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
2022-01-07 10:37:48 +01:00
"""Liste d'informations (compat NotesTable) sur évaluations completes
de ce module.
Évaluation "complete" ssi toutes notes saisies ou en attente.
"""
2022-01-03 12:31:20 +01:00
modimpl = ModuleImpl.query.get(moduleimpl_id)
2021-12-24 00:08:25 +01:00
evals_results = []
2022-01-03 12:31:20 +01:00
for e in modimpl.evaluations:
2022-01-07 10:37:48 +01:00
if self.modimpls_results[moduleimpl_id].evaluations_completes_dict[e.id]:
d = e.to_dict()
2022-01-16 23:47:52 +01:00
moduleimpl_results = self.modimpls_results[e.moduleimpl_id]
2022-01-07 10:37:48 +01:00
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,
2022-01-16 23:47:52 +01:00
"value": moduleimpl_results.evals_notes[e.id][etud.id],
2022-01-07 10:37:48 +01:00
}
for etud in self.etuds
2021-12-24 00:08:25 +01:00
}
2022-01-16 23:47:52 +01:00
d["etat"] = {
"evalattente": moduleimpl_results.evaluations_etat[e.id].nb_attente,
}
2022-01-07 10:37:48 +01:00
evals_results.append(d)
2021-12-24 00:08:25 +01:00
return evals_results
2021-12-30 23:58:38 +01:00
def get_moduleimpls_attente(self):
return [] # XXX TODO
2022-01-07 10:37:48 +01:00
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl
Vide en APC
"""
2021-12-30 23:58:38 +01:00
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
2022-01-25 10:45:13 +01:00
t = (
["-"]
+ ["0.00"] * len(self.ues)
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
)
2021-12-30 23:58:38 +01:00
else:
moy_ues = self.etud_moy_ue.loc[etudid]
t = [moy_gen] + list(moy_ues)
# TODO UE capitalisées: ne pas afficher moyennes modules
2022-01-25 10:45:13 +01:00
for modimpl in self.formsemestre.modimpls_sorted:
2021-12-30 23:58:38 +01:00
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
}