Corrige bug sur l'analyse des abandons de formation

This commit is contained in:
Cléo Baras 2024-02-14 14:34:22 +01:00
parent a200be586a
commit e78a2d3ffe
5 changed files with 119 additions and 53 deletions

View File

@ -284,3 +284,13 @@ def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
cosemestres[fid] = cosem cosemestres[fid] = cosem
return cosemestres 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

View File

@ -37,6 +37,7 @@ Created on 17/01/2024
""" """
import pandas as pd import pandas as pd
from app import ScoValueError
from app.models import FormSemestre, Identite, Formation from app.models import FormSemestre, Identite, Formation
from app.pe import pe_comp, pe_affichage from app.pe import pe_comp, pe_affichage
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
@ -87,7 +88,7 @@ class EtudiantsJuryPE:
self.abandons_ids = {} self.abandons_ids = {}
"""Les etudids des étudiants redoublants/réorientés""" """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 """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``. 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) self.identites[etudid] = Identite.get_etud(etudid)
# Analyse son cursus # 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 # Analyse son parcours pour atteindre chaque semestre de la formation
self.structure_cursus_etudiant(etudid) self.structure_cursus_etudiant(etudid)
@ -187,7 +188,12 @@ class EtudiantsJuryPE:
etudiants = {etudid: self.identites[etudid] for etudid in etudids} etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants 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 : """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é) * 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é, * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité,
avec son nom, prénom, etc... avec son nom, prénom, etc...
* à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de * à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme)
route (cf. clé abandon) 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: Args:
etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury 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 "abandon": False, # va être traité en dessous
} }
# 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 ? # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
dernier_semes_etudiant = formsemestres[0] dernier_semes_etudiant = formsemestres[0]
res = load_formsemestre_results(dernier_semes_etudiant) res = load_formsemestre_results(dernier_semes_etudiant)
etud_etat = res.get_etud_etat(etudid) etud_etat = res.get_etud_etat(etudid)
if etud_etat == scu.DEMISSION: if etud_etat == scu.DEMISSION:
self.cursus[etudid]["abandon"] |= True self.cursus[etudid]["abandon"] = True
else: else:
# Est-il réorienté ou a-t-il arrêté volontairement sa formation ? # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ?
self.cursus[etudid]["abandon"] |= arret_de_formation(identite, cosemestres) self.cursus[etudid]["abandon"] = arret_de_formation(
identite, cosemestres, formsemestre_base
)
def get_semestres_significatifs(self, etudid: int): def get_semestres_significatifs(self, etudid: int):
"""Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé """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 return semestres_apc
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: def arret_de_formation(
"""Détermine si un étudiant a arrêté sa formation. Il peut s'agir : 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 * 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 (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 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) 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), 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 l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
@ -493,42 +509,79 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
if not semestres_apc: if not semestres_apc:
return True return True
# Son dernier semestre APC en date # Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang,
dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc) # sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}``
numero_dernier_formsemestre = dernier_formsemestre.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 cosemestres_superieurs = {}
# semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) for rang in cosemestres_tries_par_rang:
if numero_dernier_formsemestre % 2 == 1: if rang >= formsemestre_base.semestre_id:
numeros_possibles = list( cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang]
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
# Si pas d'autres cosemestres postérieurs
if not cosemestres_superieurs:
return False return False
# 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
# 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: def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:
"""Renvoie le dernier semestre en **date de fin** d'un dictionnaire """Renvoie le dernier semestre en **date de fin** d'un dictionnaire

View File

@ -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) 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() pe_affichage.pe_start_log()
self.diplome = diplome self.diplome = diplome
"L'année du 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}" self.nom_export_zip = f"Jury_PE_{self.diplome}"
"Nom du zip où ranger les fichiers générés" "Nom du zip où ranger les fichiers générés"
@ -87,7 +90,7 @@ class JuryPE(object):
self.diplome}""" self.diplome}"""
) )
self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants 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.diplomes_ids = self.etudiants.diplomes_ids
self.zipdata = io.BytesIO() self.zipdata = io.BytesIO()

View File

@ -83,7 +83,7 @@ def pe_view_sem_recap(formsemestre_id: int):
) )
# request.method == "POST" # request.method == "POST"
jury = pe_jury.JuryPE(annee_diplome) jury = pe_jury.JuryPE(annee_diplome, formsemestre)
if not jury.diplomes_ids: if not jury.diplomes_ids:
flash("aucun étudiant à considérer !") flash("aucun étudiant à considérer !")
return redirect( return redirect(

View File

@ -61,11 +61,11 @@ class DevConfig(Config):
DEBUG = True DEBUG = True
TESTING = False TESTING = False
SQLALCHEMY_DATABASE_URI = ( 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" SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a"
# pour le avoir url_for dans le shell: # 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): class TestConfig(DevConfig):