From e78a2d3ffe74bae379bccb059c9c419f30cfa7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 14 Feb 2024 14:34:22 +0100 Subject: [PATCH] Corrige bug sur l'analyse des abandons de formation --- app/pe/pe_comp.py | 10 +++ app/pe/pe_etudiant.py | 149 ++++++++++++++++++++++++++++-------------- app/pe/pe_jury.py | 7 +- app/pe/pe_view.py | 2 +- config.py | 4 +- 5 files changed, 119 insertions(+), 53 deletions(-) diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py index 67805dfa4..24edf207a 100644 --- a/app/pe/pe_comp.py +++ b/app/pe/pe_comp.py @@ -284,3 +284,13 @@ def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]: cosemestres[fid] = cosem return cosemestres + + +def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]): + """Partant d'un dictionnaire de cosemestres, les tri par rang (semestre_id) dans un + dictionnaire {rang: [liste des semestres du dit rang]}""" + cosemestres_tries = {} + for sem in cosemestres.values(): + cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(sem.semestre_id, []) + [sem] + return cosemestres_tries + diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 9bcb6c495..8b941d7f0 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -37,6 +37,7 @@ Created on 17/01/2024 """ import pandas as pd +from app import ScoValueError from app.models import FormSemestre, Identite, Formation from app.pe import pe_comp, pe_affichage from app.scodoc import codes_cursus @@ -87,7 +88,7 @@ class EtudiantsJuryPE: self.abandons_ids = {} """Les etudids des étudiants redoublants/réorientés""" - def find_etudiants(self): + def find_etudiants(self, formsemestre_base: FormSemestre): """Liste des étudiants à prendre en compte dans le jury PE, en les recherchant de manière automatique par rapport à leur année de diplomation ``annee_diplome``. @@ -116,7 +117,7 @@ class EtudiantsJuryPE: self.identites[etudid] = Identite.get_etud(etudid) # Analyse son cursus - self.analyse_etat_etudiant(etudid, cosemestres) + self.analyse_etat_etudiant(etudid, cosemestres, formsemestre_base) # Analyse son parcours pour atteindre chaque semestre de la formation self.structure_cursus_etudiant(etudid) @@ -187,7 +188,12 @@ class EtudiantsJuryPE: etudiants = {etudid: self.identites[etudid] for etudid in etudids} return etudiants - def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]): + def analyse_etat_etudiant( + self, + etudid: int, + cosemestres: dict[int, FormSemestre], + formsemestre_base: FormSemestre, + ): """Analyse le cursus d'un étudiant pouvant être : * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré) @@ -198,8 +204,10 @@ class EtudiantsJuryPE: * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité, avec son nom, prénom, etc... - * à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de - route (cf. clé abandon) + * à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme) + ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré en abandon s'il n'est + inscrit à aucun cosemestres de rang supérieur ou égal (et donc de dates) + à celui ayant servi à lancer le jury (`formsemestre_base`) Args: etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury @@ -232,15 +240,19 @@ class EtudiantsJuryPE: "abandon": False, # va être traité en dessous } - # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? - dernier_semes_etudiant = formsemestres[0] - res = load_formsemestre_results(dernier_semes_etudiant) - etud_etat = res.get_etud_etat(etudid) - if etud_etat == scu.DEMISSION: - self.cursus[etudid]["abandon"] |= True - else: - # Est-il réorienté ou a-t-il arrêté volontairement sa formation ? - self.cursus[etudid]["abandon"] |= arret_de_formation(identite, cosemestres) + # Si l'étudiant est succeptible d'être diplomé + if self.cursus[etudid]["diplome"] == self.annee_diplome: + # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? + dernier_semes_etudiant = formsemestres[0] + res = load_formsemestre_results(dernier_semes_etudiant) + etud_etat = res.get_etud_etat(etudid) + if etud_etat == scu.DEMISSION: + self.cursus[etudid]["abandon"] = True + else: + # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ? + self.cursus[etudid]["abandon"] = arret_de_formation( + identite, cosemestres, formsemestre_base + ) def get_semestres_significatifs(self, etudid: int): """Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé @@ -446,8 +458,10 @@ def get_semestres_apc(identite: Identite) -> list: return semestres_apc -def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: - """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : +def arret_de_formation( + etud: Identite, cosemestres: dict[int, FormSemestre], formsemestre_base: FormSemestre +) -> bool: + """Détermine si un étudiant a arrêté sa formation (volontairement ou non). Il peut s'agir : * d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire @@ -458,7 +472,9 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation) - connu dans Scodoc. + connu dans Scodoc. Par "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres + de rang/semestre_id supérieur ou égal (et donc de dates) à celui du ``formsemestre_base`` ayant servi à lancer + le jury PE. Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans @@ -493,41 +509,78 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: if not semestres_apc: return True - # Son dernier semestre APC en date - dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc) - numero_dernier_formsemestre = dernier_formsemestre.semestre_id + # Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang, + # sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}`` + cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres) - # Les numéro de semestres possible dans lesquels il pourrait s'incrire - # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) - if numero_dernier_formsemestre % 2 == 1: - numeros_possibles = list( - range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT) - ) - # semestre pair => passage en année supérieure ou redoublement - else: # - numeros_possibles = list( - range( - max(numero_dernier_formsemestre - 1, 1), - pe_comp.NBRE_SEMESTRES_DIPLOMANT, - ) - ) + cosemestres_superieurs = {} + for rang in cosemestres_tries_par_rang: + if rang >= formsemestre_base.semestre_id: + cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang] - # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ? - formsestres_superieurs_possibles = [] - for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits - if ( - fid != dernier_formsemestre.formsemestre_id - and sem.semestre_id in numeros_possibles - and sem.date_debut.year >= dernier_formsemestre.date_debut.year - ): - # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant - # et de niveau plus élevé que le dernier semestre valide de l'étudiant - formsestres_superieurs_possibles.append(fid) + # Si pas d'autres cosemestres postérieurs + if not cosemestres_superieurs: + return False - if len(formsestres_superieurs_possibles) > 0: - return True + # Pour chaque rang de (co)semestres, y-a-il un dans lequel il est inscrit ? + etat_inscriptions = {rang: False for rang in cosemestres_superieurs} + for rang in etat_inscriptions: + for sem in cosemestres_superieurs[rang]: + etudiants_du_sem = {ins.etudid for ins in sem.inscriptions} + if etud.etudid in etudiants_du_sem: + etat_inscriptions[rang] = True - return False + # Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres + rangs = etat_inscriptions.keys() + if list(rangs) != list(range(min(rangs), max(rangs)+1)): + difference = set(range(min(rangs), max(rangs)+1)) - set(rangs) + affichage = ",".join([f"S{val}" for val in difference]) + raise ScoValueError(f"Il manque le(s) semestre(s) {affichage} au cursus de vos étudiants.") + + # Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire + est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs) + if est_demissionnaire: + non_inscrit_a = [rang for rang in etat_inscriptions if not etat_inscriptions[rang]] + affichage = ",".join([f"S{val}" for val in non_inscrit_a]) + pe_affichage.pe_print(f"{etud.etat_civil} ({etud.etudid} considéré en abandon car non inscrit dans un (ou des) semestre(s) {affichage} amenant à diplômation") + + return est_demissionnaire + + # # Son dernier semestre APC en date + # dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc) + # numero_dernier_formsemestre = dernier_formsemestre.semestre_id + # + # # Les numéro de semestres possible dans lesquels il pourrait s'incrire + # # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) + # if numero_dernier_formsemestre % 2 == 1: + # numeros_possibles = list( + # range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT) + # ) + # # semestre pair => passage en année supérieure ou redoublement + # else: # + # numeros_possibles = list( + # range( + # max(numero_dernier_formsemestre - 1, 1), + # pe_comp.NBRE_SEMESTRES_DIPLOMANT, + # ) + # ) + # + # # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ? + # formsestres_superieurs_possibles = [] + # for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits + # if ( + # fid != dernier_formsemestre.formsemestre_id + # and sem.semestre_id in numeros_possibles + # and sem.date_debut.year >= dernier_formsemestre.date_debut.year + # ): + # # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant + # # et de niveau plus élevé que le dernier semestre valide de l'étudiant + # formsestres_superieurs_possibles.append(fid) + # + # if len(formsestres_superieurs_possibles) > 0: + # return True + # + # return False def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index a6c15b8c5..b83f40f21 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -70,11 +70,14 @@ class JuryPE(object): diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) """ - def __init__(self, diplome): + def __init__(self, diplome: int, formsemestre_base: FormSemestre): pe_affichage.pe_start_log() self.diplome = diplome "L'année du diplome" + self.formsemestre_base = formsemestre_base + "Le formsemestre ayant servi à lancer le jury PE (souvent un S3 ou un S5)" + self.nom_export_zip = f"Jury_PE_{self.diplome}" "Nom du zip où ranger les fichiers générés" @@ -87,7 +90,7 @@ class JuryPE(object): self.diplome}""" ) self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants - self.etudiants.find_etudiants() + self.etudiants.find_etudiants(formsemestre_base) self.diplomes_ids = self.etudiants.diplomes_ids self.zipdata = io.BytesIO() diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 8ef4fe444..0c2b733d0 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -83,7 +83,7 @@ def pe_view_sem_recap(formsemestre_id: int): ) # request.method == "POST" - jury = pe_jury.JuryPE(annee_diplome) + jury = pe_jury.JuryPE(annee_diplome, formsemestre) if not jury.diplomes_ids: flash("aucun étudiant à considérer !") return redirect( diff --git a/config.py b/config.py index d98e95138..af1a173ee 100755 --- a/config.py +++ b/config.py @@ -61,11 +61,11 @@ class DevConfig(Config): DEBUG = True TESTING = False SQLALCHEMY_DATABASE_URI = ( - os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_DEV" + os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC" ) SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" # pour le avoir url_for dans le shell: - # SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "localhost" + # SERVER_NAME = "http://localhost:8080" class TestConfig(DevConfig):