diff --git a/README.md b/README.md index 29ac566a..5827703c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ScoDoc - Gestion de la scolarité - Version ScoDoc 9 -(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt) +(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt) Installation: voir instructions à jour sur diff --git a/app/api/logos.py b/app/api/logos.py index 4fdb1099..e32f6595 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/api/sco_api.py b/app/api/sco_api.py index 6aa488c2..2051ad82 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 4ff2849f..0eb28fe4 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -1,121 +1,44 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## -from collections import defaultdict +"""Génération bulletin BUT +""" + import datetime from flask import url_for, g -import numpy as np -import pandas as pd -from app import db - -from app.comp import moy_ue, moy_sem, inscr_mod -from app.models import ModuleImpl from app.scodoc import sco_utils as scu -from app.scodoc.sco_cache import ResultatsSemestreBUTCache from app.scodoc import sco_bulletins_json from app.scodoc import sco_preferences -from app.scodoc.sco_utils import jsnan, fmt_note +from app.scodoc.sco_utils import fmt_note +from app.comp.res_but import ResultatsSemestreBUT -class ResultatsSemestreBUT: - """Structure légère pour stocker les résultats du semestre et - générer les bulletins. - __init__ : charge depuis le cache ou calcule +class BulletinBUT(ResultatsSemestreBUT): + """Génération du bulletin BUT. + Cette classe génère des dictionnaires avec toutes les informations + du bulletin, qui sont immédiatement traduisibles en JSON. """ - _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 - self.ues = formsemestre.query_ues().all() - self.modimpls = formsemestre.modimpls.all() - self.etuds = self.formsemestre.get_inscrits(include_dem=False) - self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)} - self.saes = [ - m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE - ] - self.ressources = [ - m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE - ] - if not self.load_cached(): - self.compute() - self.store() - - def load_cached(self) -> bool: - "Load cached dataframes, returns False si pas en cache" - data = ResultatsSemestreBUTCache.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 dataframes" - ResultatsSemestreBUTCache.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" - ( - self.sem_cube, - self.modimpls_evals_poids, - self.modimpls_evals_notes, - modimpls_evaluations, - self.modimpls_evaluations_complete, - ) = moy_ue.notes_sem_load_cube(self.formsemestre) - self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) - self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( - self.formsemestre, ues=self.ues, modimpls=self.modimpls - ) - # l'idx de la colonne du mod modimpl.id est - # modimpl_coefs_df.columns.get_loc(modimpl.id) - # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) - self.etud_moy_ue = moy_ue.compute_ue_moys( - self.sem_cube, - self.etuds, - self.modimpls, - self.ues, - self.modimpl_inscr_df, - self.modimpl_coefs_df, - ) - self.etud_moy_gen = moy_sem.compute_sem_moys( - self.etud_moy_ue, self.modimpl_coefs_df - ) - self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) - def etud_ue_mod_results(self, etud, ue, modimpls) -> dict: "dict synthèse résultats dans l'UE pour les modules indiqués" d = {} etud_idx = self.etud_index[etud.id] ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id) etud_moy_module = self.sem_cube[etud_idx] # module x UE - for mi in modimpls: - coef = self.modimpl_coefs_df[mi.id][ue.id] + for modimpl in modimpls: + coef = self.modimpl_coefs_df[modimpl.id][ue.id] if coef > 0: - d[mi.module.code] = { - "id": mi.id, + d[modimpl.module.code] = { + "id": modimpl.id, "coef": coef, "moyenne": fmt_note( - etud_moy_module[self.modimpl_coefs_df.columns.get_loc(mi.id)][ - ue_idx - ] + etud_moy_module[ + self.modimpl_coefs_df.columns.get_loc(modimpl.id) + ][ue_idx] ), } return d @@ -149,7 +72,7 @@ class ResultatsSemestreBUT: avec évaluations de chacun.""" d = {} # etud_idx = self.etud_index[etud.id] - for mi in modimpls: + for modimpl in modimpls: # mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id) # # moyennes indicatives (moyennes de moyennes d'UE) # try: @@ -163,14 +86,15 @@ class ResultatsSemestreBUT: # moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx]) # except RuntimeWarning: # all nans in np.nanmean # pass - d[mi.module.code] = { - "id": mi.id, - "titre": mi.module.titre, - "code_apogee": mi.module.code_apogee, + modimpl_results = self.modimpls_results[modimpl.id] + d[modimpl.module.code] = { + "id": modimpl.id, + "titre": modimpl.module.titre, + "code_apogee": modimpl.module.code_apogee, "url": url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, - moduleimpl_id=mi.id, + moduleimpl_id=modimpl.id, ), "moyenne": { # # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan) @@ -181,16 +105,17 @@ class ResultatsSemestreBUT: }, "evaluations": [ self.etud_eval_results(etud, e) - for eidx, e in enumerate(mi.evaluations) + for e in modimpl.evaluations if e.visibulletin - and self.modimpls_evaluations_complete[mi.id][eidx] + and modimpl_results.evaluations_etat[e.id].is_complete ], } return d def etud_eval_results(self, etud, e) -> dict: "dict resultats d'un étudiant à une évaluation" - eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series + # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits + eval_notes = self.modimpls_results[e.moduleimpl_id].evals_notes[e.id] notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna() d = { "id": e.id, @@ -202,7 +127,7 @@ class ResultatsSemestreBUT: "poids": {p.ue.acronyme: p.poids for p in e.ue_poids}, "note": { "value": fmt_note( - self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id], + eval_notes[etud.id], note_max=e.note_max, ), "min": fmt_note(notes_ok.min()), @@ -233,7 +158,7 @@ class ResultatsSemestreBUT: }, "formsemestre_id": formsemestre.id, "etat_inscription": etat_inscription, - "options": bulletin_option_affichage(formsemestre), + "options": sco_preferences.bulletin_option_affichage(formsemestre.id), } semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], @@ -244,8 +169,8 @@ class ResultatsSemestreBUT: "numero": formsemestre.semestre_id, "groupes": [], # XXX TODO "absences": { # XXX TODO - "injustifie": 1, - "total": 33, + "injustifie": -1, + "total": -1, }, } semestre_infos.update( @@ -298,125 +223,3 @@ class ResultatsSemestreBUT: ) return d - - -def bulletin_option_affichage(formsemestre): - "dict avec les options d'affichages (préférences) pour ce semestre" - prefs = sco_preferences.SemPreferences(formsemestre.id) - fields = ( - "bul_show_abs", - "bul_show_abs_modules", - "bul_show_ects", - "bul_show_codemodules", - "bul_show_matieres", - "bul_show_rangs", - "bul_show_ue_rangs", - "bul_show_mod_rangs", - "bul_show_moypromo", - "bul_show_minmax", - "bul_show_minmax_mod", - "bul_show_minmax_eval", - "bul_show_coef", - "bul_show_ue_cap_details", - "bul_show_ue_cap_current", - "bul_show_temporary", - "bul_temporary_txt", - "bul_show_uevalid", - "bul_show_date_inscr", - ) - # on enlève le "bul_" de la clé: - return {field[4:]: prefs[field] for field in fields} - - -# Pour raccorder le code des anciens bulletins qui attendent une NoteTable -class APCNotesTableCompat: - """Implementation partielle de NotesTable pour les formations APC - Accès aux notes et rangs. - """ - - def __init__(self, formsemestre): - self.results = ResultatsSemestreBUT(formsemestre) - nb_etuds = len(self.results.etuds) - self.rangs = self.results.etud_moy_gen_ranks - self.moy_min = self.results.etud_moy_gen.min() - self.moy_max = self.results.etud_moy_gen.max() - self.moy_moy = self.results.etud_moy_gen.mean() - self.bonus = defaultdict(lambda: 0.0) # XXX - self.ue_rangs = { - u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues - } - self.mod_rangs = { - m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls - } - - def get_ues(self): - ues = [] - for ue in self.results.ues: - d = ue.to_dict() - d.update( - { - "max": self.results.etud_moy_ue[ue.id].max(), - "min": self.results.etud_moy_ue[ue.id].min(), - "moy": self.results.etud_moy_ue[ue.id].mean(), - "nb_moy": len(self.results.etud_moy_ue), - } - ) - 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.rangs[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 diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 9e234dc0..9743c218 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -108,8 +108,8 @@ def bulletin_but_xml_compat( code_ine=etud.code_ine or "", nom=scu.quote_xml_attr(etud.nom), prenom=scu.quote_xml_attr(etud.prenom), - civilite=scu.quote_xml_attr(etud.civilite_str()), - sexe=scu.quote_xml_attr(etud.civilite_str()), # compat + civilite=scu.quote_xml_attr(etud.civilite_str), + sexe=scu.quote_xml_attr(etud.civilite_str), # compat photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)), email=scu.quote_xml_attr(etud.get_first_email() or ""), emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""), @@ -216,9 +216,9 @@ def bulletin_but_xml_compat( Element( "note", value=scu.fmt_note( - results.modimpls_evals_notes[e.moduleimpl_id][ - e.id - ][etud.id] + results.modimpls_results[ + e.moduleimpl_id + ].evals_notes[e.id][etud.id] ), ) ) @@ -314,13 +314,3 @@ def bulletin_but_xml_compat( return None else: return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) - - -""" -formsemestre_id=718 -etudid=12496 -from app.but.bulletin_but import * -mapp.set_sco_dept("RT") -sem = FormSemestre.query.get(formsemestre_id) -r = ResultatsSemestreBUT(sem) -""" diff --git a/app/but/forms/refcomp_forms.py b/app/but/forms/refcomp_forms.py index a1cab459..6fcd5950 100644 --- a/app/but/forms/refcomp_forms.py +++ b/app/but/forms/refcomp_forms.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/import_refcomp.py b/app/but/import_refcomp.py index e0e59ca8..04e96b50 100644 --- a/app/but/import_refcomp.py +++ b/app/but/import_refcomp.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## from xml.etree import ElementTree diff --git a/app/comp/aux.py b/app/comp/aux.py new file mode 100644 index 00000000..92ee22c1 --- /dev/null +++ b/app/comp/aux.py @@ -0,0 +1,37 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +import numpy as np + +"""Quelques classes auxiliaires pour les calculs des notes +""" + + +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): + "Tous les attributs dans un dict" + return { + "min": self.min, + "max": self.max, + "moy": self.moy, + "size": self.size, + "nb_vals": self.nb_vals, + } diff --git a/app/comp/df_cache.py b/app/comp/df_cache.py index dec325e2..5b555fec 100644 --- a/app/comp/df_cache.py +++ b/app/comp/df_cache.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -43,7 +43,7 @@ class ModuleCoefsCache(sco_cache.ScoDocCache): class EvaluationsPoidsCache(sco_cache.ScoDocCache): """Cache for poids evals Clé: moduleimpl_id - Valeur: DataFrame (df_load_evaluations_poids) + Valeur: DataFrame (load_evaluations_poids) """ prefix = "EPC" diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 40616cba..39de5a81 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,21 +27,241 @@ """Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ) +Pour les formations classiques et le BUT + Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la moyenne générale d'une UE. """ +from dataclasses import dataclass import numpy as np import pandas as pd -from pandas.core.frame import DataFrame from app import db -from app import models from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu -def df_load_evaluations_poids( +@dataclass +class EvaluationEtat: + """Classe pour stocker quelques infos sur les résultats d'une évaluation""" + + evaluation_id: int + nb_attente: int + is_complete: bool + + +class ModuleImplResults: + """Classe commune à toutes les formations (standard et APC). + Les notes des étudiants d'un moduleimpl. + Les poids des évals sont à part car on en a besoin sans les notes pour les + tableaux de bord. + Les attributs sont tous des objets simples cachables dans Redis; + les caches sont gérés par ResultatsSemestre. + """ + + def __init__(self, moduleimpl: ModuleImpl): + self.moduleimpl_id = moduleimpl.id + self.module_id = moduleimpl.module.id + self.etudids = None + "liste des étudiants inscrits au SEMESTRE" + self.nb_inscrits_module = None + "nombre d'inscrits (non DEM) au module" + self.evaluations_completes = [] + "séquence de booléens, indiquant les évals à prendre en compte." + self.evaluations_etat = {} + "{ evaluation_id: EvaluationEtat }" + # + self.evals_notes = None + """DataFrame, colonnes: EVALS, Lignes: etudid + valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE, + NOTES_ABSENCE. + Les NaN désignent les notes manquantes (non saisies). + """ + self.etuds_moy_module = None + """DataFrame, colonnes UE, lignes etud + = la note de l'étudiant dans chaque UE pour ce module. + ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) + ne donnent pas de coef vers cette UE. + """ + self.load_notes() + + def load_notes(self): # ré-écriture de df_load_modimpl_notes + """Charge toutes les notes de toutes les évaluations du module. + Dataframe evals_notes + colonnes: le nom de la colonne est l'evaluation_id (int) + index (lignes): etudid (int) + + L'ensemble des étudiants est celui des inscrits au SEMESTRE. + + Les notes sont "brutes" (séries de floats) et peuvent prendre les valeurs: + note : float (valeur enregistrée brute, NON normalisée sur 20) + pas de note: NaN (rien en bd, ou étudiant non inscrit au module) + absent: NOTES_ABSENCE (NULL en bd) + excusé: NOTES_NEUTRALISE (voir sco_utils) + attente: NOTES_ATTENTE + + Évaluation "complete" (prise en compte dans les calculs) si: + - soit tous les étudiants inscrits au module ont des notes + - soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete) + + Évaluation "attente" (prise en compte dans les calculs, mais il y + manque des notes) ssi il y a des étudiants inscrits au semestre et au module + qui ont des notes ATT. + """ + moduleimpl = ModuleImpl.query.get(self.moduleimpl_id) + self.etudids = self._etudids() + + # --- Calcul nombre d'inscrits pour déterminer les évaluations "completes": + # on prend les inscrits au module ET au semestre (donc sans démissionnaires) + inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection( + self.etudids + ) + self.nb_inscrits_module = len(inscrits_module) + + # dataFrame vide, index = tous les inscrits au SEMESTRE + evals_notes = pd.DataFrame(index=self.etudids, dtype=float) + self.evaluations_completes = [] + for evaluation in moduleimpl.evaluations: + eval_df = self._load_evaluation_notes(evaluation) + # is_complete ssi tous les inscrits (non dem) au semestre ont une note + # ou évaluaton déclarée "à prise en compte immédiate" + is_complete = ( + len(set(eval_df.index).intersection(self.etudids)) + == self.nb_inscrits_module + ) or evaluation.publish_incomplete # immédiate + self.evaluations_completes.append(is_complete) + + # NULL en base => ABS (= -999) + eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) + # Ce merge ne garde que les étudiants inscrits au module + # et met à NULL les notes non présentes + # (notes non saisies ou etuds non inscrits au module): + evals_notes = evals_notes.merge( + eval_df, how="left", left_index=True, right_index=True + ) + # Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.) + nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE) + self.evaluations_etat[evaluation.id] = EvaluationEtat( + evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete + ) + + # Force columns names to integers (evaluation ids) + evals_notes.columns = pd.Int64Index( + [int(x) for x in evals_notes.columns], dtype="int" + ) + self.evals_notes = evals_notes + + def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame: + """Charge les notes de l'évaluation + Resultat: dataframe, index: etudid ayant une note, valeur: note brute. + """ + eval_df = pd.read_sql_query( + """SELECT n.etudid, n.value AS "%(evaluation_id)s" + FROM notes_notes n, notes_moduleimpl_inscription i + WHERE evaluation_id=%(evaluation_id)s + AND n.etudid = i.etudid + AND i.moduleimpl_id = %(moduleimpl_id)s + """, + db.engine, + params={ + "evaluation_id": evaluation.id, + "moduleimpl_id": evaluation.moduleimpl.id, + }, + index_col="etudid", + ) + eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)]) + return eval_df + + def _etudids(self): + """L'index du dataframe est la liste des étudiants inscrits au semestre, + sans les démissionnaires. + """ + return [ + e.etudid + for e in ModuleImpl.query.get(self.moduleimpl_id).formsemestre.get_inscrits( + include_dem=False + ) + ] + + def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array: + """Coefficients des évaluations, met à zéro ceux des évals incomplètes. + Résultat: 2d-array of floats, shape (nb_evals, 1) + """ + return ( + np.array( + [e.coefficient for e in moduleimpl.evaluations], + dtype=float, + ) + * self.evaluations_completes + ).reshape(-1, 1) + + def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array: + """Les notes des évaluations, + remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20. + Résultat: 2d array of floats, shape nb_etuds x nb_evaluations + """ + return np.where( + self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0 + ) / [e.note_max / 20.0 for e in moduleimpl.evaluations] + + +class ModuleImplResultsAPC(ModuleImplResults): + "Calcul des moyennes de modules à la mode BUT" + + def compute_module_moy( + self, + evals_poids_df: pd.DataFrame, + ) -> pd.DataFrame: + """Calcule les moyennes des étudiants dans ce module + + Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs + + Résultat: DataFrame, colonnes UE, lignes etud + = la note de l'étudiant dans chaque UE pour ce module. + ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) + ne donnent pas de coef vers cette UE. + """ + moduleimpl = ModuleImpl.query.get(self.moduleimpl_id) + nb_etuds, nb_evals = self.evals_notes.shape + nb_ues = evals_poids_df.shape[1] + assert evals_poids_df.shape[0] == nb_evals # compat notes/poids + if nb_etuds == 0: + return pd.DataFrame(index=[], columns=evals_poids_df.columns) + evals_coefs = self.get_evaluations_coefs(moduleimpl) + evals_poids = evals_poids_df.values * evals_coefs + # -> evals_poids shape : (nb_evals, nb_ues) + assert evals_poids.shape == (nb_evals, nb_ues) + evals_notes_20 = self.get_eval_notes_sur_20(moduleimpl) + + # Les poids des évals pour chaque étudiant: là où il a des notes + # non neutralisées + # (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui) + # Note: les NaN sont remplacés par des 0 dans evals_notes + # et dans dans evals_poids_etuds + # (rappel: la comparaison est toujours false face à un NaN) + # shape: (nb_etuds, nb_evals, nb_ues) + poids_stacked = np.stack([evals_poids] * nb_etuds) + evals_poids_etuds = np.where( + np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, + poids_stacked, + 0, + ) + # Calcule la moyenne pondérée sur les notes disponibles: + evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etuds_moy_module = np.sum( + evals_poids_etuds * evals_notes_stacked, axis=1 + ) / np.sum(evals_poids_etuds, axis=1) + self.etuds_moy_module = pd.DataFrame( + etuds_moy_module, + index=self.evals_notes.index, + columns=evals_poids_df.columns, + ) + return self.etuds_moy_module + + +def load_evaluations_poids( moduleimpl_id: int, default_poids=1.0 ) -> tuple[pd.DataFrame, list]: """Charge poids des évaluations d'un module et retourne un dataframe @@ -55,23 +275,25 @@ def df_load_evaluations_poids( ues = modimpl.formsemestre.query_ues(with_sport=False).all() ue_ids = [ue.id for ue in ues] evaluation_ids = [evaluation.id for evaluation in evaluations] - df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) - for eval_poids in EvaluationUEPoids.query.join( + evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) + for ue_poids in EvaluationUEPoids.query.join( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): - df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids + evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids if default_poids is not None: - df.fillna(value=default_poids, inplace=True) - return df, ues + evals_poids.fillna(value=default_poids, inplace=True) + return evals_poids, ues -def check_moduleimpl_conformity( +def moduleimpl_is_conforme( moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame ) -> bool: """Vérifie que les évaluations de ce moduleimpl sont bien conformes au PN. Un module est dit *conforme* si et seulement si la somme des poids de ses évaluations vers une UE de coefficient non nul est non nulle. + + Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs """ nb_evals, nb_ues = evals_poids.shape if nb_evals == 0: @@ -79,160 +301,52 @@ def check_moduleimpl_conformity( if nb_ues == 0: return False # situation absurde (pas d'UE) if len(modules_coefficients) != nb_ues: - raise ValueError("check_moduleimpl_conformity: nb ue incoherent") + raise ValueError("moduleimpl_is_conforme: nb ue incoherent") module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 check = all( - (modules_coefficients[moduleimpl.module.id].to_numpy() != 0) + (modules_coefficients[moduleimpl.module_id].to_numpy() != 0) == module_evals_poids ) return check -def df_load_modimpl_notes(moduleimpl_id: int) -> tuple: - """Construit un dataframe avec toutes les notes de toutes les évaluations du module. - colonnes: le nom de la colonne est l'evaluation_id (int) - index (lignes): etudid (int) +class ModuleImplResultsClassic(ModuleImplResults): + "Calcul des moyennes de modules des formations classiques" - Résultat: (evals_notes, liste de évaluations du moduleimpl, - liste de booleens indiquant si l'évaluation est "complete") + def compute_module_moy(self) -> pd.Series: + """Calcule les moyennes des étudiants dans ce module - L'ensemble des étudiants est celui des inscrits au SEMESTRE. - - Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs: - note : float (valeur enregistrée brute, non normalisée sur 20) - pas de note: NaN (rien en bd, ou étudiant non inscrit au module) - absent: NOTES_ABSENCE (NULL en bd) - excusé: NOTES_NEUTRALISE (voir sco_utils) - attente: NOTES_ATTENTE - - L'évaluation "complete" (prise en compte dans les calculs) si: - - soit tous les étudiants inscrits au module ont des notes - - soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete) - - N'utilise pas de cache ScoDoc. - """ - # L'index du dataframe est la liste des étudiants inscrits au semestre, - # sans les démissionnaires - etudids = [ - e.etudid - for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits( - include_dem=False + Résultat: Series, lignes etud + = la note (moyenne) de l'étudiant pour ce module. + ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) + ne donnent pas de coef. + """ + modimpl = ModuleImpl.query.get(self.moduleimpl_id) + nb_etuds, nb_evals = self.evals_notes.shape + if nb_etuds == 0: + return pd.Series() + evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1) + assert evals_coefs.shape == (nb_evals,) + evals_notes_20 = self.get_eval_notes_sur_20(modimpl) + # Les coefs des évals pour chaque étudiant: là où il a des notes + # non neutralisées + # (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui) + # Note: les NaN sont remplacés par des 0 dans evals_notes + # et dans dans evals_poids_etuds + # (rappel: la comparaison est toujours False face à un NaN) + # shape: (nb_etuds, nb_evals) + coefs_stacked = np.stack([evals_coefs] * nb_etuds) + evals_coefs_etuds = np.where( + self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0 ) - ] - evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() - # --- Calcul nombre d'inscrits pour détermnier si évaluation "complete": - if evaluations: - # on prend les inscrits au module ET au semestre (donc sans démissionnaires) - inscrits_module = { - ins.etud.id for ins in evaluations[0].moduleimpl.inscriptions - }.intersection(etudids) - nb_inscrits_module = len(inscrits_module) - else: - nb_inscrits_module = 0 - # empty df with all students: - evals_notes = pd.DataFrame(index=etudids, dtype=float) - evaluations_completes = [] - for evaluation in evaluations: - eval_df = pd.read_sql_query( - """SELECT n.etudid, n.value AS "%(evaluation_id)s" - FROM notes_notes n, notes_moduleimpl_inscription i - WHERE evaluation_id=%(evaluation_id)s - AND n.etudid = i.etudid - AND i.moduleimpl_id = %(moduleimpl_id)s - ORDER BY n.etudid - """, - db.engine, - params={ - "evaluation_id": evaluation.id, - "moduleimpl_id": evaluation.moduleimpl.id, - }, - index_col="etudid", + # Calcule la moyenne pondérée sur les notes disponibles: + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etuds_moy_module = np.sum( + evals_coefs_etuds * evals_notes_20, axis=1 + ) / np.sum(evals_coefs_etuds, axis=1) + + self.etuds_moy_module = pd.Series( + etuds_moy_module, + index=self.evals_notes.index, ) - eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)]) - # is_complete ssi tous les inscrits (non dem) au semestre ont une note - is_complete = ( - len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module - ) or evaluation.publish_incomplete - evaluations_completes.append(is_complete) - # NULL en base => ABS (= -999) - eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) - # Ce merge met à NULL les élements non présents - # (notes non saisies ou etuds non inscrits au module): - evals_notes = evals_notes.merge( - eval_df, how="left", left_index=True, right_index=True - ) - # Force columns names to integers (evaluation ids) - evals_notes.columns = pd.Int64Index( - [int(x) for x in evals_notes.columns], dtype="int64" - ) - return evals_notes, evaluations, evaluations_completes - - -def compute_module_moy( - evals_notes_df: pd.DataFrame, - evals_poids_df: pd.DataFrame, - evaluations: list, - evaluations_completes: list, -) -> pd.DataFrame: - """Calcule les moyennes des étudiants dans ce module - - - evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid - valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE, - NOTES_ABSENCE. - Les NaN désignent les notes manquantes (non saisies). - - - evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs - - - evaluations: séquence d'évaluations (utilisées pour le coef et - le barème) - - - evaluations_completes: séquence de booléens indiquant les - évals à prendre en compte. - - Résultat: DataFrame, colonnes UE, lignes etud - = la note de l'étudiant dans chaque UE pour ce module. - ou NaN si les évaluations (dans lesquelles l'étudiant à des notes) - ne donnent pas de coef vers cette UE. - """ - nb_etuds, nb_evals = evals_notes_df.shape - nb_ues = evals_poids_df.shape[1] - assert evals_poids_df.shape[0] == nb_evals # compat notes/poids - if nb_etuds == 0: - return pd.DataFrame(index=[], columns=evals_poids_df.columns) - # Coefficients des évaluations, met à zéro ceux des évals incomplètes: - evals_coefs = ( - np.array( - [e.coefficient for e in evaluations], - dtype=float, - ) - * evaluations_completes - ).reshape(-1, 1) - evals_poids = evals_poids_df.values * evals_coefs - # -> evals_poids shape : (nb_evals, nb_ues) - assert evals_poids.shape == (nb_evals, nb_ues) - # Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20: - evals_notes = np.where( - evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0 - ) / [e.note_max / 20.0 for e in evaluations] - # Les poids des évals pour les étudiant: là où il a des notes non neutralisées - # (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui) - # Note: les NaN sont remplacés par des 0 dans evals_notes - # et dans dans evals_poids_etuds - # (rappel: la comparaison est toujours false face à un NaN) - # shape: (nb_etuds, nb_evals, nb_ues) - poids_stacked = np.stack([evals_poids] * nb_etuds) - evals_poids_etuds = np.where( - np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, - poids_stacked, - 0, - ) - # Calcule la moyenne pondérée sur les notes disponibles: - evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2) - with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) - etuds_moy_module = np.sum( - evals_poids_etuds * evals_notes_stacked, axis=1 - ) / np.sum(evals_poids_etuds, axis=1) - etuds_moy_module_df = pd.DataFrame( - etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns - ) - return etuds_moy_module_df + return self.etuds_moy_module diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 037b4cd0..8797b856 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -31,7 +31,7 @@ import numpy as np import pandas as pd -def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df): +def compute_sem_moys_apc(etud_moy_ue_df, modimpl_coefs_df): """Calcule la moyenne générale indicative = moyenne des moyennes d'UE, pondérée par la somme de leurs coefs diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 74994f26..855e3b7f 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,7 @@ # ############################################################################## -"""Fonctions de calcul des moyennes d'UE +"""Fonctions de calcul des moyennes d'UE (classiques ou BUT) """ import numpy as np import pandas as pd @@ -39,9 +39,9 @@ from app.scodoc import sco_codes_parcours def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame: - """Charge les coefs des modules de la formation pour le semestre indiqué. + """Charge les coefs APC des modules de la formation pour le semestre indiqué. - Ces coefs lient les modules à chaque UE. + En APC, ces coefs lient les modules à chaque UE. Résultat: (module_coefs_df, ues, modules) DataFrame rows = UEs, columns = modules, value = coef. @@ -86,7 +86,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data def df_load_modimpl_coefs( formsemestre: models.FormSemestre, ues=None, modimpls=None ) -> pd.DataFrame: - """Charge les coefs des modules du formsemestre indiqué. + """Charge les coefs APC des modules du formsemestre indiqué. Comme df_load_module_coefs mais prend seulement les UE et modules du formsemestre. @@ -127,45 +127,32 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray: return modimpls_notes.swapaxes(0, 1) -def notes_sem_load_cube(formsemestre): +def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: """Calcule le cube des notes du semestre (charge toutes les notes, calcule les moyenne des modules et assemble le cube) Resultat: sem_cube : ndarray (etuds x modimpls x UEs) modimpls_evals_poids dict { modimpl.id : evals_poids } - modimpls_evals_notes dict { modimpl.id : evals_notes } - modimpls_evaluations dict { modimpl.id : liste des évaluations } - modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)} + modimpls_results dict { modimpl.id : ModuleImplResultsAPC } """ + modimpls_results = {} modimpls_evals_poids = {} - modimpls_evals_notes = {} - modimpls_evaluations = {} - modimpls_evaluations_complete = {} modimpls_notes = [] for modimpl in formsemestre.modimpls: - evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( - modimpl.id - ) - evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id) - etuds_moy_module = moy_mod.compute_module_moy( - evals_notes, evals_poids, evaluations, evaluations_completes - ) - modimpls_evals_poids[modimpl.id] = evals_poids - modimpls_evals_notes[modimpl.id] = evals_notes - modimpls_evaluations[modimpl.id] = evaluations - modimpls_evaluations_complete[modimpl.id] = evaluations_completes + mod_results = moy_mod.ModuleImplResultsAPC(modimpl) + evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id) + etuds_moy_module = mod_results.compute_module_moy(evals_poids) + modimpls_results[modimpl.id] = mod_results modimpls_notes.append(etuds_moy_module) return ( notes_sem_assemble_cube(modimpls_notes), modimpls_evals_poids, - modimpls_evals_notes, - modimpls_evaluations, - modimpls_evaluations_complete, + modimpls_results, ) -def compute_ue_moys( +def compute_ue_moys_apc( sem_cube: np.array, etuds: list, modimpls: list, @@ -173,7 +160,7 @@ def compute_ue_moys( modimpl_inscr_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame, ) -> pd.DataFrame: - """Calcul de la moyenne d'UE + """Calcul de la moyenne d'UE en mode APC (BUT). La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR NI non inscrit à (au moins un) module de cette UE NA pas de notes disponibles @@ -182,11 +169,11 @@ def compute_ue_moys( sem_cube: notes moyennes aux modules ndarray (etuds x modimpls x UEs) (floats avec des NaN) - etuds : lites des étudiants (dim. 0 du cube) + etuds : listes des étudiants (dim. 0 du cube) modimpls : liste des modules à considérer (dim. 1 du cube) ues : liste des UE (dim. 2 du cube) - module_inscr_df: matrice d'inscription du semestre (etud x modimpl) - module_coefs_df: matrice coefficients (UE x modimpl) + modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) + modimpl_coefs_df: matrice coefficients (UE x modimpl) Resultat: DataFrame columns UE, rows etudid """ @@ -228,3 +215,70 @@ def compute_ue_moys( return pd.DataFrame( etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index ) + + +def compute_ue_moys_classic( + formsemestre: FormSemestre, + sem_matrix: np.array, + ues: list, + modimpl_inscr_df: pd.DataFrame, + modimpl_coefs: np.array, +) -> pd.DataFrame: + """Calcul de la moyenne d'UE en mode classique. + La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR + NI non inscrit à (au moins un) module de cette UE + NA pas de notes disponibles + ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici] + + sem_matrix: notes moyennes aux modules + ndarray (etuds x modimpls) + (floats avec des NaN) + etuds : listes des étudiants (dim. 0 de la matrice) + ues : liste des UE + modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) + modimpl_coefs: vecteur des coefficients de modules + + Résultat: + - moyennes générales: pd.Series, index etudid + - moyennes d'UE: DataFrame columns UE, rows etudid + """ + nb_etuds, nb_modules = sem_matrix.shape + assert len(modimpl_coefs) == nb_modules + nb_ues = len(ues) + modimpl_inscr = modimpl_inscr_df.values + # Enlève les NaN du numérateur: + sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0) + # Ne prend pas en compte les notes des étudiants non inscrits au module: + # Annule les notes: + sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0) + # Annule les coefs des modules où l'étudiant n'est pas inscrit: + modimpl_coefs_etuds = np.where( + modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0 + ) + # Annule les coefs des modules NaN (nb_etuds x nb_mods) + modimpl_coefs_etuds_no_nan = np.where( + np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds + ) + # Calcul des moyennes générales: + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_gen = np.sum( + modimpl_coefs_etuds_no_nan * sem_matrix_inscrits, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index) + # Calcul des moyennes d'UE + ue_modules = np.array( + [[m.module.ue == ue for m in formsemestre.modimpls] for ue in ues] + )[..., np.newaxis] + modimpl_coefs_etuds_no_nan_stacked = np.stack( + [modimpl_coefs_etuds_no_nan.T] * nb_ues + ) + # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions + coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_ue = ( + np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2) + ).T + etud_moy_ue_df = pd.DataFrame( + etud_moy_ue, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues] + ) + return etud_moy_gen_s, etud_moy_ue_df diff --git a/app/comp/res_but.py b/app/comp/res_but.py new file mode 100644 index 00000000..cc0b8807 --- /dev/null +++ b/app/comp/res_but.py @@ -0,0 +1,65 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Résultats semestres BUT +""" + +from app.comp import moy_ue, moy_sem, inscr_mod +from app.comp.res_sem import NotesTableCompat + + +class ResultatsSemestreBUT(NotesTableCompat): + """Résultats BUT: organisation des calculs""" + + _cached_attrs = NotesTableCompat._cached_attrs + ( + "modimpl_coefs_df", + "modimpls_evals_poids", + "sem_cube", + ) + + def __init__(self, formsemestre): + super().__init__(formsemestre) + + if not self.load_cached(): + self.compute() + self.store() + + def compute(self): + "Charge les notes et inscriptions et calcule les moyennes d'UE et gen." + ( + self.sem_cube, + self.modimpls_evals_poids, + self.modimpls_results, + ) = moy_ue.notes_sem_load_cube(self.formsemestre) + self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) + self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( + self.formsemestre, ues=self.ues, modimpls=self.modimpls + ) + # l'idx de la colonne du mod modimpl.id est + # modimpl_coefs_df.columns.get_loc(modimpl.id) + # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) + self.etud_moy_ue = moy_ue.compute_ue_moys_apc( + self.sem_cube, + self.etuds, + self.modimpls, + self.ues, + self.modimpl_inscr_df, + self.modimpl_coefs_df, + ) + self.etud_moy_gen = moy_sem.compute_sem_moys_apc( + self.etud_moy_ue, self.modimpl_coefs_df + ) + self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + + def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: + """La moyenne de l'étudiant dans le moduleimpl + En APC, il s'agit d'une moyenne indicative sans valeur. + Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM) + """ + mod_idx = self.modimpl_coefs_df.columns.get_loc(moduleimpl_id) + etud_idx = self.etud_index[etudid] + # moyenne sur les UE: + return self.sem_cube[etud_idx, mod_idx].mean() diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py new file mode 100644 index 00000000..b203ec58 --- /dev/null +++ b/app/comp/res_classic.py @@ -0,0 +1,95 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Résultats semestres classiques (non APC) +""" +import numpy as np +import pandas as pd +from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod +from app.comp.res_sem import NotesTableCompat +from app.models.formsemestre import FormSemestre + + +class ResultatsSemestreClassic(NotesTableCompat): + """Résultats du semestre (formation classique): organisation des calculs.""" + + _cached_attrs = NotesTableCompat._cached_attrs + ( + "modimpl_coefs", + "modimpl_idx", + "sem_matrix", + ) + + def __init__(self, formsemestre): + super().__init__(formsemestre) + + if not self.load_cached(): + self.compute() + self.store() + # recalculé (aussi rapide que de les cacher) + self.moy_min = self.etud_moy_gen.min() + self.moy_max = self.etud_moy_gen.max() + self.moy_moy = self.etud_moy_gen.mean() + + def compute(self): + "Charge les notes et inscriptions et calcule les moyennes d'UE et gen." + self.sem_matrix, self.modimpls_results = notes_sem_load_matrix( + self.formsemestre + ) + self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) + self.modimpl_coefs = np.array( + [m.module.coefficient for m in self.formsemestre.modimpls] + ) + self.modimpl_idx = {m.id: i for i, m in enumerate(self.formsemestre.modimpls)} + "l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]" + + self.etud_moy_gen, self.etud_moy_ue = moy_ue.compute_ue_moys_classic( + self.formsemestre, + self.sem_matrix, + self.ues, + self.modimpl_inscr_df, + self.modimpl_coefs, + ) + self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + + def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: + """La moyenne de l'étudiant dans le moduleimpl + Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) + """ + return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI") + + +def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple: + """Calcule la matrice des notes du semestre + (charge toutes les notes, calcule les moyenne des modules + et assemble la matrice) + Resultat: + sem_matrix : 2d-array (etuds x modimpls) + modimpls_results dict { modimpl.id : ModuleImplResultsClassic } + """ + modimpls_results = {} + modimpls_notes = [] + for modimpl in formsemestre.modimpls: + mod_results = moy_mod.ModuleImplResultsClassic(modimpl) + etuds_moy_module = mod_results.compute_module_moy() + modimpls_results[modimpl.id] = mod_results + modimpls_notes.append(etuds_moy_module) + return ( + notes_sem_assemble_matrix(modimpls_notes), + modimpls_results, + ) + + +def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray: + """Réuni les notes moyennes des modules du semestre en une matrice + + modimpls_notes : liste des moyennes de module + (Series rendus par compute_module_moy, index: etud) + Resultat: ndarray (etud x module) + """ + modimpls_notes_arr = [s.values for s in modimpls_notes] + modimpls_notes = np.stack(modimpls_notes_arr) + # passe de (mod x etud) à (etud x mod) + return modimpls_notes.T diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py new file mode 100644 index 00000000..4f3a5dda --- /dev/null +++ b/app/comp/res_sem.py @@ -0,0 +1,337 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 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 FormSemestre, 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, ATT, DEF + +# 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 = ( + "etud_moy_gen_ranks", + "etud_moy_gen", + "etud_moy_ue", + "modimpl_inscr_df", + "modimpls_results", + ) + + def __init__(self, formsemestre: 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 la 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 + + 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 + () + + def __init__(self, formsemestre: 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 + } + self.moy_min = "NA" + self.moy_max = "NA" + + 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] + + @cached_property + def inscrlist(self) -> list[dict]: # utilisé par PE seulement + """Liste de dict etud, avec démissionnaires + classée dans l'ordre alphabétique de noms. + """ + etuds = self.formsemestre.get_inscrits(include_dem=True) + etuds.sort(key=lambda e: e.sort_key) + 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): # 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_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 + + 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_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_ue_status(self, etudid: int, ue_id: int): + return { + "cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid], + "is_capitalized": False, # XXX TODO + } + + def get_etud_rang(self, etudid: int): + return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX + + 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 des évaluations valides dans un module" + 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 + + def get_moduleimpls_attente(self): + return [] # XXX TODO + + def get_mod_stats(self, moduleimpl_id): + 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 + t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls) + else: + moy_ues = self.etud_moy_ue.loc[etudid] + t = [moy_gen] + list(moy_ues) + # TODO UE capitalisées: ne pas afficher moyennes modules + for modimpl in self.modimpls: + 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 + } diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index 2abc7db3..74aa24ca 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -1,15 +1,46 @@ +<<<<<<< HEAD +======= +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# +############################################################################## + +import re +import requests + +>>>>>>> e0be0f8feef8cfb7f5da4fe516b33529ec1712c9 from flask_wtf import FlaskForm -from markupsafe import Markup -import requests, re -from wtforms import StringField, SubmitField, TextAreaField, SelectField, HiddenField -from wtforms.fields.html5 import EmailField, DateField from flask_wtf.file import FileField, FileAllowed, FileRequired +from markupsafe import Markup +from sqlalchemy import text +from wtforms import StringField, SubmitField, TextAreaField, SelectField, HiddenField +from wtforms.fields import EmailField, DateField from wtforms.validators import ValidationError, DataRequired, Email + from app.entreprises.models import Entreprise, EntrepriseContact from app.models import Identite from app.auth.models import User -from app.scodoc import sco_etud -from sqlalchemy import text CHAMP_REQUIS = "Ce champ est requis" diff --git a/app/forms/main/config_forms.py b/app/forms/main/config_forms.py index 609066c5..03589795 100644 --- a/app/forms/main/config_forms.py +++ b/app/forms/main/config_forms.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/forms/main/create_dept.py b/app/forms/main/create_dept.py index 11b36129..7bc26b42 100644 --- a/app/forms/main/create_dept.py +++ b/app/forms/main/create_dept.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/models/absences.py b/app/models/absences.py index 658a390d..bbff2c5c 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -4,9 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN class Absence(db.Model): diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index b930bf9e..bf1b4cb6 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 models : Référentiel Compétence BUT 2021 diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 2dab949c..42e22e00 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -4,13 +4,14 @@ et données rattachées (adresses, annotations, ...) """ -from flask import g, url_for +from functools import cached_property +from flask import abort, url_for +from flask import g, request from app import db from app import models -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN + +from app.scodoc import notesdb as ndb class Identite(db.Model): @@ -53,14 +54,24 @@ class Identite(db.Model): def __repr__(self): return f"" + @classmethod + def from_request(cls, etudid=None, code_nip=None): + """Etudiant à partir de l'etudid ou du code_nip, soit + passés en argument soit retrouvés directement dans la requête web. + Erreur 404 si inexistant. + """ + args = make_etud_args(etudid=etudid, code_nip=code_nip) + return Identite.query.filter_by(**args).first_or_404() + + @property def civilite_str(self): """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, personnes ne souhaitant pas d'affichage). """ return {"M": "M.", "F": "Mme", "X": ""}[self.civilite] - def nom_disp(self): - "nom à afficher" + def nom_disp(self) -> str: + "Nom à afficher" if self.nom_usuel: return ( (self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel @@ -68,10 +79,50 @@ class Identite(db.Model): else: return self.nom + @cached_property + def nomprenom(self, reverse=False) -> str: + """Civilité/nom/prenom pour affichages: "M. Pierre Dupont" + Si reverse, "Dupont Pierre", sans civilité. + """ + nom = self.nom_usuel or self.nom + prenom = self.prenom_str + if reverse: + fields = (nom, prenom) + else: + fields = (self.civilite_str, prenom, nom) + return " ".join([x for x in fields if x]) + + @property + def prenom_str(self): + """Prénom à afficher. Par exemple: "Jean-Christophe" """ + if not self.prenom: + return "" + frags = self.prenom.split() + r = [] + for frag in frags: + fields = frag.split("-") + r.append("-".join([x.lower().capitalize() for x in fields])) + return " ".join(r) + + @cached_property + def sort_key(self) -> tuple: + "clé pour tris par ordre alphabétique" + return (self.nom_usuel or self.nom).lower(), self.prenom.lower() + def get_first_email(self, field="email") -> str: - "le mail associé à la première adrese de l'étudiant, ou None" + "Le mail associé à la première adrese de l'étudiant, ou None" return self.adresses[0].email or None if self.adresses.count() > 0 else None + def to_dict_scodoc7(self): + """Représentation dictionnaire, + compatible ScoDoc7 mais sans infos admission + """ + e = dict(self.__dict__) + e.pop("_sa_instance_state", None) + # ScoDoc7 output_formators: (backward compat) + e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"]) + return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty + def to_dict_bul(self, include_urls=True): """Infos exportées dans les bulletins""" from app.scodoc import sco_photos @@ -131,6 +182,42 @@ class Identite(db.Model): return False +def make_etud_args( + etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True +) -> dict: + """forme args dict pour requete recherche etudiant + On peut specifier etudid + ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine + (dans cet ordre). + + Résultat: dict avec soit "etudid", soit "code_nip", soit "code_ine" + """ + args = None + if etudid: + args = {"etudid": etudid} + elif code_nip: + args = {"code_nip": code_nip} + elif use_request: # use form from current request (Flask global) + if request.method == "POST": + vals = request.form + elif request.method == "GET": + vals = request.args + else: + vals = {} + if "etudid" in vals: + args = {"etudid": int(vals["etudid"])} + elif "code_nip" in vals: + args = {"code_nip": str(vals["code_nip"])} + elif "code_ine" in vals: + args = {"code_ine": str(vals["code_ine"])} + if not args: + if abort_404: + abort(404, "pas d'étudiant sélectionné") + elif raise_exc: + raise ValueError("make_etud_args: pas d'étudiant sélectionné !") + return args + + class Adresse(db.Model): """Adresse d'un étudiant (le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule) diff --git a/app/models/evaluations.py b/app/models/evaluations.py index e0733fb7..4f06fb75 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -4,9 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN from app.models import UniteEns import app.scodoc.notesdb as ndb diff --git a/app/models/events.py b/app/models/events.py index 0cdd7e93..55b34d38 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -4,9 +4,7 @@ """ from app import db -from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN class Scolog(db.Model): diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 28be4155..928dd609 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -3,6 +3,7 @@ """ScoDoc models: formsemestre """ import datetime +from functools import cached_property import flask_sqlalchemy @@ -84,7 +85,11 @@ class FormSemestre(db.Model): etapes = db.relationship( "FormSemestreEtape", cascade="all,delete", backref="formsemestre" ) - modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic") + modimpls = db.relationship( + "ModuleImpl", + backref="formsemestre", + lazy="dynamic", + ) etuds = db.relationship( "Identite", secondary="notes_formsemestre_inscription", @@ -247,7 +252,7 @@ class FormSemestre(db.Model): etudid, self.date_debut.isoformat(), self.date_fin.isoformat() ) - def get_inscrits(self, include_dem=False) -> list: + def get_inscrits(self, include_dem=False) -> list[Identite]: """Liste des étudiants inscrits à ce semestre Si all, tous les étudiants, avec les démissionnaires. """ @@ -256,6 +261,11 @@ class FormSemestre(db.Model): else: return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT] + @cached_property + def etuds_inscriptions(self) -> dict: + """Map { etudid : inscription }""" + return {ins.etud.id: ins for ins in self.inscriptions} + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( diff --git a/app/models/groups.py b/app/models/groups.py index 688744b0..902298cc 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -5,9 +5,7 @@ from typing import Any from app import db -from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN from app.models import GROUPNAME_STR_LEN diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 172fe076..fe48555e 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -50,7 +50,7 @@ class ModuleImpl(db.Model): if evaluations_poids is None: from app.comp import moy_mod - evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id) + evaluations_poids, _ = moy_mod.load_evaluations_poids(self.id) df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids) return evaluations_poids @@ -69,7 +69,7 @@ class ModuleImpl(db.Model): return True from app.comp import moy_mod - return moy_mod.check_moduleimpl_conformity( + return moy_mod.moduleimpl_is_conforme( self, self.get_evaluations_poids(), self.module.formation.get_module_coefs(self.module.semestre_id), diff --git a/app/models/notes.py b/app/models/notes.py index df2766d0..fa8dc8d1 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -4,7 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN diff --git a/app/pe/pe_avislatex.py b/app/pe/pe_avislatex.py index b98218ee..2ff50715 100644 --- a/app/pe/pe_avislatex.py +++ b/app/pe/pe_avislatex.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index a7d302ad..694a410f 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index a6729def..9a33f57c 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index 8fde8d20..f4ada213 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index e32a1173..54e5e395 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -68,7 +68,7 @@ class TableTag(object): self.taglist = [] self.resultats = {} - self.rangs = {} + self.etud_moy_gen_ranks = {} self.statistiques = {} # ***************************************************************************************************************** @@ -117,15 +117,15 @@ class TableTag(object): # ----------------------------------------------------------------------------------------------------------- def get_moy_from_stats(self, tag): - """ Renvoie la moyenne des notes calculées pour d'un tag donné""" + """Renvoie la moyenne des notes calculées pour d'un tag donné""" return self.statistiques[tag][0] if tag in self.statistiques else None def get_min_from_stats(self, tag): - """ Renvoie la plus basse des notes calculées pour d'un tag donné""" + """Renvoie la plus basse des notes calculées pour d'un tag donné""" return self.statistiques[tag][1] if tag in self.statistiques else None def get_max_from_stats(self, tag): - """ Renvoie la plus haute des notes calculées pour d'un tag donné""" + """Renvoie la plus haute des notes calculées pour d'un tag donné""" return self.statistiques[tag][2] if tag in self.statistiques else None # ----------------------------------------------------------------------------------------------------------- @@ -236,7 +236,7 @@ class TableTag(object): return moystr def str_res_d_un_etudiant(self, etudid, delim=";"): - """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique). """ + """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique).""" return delim.join( [self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()] ) @@ -256,7 +256,7 @@ class TableTag(object): # ----------------------------------------------------------------------- def str_tagtable(self, delim=";", decimal_sep=","): - """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags. """ + """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags.""" entete = ["etudid", "nom", "prenom"] for tag in self.get_all_tags(): entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]] diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index 99adbedd..a495f965 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 14efdada..43a00ebe 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/__init__.py b/app/scodoc/__init__.py index 0481bb53..54c845c8 100644 --- a/app/scodoc/__init__.py +++ b/app/scodoc/__init__.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 4a3f8aba..afc05e26 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 78a0f6a2..a9ed10aa 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index faf1fcf3..e2b4d0bd 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index c35f3042..b3bf18a3 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/htmlutils.py b/app/scodoc/htmlutils.py index c06e86e9..3306b80d 100644 --- a/app/scodoc/htmlutils.py +++ b/app/scodoc/htmlutils.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 052b387e..828ea228 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,9 @@ # ############################################################################## -"""Calculs sur les notes et cache des resultats +"""Calculs sur les notes et cache des résultats + + Ancien code ScoDoc 7 en cours de rénovation """ from operator import itemgetter @@ -102,7 +104,7 @@ def comp_ranks(T): def get_sem_ues_modimpls(formsemestre_id, modimpls=None): """Get liste des UE du semestre (à partir des moduleimpls) - (utilisé quand on ne peut pas construire nt et faire nt.get_ues()) + (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict()) """ if modimpls is None: modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) @@ -200,7 +202,7 @@ class NotesTable: self.inscrlist.sort(key=itemgetter("nomp")) # { etudid : rang dans l'ordre alphabetique } - self.rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)} + self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)} self.bonus = scu.DictDefault(defaultvalue=0) # Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } } @@ -294,7 +296,7 @@ class NotesTable: for ue in self._ues: is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"] - for modimpl in self.get_modimpls(): + for modimpl in self.get_modimpls_dict(): val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) if is_cap[modimpl["module"]["ue_id"]]: t.append("-c-") @@ -316,7 +318,7 @@ class NotesTable: self.moy_min = self.moy_max = "NA" # calcul rangs (/ moyenne generale) - self.rangs = comp_ranks(T) + self.etud_moy_gen_ranks = comp_ranks(T) self.rangs_groupes = ( {} @@ -364,7 +366,7 @@ class NotesTable: moy = -float(x[0]) except (ValueError, TypeError): moy = 1000.0 - return (moy, self.rang_alpha[x[-1]]) + return (moy, self._rang_alpha[x[-1]]) def get_etudids(self, sorted=False): if sorted: @@ -417,46 +419,17 @@ class NotesTable: else: return ' (%s) ' % etat - def get_ues(self, filter_sport=False, filter_non_inscrit=False, etudid=None): - """liste des ue, ordonnée par numero. - Si filter_non_inscrit, retire les UE dans lesquelles l'etudiant n'est - inscrit à aucun module. + 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 """ - if not filter_sport and not filter_non_inscrit: + if not filter_sport: return self._ues - - if filter_sport: - ues_src = [ue for ue in self._ues if ue["type"] != UE_SPORT] else: - ues_src = self._ues - if not filter_non_inscrit: - return ues_src - ues = [] - for ue in ues_src: - if self.get_etud_ue_status(etudid, ue["ue_id"])["is_capitalized"]: - # garde toujours les UE capitalisees - has_note = True - else: - has_note = False - # verifie que l'etud. est inscrit a au moins un module de l'UE - # (en fait verifie qu'il a une note) - modimpls = self.get_modimpls(ue["ue_id"]) + return [ue for ue in self._ues if ue["type"] != UE_SPORT] - for modi in modimpls: - moy = self.get_etud_mod_moy(modi["moduleimpl_id"], etudid) - try: - float(moy) - has_note = True - break - except: - pass - if has_note: - ues.append(ue) - return ues - - def get_modimpls(self, ue_id=None): - "liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." + def get_modimpls_dict(self, ue_id=None): + "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." if ue_id is None: r = self._modimpls else: @@ -522,7 +495,7 @@ class NotesTable: Les moyennes d'UE ne tiennent pas compte des capitalisations. """ - ues = self.get_ues() + ues = self.get_ues_stat_dict() sum_moy = 0 # la somme des moyennes générales valides nb_moy = 0 # le nombre de moyennes générales valides for ue in ues: @@ -561,9 +534,9 @@ class NotesTable: i = 0 for ue in ues: i += 1 - ue["nb_moy"] = len(ue["_notes"]) - if ue["nb_moy"] > 0: - ue["moy"] = sum(ue["_notes"]) / ue["nb_moy"] + ue["nb_vals"] = len(ue["_notes"]) + if ue["nb_vals"] > 0: + ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"] ue["max"] = max(ue["_notes"]) ue["min"] = min(ue["_notes"]) else: @@ -591,7 +564,7 @@ class NotesTable: Si non inscrit, moy == 'NI' et sum_coefs==0 """ assert ue_id - modimpls = self.get_modimpls(ue_id) + modimpls = self.get_modimpls_dict(ue_id) nb_notes = 0 # dans cette UE sum_notes = 0.0 sum_coefs = 0.0 @@ -767,7 +740,7 @@ class NotesTable: sem_ects_pot_fond = 0.0 sem_ects_pot_pro = 0.0 - for ue in self.get_ues(): + for ue in self.get_ues_stat_dict(): # - On calcule la moyenne d'UE courante: if not block_computation: mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx) @@ -948,7 +921,7 @@ class NotesTable: return infos - def get_etud_moy_gen(self, etudid): + def get_etud_moy_gen(self, etudid): # -> float | str """Moyenne generale de cet etudiant dans ce semestre. Prend en compte les UE capitalisées. Si pas de notes: 'NA' @@ -981,7 +954,7 @@ class NotesTable: return self.T def get_etud_rang(self, etudid) -> str: - return self.rangs.get(etudid, "999") + return self.etud_moy_gen_ranks.get(etudid, "999") def get_etud_rang_group(self, etudid, group_id): """Returns rank of etud in this group and number of etuds in group. @@ -1347,7 +1320,7 @@ class NotesTable: # Rappel des épisodes précédents: T est une liste de liste # Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid - ues = self.get_ues() # incluant le(s) UE de sport + ues = self.get_ues_stat_dict() # incluant le(s) UE de sport for t in self.T: etudid = t[-1] if etudid in results.etud_moy_gen: # evite les démissionnaires @@ -1358,4 +1331,4 @@ class NotesTable: # re-trie selon la nouvelle moyenne générale: self.T.sort(key=self._row_key) # Remplace aussi le rang: - self.rangs = results.etud_moy_gen_ranks + self.etud_moy_gen_ranks = results.etud_moy_gen_ranks diff --git a/app/scodoc/notes_users.py b/app/scodoc/notes_users.py index 8afc81df..6dd5bf40 100644 --- a/app/scodoc/notes_users.py +++ b/app/scodoc/notes_users.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 5418c3ce..3de5c281 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -265,8 +265,11 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu cursor.execute(req, vals) # log('req=%s\n'%req) # log('vals=%s\n'%vals) + except psycopg2.errors.StringDataRightTruncation: + cnx.rollback() + raise ScoValueError("champs de texte trop long !") except: - cnx.commit() # get rid of this transaction + cnx.rollback() # get rid of this transaction log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals)) raise # and re-raise exception if commit: diff --git a/app/scodoc/safehtml.py b/app/scodoc/safehtml.py index f9deecfe..c7f08422 100644 --- a/app/scodoc/safehtml.py +++ b/app/scodoc/safehtml.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 4ec994ab..0b38559f 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index bf7c5e09..f15e7d4c 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index 4e5aaa46..2f108c24 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -32,6 +32,8 @@ import datetime from flask import url_for, g, request, abort +from app import log +from app.models import Identite import app.scodoc.sco_utils as scu from app.scodoc import notesdb as ndb from app.scodoc.scolog import logdb @@ -46,7 +48,6 @@ from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_photos from app.scodoc import sco_preferences -from app import log from app.scodoc.sco_exceptions import ScoValueError @@ -71,8 +72,8 @@ def doSignaleAbsence( etudid: etudiant concerné. Si non spécifié, cherche dans les paramètres de la requête courante. """ - etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] - etudid = etud["etudid"] + etud = Identite.from_request(etudid) + if not moduleimpl_id: moduleimpl_id = None description_abs = description @@ -82,7 +83,7 @@ def doSignaleAbsence( for jour in dates: if demijournee == 2: sco_abs.add_absence( - etudid, + etud.id, jour, False, estjust, @@ -90,7 +91,7 @@ def doSignaleAbsence( moduleimpl_id, ) sco_abs.add_absence( - etudid, + etud.id, jour, True, estjust, @@ -100,7 +101,7 @@ def doSignaleAbsence( nbadded += 2 else: sco_abs.add_absence( - etudid, + etud.id, jour, demijournee, estjust, @@ -113,27 +114,27 @@ def doSignaleAbsence( J = "" else: J = "NON " - M = "" + indication_module = "" if moduleimpl_id and moduleimpl_id != "NULL": mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] formsemestre_id = mod["formsemestre_id"] nt = sco_cache.NotesTableCache.get(formsemestre_id) - ues = nt.get_ues(etudid=etudid) + ues = nt.get_ues_stat_dict() for ue in ues: - modimpls = nt.get_modimpls(ue_id=ue["ue_id"]) + modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"]) for modimpl in modimpls: if modimpl["moduleimpl_id"] == moduleimpl_id: - M = "dans le module %s" % modimpl["module"]["code"] + indication_module = "dans le module %s" % modimpl["module"]["code"] H = [ html_sco_header.sco_header( - page_title="Signalement d'une absence pour %(nomprenom)s" % etud, + page_title=f"Signalement d'une absence pour {etud.nomprenom}", ), """

Signalement d'absences

""", ] if dates: H.append( """

Ajout de %d absences %sjustifiées du %s au %s %s

""" - % (nbadded, J, datedebut, datefin, M) + % (nbadded, J, datedebut, datefin, indication_module) ) else: H.append( @@ -142,11 +143,18 @@ def doSignaleAbsence( ) H.append( - """ -
""" - % etud + f""" +
+ """ ) H.append(sco_find_etud.form_search_etud()) H.append(html_sco_header.sco_footer()) @@ -175,7 +183,7 @@ def SignaleAbsenceEtud(): # etudid implied "abs_require_module", formsemestre_id ) nt = sco_cache.NotesTableCache.get(formsemestre_id) - ues = nt.get_ues(etudid=etudid) + ues = nt.get_ues_stat_dict() if require_module: menu_module = """