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( '