############################################################################## # 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.comp.aux import StatsMoyenne from app.models import ModuleImpl 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_results", "etud_moy_gen", "etud_moy_gen_ranks", ) def __init__(self, formsemestre): self.formsemestre = formsemestre # BUT ou standard ? (apc == "approche par compétences") self.is_apc = formsemestre.formation.is_apc() # Attributs "virtuels", définis pas les sous-classes # ResultatsSemestreBUT ou ResultatsSemestreStd self.etud_moy_ue = {} self.etud_moy_gen = {} self.etud_moy_gen_ranks = {} # 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() @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 en APC - triée par numéros d'UE/matières/modules pour les formations standard. """ modimpls = self.formsemestre.modimpls.all() if self.is_apc: modimpls.sort(key=lambda m: (m.module.numero, m.module.code)) else: modimpls.sort( key=lambda m: ( m.module.ue.numero, m.module.matiere.numero, m.module.numero, m.module.code, ) ) 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] # 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_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: return [m.to_dict() for m in self.modimpls] else: return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id] def get_etud_moy_gen(self, etudid): # -> float | str """Moyenne générale de cet etudiant dans ce semestre. Prend en compte les UE capitalisées. (TODO) Si apc, moyenne indicative. Si pas de notes: 'NA' """ return self.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