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
5 changed files with 109 additions and 236 deletions
Showing only changes of commit 02b057ca5a - Show all commits

View File

@ -84,12 +84,7 @@ class TableTag(object):
return sorted(tags) return sorted(tags)
def to_df( def to_df(
self, self, administratif=True, aggregat=None, tags_cibles=None, cohorte=None
administratif=True,
aggregat=None,
tags_cibles=None,
cohorte=None,
type_colonnes=True,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Renvoie un dataframe listant toutes les données """Renvoie un dataframe listant toutes les données
des moyennes/classements/nb_inscrits/min/max/moy des moyennes/classements/nb_inscrits/min/max/moy
@ -136,13 +131,6 @@ class TableTag(object):
for champ in CHAMPS_ADMINISTRATIFS[1:] for champ in CHAMPS_ADMINISTRATIFS[1:]
] # Nom + Prénom ] # Nom + Prénom
df = df.sort_values(by=colonnes_tries) df = df.sort_values(by=colonnes_tries)
# Conversion des colonnes en multiindex
if type_colonnes:
colonnes = list(df.columns)
colonnes = [tuple(col.split("|")) for col in colonnes]
df.columns = pd.MultiIndex.from_tuples(colonnes)
return df return df
def has_etuds(self): def has_etuds(self):

View File

@ -41,5 +41,3 @@ def pe_get_log() -> str:
# 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"
NOM_STAT_PROMO = "statistiques de la promo"

View File

@ -52,7 +52,7 @@ import pandas as pd
from app.pe.rcss import pe_rcs from app.pe.rcss import pe_rcs
from app.pe.moys import pe_sxtag from app.pe.moys import pe_sxtag
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
import app.pe.pe_affichage as pe_affichage import app.pe.pe_affichage as pe_affichage
import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_etudiant as pe_etudiant
from app.pe.moys import ( from app.pe.moys import (
@ -116,7 +116,7 @@ class JuryPE(object):
self._gen_xls_rcstags(zipfile) self._gen_xls_rcstags(zipfile)
self._gen_xls_interclasstags(zipfile) self._gen_xls_interclasstags(zipfile)
self._gen_xls_synthese_jury_par_tag(zipfile) self._gen_xls_synthese_jury_par_tag(zipfile)
# self._gen_xls_synthese_par_etudiant(zipfile) self._gen_html_synthese_par_etudiant(zipfile)
except Exception as e: except Exception as e:
raise e raise e
# et le log # et le log
@ -175,6 +175,8 @@ class JuryPE(object):
onglet = res_sem_tag.get_repr(verbose=True) onglet = res_sem_tag.get_repr(verbose=True)
onglets += [] onglets += []
df = res_sem_tag.to_df() df = res_sem_tag.to_df()
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
# écriture dans l'onglet # écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True) df.to_excel(writer, onglet, index=True, header=True)
pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}")
@ -235,6 +237,8 @@ class JuryPE(object):
onglet = sxtag.get_repr(verbose=False) onglet = sxtag.get_repr(verbose=False)
if sxtag.is_significatif(): if sxtag.is_significatif():
df = sxtag.to_df() df = sxtag.to_df()
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
onglets += [onglet] onglets += [onglet]
# écriture dans l'onglet # écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True) df.to_excel(writer, onglet, index=True, header=True)
@ -295,6 +299,8 @@ class JuryPE(object):
onglet = rcs_tag.get_repr(verbose=False) onglet = rcs_tag.get_repr(verbose=False)
if rcs_tag.is_significatif(): if rcs_tag.is_significatif():
df = rcs_tag.to_df() df = rcs_tag.to_df()
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
onglets += [onglet] onglets += [onglet]
# écriture dans l'onglet # écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True) df.to_excel(writer, onglet, index=True, header=True)
@ -370,6 +376,8 @@ class JuryPE(object):
onglet = interclass.get_repr() onglet = interclass.get_repr()
if interclass.is_significatif(): if interclass.is_significatif():
df = interclass.to_df(cohorte="Promo") df = interclass.to_df(cohorte="Promo")
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
onglets += [onglet] onglets += [onglet]
# écriture dans l'onglet # écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True) df.to_excel(writer, onglet, index=True, header=True)
@ -387,8 +395,20 @@ class JuryPE(object):
def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile): 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 tag par tag"""
# Synthèse des éléments du jury PE pe_affichage.pe_print(
self.synthese = self.synthetise_jury_par_tags() "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***"
)
self.synthese = {}
pe_affichage.pe_print(" -> Synthèse des données administratives")
self.synthese["administratif"] = self.etudiants.df_administratif(
self.diplomes_ids
)
tags = self._do_tags_list(self.interclasstags)
for tag in tags:
for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]:
self.synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy)
# 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 par tags") pe_affichage.pe_print("*** Export du jury de synthese par tags")
@ -397,36 +417,15 @@ class JuryPE(object):
output, engine="openpyxl" output, engine="openpyxl"
) as writer: ) as writer:
onglets = [] onglets = []
for (tag, type_moy), df in self.synthese.items(): for onglet, df in self.synthese.items():
onglet = f"{tag} {type_moy}" # Conversion colonnes en multiindex
onglets += [onglet] df = convert_colonnes_to_multiindex(df)
# écriture dans l'onglet: # Nom de l'onglet
df.to_excel(writer, onglet, index=True, header=True)
pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}")
output.seek(0)
self.add_file_to_zip(
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:
onglets = []
for onglet, df in synthese.items():
if isinstance(onglet, tuple): if isinstance(onglet, tuple):
if onglet[1] == pe_moytag.CODE_MOY_COMPETENCES: if onglet[1] == pe_moytag.CODE_MOY_COMPETENCES:
nom_onglet = onglet[0][: 31 - 5] + "/Comp." nom_onglet = onglet[0][: 31 - 7] + " (Comp)"
else: else:
nom_onglet = onglet[0][: 31 - 3] + "/UE" nom_onglet = onglet[0][: 31 - 5] + " (UE)"
else: else:
nom_onglet = onglet nom_onglet = onglet
onglets += [nom_onglet] onglets += [nom_onglet]
@ -436,7 +435,19 @@ 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}_par_etudiant.xlsx", output.read() zipfile, f"synthese_jury_{self.diplome}_par_tag.xlsx", output.read()
)
def _gen_html_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
pe_affichage.pe_print("*** Synthèse finale étudiant par étudiant***")
etudids = list(self.diplomes_ids)
for etudid in etudids:
nom, prenom, html = self.synthetise_jury_etudiant(etudid)
self.add_file_to_zip(
zipfile, f"{nom}_{prenom}.html", html, path="etudiants"
) )
def _add_log_to_zip(self, zipfile): def _add_log_to_zip(self, zipfile):
@ -482,26 +493,7 @@ class JuryPE(object):
# Méthodes pour la synthèse du juryPE # Méthodes pour la synthèse du juryPE
# ***************************************************************************************************************** # *****************************************************************************************************************
def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]: def df_tag_type(self, tag, type_moy):
"""Synthétise tous les résultats du jury PE dans des dataframes,
dont les onglets sont les tags et des types de calcul de moyennes
(par UEs ou par compétences)"""
pe_affichage.pe_print(
"*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***"
)
synthese = {}
pe_affichage.pe_print(" -> Synthèse des données administratives")
synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids)
tags = self._do_tags_list(self.interclasstags)
for tag in tags:
for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]:
synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy)
return synthese
def df_tag_type(self, tag, type_moy, type_colonnes=True):
"""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, en fonction interclassement promo) pour tous les aggrégats prévus, en fonction
du type (UEs ou Compétences) de données souhaitées, du type (UEs ou Compétences) de données souhaitées,
@ -549,7 +541,6 @@ class JuryPE(object):
aggregat=aggregat, aggregat=aggregat,
tags_cibles=[tag], tags_cibles=[tag],
cohorte="Promo", cohorte="Promo",
type_colonnes=False,
) )
if not df_promo.empty: if not df_promo.empty:
aff_aggregat += [aggregat] aff_aggregat += [aggregat]
@ -562,81 +553,31 @@ class JuryPE(object):
) )
else: else:
pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy} : <vide>") pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy} : <vide>")
# Conversion des colonnes en multiindex
if type_colonnes:
colonnes = list(df.columns)
colonnes = [tuple(col.split("|")) for col in colonnes]
df.columns = pd.MultiIndex.from_tuples(colonnes)
return df return df
# Fin de l'aggrégat # Fin de l'aggrégat
def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]: def synthetise_jury_etudiant(self, etudid) -> (str, str, str):
"""Synthétise tous les résultats du jury PE dans des dataframes, """Synthétise les résultats d'un étudiant dans un
dont les onglets sont les étudiants""" fichier html à son nom en s'appuyant sur la synthese final
pe_affichage.pe_print("*** Synthèse finale des moyennes par étudiants***")
synthese = {} Returns:
pe_affichage.pe_print(" -> Synthèse des données administratives") Un tuple nom, prenom, html
synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) """
etudids = list(self.diplomes_ids)
for etudid in etudids:
etudiant = self.etudiants.identites[etudid] etudiant = self.etudiants.identites[etudid]
nom = etudiant.nom nom = etudiant.nom
prenom = etudiant.prenom[0] # initial du prénom prenom = etudiant.prenom # initial du prénom
html = "toto"
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.interclasstags) tags = self._do_tags_list(self.interclasstags)
# for onglet, df_synthese in self.synthese.items():
# if isinstance(onglet, tuple): # Les onglets autres que "administratif"
# tag = onglet[0]
# type_moy = onglet[1]
donnees = {} # colonnes = list(df_synthese.columns)
for tag in tags: return (nom, prenom, html)
# Une ligne pour le tag
donnees[tag] = {("", "", "tag"): tag}
for aggregat in pe_rcs.TOUS_LES_RCS:
# 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.rcss_jury.trajectoires_suivies[etudid][aggregat]
if trajectoire:
trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id]
if tag in trajectoire_tagguee.moyennes_tags:
# L'interclassement
interclass = self.interclasstags[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 get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict:
@ -660,102 +601,10 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict:
return semestres return semestres
def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: def convert_colonnes_to_multiindex(df):
"""Renvoie le dictionnaire de synthèse (à intégrer dans """Convertit les colonnes d'un df pour obtenir des colonnes
un tableur excel) pour décrire les résultats d'un aggrégat multiindex"""
colonnes = list(df.columns)
Args: colonnes = [tuple(col.split("|")) for col in colonnes]
nom_rcs : Le nom du RCS visé df.columns = pd.MultiIndex.from_tuples(colonnes)
diplôme : l'année du diplôme return df
"""
# L'affichage de l'aggrégat dans le tableur excel
descr = pe_rcs.get_descr_rcs(nom_rcs)
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: pe_rcstag.RCSTag,
interclassement_taggue: pe_interclasstag.InterClassTag,
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_rcs.get_descr_rcs(aggregat)
# La note de l'étudiant (chargement à venir)
note = np.nan
# Les données de la trajectoire tagguée pour le tag considéré
moy_tag = trajectoire_tagguee.moyennes_tags[tag]
# Les données de l'étudiant
note = moy_tag.get_note_for_df(etudid)
classement = moy_tag.get_class_for_df(etudid)
nmin = moy_tag.get_min_for_df()
nmax = moy_tag.get_max_for_df()
nmoy = moy_tag.get_moy_for_df()
# Statistiques sur le groupe
if not pd.isna(note) and note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
(descr, "", "note"): note,
(descr, NOM_STAT_GROUPE, "class."): classement,
(descr, NOM_STAT_GROUPE, "min"): nmin,
(descr, NOM_STAT_GROUPE, "moy"): nmoy,
(descr, NOM_STAT_GROUPE, "max"): nmax,
}
# L'interclassement
moy_tag = interclassement_taggue.moyennes_tags[tag]
classement = moy_tag.get_class_for_df(etudid)
nmin = moy_tag.get_min_for_df()
nmax = moy_tag.get_max_for_df()
nmoy = moy_tag.get_moy_for_df()
if not pd.isna(note) and note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees |= {
(descr, nom_stat_promo, "class."): classement,
(descr, nom_stat_promo, "min"): nmin,
(descr, nom_stat_promo, "moy"): nmoy,
(descr, nom_stat_promo, "max"): nmax,
}
return donnees

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Service d'un prof{% endblock %}</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<style>
.flash { padding: 8px; color: white; border-radius: 15px; font-weight: bold; }
.success { background-color: limegreen; }
.error { background-color: lightcoral; }
</style>
</head>
<body>
<h1>Résultats PE de {{prenom}} {{nom}}</h1>
<h2>Résultats calculés par UEs</h2>
{% for tag in tags %}
<h3>{{ tag }}</h3>
{% endfor %}
<h2>Résultats calculés par Compétences</h2>
{% for tag in tags %}
<h3>{{ tag }}</h3>
{% endfor %}
</body>
</html>

View File

@ -30,7 +30,7 @@
<p> <p>
Cette fonction génère un ensemble de feuilles de calcul (xlsx) Cette fonction génère un ensemble de feuilles de calcul (xlsx)
permettant d'éditer des avis de poursuites d'études pour les étudiants permettant d'éditer des avis de poursuites d'études pour les étudiants
de BUT diplômés. de BUT diplômés. Les calculs sous-jacents peuvent prendre un peu de temps (1 à 2 minutes).
<br> <br>
De nombreux aspects sont paramétrables: De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes" <a href="https://scodoc.org/AvisPoursuiteEtudes"
@ -43,7 +43,7 @@
<h3>Avis de poursuites d'études de la promo {{ annee_diplome }}</h3> <h3>Avis de poursuites d'études de la promo {{ annee_diplome }}</h3>
<div class="help"> <div class="help">
Seront pris en compte les étudiants ayant été inscrits à l'un des semestres suivants : Seront pris en compte les étudiants ayant (au moins) été inscrits à l'un des semestres suivants :
<ul> <ul>
{% for rang in rangs_tries %} {% for rang in rangs_tries %}