This commit is contained in:
Emmanuel Viennet 2024-02-02 16:00:09 +01:00
commit af659d5f09
6 changed files with 363 additions and 149 deletions

View File

@ -1,5 +1,3 @@
from app.models import Formation, FormSemestre
from app.scodoc import codes_cursus
from app import log
PE_DEBUG = 0
@ -9,71 +7,13 @@ if not PE_DEBUG:
def pe_print(*a, **kw):
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
"""Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité
d'un étudiant.
Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec :
* 2 le numéro du semestre,
* FI la modalité,
* 2014-2015 les dates
Args:
semestre: Un ``FormSemestre``
avec_fid: Ajoute le n° du semestre à la description
Returns:
La chaine de caractères décrivant succintement le semestre
"""
formation: Formation = semestre.formation
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
description = [
parcours.SESSION_NAME.capitalize(),
str(semestre.semestre_id),
semestre.modalite, # eg FI ou FC
f"{semestre.date_debut.year}-{semestre.date_fin.year}",
]
if avec_fid:
description.append(f"({semestre.formsemestre_id})")
return " ".join(description)
# Affichage dans le tableur pe en cas d'absence de notes
SANS_NOTE = "-"
NOM_STAT_GROUPE = "statistiques du groupe"
NOM_STAT_PROMO = "statistiques de la promo"
def etapes_du_cursus(semestres: dict[int, FormSemestre], nbre_etapes_max: int) -> list[str]:
"""Partant d'un dictionnaire de semestres (qui retrace
la scolarité d'un étudiant), liste les noms des
semestres (en version abbrégée)
qu'un étudiant a suivi au cours de sa scolarité à l'IUT.
Les noms des semestres sont renvoyés dans un dictionnaire
``{"etape i": nom_semestre_a_etape_i}``
avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i,
le nom affiché est vide.
La fonction suppose la liste des semestres triées par ordre
décroissant de date.
Args:
semestres: une liste de ``FormSemestre``
nbre_etapes_max: le nombre d'étapes max prise en compte
Returns:
Une liste de nom de semestre (dans le même ordre que les ``semestres``)
See also:
app.pe.pe_affichage.nom_semestre_etape
"""
assert len(semestres) <= nbre_etapes_max
noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()]
noms = noms[::-1] # trie par ordre croissant
dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)}
for (i, nom) in enumerate(noms): # Charge les noms de semestres
dico[f"Etape {i+1}"] = nom
return dico

View File

@ -75,81 +75,55 @@ TODO:: A améliorer si BUT en moins de 6 semestres
PARCOURS = {
"S1": {
"aggregat": ["S1"],
"ordre": 1,
"affichage_court": "S1",
"affichage_long": "Semestre 1",
"descr": "Semestre 1 (S1)",
},
"S2": {
"aggregat": ["S2"],
"ordre": 2,
"affichage_court": "S2",
"affichage_long": "Semestre 2",
"descr": "Semestre 2 (S2)",
},
"1A": {
"aggregat": ["S1", "S2"],
"ordre": 3,
"affichage_court": "1A",
"affichage_long": "1ère année",
"descr": "BUT1 (S1+S2)",
},
"S3": {
"aggregat": ["S3"],
"ordre": 4,
"affichage_court": "S3",
"affichage_long": "Semestre 3",
"descr": "Semestre 3 (S3)",
},
"S4": {
"aggregat": ["S4"],
"ordre": 5,
"affichage_court": "S4",
"affichage_long": "Semestre 4",
"descr": "Semestre 4 (S4)",
},
"2A": {
"aggregat": ["S3", "S4"],
"ordre": 6,
"affichage_court": "2A",
"affichage_long": "2ème année",
"descr": "BUT2 (S3+S4)",
},
"3S": {
"aggregat": ["S1", "S2", "S3"],
"ordre": 7,
"affichage_court": "S1+S2+S3",
"affichage_long": "BUT du semestre 1 au semestre 3",
"descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)",
},
"4S": {
"aggregat": ["S1", "S2", "S3", "S4"],
"ordre": 8,
"affichage_court": "BUT",
"affichage_long": "BUT du semestre 1 au semestre 4",
"descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)",
},
"S5": {
"aggregat": ["S5"],
"ordre": 9,
"affichage_court": "S5",
"affichage_long": "Semestre 5",
"descr": "Semestre 5 (S5)",
},
"S6": {
"aggregat": ["S6"],
"ordre": 10,
"affichage_court": "S6",
"affichage_long": "Semestre 6",
"descr": "Semestre 6 (S6)",
},
"3A": {
"aggregat": ["S5", "S6"],
"ordre": 11,
"affichage_court": "3A",
"affichage_long": "3ème année",
"descr": "3ème année (S5+S6)",
},
"5S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
"ordre": 12,
"affichage_court": "S1+S2+S3+S4+S5",
"affichage_long": "BUT du semestre 1 au semestre 5",
"descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)",
},
"6S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
"ordre": 13,
"affichage_court": "BUT",
"affichage_long": "BUT (tout semestre inclus)",
"descr": "Moyenne globale BUT (S1+S2+S3+S4+S5+S6)",
},
}
NBRE_SEMESTRES_DIPLOMANT = 6

View File

@ -37,8 +37,9 @@ Created on 17/01/2024
"""
import pandas as pd
from app.models import FormSemestre, Identite
from app.models import FormSemestre, Identite, Formation
from app.pe import pe_comp, pe_affichage
from app.scodoc import codes_cursus
class EtudiantsJuryPE:
@ -467,7 +468,7 @@ class EtudiantsJuryPE:
}
# Ajout des noms de semestres parcourus
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
etapes = etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
# Construction du dataframe
@ -647,3 +648,71 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSeme
dernier_semestre = semestres[fid]
return dernier_semestre
return None
def etapes_du_cursus(
semestres: dict[int, FormSemestre], nbre_etapes_max: int
) -> list[str]:
"""Partant d'un dictionnaire de semestres (qui retrace
la scolarité d'un étudiant), liste les noms des
semestres (en version abbrégée)
qu'un étudiant a suivi au cours de sa scolarité à l'IUT.
Les noms des semestres sont renvoyés dans un dictionnaire
``{"etape i": nom_semestre_a_etape_i}``
avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i,
le nom affiché est vide.
La fonction suppose la liste des semestres triées par ordre
décroissant de date.
Args:
semestres: une liste de ``FormSemestre``
nbre_etapes_max: le nombre d'étapes max prise en compte
Returns:
Une liste de nom de semestre (dans le même ordre que les ``semestres``)
See also:
app.pe.pe_affichage.nom_semestre_etape
"""
assert len(semestres) <= nbre_etapes_max
noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()]
noms = noms[::-1] # trie par ordre croissant
dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)}
for i, nom in enumerate(noms): # Charge les noms de semestres
dico[f"Etape {i+1}"] = nom
return dico
def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
"""Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité
d'un étudiant.
Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec :
* 2 le numéro du semestre,
* FI la modalité,
* 2014-2015 les dates
Args:
semestre: Un ``FormSemestre``
avec_fid: Ajoute le n° du semestre à la description
Returns:
La chaine de caractères décrivant succintement le semestre
"""
formation: Formation = semestre.formation
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
description = [
parcours.SESSION_NAME.capitalize(),
str(semestre.semestre_id),
semestre.modalite, # eg FI ou FC
f"{semestre.date_debut.year}-{semestre.date_fin.year}",
]
if avec_fid:
description.append(f"({semestre.formsemestre_id})")
return " ".join(description)

View File

@ -46,6 +46,12 @@ import io
import os
from zipfile import ZipFile
import numpy as np
from app.pe import pe_comp
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
from app.pe.pe_tabletags import TableTag
from app.scodoc.gen_tables import SeqGenTable
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire
@ -116,7 +122,9 @@ class JuryPE(object):
self._gen_xls_semestre_taggues(zipfile)
self._gen_xls_trajectoires(zipfile)
self._gen_xls_aggregats(zipfile)
self._gen_xls_synthese(zipfile)
self._gen_xls_synthese_jury_par_tag(zipfile)
self._gen_xls_synthese_par_etudiant(zipfile)
# Fin !!!! Tada :)
@ -234,13 +242,13 @@ class JuryPE(object):
path="details",
)
def _gen_xls_synthese(self, zipfile: ZipFile):
"""Synthèse des éléments du jury PE"""
def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile):
"""Synthèse des éléments du jury PE tag par tag"""
# Synthèse des éléments du jury PE
self.synthese = self.synthetise_juryPE()
self.synthese = self.synthetise_jury_par_tags()
# Export des données => mode 1 seule feuille -> supprimé
pe_affichage.pe_print("*** Export du jury de synthese")
pe_affichage.pe_print("*** Export du jury de synthese par tags")
output = io.BytesIO()
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
output, engine="openpyxl"
@ -251,7 +259,27 @@ class JuryPE(object):
output.seek(0)
self.add_file_to_zip(
zipfile, f"synthese_jury_{self.diplome}.xlsx", output.read()
zipfile, f"synthese_jury_{self.diplome}_par_tag.xlsx", output.read()
)
def _gen_xls_synthese_par_etudiant(self, zipfile: ZipFile):
"""Synthèse des éléments du jury PE, étudiant par étudiant"""
# Synthèse des éléments du jury PE
synthese = self.synthetise_jury_par_etudiants()
# Export des données => mode 1 seule feuille -> supprimé
pe_affichage.pe_print("*** Export du jury de synthese par étudiants")
output = io.BytesIO()
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
output, engine="openpyxl"
) as writer:
for onglet, df in synthese.items():
# écriture dans l'onglet:
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read()
)
def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
@ -289,10 +317,11 @@ class JuryPE(object):
# Méthodes pour la synthèse du juryPE
# *****************************************************************************************************************
def synthetise_juryPE(self):
"""Synthétise tous les résultats du jury PE dans des dataframes"""
def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]:
"""Synthétise tous les résultats du jury PE dans des dataframes,
dont les onglets sont les tags"""
pe_affichage.pe_print("*** Synthèse finale des moyennes ***")
pe_affichage.pe_print("*** Synthèse finale des moyennes par tag***")
synthese = {}
pe_affichage.pe_print(" -> Synthèse des données administratives")
@ -323,56 +352,108 @@ class JuryPE(object):
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
donnees[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
("Identité", "", "Civilite"): etudiant.civilite_str,
("Identité", "", "Nom"): etudiant.nom,
("Identité", "", "Prenom"): etudiant.prenom,
}
for aggregat in aggregats:
# Le dictionnaire par défaut des moyennes
donnees[etudid] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome)
# La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat]
# Les moyennes par tag de cette trajectoire
donnees[etudid] |= {
f"{aggregat} notes ": "-",
f"{aggregat} class. (groupe)": "-",
f"{aggregat} min | moy | max (groupe)": "-",
}
if trajectoire:
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
if tag in trajectoire_tagguee.moyennes_tags:
bilan = trajectoire_tagguee.moyennes_tags[tag]
else:
trajectoire_tagguee = None
donnees[etudid] |= {
f"{aggregat} notes ": round(bilan["notes"].loc[etudid], 2),
f"{aggregat} class. (groupe)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min | moy | max (groupe)": f"{bilan['min']:.1f} | {bilan['moy']:.1f} | {bilan['max']:.1f}",
}
"""L'interclassement"""
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
donnees[etudid] |= {
f"{aggregat} class. (promo)": "-",
f"{aggregat} min | moy | max (promo)": "-",
}
if tag in interclass.moyennes_tags:
bilan = interclass.moyennes_tags[tag]
donnees[etudid] |= {
f"{aggregat} class. (promo)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min | moy | max (promo)": f"{bilan['min']:.1f} | {bilan['moy']:.1f} | {bilan['max']:.1f}",
}
# Injection des données dans un dictionnaire
donnees[etudid] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
# Fin de l'aggrégat
# Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index")
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
df.sort_values(
by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True
)
return df
def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]:
"""Synthétise tous les résultats du jury PE dans des dataframes,
dont les onglets sont les étudiants"""
pe_affichage.pe_print("*** Synthèse finale des moyennes par étudiants***")
synthese = {}
pe_affichage.pe_print(" -> Synthèse des données administratives")
synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids)
etudids = list(self.diplomes_ids)
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
nom = etudiant.nom
prenom = etudiant.prenom[0] # initial du prénom
onglet = f"{nom} {prenom}. ({etudid})"
if len(onglet) > 32: # limite sur la taille des onglets
fin_onglet = f"{prenom}. ({etudid})"
onglet = f"{nom[:32-len(fin_onglet)-2]}." + fin_onglet
pe_affichage.pe_print(f" -> Synthèse de l'étudiant {etudid}")
synthese[onglet] = self.df_synthese_etudiant(etudid)
return synthese
def df_synthese_etudiant(self, etudid: int) -> pd.DataFrame:
"""Créé un DataFrame pour un étudiant donné par son etudid, retraçant
toutes ses moyennes aux différents tag et aggrégats"""
tags = self.do_tags_list(self.interclassements_taggues)
aggregats = pe_comp.TOUS_LES_PARCOURS
donnees = {}
for tag in tags:
# Une ligne pour le tag
donnees[tag] = {("", "", "tag"): tag}
for aggregat in aggregats:
# Le dictionnaire par défaut des moyennes
donnees[tag] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome)
# La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat]
if trajectoire:
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
else:
trajectoire_tagguee = None
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# Injection des données dans un dictionnaire
donnees[tag] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
# Fin de l'aggrégat
# Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index")
# Tri par nom/prénom
df.sort_values(
by=[("", "", "tag")], inplace=True
)
return df
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
@ -466,3 +547,122 @@ def compute_interclassements(
)
aggregats_interclasses_taggues[nom_aggregat] = interclass
return aggregats_interclasses_taggues
def get_defaut_dict_synthese_aggregat(aggregat: str, diplome: int) -> dict:
"""Renvoie le dictionnaire de synthèse (à intégrer dans
un tableur excel) pour décrire les résultats d'un aggrégat"""
# L'affichage de l'aggrégat dans le tableur excel
descr = pe_comp.PARCOURS[aggregat]["descr"]
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees = {
(descr, "", "note"): SANS_NOTE,
# Les stat du groupe
(descr, NOM_STAT_GROUPE, "class."): SANS_NOTE,
(descr, NOM_STAT_GROUPE, "min"): SANS_NOTE,
(descr, NOM_STAT_GROUPE, "moy"): SANS_NOTE,
(descr, NOM_STAT_GROUPE, "max"): SANS_NOTE,
# Les stats de l'interclassement dans la promo
(descr, nom_stat_promo, "class."): SANS_NOTE,
(
descr,
nom_stat_promo,
"min",
): SANS_NOTE,
(
descr,
nom_stat_promo,
"moy",
): SANS_NOTE,
(
descr,
nom_stat_promo,
"max",
): SANS_NOTE,
}
return donnees
def get_dict_synthese_aggregat(
aggregat: str,
trajectoire_tagguee: TrajectoireTag,
interclassement_taggue: AggregatInterclasseTag,
etudid: int,
tag: str,
diplome: int
):
"""Renvoie le dictionnaire (à intégrer au tableur excel de synthese)
traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée
à l'aggrégat donné et pour un tag donné"""
donnees = {}
# L'affichage de l'aggrégat dans le tableur excel
descr = pe_comp.PARCOURS[aggregat]["descr"]
# La note de l'étudiant (chargement à venir)
note = np.nan
# Les données de la trajectoire tagguée pour le tag considéré
if trajectoire_tagguee and tag in trajectoire_tagguee.moyennes_tags:
bilan = trajectoire_tagguee.moyennes_tags[tag]
# La moyenne de l'étudiant
note = TableTag.get_note_for_df(bilan, etudid)
# Statistiques sur le groupe
if note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
(descr, "", "note"): note,
(
descr,
NOM_STAT_GROUPE,
"class.",
): TableTag.get_class_for_df(bilan, etudid),
(
descr,
NOM_STAT_GROUPE,
"min",
): TableTag.get_min_for_df(bilan),
(
descr,
NOM_STAT_GROUPE,
"moy",
): TableTag.get_moy_for_df(bilan),
(
descr,
NOM_STAT_GROUPE,
"max",
): TableTag.get_max_for_df(bilan),
}
# L'interclassement
if tag in interclassement_taggue.moyennes_tags:
bilan = interclassement_taggue.moyennes_tags[tag]
if note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees |= {
(
descr,
nom_stat_promo,
"class.",
): TableTag.get_class_for_df(bilan, etudid),
(
descr,
nom_stat_promo,
"min",
): TableTag.get_min_for_df(bilan),
(
descr,
nom_stat_promo,
"moy",
): TableTag.get_moy_for_df(bilan),
(
descr,
nom_stat_promo,
"max",
): TableTag.get_max_for_df(bilan),
}
return donnees

View File

@ -35,7 +35,7 @@ Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
import app.pe.pe_etudiant
from app import db, log
from app.comp import res_sem, moy_ue, moy_sem
from app.comp.res_compat import NotesTableCompat
@ -133,7 +133,7 @@ class SemestreTag(TableTag):
def get_repr(self):
"""Nom affiché pour le semestre taggué"""
return pe_affichage.nom_semestre_etape(self.formsemestre, avec_fid=True)
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moyenne_tag(self, tag: str) -> list:
"""Calcule la moyenne des étudiants pour le tag indiqué,

View File

@ -48,8 +48,6 @@ TAGS_RESERVES = ["but"]
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
@ -105,3 +103,36 @@ class TableTag(object):
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
return df.to_csv(sep=";")
@classmethod
def get_min_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
revoie le min renseigné pour affichage dans un df"""
return round(bilan["min"], 2)
@classmethod
def get_max_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le max renseigné pour affichage dans un df"""
return round(bilan["max"], 2)
@classmethod
def get_moy_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la moyenne renseignée pour affichage dans un df"""
return round(bilan["moy"], 2)
@classmethod
def get_class_for_df(cls, bilan: dict, etudid: int) -> str:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
return f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}"
@classmethod
def get_note_for_df(cls, bilan: dict, etudid: int):
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la note (moyenne)
pour un étudiant donné par son etudid"""
return round(bilan["notes"].loc[etudid], 2)