forked from ScoDoc/ScoDoc
217 lines
7.1 KiB
Python
217 lines
7.1 KiB
Python
|
##############################################################################
|
||
|
# ScoDoc
|
||
|
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||
|
# See LICENSE
|
||
|
##############################################################################
|
||
|
|
||
|
from collections import defaultdict
|
||
|
from functools import cached_property
|
||
|
import numpy as np
|
||
|
import pandas as pd
|
||
|
from app.scodoc import sco_utils as scu
|
||
|
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||
|
|
||
|
# 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 = (
|
||
|
"sem_cube",
|
||
|
"modimpl_inscr_df",
|
||
|
"modimpl_coefs_df",
|
||
|
"etud_moy_ue",
|
||
|
"modimpls_evals_poids",
|
||
|
"modimpls_evals_notes",
|
||
|
"etud_moy_gen",
|
||
|
"etud_moy_gen_ranks",
|
||
|
"modimpls_evaluations_complete",
|
||
|
)
|
||
|
|
||
|
def __init__(self, formsemestre):
|
||
|
self.formsemestre = formsemestre
|
||
|
# 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"
|
||
|
"Cache our dataframes"
|
||
|
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
|
||
|
TODO
|
||
|
|
||
|
@cached_property
|
||
|
def etuds(self):
|
||
|
"Liste des inscrits au semestre, sans les démissionnaires"
|
||
|
# nb: si les liste des inscrits change, ResultatsSemestre devient invalide
|
||
|
return self.formsemestre.get_inscrits(include_dem=False)
|
||
|
|
||
|
@cached_property
|
||
|
def etud_index(self):
|
||
|
"dict { etudid : indice dans les inscrits }"
|
||
|
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
||
|
|
||
|
@cached_property
|
||
|
def ues(self):
|
||
|
"Liste des UE du semestre"
|
||
|
return self.formsemestre.query_ues().all()
|
||
|
|
||
|
@cached_property
|
||
|
def modimpls(self):
|
||
|
"Liste des modimpls du semestre (triée par numéro de module)"
|
||
|
modimpls = self.formsemestre.modimpls.all()
|
||
|
modimpls.sort(key=lambda m: m.module.numero)
|
||
|
return modimpls
|
||
|
|
||
|
@cached_property
|
||
|
def ressources(self):
|
||
|
"Liste des ressources du semestre, triées par numéro de module"
|
||
|
return [
|
||
|
m for m in self.modimpls 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.modimpls if m.module.module_type == scu.ModuleType.SAE]
|
||
|
|
||
|
|
||
|
class StatsMoyenne:
|
||
|
"""Une moyenne d'un ensemble étudiants sur quelque chose
|
||
|
(moyenne générale d'un semestre, d'un module, d'un groupe...)
|
||
|
et les statistiques associées: min, max, moy, effectif
|
||
|
"""
|
||
|
|
||
|
def __init__(self, vals):
|
||
|
"""Calcul les statistiques.
|
||
|
Les valeurs NAN ou non numériques sont toujours enlevées.
|
||
|
"""
|
||
|
self.moy = np.nanmean(vals)
|
||
|
self.min = np.nanmin(vals)
|
||
|
self.max = np.nanmax(vals)
|
||
|
self.size = len(vals)
|
||
|
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
|
||
|
|
||
|
def to_dict(self):
|
||
|
return {
|
||
|
"min": self.min,
|
||
|
"max": self.max,
|
||
|
"moy": self.moy,
|
||
|
"size": self.size,
|
||
|
"nb_vals": self.nb_vals,
|
||
|
}
|
||
|
|
||
|
|
||
|
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||
|
class NotesTableCompat(ResultatsSemestre):
|
||
|
"""Implementation partielle de NotesTable WIP TODO
|
||
|
Accès aux notes et rangs.
|
||
|
"""
|
||
|
|
||
|
_cached_attrs = ResultatsSemestre._cached_attrs + ()
|
||
|
|
||
|
def __init__(self, formsemestre):
|
||
|
super().__init__(formsemestre)
|
||
|
nb_etuds = len(self.etuds)
|
||
|
self.bonus = defaultdict(lambda: 0.0) # XXX TODO
|
||
|
self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues}
|
||
|
self.mod_rangs = {
|
||
|
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls
|
||
|
}
|
||
|
|
||
|
@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.ues:
|
||
|
if filter_sport and ue.type == UE_SPORT:
|
||
|
continue
|
||
|
d = ue.to_dict()
|
||
|
d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).to_dict())
|
||
|
ues.append(d)
|
||
|
return ues
|
||
|
|
||
|
def get_modimpls(self):
|
||
|
return [m.to_dict() for m in self.results.modimpls]
|
||
|
|
||
|
def get_etud_moy_gen(self, etudid):
|
||
|
return self.results.etud_moy_gen[etudid]
|
||
|
|
||
|
def get_moduleimpls_attente(self):
|
||
|
return [] # XXX TODO
|
||
|
|
||
|
def get_etud_rang(self, etudid):
|
||
|
return self.etud_moy_gen_ranks[etudid]
|
||
|
|
||
|
def get_etud_rang_group(self, etudid, group_id):
|
||
|
return (None, 0) # XXX unimplemented TODO
|
||
|
|
||
|
def get_etud_ue_status(self, etudid, ue_id):
|
||
|
return {
|
||
|
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
|
||
|
"is_capitalized": False, # XXX TODO
|
||
|
}
|
||
|
|
||
|
def get_etud_mod_moy(self, moduleimpl_id, etudid):
|
||
|
mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
|
||
|
etud_idx = self.results.etud_index[etudid]
|
||
|
# moyenne sur les UE:
|
||
|
self.results.sem_cube[etud_idx, mod_idx].mean()
|
||
|
|
||
|
def get_mod_stats(self, moduleimpl_id):
|
||
|
return {
|
||
|
"moy": "-",
|
||
|
"max": "-",
|
||
|
"min": "-",
|
||
|
"nb_notes": "-",
|
||
|
"nb_missing": "-",
|
||
|
"nb_valid_evals": "-",
|
||
|
}
|
||
|
|
||
|
def get_evals_in_mod(self, moduleimpl_id):
|
||
|
mi = ModuleImpl.query.get(moduleimpl_id)
|
||
|
evals_results = []
|
||
|
for e in mi.evaluations:
|
||
|
d = e.to_dict()
|
||
|
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": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
|
||
|
etud.id
|
||
|
],
|
||
|
}
|
||
|
for etud in self.results.etuds
|
||
|
}
|
||
|
evals_results.append(d)
|
||
|
return evals_results
|