From 82d08be60d3a077f1231cb193ed50dde76cad4f1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Mar 2022 23:22:06 +0200 Subject: [PATCH] refactoring NotesTabeCompat --- app/comp/res_compat.py | 456 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 app/comp/res_compat.py diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py new file mode 100644 index 000000000..690f70990 --- /dev/null +++ b/app/comp/res_compat.py @@ -0,0 +1,456 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Classe résultats pour compatibilité avec le code ScoDoc 7 +""" +from functools import cached_property + +from flask import g, flash + +from app import log +from app.comp import moy_sem +from app.comp.aux_stats import StatsMoyenne +from app.comp.res_common import ResultatsSemestre +from app.comp import res_sem +from app.models import FormSemestre +from app.models import Identite +from app.models import ModuleImpl +from app.scodoc.sco_codes_parcours import UE_SPORT, DEF +from app.scodoc import sco_utils as scu + +# Pour raccorder le code des anciens codes qui attendent une NoteTable +class NotesTableCompat(ResultatsSemestre): + """Implementation partielle de NotesTable + + 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", + "malus", + "etud_moy_gen_ranks", + "etud_moy_gen_ranks_int", + "ue_rangs", + ) + + 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 = None # sera surchargé en Classic, mais pas en APC + """{ modimpl_id : (rangs, effectif) }""" + self.moy_min = "NA" + self.moy_max = "NA" + self.moy_moy = "NA" + self.expr_diagnostics = "" + self.parcours = self.formsemestre.formation.get_parcours() + + def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: + """Liste des étudiants inscrits + order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative) + + Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace + d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]` + """ + etuds = self.formsemestre.get_inscrits( + include_demdef=include_demdef, order=(order_by == "nom") + ) + if order_by == "moy": + etuds.sort( + key=lambda e: ( + self.etud_moy_gen_ranks_int.get(e.id, 100000), + e.sort_key, + ) + ) + return etuds + + def get_etudids(self) -> list[int]: + """(deprecated) + Liste des etudids inscrits, incluant les démissionnaires. + triée par ordre alphabetique de NOM + (à éviter: renvoie les etudids, mais est moins efficace que get_inscrits) + """ + # Note: pour avoir les inscrits non triés, + # utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ] + return [x["etudid"] for x in self.inscrlist] + + @cached_property + def sem(self) -> dict: + """le formsemestre, comme un gros et gras dict (nt.sem)""" + return self.formsemestre.get_infos_dict() + + @cached_property + def inscrlist(self) -> list[dict]: # utilisé par PE + """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, order=True) + 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, check_apc_ects=True + ) -> list[dict]: # 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 = self.formsemestre.query_ues(with_sport=not filter_sport) + ues_dict = [] + for ue in ues: + 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_dict.append(d) + if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"): + g.checked_apc_ects = True + if None in [ue.ects for ue in ues if ue.type != UE_SPORT]: + flash( + """Calcul moyenne générale impossible: ECTS des UE manquants !""", + category="danger", + ) + return ues_dict + + 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 compute_rangs(self): + """Calcule les classements + Moyenne générale: etud_moy_gen_ranks + Par UE (sauf ue bonus) + """ + ( + self.etud_moy_gen_ranks, + self.etud_moy_gen_ranks_int, + ) = moy_sem.comp_ranks_series(self.etud_moy_gen) + for ue in self.formsemestre.query_ues(): + moy_ue = self.etud_moy_ue[ue.id] + self.ue_rangs[ue.id] = ( + moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine + int(moy_ue.count()), + ) + # .count() -> nb of non NaN values + + def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]: + """Le rang de l'étudiant dans cette ue + Result: rang:str, effectif:str + """ + rangs, effectif = self.ue_rangs[ue_id] + if rangs is not None: + rang = rangs[etudid] + else: + return "", "" + return rang, effectif + + def etud_check_conditions_ues(self, etudid): + """Vrai si les conditions sur les UE sont remplies. + Ne considère que les UE ayant des notes (moyenne calculée). + (les UE sans notes ne sont pas comptées comme sous la barre) + Prend en compte les éventuelles UE capitalisées. + + Pour les parcours habituels, cela revient à vérifier que + les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes) + + Pour les parcours non standards (LP2014), cela peut être plus compliqué. + + Return: True|False, message explicatif + """ + ue_status_list = [] + for ue in self.formsemestre.query_ues(): + ue_status = self.get_etud_ue_status(etudid, ue.id) + if ue_status: + ue_status_list.append(ue_status) + return self.parcours.check_barre_ues(ue_status_list) + + def all_etuds_have_sem_decisions(self): + """True si tous les étudiants du semestre ont une décision de jury. + Ne regarde pas les décisions d'UE. + """ + for ins in self.formsemestre.inscriptions: + if ins.etat != scu.INSCRIT: + continue # skip démissionnaires + if self.get_etud_decision_sem(ins.etudid) is None: + return False + return True + + def etud_has_decision(self, etudid): + """True s'il y a une décision de jury pour cet étudiant""" + return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid) + + def get_etud_decision_ues(self, etudid: int) -> dict: + """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. + Ne tient pas compte des UE capitalisées. + { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : } + Ne renvoie aucune decision d'UE pour les défaillants + """ + if self.get_etud_etat(etudid) == DEF: + return {} + else: + if not self.validations: + self.validations = res_sem.load_formsemestre_validations( + self.formsemestre + ) + return self.validations.decisions_jury_ues.get(etudid, None) + + 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: + if not self.validations: + self.validations = res_sem.load_formsemestre_validations( + self.formsemestre + ) + return self.validations.decisions_jury.get(etudid, 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: int, etudid: int) -> str: + """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" + if not self.moyennes_matieres: + return "nd" + return ( + self.moyennes_matieres[matiere_id].get(etudid, "-") + if matiere_id in self.moyennes_matieres + else "-" + ) + + 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 en compte les UE capitalisées. + Si apc, moyenne indicative. + Si pas de notes: 'NA' + """ + return self.etud_moy_gen[etudid] + + def get_etud_ects_pot(self, etudid: int) -> dict: + """ + Un dict avec les champs + ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury) + ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) + + Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non + encore enregistrées). + """ + # was nt.get_etud_moy_infos + # XXX pour compat nt, à remplacer ultérieurement + ues = self.get_etud_ue_validables(etudid) + ects_pot = 0.0 + for ue in ues: + if ( + ue.id in self.etud_moy_ue + and ue.ects is not None + and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE + ): + ects_pot += ue.ects + return { + "ects_pot": ects_pot, + "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) + } + + def get_etud_rang(self, etudid: int): + return self.etud_moy_gen_ranks.get(etudid, 99999) + + 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) + modimpl_results = self.modimpls_results.get(moduleimpl_id) + if not modimpl_results: + return [] # safeguard + evals_results = [] + for e in modimpl.evaluations: + if modimpl_results.evaluations_completes_dict.get(e.id, False): + 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": modimpl_results.evals_notes[e.id][etud.id], + } + for etud in self.etuds + } + d["etat"] = { + "evalattente": modimpl_results.evaluations_etat[e.id].nb_attente, + } + evals_results.append(d) + elif e.id not in modimpl_results.evaluations_completes_dict: + # ne devrait pas arriver ? XXX + log( + f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?" + ) + return evals_results + + def get_evaluations_etats(self): + """[ {...evaluation et son etat...} ]""" + # TODO: à moderniser + from app.scodoc import sco_evaluations + + if not hasattr(self, "_evaluations_etats"): + self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem( + self.formsemestre.id + ) + + return self._evaluations_etats + + def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: + """Liste des états des évaluations de ce module""" + # XXX TODO à moderniser: lent, recharge des données que l'on a déjà... + return [ + e + for e in self.get_evaluations_etats() + if e["moduleimpl_id"] == moduleimpl_id + ] + + def get_moduleimpls_attente(self): + """Liste des modimpls du semestre ayant des notes en attente""" + return [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if self.modimpls_results[modimpl.id].en_attente + ] + + 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 + ues = self.formsemestre.query_ues(with_sport=True) # avec bonus + 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 = [] + ue_is_cap = {} + for ue in ues: + ue_status = self.get_etud_ue_status(etudid, ue.id) + if ue_status: + moy_ues.append(ue_status["moy"]) + ue_is_cap[ue.id] = ue_status["is_capitalized"] + else: + moy_ues.append("?") + t = [moy_gen] + list(moy_ues) + # Moyennes modules: + for modimpl in self.formsemestre.modimpls_sorted: + if ue_is_cap.get(modimpl.module.ue.id, False): + val = "-c-" + else: + 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 + }