############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Stockage des décisions de jury """ import pandas as pd import sqlalchemy as sa from app import db from app.comp.res_cache import ResultatsCache from app.models import ( ApcValidationAnnee, ApcValidationRCUE, Formation, FormSemestre, Identite, ScolarAutorisationInscription, ScolarFormSemestreValidation, UniteEns, ) from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_utils as scu class ValidationsSemestre(ResultatsCache): """Les décisions de jury pour un semestre""" _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': "d/m/y", "ects" : x}}} """ self.ue_capitalisees: pd.DataFrame = None """DataFrame, index etudid 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 : note enregistrée event_date : date de la validation (jury)""" 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 RCUE). Calcule les attributs: decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' : "d/m/y", 'ects' : x }} } 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( db.text("ue_id is NULL") # 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(scu.DATE_FMT), } self.decisions_jury = decisions_jury # UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }} decisions_jury_ues = {} # Parcoure les décisions d'UE: for decision in ( decisions_jury_q.filter(db.text("ue_id is not NULL")) .join(UniteEns) .order_by(UniteEns.numero) ): if decision.etudid not in decisions_jury_ues: decisions_jury_ues[decision.etudid] = {} # Calcul des ECTS associés à cette UE: if codes_cursus.code_ue_validant(decision.code) and decision.ue: 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(scu.DATE_FMT), } self.decisions_jury_ues = decisions_jury_ues def has_decision(self, etud: Identite) -> bool: """Vrai si etud a au moins une décision enregistrée depuis ce semestre (quelle qu'elle soit) """ return (etud.id in self.decisions_jury_ues) or (etud.id in self.decisions_jury) 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 : } ] """ # Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne # and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' ) query = sa.text( """ 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 and ins.etudid = SFV.etudid and ins.formsemestre_id = :formsemestre_id and SFV.ue_id = ue.id and SFV.code = 'ADM' and ( (sem.id = SFV.formsemestre_id and sem.date_debut < :date_debut and sem.semestre_id = :semestre_id ) 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) ) ) """ ) params = { "formation_id": formsemestre.formation.id, "formsemestre_id": formsemestre.id, "semestre_id": formsemestre.semestre_id, "date_debut": formsemestre.date_debut, } with db.engine.begin() as connection: df = pd.read_sql_query(query, connection, params=params, index_col="etudid") return df def erase_decisions_annee_formation( etud: Identite, formation: Formation, annee: int, delete=False ) -> list: """Efface toutes les décisions de jury de l'étudiant dans les formations de même code que celle donnée pour cette année de la formation: UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante. Ne considère pas l'origine de la décision. annee: entier, 1, 2, 3, ... Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher. """ sem1, sem2 = annee * 2 - 1, annee * 2 # UEs validations = ( ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) .join(UniteEns) .filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2)) .join(Formation) .filter_by(formation_code=formation.formation_code) .order_by( UniteEns.acronyme, UniteEns.numero ) # acronyme d'abord car 2 semestres .all() ) # RCUEs (a priori inutile de matcher sur l'ue2_id) validations += ( ApcValidationRCUE.query.filter_by(etudid=etud.id) .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id) .filter_by(semestre_idx=sem1) .join(Formation) .filter_by(formation_code=formation.formation_code) .order_by(UniteEns.acronyme, UniteEns.numero) .all() ) # Validation de semestres classiques validations += ( ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None) .join( FormSemestre, FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id, ) .filter( db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2) ) .join(Formation) .filter_by(formation_code=formation.formation_code) .all() ) # Année BUT validations += ApcValidationAnnee.query.filter_by( etudid=etud.id, ordre=annee, referentiel_competence_id=formation.referentiel_competence_id, ).all() # Autorisations vers les semestres suivants ceux de l'année: validations += ( ScolarAutorisationInscription.query.filter_by( etudid=etud.id, formation_code=formation.formation_code ) .filter( db.or_( ScolarAutorisationInscription.semestre_id == sem1 + 1, ScolarAutorisationInscription.semestre_id == sem2 + 1, ) ) .all() ) if delete: for validation in validations: db.session.delete(validation) db.session.commit() sco_cache.invalidate_formsemestre_etud(etud) return [] return validations