Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
4 changed files with 308 additions and 120 deletions
Showing only changes of commit 78eeb9c67f - Show all commits

View File

@ -1,5 +1,3 @@
from app.models import Formation, FormSemestre
from app.scodoc import codes_cursus
from app import log from app import log
PE_DEBUG = 0 PE_DEBUG = 0
@ -9,73 +7,13 @@ if not PE_DEBUG:
def pe_print(*a, **kw): def pe_print(*a, **kw):
# kw is ignored. log always add a newline # kw is ignored. log always add a newline
log(" ".join(a)) log(" ".join(a))
else: else:
pe_print = print # print function pe_print = print # print function
# Affichage dans le tableur pe en cas d'absence de notes # Affichage dans le tableur pe en cas d'absence de notes
SANS_NOTE = "-" SANS_NOTE = "-"
NOM_STAT_GROUPE = "statistiques du groupe"
def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: NOM_STAT_PROMO = "statistiques de la promo"
"""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)
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

@ -37,8 +37,9 @@ Created on 17/01/2024
""" """
import pandas as pd 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.pe import pe_comp, pe_affichage
from app.scodoc import codes_cursus
class EtudiantsJuryPE: class EtudiantsJuryPE:
@ -467,7 +468,7 @@ class EtudiantsJuryPE:
} }
# Ajout des noms de semestres parcourus # 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 administratif[etudid] |= etapes
# Construction du dataframe # Construction du dataframe
@ -647,3 +648,71 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSeme
dernier_semestre = semestres[fid] dernier_semestre = semestres[fid]
return dernier_semestre return dernier_semestre
return None 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

@ -48,6 +48,9 @@ from zipfile import ZipFile
import numpy as np 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.pe.pe_tabletags import TableTag
from app.scodoc.gen_tables import SeqGenTable from app.scodoc.gen_tables import SeqGenTable
from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_etudiant import EtudiantsJuryPE
@ -119,7 +122,9 @@ class JuryPE(object):
self._gen_xls_semestre_taggues(zipfile) self._gen_xls_semestre_taggues(zipfile)
self._gen_xls_trajectoires(zipfile) self._gen_xls_trajectoires(zipfile)
self._gen_xls_aggregats(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 :) # Fin !!!! Tada :)
@ -237,13 +242,13 @@ class JuryPE(object):
path="details", path="details",
) )
def _gen_xls_synthese(self, zipfile: ZipFile): def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile):
"""Synthèse des éléments du jury PE""" """Synthèse des éléments du jury PE tag par tag"""
# Synthèse des éléments du jury PE # 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é # 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() output = io.BytesIO()
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
output, engine="openpyxl" output, engine="openpyxl"
@ -254,7 +259,27 @@ class JuryPE(object):
output.seek(0) output.seek(0)
self.add_file_to_zip( 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=""): def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
@ -292,10 +317,11 @@ class JuryPE(object):
# Méthodes pour la synthèse du juryPE # Méthodes pour la synthèse du juryPE
# ***************************************************************************************************************** # *****************************************************************************************************************
def synthetise_juryPE(self): def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]:
"""Synthétise tous les résultats du jury PE dans des dataframes""" """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 = {} synthese = {}
pe_affichage.pe_print(" -> Synthèse des données administratives") pe_affichage.pe_print(" -> Synthèse des données administratives")
@ -332,65 +358,102 @@ class JuryPE(object):
} }
for aggregat in aggregats: 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 # La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat] trajectoire = self.trajectoires.suivi[etudid][aggregat]
# La note de l'étudiant (chargement à venir)
note = np.nan
# L'affichage de l'aggrégat dans le tableur excel
descr = pe_comp.PARCOURS[aggregat]["descr"]
# Les moyennes par tag de cette trajectoire
donnees[etudid] |= {
(descr, "", "note"): pe_affichage.SANS_NOTE,
(descr, "statistique du groupe", "class."): pe_affichage.SANS_NOTE,
(descr, "statistique du groupe", "min"): pe_affichage.SANS_NOTE,
(descr, "statistique du groupe", "moy"): pe_affichage.SANS_NOTE,
(descr, "statistique du groupe", "max"): pe_affichage.SANS_NOTE,
}
if trajectoire: if trajectoire:
trajectoire_tagguee = self.trajectoires_tagguees[ trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id trajectoire.trajectoire_id
] ]
if tag in trajectoire_tagguee.moyennes_tags: else:
bilan = trajectoire_tagguee.moyennes_tags[tag] trajectoire_tagguee = None
note = TableTag.get_note_for_df(bilan, etudid)
if note != np.nan:
donnees[etudid] |= {
(descr, "", "note"): note,
(descr, "statistique du groupe", "class."): TableTag.get_class_for_df(bilan, etudid),
(descr, "statistique du groupe", "min"): TableTag.get_min_for_df(bilan),
(descr, "statistique du groupe", "moy"): TableTag.get_moy_for_df(bilan),
(descr, "statistique du groupe", "max"): TableTag.get_max_for_df(bilan)
}
"""L'interclassement""" # L'interclassement
interclass = self.interclassements_taggues[aggregat] interclass = self.interclassements_taggues[aggregat]
donnees[etudid] |= {
(descr, f"statistique de la promotion {self.diplome}", "class."): pe_affichage.SANS_NOTE,
(descr, f"statistique de la promotion {self.diplome}", "min"): pe_affichage.SANS_NOTE,
(descr, f"statistique de la promotion {self.diplome}", "moy"): pe_affichage.SANS_NOTE,
(descr, f"statistique de la promotion {self.diplome}", "max"): pe_affichage.SANS_NOTE,
}
if tag in interclass.moyennes_tags:
bilan = interclass.moyennes_tags[tag]
if note != np.nan: # Injection des données dans un dictionnaire
donnees[etudid] |= { donnees[etudid] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
(descr, f"statistique de la promotion {self.diplome}", "class."): TableTag.get_class_for_df(bilan, etudid),
(descr, f"statistique de la promotion {self.diplome}", "min"): TableTag.get_min_for_df(bilan),
(descr, f"statistique de la promotion {self.diplome}", "moy"): TableTag.get_moy_for_df(bilan),
(descr, f"statistique de la promotion {self.diplome}", "max"): TableTag.get_max_for_df(bilan)
}
# Fin de l'aggrégat # Fin de l'aggrégat
# Construction du dataFrame # Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index") df = pd.DataFrame.from_dict(donnees, orient="index")
# Tri par nom/prénom # Tri par nom/prénom
df.sort_values(by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True) df.sort_values(
by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True
)
return df 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: 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.
@ -485,3 +548,121 @@ def compute_interclassements(
aggregats_interclasses_taggues[nom_aggregat] = interclass aggregats_interclasses_taggues[nom_aggregat] = interclass
return aggregats_interclasses_taggues 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 @author: barasc
""" """
import app.pe.pe_etudiant
from app import db, log from app import db, log
from app.comp import res_sem, moy_ue, moy_sem from app.comp import res_sem, moy_ue, moy_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
@ -133,7 +133,7 @@ class SemestreTag(TableTag):
def get_repr(self): def get_repr(self):
"""Nom affiché pour le semestre taggué""" """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: def compute_moyenne_tag(self, tag: str) -> list:
"""Calcule la moyenne des étudiants pour le tag indiqué, """Calcule la moyenne des étudiants pour le tag indiqué,