############################################################################## # 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