From e6bd6cf28abecb98b31164176f147eb7d144d237 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 6 Feb 2022 16:09:17 +0100 Subject: [PATCH] WIP: validations d'UE et de semestres --- app/comp/jury.py | 147 ++++++++++++++++++++++ app/comp/moy_mod.py | 13 +- app/comp/res_cache.py | 34 +++++ app/comp/res_common.py | 144 +++++++++++++++------ app/comp/res_sem.py | 32 +++-- app/models/__init__.py | 8 +- app/models/formsemestre.py | 2 +- app/models/notes.py | 94 -------------- app/models/validations.py | 109 ++++++++++++++++ app/scodoc/notes_table.py | 17 ++- app/scodoc/sco_bulletins.py | 2 +- app/scodoc/sco_cache.py | 15 ++- app/scodoc/sco_codes_parcours.py | 9 +- app/scodoc/sco_edit_apc.py | 2 +- app/scodoc/sco_evaluations.py | 5 +- app/scodoc/sco_formsemestre_status.py | 6 +- app/scodoc/sco_formsemestre_validation.py | 8 +- app/scodoc/sco_liste_notes.py | 2 +- app/scodoc/sco_parcours_dut.py | 47 ++++--- app/scodoc/sco_pvjury.py | 15 +-- app/scodoc/sco_recapcomplet.py | 105 +++++++--------- app/views/notes.py | 2 +- 22 files changed, 562 insertions(+), 256 deletions(-) create mode 100644 app/comp/jury.py create mode 100644 app/comp/res_cache.py create mode 100644 app/models/validations.py diff --git a/app/comp/jury.py b/app/comp/jury.py new file mode 100644 index 000000000..bb7cd0d4e --- /dev/null +++ b/app/comp/jury.py @@ -0,0 +1,147 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Stockage des décisions de jury +""" +import pandas as pd + +from app import db +from app.models import FormSemestre, ScolarFormSemestreValidation +from app.comp.res_cache import ResultatsCache +from app.scodoc import sco_cache +from app.scodoc import sco_codes_parcours + + +class ValidationsSemestre(ResultatsCache): + """ """ + + _cached_attrs = ( + "decisions_jury", + "decisions_jury_ues", + "ue_capitalisees", + ) + + def __init__(self, formsemestre: FormSemestre): + super().__init__(formsemestre, sco_cache.ValidationsSemestreCache) + + self.decisions_jury = {} + """Décisions prises dans ce semestre: + { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}""" + self.decisions_jury_ues = {} + """Décisions sur des UEs dans ce semestre: + { etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} + """ + + if not self.load_cached(): + self.compute() + self.store() + + def compute(self): + "Charge les résultats de jury et UEs capitalisées" + self.ue_capitalisees = formsemestre_get_ue_capitalisees(self.formsemestre) + self.comp_decisions_jury() + + def comp_decisions_jury(self): + """Cherche les decisions du jury pour le semestre (pas les UE). + Calcule les attributs: + decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} + decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} + Si la décision n'a pas été prise, la clé etudid n'est pas présente. + Si l'étudiant est défaillant, pas de décisions d'UE. + """ + # repris de NotesTable.comp_decisions_jury pour la compatibilité + decisions_jury_q = ScolarFormSemestreValidation.query.filter_by( + formsemestre_id=self.formsemestre.id + ) + decisions_jury = {} + for decision in decisions_jury_q.filter( + ScolarFormSemestreValidation.ue_id == None # slt dec. sem. + ): + decisions_jury[decision.etudid] = { + "code": decision.code, + "assidu": decision.assidu, + "compense_formsemestre_id": decision.compense_formsemestre_id, + "event_date": decision.event_date.strftime("%d/%m/%Y"), + } + self.decisions_jury = decisions_jury + + # UEs: + decisions_jury_ues = {} + for decision in decisions_jury_q.filter( + ScolarFormSemestreValidation.ue_id != None # slt dec. sem. + ): + if decision.etudid not in decisions_jury_ues: + decisions_jury_ues[decision.etudid] = {} + # Calcul des ECTS associés à cette UE: + if sco_codes_parcours.code_ue_validant(decision.code): + ects = decision.ue.ects or 0.0 # 0 if None + else: + ects = 0.0 + + decisions_jury_ues[decision.etudid][decision.ue.id] = { + "code": decision.code, + "ects": ects, # 0. si UE non validée + "event_date": decision.event_date.strftime("%d/%m/%Y"), + } + + self.decisions_jury_ues = decisions_jury_ues + + +def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame: + """Liste des UE capitalisées (ADM) utilisables dans ce formsemestre + + Recherche dans les semestres des formations de même code, avec le même semestre_id + et une date de début antérieure que celle du formsemestre. + Prend aussi les UE externes validées. + + Attention: fonction très coûteuse, cacher le résultat. + + Résultat: DataFrame avec + etudid (index) + formsemestre_id : origine de l'UE capitalisée + is_external : vrai si validation effectuée dans un semestre extérieur + ue_id : dans le semestre origine (pas toujours de la même formation) + ue_code : code de l'UE + moy_ue : + event_date : + } ] + """ + query = """ + SELECT DISTINCT SFV.*, ue.ue_code + FROM + notes_ue ue, + notes_formations nf, + notes_formations nf2, + scolar_formsemestre_validation SFV, + notes_formsemestre sem, + notes_formsemestre_inscription ins + + WHERE ue.formation_id = nf.id + and nf.formation_code = nf2.formation_code + and nf2.id=%(formation_id)s + and ins.etudid = SFV.etudid + and ins.formsemestre_id = %(formsemestre_id)s + + and SFV.ue_id = ue.id + and SFV.code = 'ADM' + + and ( (sem.id = SFV.formsemestre_id + and sem.date_debut < %(date_debut)s + and sem.semestre_id = %(semestre_id)s ) + or ( + ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" + AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) + ) ) + """ + params = { + "formation_id": formsemestre.formation.id, + "formsemestre_id": formsemestre.id, + "semestre_id": formsemestre.semestre_id, + "date_debut": formsemestre.date_debut, + } + + df = pd.read_sql_query(query, db.engine, params=params, index_col="etudid") + return df diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index f890b6a61..f30f04912 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -77,6 +77,8 @@ class ModuleImplResults: "{ evaluation.id : bool } indique si à prendre en compte ou non." self.evaluations_etat = {} "{ evaluation_id: EvaluationEtat }" + self.en_attente = False + "Vrai si au moins une évaluation a une note en attente" # self.evals_notes = None """DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE) @@ -133,7 +135,7 @@ class ModuleImplResults: evals_notes = pd.DataFrame(index=self.etudids, dtype=float) self.evaluations_completes = [] self.evaluations_completes_dict = {} - + self.en_attente = False 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 @@ -160,6 +162,8 @@ class ModuleImplResults: self.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete ) + if nb_att > 0: + self.en_attente = True # Force columns names to integers (evaluation ids) evals_notes.columns = pd.Int64Index( @@ -209,6 +213,13 @@ class ModuleImplResults: * self.evaluations_completes ).reshape(-1, 1) + # was _list_notes_evals_titles + def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list: + "Liste des évaluations complètes" + return [ + e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id] + ] + 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. diff --git a/app/comp/res_cache.py b/app/comp/res_cache.py new file mode 100644 index 000000000..47c40b7e4 --- /dev/null +++ b/app/comp/res_cache.py @@ -0,0 +1,34 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Cache pour résultats (super classe) +""" + +from app.models import FormSemestre + + +class ResultatsCache: + _cached_attrs = () # virtual + + def __init__(self, formsemestre: FormSemestre, cache_class=None): + self.formsemestre: FormSemestre = formsemestre + self.cache_class = cache_class + + def load_cached(self) -> bool: + "Load cached dataframes, returns False si pas en cache" + data = self.cache_class.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" + self.cache_class.set( + self.formsemestre.id, + {attr: getattr(self, attr) for attr in self._cached_attrs}, + ) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 601f76a99..3c408257d 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,10 +9,13 @@ from functools import cached_property import numpy as np import pandas as pd from app.comp.aux_stats import StatsMoyenne +from app.comp.res_cache import ResultatsCache +from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.models import FormSemestre, Identite, ModuleImpl from app.models.ues import UniteEns from app.scodoc import sco_utils as scu +from app.scodoc import sco_evaluations from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF @@ -25,7 +28,7 @@ from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF # (durée de vie de l'instance de ResultatsSemestre) # qui sont notamment les attributs décorés par `@cached_property`` # -class ResultatsSemestre: +class ResultatsSemestre(ResultatsCache): _cached_attrs = ( "etud_moy_gen_ranks", "etud_moy_gen", @@ -36,7 +39,7 @@ class ResultatsSemestre: ) def __init__(self, formsemestre: FormSemestre): - self.formsemestre: FormSemestre = formsemestre + super().__init__(formsemestre, ResultatsSemestreCache) # BUT ou standard ? (apc == "approche par compétences") self.is_apc = formsemestre.formation.is_apc() # Attributs "virtuels", définis dans les sous-classes @@ -46,26 +49,9 @@ class ResultatsSemestre: self.etud_moy_gen = {} self.etud_moy_gen_ranks = {} self.modimpls_results: ModuleImplResults = None + "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" self.etud_coef_ue_df = None - """coefs d'UE effectifs pour chaque etudiant (pour form. classiques)""" - - # 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}, - ) + """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" def compute(self): "Charge les notes et inscriptions et calcule toutes les moyennes" @@ -101,7 +87,8 @@ class ResultatsSemestre: @cached_property def ues(self) -> list[UniteEns]: """Liste des UEs du semestre (avec les UE bonus sport) - (indices des DataFrames) + (indices des DataFrames). + Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs. """ return self.formsemestre.query_ues(with_sport=True).all() @@ -123,15 +110,34 @@ class ResultatsSemestre: if m.module.module_type == scu.ModuleType.SAE ] - @cached_property - def ue_validables(self) -> list: - """Liste des UE du semestre qui doivent être validées - (toutes sauf le sport) - """ - return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all() + def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: + """Liste des UEs du semestre qui doivent être validées - def modimpls_in_ue(self, ue_id, etudid): - """Liste des modimpl de cet ue auxquels l'étudiant est inscrit""" + Rappel: l'étudiant est inscrit à des modimpls et non à des UEs. + + - En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules + du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre. + + - En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont + susceptibles d'être validées. + + Les UE "bonus" (sport) ne sont jamais "validables". + """ + if self.is_apc: + # TODO: introduire la notion de parcours (#sco93) + return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all() + else: + # restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls) + ues = { + modimpl.module.ue + for modimpl in self.formsemestre.modimpls_sorted + if self.modimpl_inscr_df[modimpl.id][etudid] + } + ues = sorted(list(ues), key=lambda x: x.numero or 0) + return ues + + def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]: + """Liste des modimpl de cette UE auxquels l'étudiant est inscrit""" # sert pour l'affichage ou non de l'UE sur le bulletin return [ modimpl @@ -180,6 +186,7 @@ class NotesTableCompat(ResultatsSemestre): self.moy_moy = "NA" self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() + self.validations = None def get_etudids(self, sorted=False) -> list[int]: """Liste des etudids inscrits, incluant les démissionnaires. @@ -243,6 +250,21 @@ class NotesTableCompat(ResultatsSemestre): modimpls_dict.append(d) return modimpls_dict + 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 } @@ -256,12 +278,11 @@ class NotesTableCompat(ResultatsSemestre): "compense_formsemestre_id": None, } else: - return { - "code": ATT, # XXX TODO - "assidu": True, # XXX TODO - "event_date": "", - "compense_formsemestre_id": None, - } + 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)" @@ -290,6 +311,31 @@ class NotesTableCompat(ResultatsSemestre): """ 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_fond": 0.0, # not implemented (anciennemment pour école ingé) + } + def get_etud_ue_status(self, etudid: int, ue_id: int): coef_ue = self.etud_coef_ue_df[ue_id][etudid] return { @@ -333,8 +379,32 @@ class NotesTableCompat(ResultatsSemestre): evals_results.append(d) return evals_results + def get_evaluations_etats(self): + """[ {...evaluation et son etat...} ]""" + # TODO: à moderniser + 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 donénes 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): - return [] # XXX TODO + """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 diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index 8e14d5afe..5da2c7f06 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -8,31 +8,49 @@ """ from flask import g +from app.comp.jury import ValidationsSemestre from app.comp.res_common import ResultatsSemestre from app.comp.res_classic import ResultatsSemestreClassic from app.comp.res_but import ResultatsSemestreBUT from app.models.formsemestre import FormSemestre -def load_formsemestre_result(formsemestre: FormSemestre) -> ResultatsSemestre: +def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: """Returns ResultatsSemestre for this formsemestre. Suivant le type de formation, retour une instance de ResultatsSemestreClassic ou de ResultatsSemestreBUT. Search in local cache (g.formsemestre_result_cache) - then global app cache (eg REDIS) If not in cache, build it and cache it. """ # --- Try local cache (within the same request context) - if not hasattr(g, "formsemestre_result_cache"): - g.formsemestre_result_cache = {} # pylint: disable=C0237 + if not hasattr(g, "formsemestre_results_cache"): + g.formsemestre_results_cache = {} # pylint: disable=C0237 else: - if formsemestre.id in g.formsemestre_result_cache: - return g.formsemestre_result_cache[formsemestre.id] + if formsemestre.id in g.formsemestre_results_cache: + return g.formsemestre_results_cache[formsemestre.id] klass = ( ResultatsSemestreBUT if formsemestre.formation.is_apc() else ResultatsSemestreClassic ) - return klass(formsemestre) + g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre) + return g.formsemestre_results_cache[formsemestre.id] + + +def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre: + """Charge les résultats de jury de ce semestre. + Search in local cache (g.formsemestre_result_cache) + If not in cache, build it and cache it. + """ + if not hasattr(g, "formsemestre_validation_cache"): + g.formsemestre_validations_cache = {} # pylint: disable=C0237 + else: + if formsemestre.id in g.formsemestre_validations_cache: + return g.formsemestre_validations_cache[formsemestre.id] + + g.formsemestre_validations_cache[formsemestre.id] = ValidationsSemestre( + formsemestre + ) + return g.formsemestre_validations_cache[formsemestre.id] diff --git a/app/models/__init__.py b/app/models/__init__.py index f11084934..d29b6bf3b 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -49,13 +49,15 @@ from app.models.evaluations import ( ) from app.models.groups import Partition, GroupDescr, group_membership from app.models.notes import ( - ScolarEvent, - ScolarFormSemestreValidation, - ScolarAutorisationInscription, BulAppreciations, NotesNotes, NotesNotesLog, ) +from app.models.validations import ( + ScolarEvent, + ScolarFormSemestreValidation, + ScolarAutorisationInscription, +) from app.models.preferences import ScoPreference from app.models.but_refcomp import ( diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 2234fdfa1..e4b8fc8c8 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -158,7 +158,7 @@ class FormSemestre(db.Model): @cached_property def modimpls_sorted(self) -> list[ModuleImpl]: - """Liste des modimpls du semestre + """Liste des modimpls du semestre (y compris bonus) - triée par type/numéro/code en APC - triée par numéros d'UE/matières/modules pour les formations standard. """ diff --git a/app/models/notes.py b/app/models/notes.py index fa8dc8d10..7e5583579 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -8,100 +8,6 @@ from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN -class ScolarEvent(db.Model): - """Evenement dans le parcours scolaire d'un étudiant""" - - __tablename__ = "scolar_events" - id = db.Column(db.Integer, primary_key=True) - event_id = db.synonym("id") - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id"), - ) - event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - ue_id = db.Column( - db.Integer, - db.ForeignKey("notes_ue.id"), - ) - # 'CREATION', 'INSCRIPTION', 'DEMISSION', - # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' - # 'ECHEC_SEM' - # 'UTIL_COMPENSATION' - event_type = db.Column(db.String(SHORT_STR_LEN)) - # Semestre compensé par formsemestre_id: - comp_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - - -class ScolarFormSemestreValidation(db.Model): - """Décisions de jury""" - - __tablename__ = "scolar_formsemestre_validation" - # Assure unicité de la décision: - __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),) - - id = db.Column(db.Integer, primary_key=True) - formsemestre_validation_id = db.synonym("id") - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id"), - index=True, - ) - formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - index=True, - ) - ue_id = db.Column( - db.Integer, - db.ForeignKey("notes_ue.id"), - index=True, - ) - code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) - # NULL pour les UE, True|False pour les semestres: - assidu = db.Column(db.Boolean) - event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - # NULL sauf si compense un semestre: - compense_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - moy_ue = db.Column(db.Float) - # (normalement NULL) indice du semestre, utile seulement pour - # UE "antérieures" et si la formation définit des UE utilisées - # dans plusieurs semestres (cas R&T IUTV v2) - semestre_id = db.Column(db.Integer) - # Si UE validée dans le cursus d'un autre etablissement - is_external = db.Column(db.Boolean, default=False, server_default="false") - - -class ScolarAutorisationInscription(db.Model): - """Autorisation d'inscription dans un semestre""" - - __tablename__ = "scolar_autorisation_inscription" - id = db.Column(db.Integer, primary_key=True) - autorisation_inscription_id = db.synonym("id") - - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id"), - ) - formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) - # semestre ou on peut s'inscrire: - semestre_id = db.Column(db.Integer) - date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - origin_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - - class BulAppreciations(db.Model): """Appréciations sur bulletins""" diff --git a/app/models/validations.py b/app/models/validations.py new file mode 100644 index 000000000..0bf487f3a --- /dev/null +++ b/app/models/validations.py @@ -0,0 +1,109 @@ +# -*- coding: UTF-8 -* + +"""Notes, décisions de jury, évènements scolaires +""" + +from app import db +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class ScolarFormSemestreValidation(db.Model): + """Décisions de jury""" + + __tablename__ = "scolar_formsemestre_validation" + # Assure unicité de la décision: + __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),) + + id = db.Column(db.Integer, primary_key=True) + formsemestre_validation_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + index=True, + ) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + index=True, + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + index=True, + ) + code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) + # NULL pour les UE, True|False pour les semestres: + assidu = db.Column(db.Boolean) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + # NULL sauf si compense un semestre: + compense_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + moy_ue = db.Column(db.Float) + # (normalement NULL) indice du semestre, utile seulement pour + # UE "antérieures" et si la formation définit des UE utilisées + # dans plusieurs semestres (cas R&T IUTV v2) + semestre_id = db.Column(db.Integer) + # Si UE validée dans le cursus d'un autre etablissement + is_external = db.Column( + db.Boolean, default=False, server_default="false", index=True + ) + + ue = db.relationship("UniteEns", lazy="select", uselist=False) + + def __repr__(self): + return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})" + + +class ScolarAutorisationInscription(db.Model): + """Autorisation d'inscription dans un semestre""" + + __tablename__ = "scolar_autorisation_inscription" + id = db.Column(db.Integer, primary_key=True) + autorisation_inscription_id = db.synonym("id") + + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) + # semestre ou on peut s'inscrire: + semestre_id = db.Column(db.Integer) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + origin_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + + +class ScolarEvent(db.Model): + """Evenement dans le parcours scolaire d'un étudiant""" + + __tablename__ = "scolar_events" + id = db.Column(db.Integer, primary_key=True) + event_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + ) + # 'CREATION', 'INSCRIPTION', 'DEMISSION', + # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' + # 'ECHEC_SEM' + # 'UTIL_COMPENSATION' + event_type = db.Column(db.String(SHORT_STR_LEN)) + # Semestre compensé par formsemestre_id: + comp_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 07cbd1336..b9b4630ce 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -935,7 +935,7 @@ class NotesTable: """ return self.moy_gen[etudid] - def get_etud_moy_infos(self, etudid): + def get_etud_moy_infos(self, etudid): # XXX OBSOLETE """Infos sur moyennes""" return self.etud_moy_infos[etudid] @@ -1011,7 +1011,10 @@ class NotesTable: cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - "select etudid, code, assidu, compense_formsemestre_id, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is NULL;", + """SELECT etudid, code, assidu, compense_formsemestre_id, event_date + FROM scolar_formsemestre_validation + WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL; + """, {"formsemestre_id": self.formsemestre_id}, ) decisions_jury = {} @@ -1137,8 +1140,14 @@ class NotesTable: """ self.ue_capitalisees = scu.DictDefault(defaultvalue=[]) cnx = None + semestre_id = self.sem["semestre_id"] for etudid in self.get_etudids(): - capital = formsemestre_get_etud_capitalisation(self.sem, etudid) + capital = formsemestre_get_etud_capitalisation( + self.formation["id"], + semestre_id, + ndb.DateDMYtoISO(self.sem["date_debut"]), + etudid, + ) for ue_cap in capital: # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc) # il faut la calculer ici et l'enregistrer @@ -1308,7 +1317,7 @@ class NotesTable: """Liste des evaluations de ce semestre, avec leur etat""" return self.get_evaluations_etats() - def get_mod_evaluation_etat_list(self, moduleimpl_id): + def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: """Liste des évaluations de ce module""" return [ e diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 23833f88c..d1127110c 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -142,7 +142,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): prefs = sco_preferences.SemPreferences(formsemestre_id) # nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if not nt.get_etud_etat(etudid): raise ScoValueError("Etudiant non inscrit à ce semestre") I = scu.DictDefault(defaultvalue="") diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 59ebab2d5..5fd0cec68 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -59,9 +59,9 @@ import traceback from flask import g +from app import log from app.scodoc import notesdb as ndb from app.scodoc import sco_utils as scu -from app import log CACHE = None # set in app.__init__.py @@ -293,6 +293,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) ResultatsSemestreCache.delete_many(formsemestre_ids) + ValidationsSemestreCache.delete_many(formsemestre_ids) class DefferedSemCacheManager: @@ -319,10 +320,20 @@ class DefferedSemCacheManager: # ---- Nouvelles classes ScoDoc 9.2 class ResultatsSemestreCache(ScoDocCache): - """Cache pour les résultats ResultatsSemestre. + """Cache pour les résultats ResultatsSemestre (notes et moyennes) Clé: formsemestre_id Valeur: { un paquet de dataframes } """ prefix = "RSEM" timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) + + +class ValidationsSemestreCache(ScoDocCache): + """Cache pour les résultats de jury d'un semestre + Clé: formsemestre_id + Valeur: un paquet de DataFrames + """ + + prefix = "VSC" + timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 823dd19fc..59372b8d6 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -170,18 +170,18 @@ CODES_SEM_REO = {NAR: 1} # reorientation CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée -def code_semestre_validant(code): +def code_semestre_validant(code: str) -> bool: "Vrai si ce CODE entraine la validation du semestre" return CODES_SEM_VALIDES.get(code, False) -def code_semestre_attente(code): +def code_semestre_attente(code: str) -> bool: "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" return CODES_SEM_ATTENTES.get(code, False) -def code_ue_validant(code): - "Vrai si ce code entraine la validation de l'UE" +def code_ue_validant(code: str) -> bool: + "Vrai si ce code entraine la validation des UEs du semestre." return CODES_UE_VALIDES.get(code, False) @@ -259,6 +259,7 @@ class TypeParcours(object): ) # par defaut, autorise tous les types d'UE APC_SAE = False # Approche par compétences avec ressources et SAÉs USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp. + ECTS_FONDAMENTAUX_PER_YEAR = 0.0 # pour ISCID, deprecated def check(self, formation=None): return True, "" # status, diagnostic_message diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index c1c75319b..c6b0151d6 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -32,7 +32,7 @@ from flask_login import current_user from app import db from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl -from app.models.notes import ScolarFormSemestreValidation +from app.models.validations import ScolarFormSemestreValidation from app.scodoc.sco_codes_parcours import UE_SPORT import app.scodoc.sco_utils as scu from app.scodoc import sco_groups diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 15b690a3c..c54806e23 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -393,9 +393,8 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id): """""" evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) etat = _eval_etat(evals) - etat["attente"] = moduleimpl_id in [ - m["moduleimpl_id"] for m in nt.get_moduleimpls_attente() - ] # > liste moduleimpl en attente + # Il y a-t-il des notes en attente dans ce module ? + etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente return etat diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 1f440c8f2..fcab0e7d9 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -991,9 +991,9 @@ def formsemestre_status(formsemestre_id=None): modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id ) - nt = sco_cache.NotesTableCache.get(formsemestre_id) - # WIP formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - # WIP nt = res_sem.load_formsemestre_result(formsemestre) + # nt = sco_cache.NotesTableCache.get(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + nt = res_sem.load_formsemestre_results(formsemestre) # Construit la liste de tous les enseignants de ce semestre: mails_enseignants = set( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index ed5325ce8..e835f1dd8 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -549,7 +549,7 @@ def formsemestre_recap_parcours_table( ass = "" formsemestre = FormSemestre.query.get(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) # nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) if is_cur: type_sem = "*" # now unused @@ -692,7 +692,7 @@ def formsemestre_recap_parcours_table( sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"]) or nt.parcours.ECTS_ONLY ): - etud_moy_infos = nt.get_etud_moy_infos(etudid) + etud_ects_infos = nt.get_etud_ects_pot(etudid) H.append( '' % (class_sem, sem["formsemestre_id"]) ) @@ -703,7 +703,7 @@ def formsemestre_recap_parcours_table( # total ECTS (affiché sous la moyenne générale) H.append( 'ECTS:%g %g' - % (etud_moy_infos["ects_pot"], etud_moy_infos["ects_pot_fond"]) + % (etud_ects_infos["ects_pot"], etud_ects_infos["ects_pot_fond"]) ) H.append('') # ECTS validables dans chaque UE @@ -1062,7 +1062,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid): "title": "Indice du semestre", "explanation": "Facultatif: indice du semestre dans la formation", "allow_null": True, - "allowed_values": [""] + [str(x) for x in range(11)], + "allowed_values": [""] + [x for x in range(11)], "labels": ["-"] + list(range(11)), }, ), diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index a342e3126..5d2871af6 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -837,7 +837,7 @@ def _add_apc_columns( # => On recharge tout dans les nouveaux modèles # rows est une liste de dict avec une clé "etudid" # on va y ajouter une clé par UE du semestre - nt: NotesTableCompat = res_sem.load_formsemestre_result(modimpl.formsemestre) + nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre) modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id] # XXX A ENLEVER TODO diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index 49c78263a..0d682c017 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -28,6 +28,7 @@ """Semestres: gestion parcours DUT (Arreté du 13 août 2005) """ +from app.models.ues import UniteEns import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log @@ -678,10 +679,10 @@ class SituationEtudParcoursECTS(SituationEtudParcoursGeneric): Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?). """ - etud_moy_infos = self.nt.get_etud_moy_infos(self.etudid) + etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid) if ( - etud_moy_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR - and etud_moy_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR + etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR + and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR ): choices = [ DecisionSem( @@ -954,6 +955,9 @@ def do_formsemestre_validate_ue( is_external=False, ): """Ajoute ou change validation UE""" + if semestre_id is None: + ue = UniteEns.query.get_or_404(ue_id) + semestre_id = ue.semestre_idx args = { "formsemestre_id": formsemestre_id, "etudid": etudid, @@ -971,7 +975,8 @@ def do_formsemestre_validate_ue( if formsemestre_id: cond += " and formsemestre_id=%(formsemestre_id)s" if semestre_id: - cond += " and semestre_id=%(semestre_id)s" + cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)" + log(f"formsemestre_validate_ue: deleting where {cond}, args={args})") cursor.execute("delete from scolar_formsemestre_validation where " + cond, args) # insert args["code"] = code @@ -980,7 +985,7 @@ def do_formsemestre_validate_ue( # stocke la moyenne d'UE capitalisée: moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"] args["moy_ue"] = moy_ue - log("formsemestre_validate_ue: %s" % args) + log("formsemestre_validate_ue: create %s" % args) if code != None: scolar_formsemestre_validation_create(cnx, args) else: @@ -1039,7 +1044,9 @@ def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id): ) -def formsemestre_get_etud_capitalisation(sem, etudid): +def formsemestre_get_etud_capitalisation( + formation_id: int, semestre_idx: int, date_debut, etudid: int +) -> list[dict]: """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant. Recherche dans les semestres de la même formation (code) avec le même @@ -1057,30 +1064,32 @@ def formsemestre_get_etud_capitalisation(sem, etudid): cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - """select distinct SFV.*, ue.ue_code from notes_ue ue, notes_formations nf, - notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem + """ + SELECT DISTINCT SFV.*, ue.ue_code + FROM notes_ue ue, notes_formations nf, + notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem - WHERE ue.formation_id = nf.id + WHERE ue.formation_id = nf.id and nf.formation_code = nf2.formation_code and nf2.id=%(formation_id)s and SFV.ue_id = ue.id and SFV.code = 'ADM' and SFV.etudid = %(etudid)s - - and ( (sem.id = SFV.formsemestre_id - and sem.date_debut < %(date_debut)s - and sem.semestre_id = %(semestre_id)s ) - or ( - ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" - AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) + + and ( (sem.id = SFV.formsemestre_id + and sem.date_debut < %(date_debut)s + and sem.semestre_id = %(semestre_id)s ) + or ( + ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" + AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) ) ) """, { "etudid": etudid, - "formation_id": sem["formation_id"], - "semestre_id": sem["semestre_id"], - "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), + "formation_id": formation_id, + "semestre_id": semestre_idx, + "date_debut": date_debut, }, ) diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index ea43cb2d2..292c618a2 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -140,23 +140,23 @@ def descr_autorisations(autorisations): return ", ".join(alist) -def _comp_ects_by_ue_code_and_type(nt, decision_ues): +def _comp_ects_by_ue_code(nt, decision_ues): """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées) decision_ues est le resultat de nt.get_etud_decision_ues Chaque resultat est un dict: { ue_code : ects } """ + raise NotImplementedError() # XXX #sco92 + # ré-écrire en utilisant if not decision_ues: - return {}, {} + return {} ects_by_ue_code = {} - ects_by_ue_type = scu.DictDefault(defaultvalue=0) # { ue_type : ects validés } for ue_id in decision_ues: d = decision_ues[ue_id] ue = nt.uedict[ue_id] ects_by_ue_code[ue["ue_code"]] = d["ects"] - ects_by_ue_type[ue["type"]] += d["ects"] - return ects_by_ue_code, ects_by_ue_type + return ects_by_ue_code def _comp_ects_capitalises_by_ue_code(nt, etudid): @@ -249,11 +249,8 @@ def dict_pvjury( ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) - ects_by_ue_code, ects_by_ue_type = _comp_ects_by_ue_code_and_type( - nt, d["decisions_ue"] - ) + ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"]) d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) - d["sum_ects_by_type"] = ects_by_ue_type if d["decision_sem"] and sco_codes_parcours.code_semestre_validant( d["decision_sem"]["code"] diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 997e835ea..87ab8c26a 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Tableau recapitulatif des notes d'un semestre +"""Tableau récapitulatif des notes d'un semestre """ import datetime import json @@ -41,6 +41,7 @@ from app.comp import res_sem from app.comp.res_common import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite +from app.models.evaluations import Evaluation import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header @@ -308,8 +309,8 @@ def make_formsemestre_recapcomplet( # nt = sco_cache.NotesTableCache.get(formsemestre_id) # sco91 # sco92 : - nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) - modimpls = nt.get_modimpls_dict() + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + modimpls = formsemestre.modimpls_sorted ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport # # if formsemestre.formation.is_apc(): @@ -367,15 +368,16 @@ def make_formsemestre_recapcomplet( pass if not hidemodules and not ue["is_external"]: for modimpl in modimpls: - if modimpl["module"]["ue_id"] == ue["ue_id"]: - code = modimpl["module"]["code"] + if modimpl.module.ue_id == ue["ue_id"]: + code = modimpl.module.code h.append(code) cod2mod[code] = modimpl # pour fabriquer le lien if format == "xlsall": - evals = nt.get_mod_evaluation_etat_list( - modimpl["moduleimpl_id"] - ) - mod_evals[modimpl["moduleimpl_id"]] = evals + evals = nt.modimpls_results[ + modimpl.id + ].get_evaluations_completes(modimpl) + # evals = nt.get_mod_evaluation_etat_list(... + mod_evals[modimpl.id] = evals h += _list_notes_evals_titles(code, evals) h += admission_extra_cols @@ -483,7 +485,7 @@ def make_formsemestre_recapcomplet( if not hidemodules and not ue["is_external"]: j = 0 for modimpl in modimpls: - if modimpl["module"]["ue_id"] == ue["ue_id"]: + if modimpl.module.ue_id == ue["ue_id"]: l.append( fmtnum( scu.fmt_note( @@ -492,9 +494,7 @@ def make_formsemestre_recapcomplet( ) ) # moyenne etud dans module if format == "xlsall": - l += _list_notes_evals( - mod_evals[modimpl["moduleimpl_id"]], etudid - ) + l += _list_notes_evals(mod_evals[modimpl.id], etudid) j += 1 if not hidebac: for k in admission_extra_cols: @@ -509,9 +509,7 @@ def make_formsemestre_recapcomplet( if not hidemodules: # moy/min/max dans chaque module mods_stats = {} # moduleimpl_id : stats for modimpl in modimpls: - mods_stats[modimpl["moduleimpl_id"]] = nt.get_mod_stats( - modimpl["moduleimpl_id"] - ) + mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id) def add_bottom_stat(key, title, corner_value=""): l = ["", title] @@ -551,16 +549,16 @@ def make_formsemestre_recapcomplet( # ue_index.append(len(l) - 1) if not hidemodules and not ue["is_external"]: for modimpl in modimpls: - if modimpl["module"]["ue_id"] == ue["ue_id"]: + if modimpl.module.ue_id == ue["ue_id"]: if key == "coef": - coef = modimpl["module"]["coefficient"] + coef = modimpl.module.coefficient if format[:3] != "xls": coef = str(coef) l.append(coef) elif key == "ects": l.append("") # ECTS module ? else: - val = mods_stats[modimpl["moduleimpl_id"]][key] + val = mods_stats[modimpl.id][key] if key == "nb_valid_evals": if ( format[:3] != "xls" @@ -571,9 +569,7 @@ def make_formsemestre_recapcomplet( l.append(val) if format == "xlsall": - l += _list_notes_evals_stats( - mod_evals[modimpl["moduleimpl_id"]], key - ) + l += _list_notes_evals_stats(mod_evals[modimpl.id], key) if modejury: l.append("") # case vide sur ligne "Moyennes" @@ -595,7 +591,7 @@ def make_formsemestre_recapcomplet( add_bottom_stat("nb_valid_evals", "Nb évals") add_bottom_stat("ects", "ECTS") - # Generation table au format demandé + # Génération de la table au format demandé if format == "html": # Table format HTML H = [ @@ -838,63 +834,50 @@ def make_formsemestre_recapcomplet( raise ValueError("unknown format %s" % format) -def _list_notes_evals(evals, etudid): +def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]: """Liste des notes des evaluations completes de ce module (pour table xls avec evals) """ L = [] for e in evals: - if ( - e["etat"]["evalcomplete"] - or e["etat"]["evalattente"] - or e["publish_incomplete"] - ): - notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"]) - if etudid in notes_db: - val = notes_db[etudid]["value"] - else: - # Note manquante mais prise en compte immédiate: affiche ATT - val = scu.NOTES_ATTENTE - val_fmt = scu.fmt_note(val, keep_numeric=True) - L.append(val_fmt) + notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id) + if etudid in notes_db: + val = notes_db[etudid]["value"] + else: + # Note manquante mais prise en compte immédiate: affiche ATT + val = scu.NOTES_ATTENTE + val_fmt = scu.fmt_note(val, keep_numeric=True) + L.append(val_fmt) return L -def _list_notes_evals_titles(codemodule, evals): +def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]: """Liste des titres des evals completes""" L = [] eval_index = len(evals) - 1 for e in evals: - if ( - e["etat"]["evalcomplete"] - or e["etat"]["evalattente"] - or e["publish_incomplete"] - ): - L.append(codemodule + "-" + str(eval_index) + "-" + e["jour"].isoformat()) + L.append(codemodule + "-" + str(eval_index) + "-" + e.jour.isoformat()) eval_index -= 1 return L -def _list_notes_evals_stats(evals, key): +def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]: """Liste des stats (moy, ou rien!) des evals completes""" L = [] for e in evals: - if ( - e["etat"]["evalcomplete"] - or e["etat"]["evalattente"] - or e["publish_incomplete"] - ): - if key == "moy": - val = e["etat"]["moy_num"] - L.append(scu.fmt_note(val, keep_numeric=True)) - elif key == "max": - L.append(e["note_max"]) - elif key == "min": - L.append(0.0) - elif key == "coef": - L.append(e["coefficient"]) - else: - L.append("") # on n'a pas sous la main min/max + if key == "moy": + # TODO #sco92 + # val = e["etat"]["moy_num"] + # L.append(scu.fmt_note(val, keep_numeric=True)) + L.append("") + elif key == "max": + L.append(e.note_max) + elif key == "min": + L.append(0.0) + elif key == "coef": + L.append(e.coefficient) + else: + L.append("") # on n'a pas sous la main min/max return L diff --git a/app/views/notes.py b/app/views/notes.py index 4b299ee52..0762a5cee 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2140,7 +2140,7 @@ def formsemestre_validation_etud_manu( ) -@bp.route("/formsemestre_validate_previous_ue") +@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func