Ajoute un tableur pour visualiser les étudiants traités par le jury (diplômé ou redoublants/démissionnaires)

This commit is contained in:
Cléo Baras 2024-01-27 10:13:04 +01:00
parent 9c6d988fc3
commit fce23aa066
2 changed files with 224 additions and 160 deletions

View File

@ -35,41 +35,56 @@ Created on 17/01/2024
@author: barasc @author: barasc
""" """
import pandas as pd
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
import app.pe.pe_affichage as pe_affichage import app.pe.pe_affichage as pe_affichage
import app.pe.pe_comp as pe_comp import app.pe.pe_comp as pe_comp
from app.pe import pe_comp, pe_affichage
class EtudiantsJuryPE: class EtudiantsJuryPE:
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
def __init__(self, annee_diplome: int): def __init__(self, annee_diplome: int):
""" """
Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE
Args: Args:
annee_diplome: L'année de diplomation annee_diplome: L'année de diplomation
""" """
self.annee_diplome = annee_diplome self.annee_diplome = annee_diplome
"""L'année du diplôme"""
"Les identités des étudiants traités pour le jury"
self.identites = {} # ex. ETUDINFO_DICT self.identites = {} # ex. ETUDINFO_DICT
"Les cursus (semestres suivis, abandons) des étudiants" "Les identités des étudiants traités pour le jury"
self.cursus = {} self.cursus = {}
"""Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants""" "Les cursus (semestres suivis, abandons) des étudiants"
self.trajectoires = {} self.trajectoires = {}
"""Les trajectoires/chemins de semestres suivis par les étudiants
pour atteindre un aggrégat donné
(par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)"""
"Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)"
self.etudiants_diplomes = {} self.etudiants_diplomes = {}
self.diplomes_ids = {} """Les identités des étudiants à considérer au jury (ceux qui seront effectivement diplômés)"""
self.diplomes_ids = {}
"""Les etudids des étudiants diplômés"""
"Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)"
self.etudiants_ids = {} self.etudiants_ids = {}
"""Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)""" """Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons).
Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant
été réorientés ou ayant abandonnés)"""
self.cosemestres: dict[int, FormSemestre] = None self.cosemestres: dict[int, FormSemestre] = None
"Les cosemestres donnant lieu à même année de diplome" "Les cosemestres donnant lieu à même année de diplome"
self.abandons = {}
"""Les étudiants qui ne seront pas diplômés à ce jury (redoublants/réorientés)"""
self.abandons_ids = {}
"""Les etudids des étudiants redoublants/réorientés"""
def find_etudiants(self, formation_id: int): def find_etudiants(self, formation_id: int):
"""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``
@ -85,7 +100,9 @@ class EtudiantsJuryPE:
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None) cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None)
self.cosemestres = cosemestres self.cosemestres = cosemestres
pe_affichage.pe_print(f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés") pe_affichage.pe_print(
f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés"
)
pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres") pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres")
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
@ -109,23 +126,30 @@ class EtudiantsJuryPE:
# 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)
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris # Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
self.etudiants_diplomes = self.get_etudiants_diplomes() self.etudiants_diplomes = self.get_etudiants_diplomes()
self.diplomes_ids = set(self.etudiants_diplomes.keys()) self.diplomes_ids = set(self.etudiants_diplomes.keys())
self.etudiants_ids = set(self.identites) self.etudiants_ids = set(self.identites.keys())
"""Les étudiants dont il faut calculer les moyennes""" """Les étudiants dont il faut calculer les moyennes"""
self.formsemestres_jury_ids = self.get_formsemestres() self.formsemestres_jury_ids = self.get_formsemestres()
"""Les formsemestres (des étudiants) dont il faut calculer les moyennes""" """Les formsemestres (des étudiants) dont il faut calculer les moyennes"""
# Les abandons (pour debug)
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
self.abandons_ids = set(self.abandons)
# Synthèse # Synthèse
pe_affichage.pe_print( pe_affichage.pe_print(
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
) )
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
pe_affichage.pe_print(f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon") assert nbre_abandons == len(self.abandons_ids)
pe_affichage.pe_print(
f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon"
)
pe_affichage.pe_print( pe_affichage.pe_print(
f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne"
) )
@ -137,14 +161,8 @@ class EtudiantsJuryPE:
# " => semestres dont il faut calculer les moyennes : " # " => semestres dont il faut calculer les moyennes : "
# + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) # + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
# ) # )
# Les abandons (pour debug)
self.abandons = sorted(
[
cursus["nom"]
for etudid, cursus in self.cursus.items()
if etudid not in self.diplomes_ids
]
)
def get_etudiants_diplomes(self) -> dict[int, Identite]: def get_etudiants_diplomes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
@ -164,6 +182,23 @@ 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 get_etudiants_redoublants_ou_reorientes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
dont les notes seront prises en compte (pour les classements) mais qui n'apparaitront
pas dans le jury car diplômé une autre année (redoublants) ou réorienté ou démissionnaire.
Returns:
Un dictionnaire `{etudid: Identite(etudid)}`
"""
etudids = [
etudid
for etudid in self.cursus
if self.cursus[etudid]["diplome"] != self.annee_diplome
or self.cursus[etudid]["abandon"] is True
]
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]):
"""Analyse le cursus d'un étudiant pouvant être : """Analyse le cursus d'un étudiant pouvant être :
@ -254,7 +289,9 @@ class EtudiantsJuryPE:
} # les semestres de n°i de l'étudiant } # les semestres de n°i de l'étudiant
self.cursus[etudid][nom_sem] = semestres_i self.cursus[etudid][nom_sem] = semestres_i
def get_trajectoire(self, etudid: int, formsemestre_final: FormSemestre, nom_aggregat: str): def get_trajectoire(
self, etudid: int, formsemestre_final: FormSemestre, nom_aggregat: str
):
"""Ensemble des semestres parcourus par """Ensemble des semestres parcourus par
un étudiant pour l'amener à un semestre terminal. un étudiant pour l'amener à un semestre terminal.
@ -282,7 +319,9 @@ class EtudiantsJuryPE:
if nom_aggregat.startswith("S"): # les semestres if nom_aggregat.startswith("S"): # les semestres
numero_semestres_possibles = [numero_semestre_terminal] numero_semestres_possibles = [numero_semestre_terminal]
elif nom_aggregat.endswith("A"): # les années elif nom_aggregat.endswith("A"): # les années
numero_semestres_possibles = [int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"]] numero_semestres_possibles = [
int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"]
]
assert numero_semestre_terminal in numero_semestres_possibles assert numero_semestre_terminal in numero_semestres_possibles
else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal) else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal)
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1)) numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
@ -382,15 +421,66 @@ class EtudiantsJuryPE:
else: else:
raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids")
def nbre_etapes_max_diplomes(self): def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
"""Connaissant les étudiants diplomes du jury PE, """Partant d'un ensemble d'étudiants,
nombre de semestres (étapes) maximum suivis par les étudiants du jury. nombre de semestres (étapes) maximum suivis par les étudiants du jury.
Args:
etudids: Liste d'étudid d'étudiants
""" """
nbres_semestres = [] nbres_semestres = []
for etudid in self.diplomes_ids: for etudid in etudids:
nbres_semestres.append(self.cursus[etudid]["nb_semestres"]) nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
if not nbres_semestres:
return 0
else:
return max(nbres_semestres) return max(nbres_semestres)
def df_administratif(self, etudids: list[int]) -> pd.DataFrame:
"""Synthétise toutes les données administratives d'un groupe
d'étudiants fournis par les etudid dans un dataFrame
Args:
etudids: La liste des étudiants à prendre en compte
"""
etudids = list(etudids)
# Récupération des données des étudiants
administratif = {}
nbre_semestres_max = self.nbre_etapes_max_diplomes(etudids)
for etudid in etudids:
etudiant = self.identites[etudid]
cursus = self.cursus[etudid]
formsemestres = cursus["formsemestres"]
if cursus["diplome"]:
diplome = cursus["diplome"]
else:
diplome = "indéterminé"
administratif[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance),
"Date d'entree": cursus["entree"],
"Date de diplome": diplome,
"Nbre de semestres": len(formsemestres),
}
# Ajout des noms de semestres parcourus
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
# Construction du dataframe
df = pd.DataFrame.from_dict(administratif, orient="index")
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set: def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
"""Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``)

View File

@ -50,7 +50,6 @@ from app.scodoc.gen_tables import SeqGenTable
from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire
import app.pe.pe_comp as pe_comp import app.pe.pe_comp as pe_comp
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_semtag import SemestreTag from app.pe.pe_semtag import SemestreTag
from app.pe.pe_interclasstag import AggregatInterclasseTag from app.pe.pe_interclasstag import AggregatInterclasseTag
from app.pe.pe_trajectoiretag import TrajectoireTag from app.pe.pe_trajectoiretag import TrajectoireTag
@ -100,6 +99,7 @@ class JuryPE(object):
"Nom du zip où ranger les fichiers générés" "Nom du zip où ranger les fichiers générés"
self.zipdata = io.BytesIO() self.zipdata = io.BytesIO()
with ZipFile(self.zipdata, "w") as zipfile: with ZipFile(self.zipdata, "w") as zipfile:
# Chargement des étudiants à prendre en compte dans le jury # Chargement des étudiants à prendre en compte dans le jury
pe_affichage.pe_print( pe_affichage.pe_print(
@ -112,6 +112,31 @@ class JuryPE(object):
self.etudiants.find_etudiants(self.formation_id) self.etudiants.find_etudiants(self.formation_id)
self.diplomes_ids = self.etudiants.diplomes_ids self.diplomes_ids = self.etudiants.diplomes_ids
# Intègre le bilan des semestres taggués au zip final
output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
if self.diplomes_ids:
onglet = "diplômés"
df_diplome = self.etudiants.df_administratif(self.diplomes_ids)
df_diplome.to_excel(writer, onglet, index=True, header=True)
if self.etudiants.abandons_ids:
onglet = "redoublants-réorientés"
df_abandon = self.etudiants.df_administratif(
self.etudiants.abandons_ids
)
df_abandon.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile,
f"etudiants_{self.diplome}.xlsx",
output.read(),
path="details",
)
if not self.diplomes_ids:
pe_affichage.pe_tools("*** Aucun étudiant diplômé")
else:
# Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE # Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE
pe_affichage.pe_print("*** Génère les semestres taggués") pe_affichage.pe_print("*** Génère les semestres taggués")
self.semestres_taggues = compute_semestres_tag(self.etudiants) self.semestres_taggues = compute_semestres_tag(self.etudiants)
@ -251,7 +276,7 @@ class JuryPE(object):
synthese = {} synthese = {}
pe_affichage.pe_print(" -> Synthèse des données administratives") pe_affichage.pe_print(" -> Synthèse des données administratives")
synthese["administratif"] = self.df_administratif() synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids)
tags = self.do_tags_list(self.interclassements_taggues) tags = self.do_tags_list(self.interclassements_taggues)
for tag in tags: for tag in tags:
@ -259,46 +284,6 @@ class JuryPE(object):
synthese[tag] = self.df_tag(tag) synthese[tag] = self.df_tag(tag)
return synthese return synthese
def df_administratif(self):
"""Synthétise toutes les données administratives des étudiants"""
etudids = list(self.diplomes_ids)
# Récupération des données des étudiants
administratif = {}
nbre_semestres_max = self.etudiants.nbre_etapes_max_diplomes()
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
cursus = self.etudiants.cursus[etudid]
formsemestres = cursus["formsemestres"]
if cursus["diplome"]:
diplome = cursus["diplome"]
else:
diplome = "indéterminé"
administratif[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance),
"Date d'entree": cursus["entree"],
"Date de diplome": diplome,
"Nbre de semestres": len(formsemestres),
}
# Ajout des noms de semestres parcourus
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
# Construction du dataframe
df = pd.DataFrame.from_dict(administratif, orient="index")
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def df_tag(self, tag): def df_tag(self, tag):
"""Génère le DataFrame synthétisant les moyennes/classements (groupe, """Génère le DataFrame synthétisant les moyennes/classements (groupe,
interclassement promo) pour tous les aggrégats prévus, interclassement promo) pour tous les aggrégats prévus,
@ -368,17 +353,6 @@ class JuryPE(object):
df.sort_values(by=["Nom", "Prenom"], inplace=True) df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df return df
def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury
"""Table(s) du jury
mode: singlesheet ou multiplesheet pour export excel
"""
sT = SeqGenTable() # le fichier excel à générer
if mode == "singlesheet":
return sT.get_genTable("singlesheet")
else:
return sT
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.