diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py index 8a093a6b3..4f5c1c31e 100644 --- a/app/pe/moys/pe_tabletags.py +++ b/app/pe/moys/pe_tabletags.py @@ -84,12 +84,7 @@ class TableTag(object): return sorted(tags) def to_df( - self, - administratif=True, - aggregat=None, - tags_cibles=None, - cohorte=None, - type_colonnes=True, + self, administratif=True, aggregat=None, tags_cibles=None, cohorte=None ) -> pd.DataFrame: """Renvoie un dataframe listant toutes les données des moyennes/classements/nb_inscrits/min/max/moy @@ -136,13 +131,6 @@ class TableTag(object): for champ in CHAMPS_ADMINISTRATIFS[1:] ] # Nom + Prénom 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 def has_etuds(self): diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py index f1ee72a7e..55d4f07ee 100644 --- a/app/pe/pe_affichage.py +++ b/app/pe/pe_affichage.py @@ -41,5 +41,3 @@ def pe_get_log() -> str: # 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" diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 81382ffac..b4802a24b 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -52,7 +52,7 @@ import pandas as pd from app.pe.rcss import pe_rcs 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_etudiant as pe_etudiant from app.pe.moys import ( @@ -116,7 +116,7 @@ class JuryPE(object): self._gen_xls_rcstags(zipfile) self._gen_xls_interclasstags(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: raise e # et le log @@ -175,6 +175,8 @@ class JuryPE(object): onglet = res_sem_tag.get_repr(verbose=True) onglets += [] df = res_sem_tag.to_df() + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") @@ -235,6 +237,8 @@ class JuryPE(object): onglet = sxtag.get_repr(verbose=False) if sxtag.is_significatif(): df = sxtag.to_df() + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -295,6 +299,8 @@ class JuryPE(object): onglet = rcs_tag.get_repr(verbose=False) if rcs_tag.is_significatif(): df = rcs_tag.to_df() + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -370,6 +376,8 @@ class JuryPE(object): onglet = interclass.get_repr() if interclass.is_significatif(): df = interclass.to_df(cohorte="Promo") + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) onglets += [onglet] # écriture dans l'onglet 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): """Synthèse des éléments du jury PE tag par tag""" - # Synthèse des éléments du jury PE - self.synthese = self.synthetise_jury_par_tags() + pe_affichage.pe_print( + "*** 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é pe_affichage.pe_print("*** Export du jury de synthese par tags") @@ -397,36 +417,15 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for (tag, type_moy), df in self.synthese.items(): - onglet = f"{tag} {type_moy}" - onglets += [onglet] - # écriture dans 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(): + for onglet, df in self.synthese.items(): + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) + # Nom de l'onglet if isinstance(onglet, tuple): if onglet[1] == pe_moytag.CODE_MOY_COMPETENCES: - nom_onglet = onglet[0][: 31 - 5] + "/Comp." + nom_onglet = onglet[0][: 31 - 7] + " (Comp)" else: - nom_onglet = onglet[0][: 31 - 3] + "/UE" + nom_onglet = onglet[0][: 31 - 5] + " (UE)" else: nom_onglet = onglet onglets += [nom_onglet] @@ -436,9 +435,21 @@ class JuryPE(object): output.seek(0) 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): """Add a text file with the log messages""" log_data = pe_affichage.pe_get_log() @@ -482,26 +493,7 @@ class JuryPE(object): # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** - 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 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): + def df_tag_type(self, tag, type_moy): """Génère le DataFrame synthétisant les moyennes/classements (groupe + interclassement promo) pour tous les aggrégats prévus, en fonction du type (UEs ou Compétences) de données souhaitées, @@ -549,7 +541,6 @@ class JuryPE(object): aggregat=aggregat, tags_cibles=[tag], cohorte="Promo", - type_colonnes=False, ) if not df_promo.empty: aff_aggregat += [aggregat] @@ -562,81 +553,31 @@ class JuryPE(object): ) else: pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy} : ") - # 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 # Fin de l'aggrégat - 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***") + def synthetise_jury_etudiant(self, etudid) -> (str, str, str): + """Synthétise les résultats d'un étudiant dans un + fichier html à son nom en s'appuyant sur la synthese final - synthese = {} - pe_affichage.pe_print(" -> Synthèse des données administratives") - synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) + Returns: + Un tuple nom, prenom, html + """ + etudiant = self.etudiants.identites[etudid] + nom = etudiant.nom + prenom = etudiant.prenom # initial du prénom + html = "toto" - 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.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: - # 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 + return (nom, prenom, html) def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: @@ -660,102 +601,10 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: return semestres -def get_defaut_dict_synthese_aggregat(nom_rcs: 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 - - Args: - nom_rcs : Le nom du RCS visé - diplôme : l'année du diplôme - """ - # 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 +def convert_colonnes_to_multiindex(df): + """Convertit les colonnes d'un df pour obtenir des colonnes + multiindex""" + colonnes = list(df.columns) + colonnes = [tuple(col.split("|")) for col in colonnes] + df.columns = pd.MultiIndex.from_tuples(colonnes) + return df diff --git a/app/templates/pe/pe_view_resultats_etudiant.j2 b/app/templates/pe/pe_view_resultats_etudiant.j2 new file mode 100644 index 000000000..4aa297e02 --- /dev/null +++ b/app/templates/pe/pe_view_resultats_etudiant.j2 @@ -0,0 +1,38 @@ + + + + + + {% block title %}Service d'un prof{% endblock %} + + + + + + + +

Résultats PE de {{prenom}} {{nom}}

+ +

Résultats calculés par UEs

+ +{% for tag in tags %} + +

{{ tag }}

+ +{% endfor %} + +

Résultats calculés par Compétences

+ +{% for tag in tags %} + +

{{ tag }}

+ +{% endfor %} + + + + \ No newline at end of file diff --git a/app/templates/pe/pe_view_sem_recap.j2 b/app/templates/pe/pe_view_sem_recap.j2 index 250dc078e..4766daac5 100644 --- a/app/templates/pe/pe_view_sem_recap.j2 +++ b/app/templates/pe/pe_view_sem_recap.j2 @@ -30,7 +30,7 @@

Cette fonction génère un ensemble de feuilles de calcul (xlsx) 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).
De nombreux aspects sont paramétrables: Avis de poursuites d'études de la promo {{ annee_diplome }}

- 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 :
    {% for rang in rangs_tries %}