diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py new file mode 100644 index 000000000..484ae5daf --- /dev/null +++ b/app/pe/pe_affichage.py @@ -0,0 +1,68 @@ +from app.models import Formation, FormSemestre +from app.scodoc import codes_cursus + + +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.forsemestre_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 diff --git a/app/pe/pe_avislatex.py b/app/pe/pe_avislatex.py deleted file mode 100644 index 3c498f47d..000000000 --- a/app/pe/pe_avislatex.py +++ /dev/null @@ -1,517 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -import os -import codecs -import re -from app.pe import pe_tagtable -from app.pe import pe_jurype -from app.pe import pe_tools - -import app.scodoc.sco_utils as scu -import app.scodoc.notesdb as ndb -from app import log -from app.scodoc.gen_tables import GenTable, SeqGenTable -from app.scodoc import sco_preferences -from app.scodoc import sco_etud - - -DEBUG = False # Pour debug et repérage des prints à changer en Log - -DONNEE_MANQUANTE = ( - "" # Caractère de remplacement des données manquantes dans un avis PE -) - - -# ---------------------------------------------------------------------------------------- -def get_code_latex_from_modele(fichier): - """Lit le code latex à partir d'un modèle. Renvoie une chaine unicode. - - Le fichier doit contenir le chemin relatif - vers le modele : attention pas de vérification du format d'encodage - Le fichier doit donc etre enregistré avec le même codage que ScoDoc (utf-8) - """ - fid_latex = codecs.open(fichier, "r", encoding=scu.SCO_ENCODING) - un_avis_latex = fid_latex.read() - fid_latex.close() - return un_avis_latex - - -# ---------------------------------------------------------------------------------------- -def get_code_latex_from_scodoc_preference(formsemestre_id, champ="pe_avis_latex_tmpl"): - """ - Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX - et s'assure qu'il est renvoyé au format unicode - """ - template_latex = sco_preferences.get_preference(champ, formsemestre_id) - - return template_latex or "" - - -# ---------------------------------------------------------------------------------------- -def get_tags_latex(code_latex): - """Recherche tous les tags présents dans un code latex (ce code étant obtenu - à la lecture d'un modèle d'avis pe). - Ces tags sont répérés par les balises **, débutant et finissant le tag - et sont renvoyés sous la forme d'une liste. - - result: liste de chaines unicode - """ - if code_latex: - # changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})" - res = re.findall(r"([\*]{2}[^\t\n\r\f\v\*]+[\*]{2})", code_latex) - return [tag[2:-2] for tag in res] - else: - return [] - - -def comp_latex_parcourstimeline(etudiant, promo, taille=17): - """Interprète un tag dans un avis latex **parcourstimeline** - et génère le code latex permettant de retracer le parcours d'un étudiant - sous la forme d'une frise temporelle. - Nota: modeles/parcourstimeline.tex doit avoir été inclu dans le préambule - - result: chaine unicode (EV:) - """ - codelatexDebut = ( - """" - \\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d} - """ - % taille - ) - - modeleEvent = """ - \\parcoursevent{**nosem**}{**nomsem**}{**descr**} - """ - - codelatexFin = """ - \\end{parcourstimeline} - """ - reslatex = codelatexDebut - reslatex = reslatex.replace("**debut**", etudiant["entree"]) - reslatex = reslatex.replace("**fin**", str(etudiant["promo"])) - reslatex = reslatex.replace("**nbreSemestres**", str(etudiant["nbSemestres"])) - # Tri du parcours par ordre croissant : de la forme descr, nom sem date-date - parcours = etudiant["parcours"][::-1] # EV: XXX je ne comprend pas ce commentaire ? - - for no_sem in range(etudiant["nbSemestres"]): - descr = modeleEvent - nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"] - descr = descr.replace("**nosem**", str(no_sem + 1)) - if no_sem % 2 == 0: - descr = descr.replace("**nomsem**", nom_semestre_dans_parcours) - descr = descr.replace("**descr**", "") - else: - descr = descr.replace("**nomsem**", "") - descr = descr.replace("**descr**", nom_semestre_dans_parcours) - reslatex += descr - reslatex += codelatexFin - return reslatex - - -# ---------------------------------------------------------------------------------------- -def interprete_tag_latex(tag): - """Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible - le résultat sous la forme d'un quadruplet. - """ - infotag = tag.split(":") - if len(infotag) == 4: - return ( - infotag[0].upper(), - infotag[1].lower(), - infotag[2].lower(), - infotag[3].lower(), - ) - else: - return (None, None, None, None) - - -# ---------------------------------------------------------------------------------------- -def get_code_latex_avis_etudiant( - donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs -): - """ - Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses - donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un - fichier modele donné - - result: chaine unicode - """ - if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide - return annotationPE if annotationPE else "" - - # Le template latex (corps + footer) - code = un_avis_latex + "\n\n" + footer_latex - - # Recherche des tags dans le fichier - tags_latex = get_tags_latex(code) - if DEBUG: - log("Les tags" + str(tags_latex)) - - # Interprète et remplace chaque tags latex par les données numériques de l'étudiant (y compris les - # tags "macros" tels que parcourstimeline - for tag_latex in tags_latex: - # les tags numériques - valeur = DONNEE_MANQUANTE - - if ":" in tag_latex: - (aggregat, groupe, tag_scodoc, champ) = interprete_tag_latex(tag_latex) - valeur = str_from_syntheseJury( - donnees_etudiant, aggregat, groupe, tag_scodoc, champ - ) - - # La macro parcourstimeline - elif tag_latex == "parcourstimeline": - valeur = comp_latex_parcourstimeline( - donnees_etudiant, donnees_etudiant["promo"] - ) - - # Le tag annotationPE - elif tag_latex == "annotation": - valeur = annotationPE - - # Le tag bilanParTag - elif tag_latex == "bilanParTag": - valeur = get_bilanParTag(donnees_etudiant) - - # Les tags "simples": par ex. nom, prenom, civilite, ... - else: - if tag_latex in donnees_etudiant: - valeur = donnees_etudiant[tag_latex] - elif tag_latex in prefs: # les champs **NomResponsablePE**, ... - valeur = pe_tools.escape_for_latex(prefs[tag_latex]) - - # Vérification des pb d'encodage (debug) - # assert isinstance(tag_latex, unicode) - # assert isinstance(valeur, unicode) - - # Substitution - code = code.replace("**" + tag_latex + "**", valeur) - return code - - -# ---------------------------------------------------------------------------------------- -def get_annotation_PE(etudid, tag_annotation_pe): - """Renvoie l'annotation PE dans la liste de ces annotations ; - Cette annotation est reconnue par la présence d'un tag **PE** - (cf. .get_preferences -> pe_tag_annotation_avis_latex). - - Result: chaine unicode - """ - if tag_annotation_pe: - cnx = ndb.GetDBConnexion() - annotations = sco_etud.etud_annotations_list( - cnx, args={"etudid": etudid} - ) # Les annotations de l'étudiant - annotationsPE = [] - - exp = re.compile(r"^" + tag_annotation_pe) - - for a in annotations: - commentaire = scu.unescape_html(a["comment"]) - if exp.match(commentaire): # tag en début de commentaire ? - a["comment_u"] = commentaire # unicode, HTML non quoté - annotationsPE.append( - a - ) # sauvegarde l'annotation si elle contient le tag - - if annotationsPE: # Si des annotations existent, prend la plus récente - annotationPE = sorted(annotationsPE, key=lambda a: a["date"], reverse=True)[ - 0 - ]["comment_u"] - - annotationPE = exp.sub( - "", annotationPE - ) # Suppression du tag d'annotation PE - annotationPE = annotationPE.replace("\r", "") # Suppression des \r - annotationPE = annotationPE.replace( - "
", "\n\n" - ) # Interprète les retours chariots html - return annotationPE - return "" # pas d'annotations - - -# ---------------------------------------------------------------------------------------- -def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ): - """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée, - une valeur indiquée par un champ ; - si champ est une liste, renvoie la liste des valeurs extraites. - - Result: chaine unicode ou liste de chaines unicode - """ - - if isinstance(champ, list): - return [ - str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, chp) - for chp in champ - ] - else: # champ = str à priori - valeur = DONNEE_MANQUANTE - if ( - (aggregat in donnees_etudiant) - and (groupe in donnees_etudiant[aggregat]) - and (tag_scodoc in donnees_etudiant[aggregat][groupe]) - ): - donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc] - if champ == "rang": - valeur = "%s/%d" % ( - donnees_numeriques[ - pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang") - ], - donnees_numeriques[ - pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index( - "nbinscrits" - ) - ], - ) - elif champ in pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS: - indice_champ = pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index( - champ - ) - if ( - len(donnees_numeriques) > indice_champ - and donnees_numeriques[indice_champ] != None - ): - if isinstance( - donnees_numeriques[indice_champ], float - ): # valeur numérique avec formattage unicode - valeur = "%2.2f" % donnees_numeriques[indice_champ] - else: - valeur = "%s" % donnees_numeriques[indice_champ] - - return valeur - - -# ---------------------------------------------------------------------------------------- -def get_bilanParTag(donnees_etudiant, groupe="groupe"): - """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans - les données étudiants, ses résultats. - result: chaine unicode - """ - - entete = [ - ( - agg, - pe_jurype.JuryPE.PARCOURS[agg]["affichage_court"], - pe_jurype.JuryPE.PARCOURS[agg]["ordre"], - ) - for agg in pe_jurype.JuryPE.PARCOURS - ] - entete = sorted(entete, key=lambda t: t[2]) - - lignes = [] - valeurs = {"note": [], "rang": []} - for indice_aggregat, (aggregat, intitule, _) in enumerate(entete): - # print("> " + aggregat) - # listeTags = jury.get_allTagForAggregat(aggregat) # les tags de l'aggrégat - listeTags = [ - tag for tag in donnees_etudiant[aggregat][groupe].keys() if tag != "dut" - ] # - for tag in listeTags: - if tag not in lignes: - lignes.append(tag) - valeurs["note"].append( - [""] * len(entete) - ) # Ajout d'une ligne de données - valeurs["rang"].append( - [""] * len(entete) - ) # Ajout d'une ligne de données - indice_tag = lignes.index(tag) # l'indice de ligne du tag - - # print(" --- " + tag + "(" + str(indice_tag) + "," + str(indice_aggregat) + ")") - [note, rang] = str_from_syntheseJury( - donnees_etudiant, aggregat, groupe, tag, ["note", "rang"] - ) - valeurs["note"][indice_tag][indice_aggregat] = "" + note + "" - valeurs["rang"][indice_tag][indice_aggregat] = ( - ("\\textit{" + rang + "}") if note else "" - ) # rang masqué si pas de notes - - code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n" - code_latex += "\\hline \n" - code_latex += ( - " & " - + " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete]) - + " \\\\ \n" - ) - code_latex += "\\hline" - code_latex += "\\hline \n" - for i, ligne_val in enumerate(valeurs["note"]): - titre = lignes[i] # règle le pb d'encodage - code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n" - code_latex += ( - " & " - + " & ".join( - ["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]] - ) - + "\\\\ \n" - ) - code_latex += "\\hline \n" - code_latex += "\\end{tabular}" - - return code_latex - - -# ---------------------------------------------------------------------------------------- -def get_avis_poursuite_par_etudiant( - jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs -): - """Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni. - result: [ chaine unicode, chaine unicode ] - """ - if pe_tools.PE_DEBUG: - pe_tools.pe_print(jury.syntheseJury[etudid]["nom"] + " " + str(etudid)) - - civilite_str = jury.syntheseJury[etudid]["civilite_str"] - nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-") - prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-") - - nom_fichier = scu.sanitize_filename( - "avis_poursuite_%s_%s_%s" % (nom, prenom, etudid) - ) - if pe_tools.PE_DEBUG: - pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier)) - - # Entete (commentaire) - contenu_latex = ( - "%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n" - ) - - # les annnotations - annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe) - if pe_tools.PE_DEBUG: - pe_tools.pe_print(annotationPE, type(annotationPE)) - - # le LaTeX - avis = get_code_latex_avis_etudiant( - jury.syntheseJury[etudid], template_latex, annotationPE, footer_latex, prefs - ) - # if pe_tools.PE_DEBUG: pe_tools.pe_print(avis, type(avis)) - contenu_latex += avis + "\n" - - return [nom_fichier, contenu_latex] - - -def get_templates_from_distrib(template="avis"): - """Récupère le template (soit un_avis.tex soit le footer.tex) à partir des fichiers mémorisés dans la distrib des avis pe (distrib local - ou par défaut et le renvoie""" - if template == "avis": - pe_local_tmpl = pe_tools.PE_LOCAL_AVIS_LATEX_TMPL - pe_default_tmpl = pe_tools.PE_DEFAULT_AVIS_LATEX_TMPL - elif template == "footer": - pe_local_tmpl = pe_tools.PE_LOCAL_FOOTER_TMPL - pe_default_tmpl = pe_tools.PE_DEFAULT_FOOTER_TMPL - - if template in ["avis", "footer"]: - # pas de preference pour le template: utilise fichier du serveur - if os.path.exists(pe_local_tmpl): - template_latex = get_code_latex_from_modele(pe_local_tmpl) - else: - if os.path.exists(pe_default_tmpl): - template_latex = get_code_latex_from_modele(pe_default_tmpl) - else: - template_latex = "" # fallback: avis vides - return template_latex - - -# ---------------------------------------------------------------------------------------- -def table_syntheseAnnotationPE(syntheseJury, tag_annotation_pe): - """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant""" - sT = SeqGenTable() # le fichier excel à générer - - # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom - donnees_tries = sorted( - [ - (etudid, syntheseJury[etudid]["nom"] + " " + syntheseJury[etudid]["prenom"]) - for etudid in syntheseJury.keys() - ], - key=lambda c: c[1], - ) - etudids = [e[0] for e in donnees_tries] - if not etudids: # Si pas d'étudiants - T = GenTable( - columns_ids=["pas d'étudiants"], - rows=[], - titles={"pas d'étudiants": "pas d'étudiants"}, - html_sortable=True, - xls_sheet_name="dut", - ) - sT.add_genTable("Annotation PE", T) - return sT - - # Si des étudiants - maxParcours = max( - [syntheseJury[etudid]["nbSemestres"] for etudid in etudids] - ) # le nombre de semestre le + grand - - infos = ["civilite", "nom", "prenom", "age", "nbSemestres"] - entete = ["etudid"] - entete.extend(infos) - entete.extend(["P%d" % i for i in range(1, maxParcours + 1)]) # ajout du parcours - entete.append("Annotation PE") - columns_ids = entete # les id et les titres de colonnes sont ici identiques - titles = {i: i for i in columns_ids} - - rows = [] - for ( - etudid - ) in etudids: # parcours des étudiants par ordre alphabétique des nom+prénom - e = syntheseJury[etudid] - # Les info générales: - row = { - "etudid": etudid, - "civilite": e["civilite"], - "nom": e["nom"], - "prenom": e["prenom"], - "age": e["age"], - "nbSemestres": e["nbSemestres"], - } - # Les parcours: P1, P2, ... - n = 1 - for p in e["parcours"]: - row["P%d" % n] = p["titreannee"] - n += 1 - - # L'annotation PE - annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe) - row["Annotation PE"] = annotationPE if annotationPE else "" - rows.append(row) - - T = GenTable( - columns_ids=columns_ids, - rows=rows, - titles=titles, - html_sortable=True, - xls_sheet_name="Annotation PE", - ) - sT.add_genTable("Annotation PE", T) - return sT diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py new file mode 100644 index 000000000..0b9000c93 --- /dev/null +++ b/app/pe/pe_comp.py @@ -0,0 +1,384 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on Thu Sep 8 09:36:33 2016 + +@author: barasc +""" + +import os +import datetime +import re +import unicodedata + + +from flask import g + +import app.scodoc.sco_utils as scu +from app import log +from app.models import FormSemestre +from app.scodoc import sco_formsemestre +from app.scodoc.sco_logos import find_logo + +PE_DEBUG = 0 + +if not PE_DEBUG: + # log to notes.log + def pe_print(*a, **kw): + # kw is ignored. log always add a newline + log(" ".join(a)) + +else: + pe_print = print # print function + + +# Generated LaTeX files are encoded as: +PE_LATEX_ENCODING = "utf-8" + +# /opt/scodoc/tools/doc_poursuites_etudes +REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/") +REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/") + +PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex" +PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex" +PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex" +PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" + +# ---------------------------------------------------------------------------------------- + +""" +Descriptif d'un parcours classique BUT + +TODO:: A améliorer si BUT en moins de 6 semestres +""" + +PARCOURS = { + "S1": { + "aggregat": ["S1"], + "ordre": 1, + "affichage_court": "S1", + "affichage_long": "Semestre 1", + }, + "S2": { + "aggregat": ["S2"], + "ordre": 2, + "affichage_court": "S2", + "affichage_long": "Semestre 2", + }, + "1A": { + "aggregat": ["S1", "S2"], + "ordre": 3, + "affichage_court": "1A", + "affichage_long": "1ère année", + }, + "S3": { + "aggregat": ["S3"], + "ordre": 4, + "affichage_court": "S3", + "affichage_long": "Semestre 3", + }, + "S4": { + "aggregat": ["S4"], + "ordre": 5, + "affichage_court": "S4", + "affichage_long": "Semestre 4", + }, + "2A": { + "aggregat": ["S3", "S4"], + "ordre": 6, + "affichage_court": "2A", + "affichage_long": "2ème année", + }, + "3S": { + "aggregat": ["S1", "S2", "S3"], + "ordre": 7, + "affichage_court": "S1+S2+S3", + "affichage_long": "BUT du semestre 1 au semestre 3", + }, + "4S": { + "aggregat": ["S1", "S2", "S3", "S4"], + "ordre": 8, + "affichage_court": "BUT", + "affichage_long": "BUT du semestre 1 au semestre 4", + }, + "S5": { + "aggregat": ["S5"], + "ordre": 9, + "affichage_court": "S5", + "affichage_long": "Semestre 5", + }, + "S6": { + "aggregat": ["S6"], + "ordre": 10, + "affichage_court": "S6", + "affichage_long": "Semestre 6", + }, + "3A": { + "aggregat": ["S5", "S6"], + "ordre": 11, + "affichage_court": "3A", + "affichage_long": "3ème année", + }, + "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", + }, + "6S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], + "ordre": 13, + "affichage_court": "BUT", + "affichage_long": "BUT (tout semestre inclus)", + }, +} +NBRE_SEMESTRES_DIPLOMANT = 6 +AGGREGAT_DIPLOMANT = ( + "6S" # aggrégat correspondant à la totalité des notes pour le diplôme +) +TOUS_LES_SEMESTRES = PARCOURS[AGGREGAT_DIPLOMANT]["aggregat"] +TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")] +TOUS_LES_PARCOURS = list(PARCOURS.keys()) + +# ---------------------------------------------------------------------------------------- +def calcul_age(born: datetime.date) -> int: + """Calcule l'age connaissant la date de naissance ``born``. (L'age est calculé + à partir de l'horloge système). + + Args: + born: La date de naissance + + Return: + L'age (au regard de la date actuelle) + """ + if not born or not isinstance(born, datetime.date): + return None + + today = datetime.date.today() + return ( + today.year + - born.year + - ((today.month, today.day) < (born.month, born.day)) + ) + + +def remove_accents(input_unicode_str): + """Supprime les accents d'une chaine unicode""" + nfkd_form = unicodedata.normalize("NFKD", input_unicode_str) + only_ascii = nfkd_form.encode("ASCII", "ignore") + return only_ascii + + +def escape_for_latex(s): + """Protège les caractères pour inclusion dans du source LaTeX""" + if not s: + return "" + conv = { + "&": r"\&", + "%": r"\%", + "$": r"\$", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\^{}", + "\\": r"\textbackslash{}", + "<": r"\textless ", + ">": r"\textgreater ", + } + exp = re.compile( + "|".join( + re.escape(key) + for key in sorted(list(conv.keys()), key=lambda item: -len(item)) + ) + ) + return exp.sub(lambda match: conv[match.group()], s) + + +# ---------------------------------------------------------------------------------------- +def list_directory_filenames(path): + """List of regular filenames in a directory (recursive) + Excludes files and directories begining with . + """ + R = [] + for root, dirs, files in os.walk(path, topdown=True): + dirs[:] = [d for d in dirs if d[0] != "."] + R += [os.path.join(root, fn) for fn in files if fn[0] != "."] + return R + + +def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): + """Read pathname server file and add content to zip under path_in_zip""" + rooted_path_in_zip = os.path.join(ziproot, path_in_zip) + zipfile.write(filename=pathname, arcname=rooted_path_in_zip) + # data = open(pathname).read() + # zipfile.writestr(rooted_path_in_zip, data) + + +def add_refs_to_register(register, directory): + """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme + filename => pathname + """ + length = len(directory) + for pathname in list_directory_filenames(directory): + filename = pathname[length + 1 :] + register[filename] = pathname + + +def add_pe_stuff_to_zip(zipfile, ziproot): + """Add auxiliary files to (already opened) zip + Put all local files found under config/doc_poursuites_etudes/local + and config/doc_poursuites_etudes/distrib + If a file is present in both subtrees, take the one in local. + + Also copy logos + """ + register = {} + # first add standard (distrib references) + distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib") + add_refs_to_register(register=register, directory=distrib_dir) + # then add local references (some oh them may overwrite distrib refs) + local_dir = os.path.join(REP_LOCAL_AVIS, "local") + add_refs_to_register(register=register, directory=local_dir) + # at this point register contains all refs (filename, pathname) to be saved + for filename, pathname in register.items(): + add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename) + + # Logos: (add to logos/ directory in zip) + logos_names = ["header", "footer"] + for name in logos_names: + logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) + if logo is not None: + add_local_file_to_zip( + zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename + ) + + +# ---------------------------------------------------------------------------------------- +def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int: + """Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres) + et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle + sera remis le diplôme BUT des étudiants qui y sont scolarisés + (en supposant qu'il n'y ait pas de redoublement à venir). + + **Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4, S6 pour des semestres décalés) + s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'année universitaire. + + Par exemple : + + * S5 débutant en 2025 finissant en 2026 : diplome en 2026 + * S3 debutant en 2025 et finissant en 2026 : diplome en 2027 + + La fonction est adaptée au cas des semestres décalés. + + Par exemple : + + * S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026 + * S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027 + + Args: + sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit : + + * un ``FormSemestre`` (Scodoc9) + * un dict (format compatible avec Scodoc7) + + nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT) + """ + + if isinstance(sem_base, FormSemestre): + sem_id = sem_base.semestre_id + annee_fin = sem_base.date_fin.year + annee_debut = sem_base.date_debut.year + else: # sem_base est un dictionnaire (Scodoc 7) + sem_id = sem_base["semestre_id"] + annee_fin = int(sem_base["annee_fin"]) + annee_debut = int(sem_base["annee_debut"]) + if ( + 1 <= sem_id <= nbre_sem_formation + ): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ?? + nbreSemRestant = ( + nbre_sem_formation - sem_id + ) # nombre de semestres restant avant diplome + nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome + # Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé + # avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon + delta = annee_fin - annee_debut + decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 + increment = decalage * (1 - delta) + return annee_fin + nbreAnRestant + increment + + +def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list: + """Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome`` + et s'intégrant à la formation donnée par son ``formation_id``. + + **Définition** : Un co-semestre est un semestre : + + * dont l'année de diplômation prédite (sans redoublement) est la même + * dont la formation est la même (optionnel) + * qui a des étudiants inscrits + + Si formation_id == None, ne prend pas en compte l'identifiant de formation + TODO:: A raccrocher à un programme + + Args: + annee_diplome: L'année de diplomation + formation_id: L'identifiant de la formation + """ + tousLesSems = ( + sco_formsemestre.do_formsemestre_list() + ) # tous les semestres memorisés dans scodoc + + if formation_id: + cosemestres_fids = { + sem["id"] + for sem in tousLesSems + if get_annee_diplome_semestre(sem) == annee_diplome + and sem["formation_id"] == formation_id + } + else: + cosemestres_fids = { + sem["id"] + for sem in tousLesSems + if get_annee_diplome_semestre(sem) == annee_diplome + } + + cosemestres = {} + for fid in cosemestres_fids: + cosem = FormSemestre.get_formsemestre(fid) + if len(cosem.etuds_inscriptions) > 0: + cosemestres[fid] = cosem + + return cosemestres + diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py new file mode 100644 index 000000000..384765f39 --- /dev/null +++ b/app/pe/pe_etudiant.py @@ -0,0 +1,527 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on 17/01/2024 + +@author: barasc +""" + +import app.pe.pe_comp as pe_comp +from app.models import FormSemestre, Identite +from app.pe.pe_comp import pe_print + + +class EtudiantsJuryPE: + """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" + + def __init__(self, annee_diplome: int): + """ + + Args: + annee_diplome: L'année de diplomation + """ + + self.annee_diplome = annee_diplome + + "Les identités des étudiants traités pour le jury" + self.identites = {} # ex. ETUDINFO_DICT + "Les cursus (semestres suivis, abandons) des étudiants" + self.cursus = {} + """Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants""" + self.trajectoires = {} + + "Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)" + self.etudiants_diplomes = {} + self.diplomes_ids = {} + + "Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)" + self.etudiants_ids = {} + + def find_etudiants(self, formation_id: int): + """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`` + dans la formation ``formation_id``. + + Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE. + + + formation_id: L'identifiant de la formation (inutilisé) + + *Remarque* : ex: JuryPE.get_etudiants_in_jury() + """ + "Les cosemestres donnant lieu à même année de diplome" + cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None) + self.cosemestres = cosemestres + pe_comp.pe_print( + "1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) + ) + + """Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)""" + pe_comp.pe_print("2) Liste des étudiants dans les différents co-semestres") + self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) + pe_comp.pe_print( + " => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids) + ) + + """Analyse des parcours étudiants pour déterminer leur année effective de diplome + avec prise en compte des redoublements, des abandons, ....""" + pe_comp.pe_print("3) Analyse des parcours individuels des étudiants") + + no_etud = 0 + for no_etud, etudid in enumerate(self.etudiants_ids): + """L'identité de l'étudiant""" + identite = Identite.get_etud(etudid) + self.identites[etudid] = identite + + """L'analyse de son cursus""" + self.analyse_etat_etudiant(etudid, cosemestres) + + """L'analyse de son parcours pour atteindre chaque semestre de la formation""" + self.structure_cursus_etudiant(etudid) + + if (no_etud + 1) % 10 == 0: + pe_comp.pe_print(f"{no_etud + 1}") + no_etud += 1 + pe_comp.pe_print() + + """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" + self.etudiants_diplomes = self.get_etudiants_diplomes() + self.diplomes_ids = set(self.etudiants_diplomes.keys()) + + """Les étudiants dont il faut calculer les moyennes""" + self.etudiants_ids = {etudid for etudid in self.identites} + + """Les formsemestres (des étudiants) dont il faut calculer les moyennes""" + self.formsemestres_jury_ids = self.get_formsemestres() + + # Synthèse + pe_comp.pe_print( + f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" + ) + nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) + pe_comp.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") + pe_comp.pe_print( + f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" + ) + pe_comp.pe_print( + f" => quelques étudiants futurs diplômés : " + + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]]) + ) + pe_comp.pe_print( + f" => semestres dont il faut calculer les moyennes : " + + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) + ) + # Les abandons : + self.abandons = sorted([self.cursus[etudid]['nom'] + for etudid in self.cursus if etudid not in self.diplomes_ids]) + + + def get_etudiants_diplomes(self) -> dict[int, Identite]: + """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` + qui vont être à traiter au jury PE pour + l'année de diplômation donnée et n'ayant ni été réorienté, ni abandonné. + + + Returns: + Un dictionnaire `{etudid: Identite(etudid)}` + """ + etudids = [ + etudid + for etudid in self.cursus + if self.cursus[etudid]["diplome"] == self.annee_diplome + and self.cursus[etudid]["abandon"] == False + ] + etudiants = {etudid: self.identites[etudid] for etudid in etudids} + return etudiants + + def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]): + """Analyse le cursus d'un étudiant pouvant être : + + * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré) + * un étudiant qui ne sera pas considéré dans le jury mais qui a participé dans sa scolarité + à un (ou plusieurs) semestres communs aux étudiants du jury (et impactera les classements) + + L'analyse consiste : + + * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité, + avec son nom, prénom, etc... + * à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de + route (cf. clé abandon) + + Args: + etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury + cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres + de même année de diplomation + """ + identite = Identite.get_etud(etudid) + + """Le cursus global de l'étudiant (restreint aux semestres APC)""" + formsemestres = identite.get_formsemestres() + + semestres_etudiant = { + frmsem.formsemestre_id: frmsem + for frmsem in formsemestres + if frmsem.formation.is_apc() + } + + self.cursus[etudid] = { + "etudid": etudid, # les infos sur l'étudiant + "etat_civil": identite.etat_civil, # Ajout à la table jury + "nom": identite.nom, + "entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT + "diplome": annee_diplome(identite), # Le date prévisionnelle de son diplôme + "formsemestres": semestres_etudiant, # les semestres de l'étudiant + "nb_semestres": len(semestres_etudiant), # le nombre de semestres de l'étudiant + "abandon": False, # va être traité en dessous + } + + """ Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?""" + self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres) + + def get_semestres_significatifs(self, etudid: int): + """Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé + l'année visée (supprime les semestres qui conduisent à une diplomation + postérieure à celle du jury visé) + + Args: + etudid: L'identifiant d'un étudiant + + Returns: + Un dictionnaire ``{fid: FormSemestre(fid)`` dans lequel les semestres + amènent à une diplomation avant l'annee de diplomation du jury + """ + + semestres_etudiant = self.cursus[etudid]["formsemestres"] + semestres_significatifs = {} + for fid in semestres_etudiant: + semestre = semestres_etudiant[fid] + if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome: + semestres_significatifs[fid] = semestre + return semestres_significatifs + + def structure_cursus_etudiant(self, etudid: int): + """Structure les informations sur les semestres suivis par un + étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs + de moyennes PE. + + Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke : + le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). Ce semestre influera les + interclassement par semestre dans la promo. + """ + semestres_significatifs = self.get_semestres_significatifs(etudid) + + """Tri des semestres par numéro de semestre""" + for nom_sem in pe_comp.TOUS_LES_SEMESTRES: + i = int(nom_sem[1]) # le n° du semestre + semestres_i = { + fid: semestres_significatifs[fid] + for fid in semestres_significatifs + if semestres_significatifs[fid].semestre_id == i + } # les semestres de n°i de l'étudiant + self.cursus[etudid][nom_sem] = semestres_i + + + def get_trajectoire(self, etudid: int, formsemestre_final: FormSemestre): + """Ensemble des semestres parcourus par + un étudiant pour l'amener à un semestre terminal. + + Par ex: si formsemestre_terminal est un S3, ensemble des S1, + S2, S3 suivi pour l'amener au S3 (il peut y avoir plusieurs S1, + ou S2, ou S3 s'il a redoublé). + + Les semestres parcourus sont antérieurs (en terme de date de fin) + au formsemestre_terminal. + + Args: + etudid: L'identifiant de l'étudiant + formsemestre_final: le semestre final visé + """ + numero_semestre_terminal = formsemestre_final.semestre_id + semestres_significatifs = self.get_semestres_significatifs(etudid) + + """Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs""" + semestres_aggreges = {} + for fid in semestres_significatifs: + semestre = semestres_significatifs[fid] + if ( + semestre.semestre_id <= numero_semestre_terminal + and semestre.date_fin <= formsemestre_final.date_fin + ): + semestres_aggreges[fid] = semestre + return semestres_aggreges + + def get_formsemestres_terminaux_aggregat(self, aggregat: str): + """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat + (pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3). + Ces formsemestres traduisent : + + * les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI + ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA. + * les éventuelles situations de redoublement (par ex pour 1 étudiant ayant redoublé sa 2ème année : + S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en renvoyant les formsemestre_id du + S3 (1ère session) et du S3 (2ème session) + + Args: + aggregat: L'aggrégat + + Returns: + Un dictionnaire ``{fid: FormSemestre(fid)}`` + """ + formsemestres_terminaux = {} + for etudid in self.trajectoires: + if self.trajectoires[etudid][aggregat]: + trajectoire = self.trajectoires[etudid][aggregat] + """Le semestre terminal de l'étudiant de l'aggrégat""" + fid = trajectoire.semestre_final.formsemestre_id + formsemestres_terminaux[fid] = trajectoire.semestre_final + return formsemestres_terminaux + + def get_formsemestres(self, semestres_recherches=None): + """Ayant connaissance des étudiants dont il faut calculer les moyennes pour + le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres + parcourus), + renvoie un dictionnaire ``{fid: FormSemestre(fid)}`` + contenant l'ensemble des formsemestres de leurs cursus, dont il faudra calculer + la moyenne. + Les formsemestres sont limités à ceux indiqués dans ``semestres_recherches``. + + Args: + semestres_recherches: Une liste ou une chaine de caractères parmi : + + * None : pour obtenir tous les formsemestres du jury + * 'Si' : pour obtenir les semestres de n° i (par ex. 'S1') + * 'iA' : pour obtenir les semestres de l'année i (par ex. '1A' donne ['S1, 'S2']) + * '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats + + Returns: + Un dictionnaire de la forme ``{fid: FormSemestre(fid)}`` + + Remarque: + Une liste de la forme ``[ 'Si', 'iA' , ... ]`` (combinant les formats précédents) est possible. + """ + if semestres_recherches is None: + """Appel récursif pour obtenir tous les semestres (validants)""" + semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT) + return semestres + elif isinstance(semestres_recherches, list): + """Appel récursif sur tous les éléments de la liste""" + semestres = {} + for elmt in semestres_recherches: + semestres_elmt = self.get_formsemestres(elmt) + semestres = semestres | semestres_elmt + return semestres + elif ( + isinstance(semestres_recherches, str) + and semestres_recherches in pe_comp.TOUS_LES_AGGREGATS + ): + """Cas d'un aggrégat avec appel récursif sur toutes les entrées de l'aggrégat""" + semestres = self.get_formsemestres( + pe_comp.PARCOURS[semestres_recherches]["aggregat"] + ) + return semestres + elif ( + isinstance(semestres_recherches, str) + and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES + ): + """semestres_recherches est un nom de semestre de type S1, + pour une recherche parmi les étudiants à prendre en compte + dans le jury (diplômé et redoublants non diplômé) + """ + nom_sem = semestres_recherches + semestres = {} + for etudid in self.etudiants_ids: + if self.cursus[etudid][nom_sem]: + semestres = semestres | self.cursus[etudid][nom_sem] + return semestres + else: + raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") + + def nbre_etapes_max_diplomes(self): + """Connaissant les étudiants diplomes du jury PE, + nombre de semestres (étapes) maximum suivis par les étudiants du jury. + """ + nbres_semestres = [] + for etudid in self.diplomes_ids: + nbres_semestres.append( self.cursus[etudid]["nb_semestres"] ) + return max(nbres_semestres) + + +def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set: + """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) + inscrits à l'un des semestres de la liste de ``semestres``. + + Remarque : Les ``cosemestres`` sont généralement obtenus avec ``sco_formsemestre.do_formsemestre_list()`` + + Args: + semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un + ensemble d'identifiant de semestres + + Returns: + Un ensemble d``etudid`` + """ + + etudiants_ids = set() + for fid, sem in semestres.items(): # pour chacun des semestres de la liste + etudiants_du_sem = {ins.etudid for ins in sem.inscriptions} + + pe_print(f" --> {sem} : {len(etudiants_du_sem)} etudiants") + etudiants_ids = ( + etudiants_ids | etudiants_du_sem + ) # incluant la suppression des doublons + + return etudiants_ids + + +def annee_diplome(identite: Identite) -> int: + """L'année de diplôme prévue d'un étudiant en fonction de ses semestres + d'inscription (pour un BUT). + + Args: + identite: L'identité d'un étudiant + + Returns: + L'année prévue de sa diplômation + + NOTE: Pourrait être déplacé dans app.models.etudiants.Identite + """ + formsemestres = identite.get_formsemestres() + if formsemestres: + return max( + [ + pe_comp.get_annee_diplome_semestre(sem_base) + for sem_base in formsemestres + ] + ) + else: + return None + + +def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> bool: + """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : + + * d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait + utiliser les code NAR pour réorienté & DEM pour démissionnaire des résultats du jury renseigné dans la BDD, + mais pas nécessaire ici) + + * d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour autant avoir été indiqué NAR ou DEM). + + Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres + (semestres conduisant à la même année de diplômation) connu dans Scodoc. + + Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une + instance des S5 qui conduisent à la diplomation dans l'année visée. S'il n'est que dans un S4, il a sans doute + arrêté. A moins qu'il ne soit parti à l'étranger et là, pas de notes. + TODO:: Cas de l'étranger, à coder/tester + + **Attention** : Cela suppose que toutes les instances d'un semestre donné (par ex: toutes les instances de S6 + accueillant un étudiant soient créées ; sinon les étudiants non inscrits dans un S6 seront considérés comme + ayant abandonnés) + TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre + + Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et + regarde s'il n'existe pas parmi les semestres existants dans Scodoc un semestre : + * dont les dates sont postérieures (en terme de date de début) + * de n° au moins égal à celui de son dernier semestre valide (S5 -> S5 ou S5 -> S6) + dans lequel il aurait pu s'inscrire mais ne l'a pas fait. + + Args: + identite: L'identité d'un étudiant + cosemestres: Les semestres donnant lieu à diplômation (sans redoublement) en date du jury + + Returns: + Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ? + + TODO:: A reprendre pour le cas des étudiants à l'étranger + TODO:: A reprendre si BUT avec semestres décalés + """ + etudid = identite.etudid + + """Son dernier semestre en date""" + semestres = {sem.semestre_id: sem for sem in identite.get_formsemestres()} + dernier_formsemestre = get_dernier_semestre_en_date(semestres) + numero_dernier_formsemestre = dernier_formsemestre.semestre_id + + """Les numéro de semestres possible dans lesquels il pourrait s'incrire""" + # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) + if numero_dernier_formsemestre % 2 == 1: + numeros_possibles = list( + range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT) + ) + # semestre pair => passage en année supérieure ou redoublement + else: # + numeros_possibles = list( + range( + max(numero_dernier_formsemestre - 1, 1), + pe_comp.NBRE_SEMESTRES_DIPLOMANT, + ) + ) + + """Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?""" + formsestres_superieurs_possibles = [] + for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits + if ( + fid != dernier_formsemestre.formsemestre_id + and sem.semestre_id in numeros_possibles + and sem.date_debut.year >= dernier_formsemestre.date_debut.year + ): # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant et de niveau plus élevé que le dernier semestre valide de l'étudiant + formsestres_superieurs_possibles.append(fid) + + if len(formsestres_superieurs_possibles) > 0: + return True + + return False + + + + +def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: + """Renvoie le dernier semestre en **date de fin** d'un dictionnaire + de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``. + + Args: + semestres: Un dictionnaire de semestres + + Return: + Le FormSemestre du semestre le plus récent + """ + if semestres: + fid_dernier_semestre = list(semestres.keys())[0] + dernier_semestre: FormSemestre = semestres[fid_dernier_semestre] + for fid in semestres: + if semestres[fid].date_fin > dernier_semestre.date_fin: + dernier_semestre = semestres[fid] + return dernier_semestre + else: + return None + + diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py new file mode 100644 index 000000000..8fc4727b9 --- /dev/null +++ b/app/pe/pe_interclasstag.py @@ -0,0 +1,125 @@ + +from app.pe.pe_tabletags import TableTag +from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE +from app.pe.pe_trajectoiretag import TrajectoireTag +from app.comp import moy_sem + +import pandas as pd +import numpy as np + + +class AggregatInterclasseTag(TableTag): + """Interclasse l'ensemble des étudiants diplômés à une année + donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S') + en reportant : + + * les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son + formsemestres) + * calculant le classement sur les étudiants diplômes + """ + + # ------------------------------------------------------------------------------------------------------------------- + def __init__( + self, + nom_aggregat: str, + etudiants: EtudiantsJuryPE, + trajectoires_jury_pe: TrajectoiresJuryPE, + trajectoires_taggues: dict[tuple, TrajectoireTag], + ): + """""" + """Table nommée au nom de l'aggrégat (par ex: 3S""" + TableTag.__init__(self, nom_aggregat) + + """Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" + self.diplomes_ids = etudiants.etudiants_diplomes + self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} + + """Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat + """ + self.trajectoires: dict[int, Trajectoire] = {} + for trajectoire_id in trajectoires_jury_pe.trajectoires: + trajectoire = trajectoires_jury_pe.trajectoires[trajectoire_id] + if trajectoire_id[0] == nom_aggregat: + self.trajectoires[trajectoire_id] = trajectoire + + self.trajectoires_taggues: dict[int, Trajectoire] = {} + for trajectoire_id in self.trajectoires: + self.trajectoires_taggues[trajectoire_id] = trajectoires_taggues[ + trajectoire_id + ] + + """Les trajectoires suivies par les étudiants du jury, en ne gardant que + celles associées aux diplomés""" + self.suivi: dict[int, Trajectoire] = {} + for etudid in self.diplomes_ids: + self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat] + + """Les tags""" + self.tags_sorted = self.do_taglist() + + """Construit la matrice de notes""" + self.notes = self.compute_notes_matrice() + + """Synthétise les moyennes/classements par tag""" + self.moyennes_tags = {} + for tag in self.tags_sorted: + moy_gen_tag = self.notes[tag] + class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int + self.moyennes_tags[tag] = { + "notes": moy_gen_tag, + "classements": class_gen_tag, + "min": moy_gen_tag.min(), + "max": moy_gen_tag.max(), + "moy": moy_gen_tag.mean(), + "nb_inscrits": len(moy_gen_tag), + } + + def get_repr(self) -> str: + """Une représentation textuelle""" + return f"Aggrégat {self.nom}" + + + def do_taglist(self): + """Synthétise les tags à partir des trajectoires_tagguées + + Returns: + Une liste de tags triés par ordre alphabétique + """ + tags = [] + for trajectoire_id in self.trajectoires_taggues: + trajectoire = self.trajectoires_taggues[trajectoire_id] + tags.extend(trajectoire.tags_sorted) + return sorted(set(tags)) + + + def compute_notes_matrice(self): + """Construit la matrice de notes (etudid x tags) + retraçant les moyennes obtenues par les étudiants dans les semestres associés à + l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat). + """ + nb_tags = len(self.tags_sorted) + nb_etudiants = len(self.diplomes_ids) + + """Index de la matrice (etudids -> dim 0, tags -> dim 1)""" + etudids = list(self.diplomes_ids) + tags = self.tags_sorted + + """Partant d'un dataframe vierge""" + df = pd.DataFrame(np.nan, index=etudids, columns=tags) + + for trajectoire_id in self.trajectoires_taggues: + """Charge les moyennes par tag de la trajectoire tagguée""" + notes = self.trajectoires_taggues[trajectoire_id].notes + + """Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées""" + etudids_communs = df.index.intersection(notes.index) + tags_communs = df.columns.intersection(notes.columns) + + """Injecte les notes par tag""" + df.loc[etudids_communs, tags_communs] = notes.loc[ + etudids_communs, tags_communs + ] + + return df + diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py new file mode 100644 index 000000000..62129ebb7 --- /dev/null +++ b/app/pe/pe_jury.py @@ -0,0 +1,480 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on Fri Sep 9 09:15:05 2016 + +@author: barasc +""" + +# ---------------------------------------------------------- +# Ensemble des fonctions et des classes +# permettant les calculs preliminaires (hors affichage) +# a l'edition d'un jury de poursuites d'etudes +# ---------------------------------------------------------- + +import io +import os +from zipfile import ZipFile + + +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.models import FormSemestre +from app.models.etudiants import Identite + +from app.scodoc.gen_tables import GenTable, SeqGenTable +import app.scodoc.sco_utils as scu +from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire +import app.pe.pe_comp as pe_comp +from app.pe.pe_semtag import SemestreTag +from app.pe.pe_interclasstag import AggregatInterclasseTag +from app.pe.pe_trajectoiretag import TrajectoireTag +import app.pe.pe_affichage as pe_affichage + +import pandas as pd +import numpy as np + +# ---------------------------------------------------------------------------------------- + + +# ---------------------------------------------------------------------------------------- +class JuryPE(object): + """Classe mémorisant toutes les informations nécessaires pour établir un jury de PE. + Modèle basé sur NotesTable. + + Attributs : + + * diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) + * juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives + + celles des semestres valides à prendre en compte permettant le calcul des moyennes ... + ``{'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }}`` + a + Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue + et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif + """ + + # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et + # leur affichage dans les avis latex + + # ------------------------------------------------------------------------------------------------------------------ + def __init__(self, diplome, formation_id): + """ + Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit : + 1. l'année d'obtention du DUT, + 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. + + Args: + sem_base: le FormSemestre donnant le semestre à la base du jury PE + semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term) + meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, + si False, permet des programmes differents + """ + "L'année du diplome" + self.diplome = diplome + + "La formation associée au diplome" + self.formation_id = formation_id + + "Un zip où ranger les fichiers générés" + self.nom_export_zip = "Jury_PE_%s" % self.diplome + self.zipdata = io.BytesIO() + self.zipfile = ZipFile(self.zipdata, "w") + + """Chargement des étudiants à prendre en compte dans le jury""" + pe_comp.pe_print( + f"*** Recherche et chargement des étudiants diplômés en {self.diplome} pour la formation {self.formation_id}" + ) + self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants + self.etudiants.find_etudiants(self.formation_id) + self.diplomes_ids = self.etudiants.diplomes_ids + + """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" + pe_comp.pe_print("*** Génère les semestres taggués") + self.semestres_taggues = compute_semestres_tag(self.etudiants) + + if pe_comp.PE_DEBUG: + """Intègre le bilan des semestres taggués au zip final""" + for fid in self.semestres_taggues: + formsemestretag = self.semestres_taggues[fid] + filename = formsemestretag.nom.replace(" ", "_") + ".csv" + pe_comp.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, formsemestretag.str_tagtable(), path="details_semestres" + ) + + """Génère les trajectoires (combinaison de semestres suivis + par un étudiant pour atteindre le semestre final d'un aggrégat) + """ + pe_comp.pe_print( + "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" + ) + self.trajectoires = TrajectoiresJuryPE(self.diplome) + self.trajectoires.cree_trajectoires(self.etudiants) + + """Génère les moyennes par tags des trajectoires""" + pe_comp.pe_print("*** Calcule les moyennes par tag des trajectoires possibles") + self.trajectoires_tagguees = compute_trajectoires_tag( + self.trajectoires, self.etudiants, self.semestres_taggues + ) + + if pe_comp.PE_DEBUG: + """Intègre le bilan des trajectoires tagguées au zip final""" + for trajectoire_id in self.trajectoires_tagguees: + trajectoire_tagguee = self.trajectoires_tagguees[trajectoire_id] + filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv" + pe_comp.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, + trajectoire_tagguee.str_tagtable(), + path="details_semestres", + ) + + """Génère les interclassements (par promo et) par (nom d') aggrégat""" + pe_comp.pe_print("*** Génère les interclassements par aggrégat") + self.interclassements_taggues = compute_interclassements( + self.etudiants, self.trajectoires, self.trajectoires_tagguees + ) + + if pe_comp.PE_DEBUG: + """Intègre le bilan des aggrégats (par promo) au zip final""" + for nom_aggregat in self.interclassements_taggues: + interclass_tag = self.interclassements_taggues[nom_aggregat] + filename = interclass_tag.get_repr().replace(" ", "_") + ".csv" + pe_comp.pe_print(f" - Export csv de {filename} ") + self.add_file_to_zip( + filename, + interclass_tag.str_tagtable(), + path="details_semestres", + ) + + """Synthèse des éléments du jury PE""" + self.synthese = self.synthetise_juryPE() + + # Export des données => mode 1 seule feuille -> supprimé + pe_comp.pe_print("*** Export du jury de synthese") + filename = "synthese_jury_" + str(self.diplome) + ".xlsx" + with pd.ExcelWriter(filename, engine="openpyxl") as writer: + for onglet in self.synthese: + df = self.synthese[onglet] + df.to_excel( + writer, onglet, index=True, header=True + ) # écriture dans l'onglet + # worksheet = writer.sheets[onglet] # l'on + + self.add_file_to_zip( + filename, + open(filename, "rb").read(), + ) + + """Fin !!!! Tada :)""" + + def add_file_to_zip(self, filename: str, data, path=""): + """Add a file to our zip + All files under NOM_EXPORT_ZIP/ + path may specify a subdirectory + + Args: + filename: Le nom du fichier à intégrer au zip + data: Les données du fichier + path: Un dossier dans l'arborescence du zip + """ + path_in_zip = os.path.join(path, filename) # self.nom_export_zip, + self.zipfile.writestr(path_in_zip, data) + + def get_zipped_data(self): + """returns file-like data with a zip of all generated (CSV) files. + Reset file cursor at the beginning ! + """ + if self.zipfile: + self.zipfile.close() + self.zipfile = None + self.zipdata.seek(0) + return self.zipdata + + def do_tags_list(self, interclassements: dict[str, AggregatInterclasseTag]): + """La liste des tags extraites des interclassements""" + tags = [] + for aggregat in interclassements: + interclass = interclassements[aggregat] + if interclass.tags_sorted: + tags.extend(interclass.tags_sorted) + tags = sorted(set(tags)) + return tags + + # **************************************************************************************************************** # + # Méthodes pour la synthèse du juryPE + # ***************************************************************************************************************** + + def synthetise_juryPE(self): + """Synthétise tous les résultats du jury PE dans des dataframes""" + + pe_comp.pe_print("*** Synthèse finale des moyennes ***") + + synthese = {} + pe_comp.pe_print(" -> Synthèse des données administratives") + synthese["administratif"] = self.df_administratif() + + tags = self.do_tags_list(self.interclassements_taggues) + for tag in tags: + pe_comp.pe_print(f" -> Synthèse du tag {tag}") + synthese[tag] = self.df_tag(tag) + 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"] + + 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": cursus["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): + """Génère le DataFrame synthétisant les moyennes/classements (groupe, + interclassement promo) pour tous les aggrégats prévus, + tels que fourni dans l'excel final. + + Args: + tag: Un des tags (a minima `but`) + + Returns: + """ + + etudids = list(self.diplomes_ids) + aggregats = pe_comp.TOUS_LES_PARCOURS + + donnees = {} + + for etudid in etudids: + etudiant = self.etudiants.identites[etudid] + donnees[etudid] = { + "Nom": etudiant.nom, + "Prenom": etudiant.prenom, + "Civilite": etudiant.civilite_str, + } + + for aggregat in aggregats: + """La trajectoire de l'étudiant sur l'aggrégat""" + trajectoire = self.trajectoires.suivi[etudid][aggregat] + """Les moyennes par tag de cette trajectoire""" + if trajectoire: + trajectoire_tagguee = self.trajectoires_tagguees[ + trajectoire.trajectoire_id + ] + bilan = trajectoire_tagguee.moyennes_tags[tag] + + donnees[etudid] |= { + f"{aggregat} notes ": f"{bilan['notes'].loc[etudid]:.1f}", + 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}", + } + else: + donnees[etudid] |= { + f"{aggregat} notes ": "-", + f"{aggregat} class. (groupe)": "-", + f"{aggregat} min/moy/max (groupe)": "-", + } + + """L'interclassement""" + interclass = self.interclassements_taggues[aggregat] + 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}", + } + else: + donnees[etudid] |= { + f"{aggregat} class. (promo)": "-", + f"{aggregat} min/moy/max (promo)": "-", + } + + # 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) + 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: + """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. + Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire + des étudiants (cf. attribut etudiants.cursus). + En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé. + . + + Args: + etudiants: Un groupe d'étudiants participant au jury + + Returns: + Un dictionnaire {fid: SemestreTag(fid)} + """ + + """Création des semestres taggués, de type 'S1', 'S2', ...""" + pe_comp.pe_print("*** Création des semestres taggués") + + formsemestres = etudiants.get_formsemestres( + semestres_recherches=pe_comp.TOUS_LES_SEMESTRES + ) + + semestres_tags = {} + for frmsem_id, formsemestre in formsemestres.items(): + """Choix d'un nom pour le semestretag""" + nom = "S%d %d %d-%d" % ( + formsemestre.semestre_id, + frmsem_id, + formsemestre.date_debut.year, + formsemestre.date_fin.year, + ) + + pe_comp.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}") + + """Créé le semestre_tag et exécute les calculs de moyennes""" + formsemestretag = SemestreTag(nom, frmsem_id) + + """Stocke le semestre taggué""" + semestres_tags[frmsem_id] = formsemestretag + + return semestres_tags + + +def compute_trajectoires_tag( + trajectoires: TrajectoiresJuryPE, + etudiants: EtudiantsJuryPE, + semestres_taggues: dict[int, SemestreTag], +): + """Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens + d'un aggrégat (par ex: '3S')), + en calculant les moyennes et les classements par tag pour chacune. + + Pour rappel : Chaque trajectoire est identifiée un nom d'aggrégat et par un formsemestre terminal. + + Par exemple : + + * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les + étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison. + + * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les + notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en + date (le S2 redoublé par les redoublants est forcément antérieur) + + + Args: + etudiants: Les données des étudiants + semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) + + Return: + Un dictionnaire de la forme {nom_aggregat: {fid_terminal: SetTag(fid_terminal)} } + """ + + pe_comp.pe_print(" *** Création des aggrégats ") + + trajectoires_tagguees = {} + + for trajectoire_id in trajectoires.trajectoires: + trajectoire = trajectoires.trajectoires[trajectoire_id] + nom = trajectoire.get_repr() + + pe_comp.pe_print(f" --> Fusion {nom}") + + """Création de la trajectoire_tagguee associée""" + trajectoire_tagguee = TrajectoireTag( + nom, trajectoire, semestres_taggues, etudiants + ) + + """Mémorise le résultat""" + trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee + + return trajectoires_tagguees + + +def compute_interclassements( + etudiants: EtudiantsJuryPE, + trajectoires_jury_pe: TrajectoiresJuryPE, + trajectoires_tagguees: dict[tuple, Trajectoire], +): + """Interclasse les étudiants, (nom d') aggrégat par aggrégat, + pour fournir un classement sur la promo. Le classement est établi au regard du nombre + d'étudiants ayant participé au même aggrégat. + """ + pe_comp.pe_print(" Interclassement sur la promo") + + aggregats_interclasses_taggues = {} + for nom_aggregat in pe_comp.TOUS_LES_SEMESTRES + pe_comp.TOUS_LES_AGGREGATS: + pe_comp.pe_print(f" --> {nom_aggregat}") + interclass = AggregatInterclasseTag( + nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees + ) + aggregats_interclasses_taggues[nom_aggregat] = interclass + return aggregats_interclasses_taggues diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py deleted file mode 100644 index d6416c2e5..000000000 --- a/app/pe/pe_jurype.py +++ /dev/null @@ -1,1274 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on Fri Sep 9 09:15:05 2016 - -@author: barasc -""" - -# ---------------------------------------------------------- -# Ensemble des fonctions et des classes -# permettant les calculs preliminaires (hors affichage) -# a l'edition d'un jury de poursuites d'etudes -# ---------------------------------------------------------- - -import io -import os -from zipfile import ZipFile - -from app.comp import res_sem -from app.comp.res_compat import NotesTableCompat -from app.models import Formation, FormSemestre - -from app.scodoc.gen_tables import GenTable, SeqGenTable -import app.scodoc.sco_utils as scu -from app.scodoc import codes_cursus # codes_cursus.NEXT -> sem suivant -from app.scodoc import sco_etud -from app.scodoc import sco_formsemestre -from app.pe import pe_tagtable -from app.pe import pe_tools -from app.pe import pe_semestretag -from app.pe import pe_settag - - -# ---------------------------------------------------------------------------------------- -def comp_nom_semestre_dans_parcours(sem): - """Le nom a afficher pour titrer un semestre - par exemple: "semestre 2 FI 2015" - """ - formation: Formation = Formation.query.get_or_404(sem["formation_id"]) - parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) - return "%s %s %s %s" % ( - parcours.SESSION_NAME, # eg "semestre" - sem["semestre_id"], # eg 2 - sem.get("modalite", ""), # eg FI ou FC - sem["annee_debut"], # eg 2015 - ) - - -# ---------------------------------------------------------------------------------------- -class JuryPE(object): - """Classe memorisant toutes les informations necessaires pour etablir un jury de PE. Modele - base sur NotesTable - - Attributs : - diplome : l'annee d'obtention du diplome DUT et du jury de PE (generalement fevrier XXXX) - - juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives + - celles des semestres valides à prendre en compte permettant le calcul des moyennes ... - {'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }} - Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue - et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif - - """ - - # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et - # leur affichage dans les avis latex - PARCOURS = { - "S1": { - "aggregat": ["S1"], - "ordre": 1, - "affichage_court": "S1", - "affichage_long": "Semestre 1", - }, - "S2": { - "aggregat": ["S2"], - "ordre": 2, - "affichage_court": "S2", - "affichage_long": "Semestre 2", - }, - "S3": { - "aggregat": ["S3"], - "ordre": 4, - "affichage_court": "S3", - "affichage_long": "Semestre 3", - }, - "S4": { - "aggregat": ["S4"], - "ordre": 5, - "affichage_court": "S4", - "affichage_long": "Semestre 4", - }, - "1A": { - "aggregat": ["S1", "S2"], - "ordre": 3, - "affichage_court": "1A", - "affichage_long": "1ère année", - }, - "2A": { - "aggregat": ["S3", "S4"], - "ordre": 6, - "affichage_court": "2A", - "affichage_long": "2ème année", - }, - "3S": { - "aggregat": ["S1", "S2", "S3"], - "ordre": 7, - "affichage_court": "S1+S2+S3", - "affichage_long": "DUT du semestre 1 au semestre 3", - }, - "4S": { - "aggregat": ["S1", "S2", "S3", "S4"], - "ordre": 8, - "affichage_court": "DUT", - "affichage_long": "DUT (tout semestre inclus)", - }, - } - - # ------------------------------------------------------------------------------------------------------------------ - def __init__(self, semBase): - """ - Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit : - 1. l'année d'obtention du DUT, - 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. - - Args: - semBase: le dictionnaire sem donnant la base du jury - meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, - si False, permet des programmes differents - """ - self.semTagDict = ( - {} - ) # Les semestres taggués à la base des calculs de moyenne par tag - self.setTagDict = ( - {} - ) # dictionnaire récapitulant les semTag impliqués dans le jury de la forme { 'formsemestre_id' : object Semestre_tag - self.promoTagDict = {} - - # L'année du diplome - self.diplome = get_annee_diplome_semestre(semBase) - - # Un zip où ranger les fichiers générés: - self.NOM_EXPORT_ZIP = "Jury_PE_%s" % self.diplome - self.zipdata = io.BytesIO() - self.zipfile = ZipFile(self.zipdata, "w") - - # - self.ETUDINFO_DICT = {} # Les infos sur les étudiants - self.PARCOURSINFO_DICT = {} # Les parcours des étudiants - self.syntheseJury = {} # Le jury de synthèse - - self.semestresDeScoDoc = sco_formsemestre.do_formsemestre_list() - - # Calcul du jury PE - self.exe_calculs_juryPE(semBase) - self.synthetise_juryPE() - - # Export des données => mode 1 seule feuille -> supprimé - # filename = self.NOM_EXPORT_ZIP + "jurySyntheseDict_" + str(self.diplome) + '.xls' - # self.xls = self.table_syntheseJury(mode="singlesheet") - # self.add_file_to_zip(filename, self.xls.excel()) - - # Fabrique 1 fichier excel résultat avec 1 seule feuille => trop gros - filename = self.NOM_EXPORT_ZIP + "_jurySyntheseDict" + scu.XLSX_SUFFIX - self.xlsV2 = self.table_syntheseJury(mode="multiplesheet") - if self.xlsV2: - self.add_file_to_zip(filename, self.xlsV2.excel()) - - # Pour debug - # self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug - - # ------------------------------------------------------------------------------------------------------------------ - def add_file_to_zip(self, filename, data, path=""): - """Add a file to our zip - All files under NOM_EXPORT_ZIP/ - path may specify a subdirectory - """ - path_in_zip = os.path.join(self.NOM_EXPORT_ZIP, path, filename) - self.zipfile.writestr(path_in_zip, data) - - # ------------------------------------------------------------------------------------------------------------------ - def get_zipped_data(self): - """returns file-like data with a zip of all generated (CSV) files. - Reset file cursor at the beginning ! - """ - if self.zipfile: - self.zipfile.close() - self.zipfile = None - self.zipdata.seek(0) - return self.zipdata - - # **************************************************************************************************************** # - # Lancement des différentes actions permettant le calcul du jury PE - # **************************************************************************************************************** # - def exe_calculs_juryPE(self, semBase): - # Liste des étudiants à traiter pour identifier ceux qui seront diplômés - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Recherche et chargement des étudiants diplômés en %d" - % (self.diplome) - ) - self.get_etudiants_in_jury( - semBase, avec_meme_formation=False - ) # calcul des coSemestres - - # Les semestres impliqués (ceux valides pour les étudiants à traiter) - # ------------------------------------------------------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print("*** Création des semestres taggués") - self.get_semtags_in_jury() - if pe_tools.PE_DEBUG: - for semtag in self.semTagDict.values(): # Export - filename = self.NOM_EXPORT_ZIP + semtag.nom + ".csv" - self.zipfile.writestr(filename, semtag.str_tagtable()) - # self.export_juryPEDict() - - # Les moyennes sur toute la scolarité - # ----------------------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Création des moyennes sur différentes combinaisons de semestres et différents groupes d'étudiant" - ) - self.get_settags_in_jury() - if pe_tools.PE_DEBUG: - for settagdict in self.setTagDict.values(): # Export - for settag in settagdict.values(): - filename = self.NOM_EXPORT_ZIP + semtag.nom + ".csv" - self.zipfile.writestr(filename, semtag.str_tagtable()) - # self.export_juryPEDict() - - # Les interclassements - # -------------------- - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres" - ) - self.get_promotags_in_jury() - - # **************************************************************************************************************** # - # Fonctions relatives à la liste des étudiants à prendre en compte dans le jury - # **************************************************************************************************************** # - - # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_in_jury(self, semBase, avec_meme_formation=False): - """ - Calcule la liste des étudiants à prendre en compte dans le jury et la renvoie sous la forme - """ - # Les cosemestres donnant lieu à meme année de diplome - coSems = get_cosemestres_diplomants( - semBase, avec_meme_formation=avec_meme_formation - ) # calcul des coSemestres - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "1) Recherche des coSemestres -> %d trouvés" % len(coSems) - ) - - # Les étudiants inscrits dans les cosemestres - if pe_tools.PE_DEBUG: - pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres") - listEtudId = self.get_etudiants_dans_semestres( - coSems - ) # étudiants faisant parti des cosemestres - if pe_tools.PE_DEBUG: - pe_tools.pe_print(" => %d étudiants trouvés" % len(listEtudId)) - - # L'analyse des parcours étudiants pour déterminer leur année effective de diplome avec prise en compte des redoublements, des abandons, .... - if pe_tools.PE_DEBUG: - pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") - - for no_etud, etudid in enumerate(listEtudId): - self.add_etudiants(etudid) - if pe_tools.PE_DEBUG: - if (no_etud + 1) % 10 == 0: - pe_tools.pe_print((no_etud + 1), " ", end="") - pe_tools.pe_print() - - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - " => %d étudiants à diplômer en %d" - % (len(self.get_etudids_du_jury()), self.diplome) - ) - pe_tools.pe_print( - " => %d étudiants éliminer pour abandon" - % (len(listEtudId) - len(self.get_etudids_du_jury())) - ) - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_etudiants_dans_semestres(self, semsListe): - """Renvoie la liste des etudid des etudiants inscrits à l'un des semestres de la liste fournie en paramètre - en supprimant les doublons (i.e. un même étudiant qui apparaîtra 2 fois)""" - - etudiants = [] - for sem in semsListe: # pour chacun des semestres de la liste - nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"]) - etudiantsDuSemestre = ( - nt.get_etudids() - ) # identification des etudiants du semestre - - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - " --> chargement du semestre %s : %d etudiants " - % (sem["formsemestre_id"], len(etudiantsDuSemestre)) - ) - etudiants.extend(etudiantsDuSemestre) - - return list(set(etudiants)) # suppression des doublons - - # ------------------------------------------------------------------------------------------------------------------ - def get_etudids_du_jury(self, ordre="aucun"): - """Renvoie la liste de tous les étudiants (concrètement leur etudid) - participant au jury c'est à dire, ceux dont la date du 'jury' est self.diplome - et n'ayant pas abandonné. - Si l'ordre est précisé, donne une liste etudid dont le nom, prenom trié par ordre alphabétique - """ - etudids = [ - etudid - for (etudid, donnees) in self.PARCOURSINFO_DICT.items() - if donnees["diplome"] == self.diplome and donnees["abandon"] == False - ] - if ordre == "alphabetique": # Tri alphabétique - etudidsAvecNom = [ - (etudid, etud["nom"] + "/" + etud["prenom"]) - for (etudid, etud) in self.PARCOURSINFO_DICT.items() - if etudid in etudids - ] - etudidsAvecNomTrie = sorted(etudidsAvecNom, key=lambda col: col[1]) - etudids = [etud[0] for etud in etudidsAvecNomTrie] - return etudids - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def add_etudiants(self, etudid): - """Ajoute un étudiant (via son etudid) au dictionnaire de synthèse jurydict. - L'ajout consiste à : - > insérer une entrée pour l'étudiant en mémorisant ses infos (get_etudInfo), - avec son nom, prénom, etc... - > à analyser son parcours, pour vérifier s'il n'a pas abandonné l'IUT en cours de route => clé abandon - > à chercher ses semestres valides (formsemestre_id) et ses années valides (formannee_id), - c'est à dire ceux pour lesquels il faudra prendre en compte ses notes dans les calculs de moyenne (type 1A=S1+S2/2) - """ - - if etudid not in self.PARCOURSINFO_DICT: - etud = self.get_cache_etudInfo_d_un_etudiant( - etudid - ) # On charge les données de l'étudiant - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(etud["nom"] + " " + etud["prenom"], end="") - - self.PARCOURSINFO_DICT[etudid] = { - "etudid": etudid, # les infos sur l'étudiant - "nom": etud["nom"], # Ajout à la table jury - } - - # Analyse du parcours de l'étudiant - - # Sa date prévisionnelle de diplome - self.PARCOURSINFO_DICT[etudid][ - "diplome" - ] = self.calcul_anneePromoDUT_d_un_etudiant(etudid) - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print( - "promo=" + str(self.PARCOURSINFO_DICT[etudid]["diplome"]), end="" - ) - - # Est-il réorienté ou démissionnaire ? - self.PARCOURSINFO_DICT[etudid][ - "abandon" - ] = self.est_un_etudiant_reoriente_ou_demissionnaire(etudid) - - # A-t-il arrêté de lui-même sa formation avant la fin ? - etatD = self.est_un_etudiant_disparu(etudid) - if etatD == True: - self.PARCOURSINFO_DICT[etudid]["abandon"] = True - # dans le jury ne seront traités que les étudiants ayant la date attendue de diplome et n'ayant pas abandonné - - # Quels sont ses semestres validant (i.e ceux dont les notes doivent être prises en compte pour le jury) - # et s'ils existent quelles sont ses notes utiles ? - sesFormsemestre_idValidants = [ - self.get_Fid_d_un_Si_valide_d_un_etudiant(etudid, nom_sem) - for nom_sem in JuryPE.PARCOURS["4S"][ - "aggregat" - ] # Recherche du formsemestre_id de son Si valide (ou a défaut en cours) - ] - for i, nom_sem in enumerate(JuryPE.PARCOURS["4S"]["aggregat"]): - fid = sesFormsemestre_idValidants[i] - self.PARCOURSINFO_DICT[etudid][nom_sem] = fid # ['formsemestre_id'] - if fid != None and pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(nom_sem + "=" + str(fid), end="") - # self.get_moyennesEtClassements_par_semestre_d_un_etudiant( etudid, fid ) - - # Quelles sont ses années validantes ('1A', '2A') et ses parcours (3S, 4S) validants ? - for parcours in ["1A", "2A", "3S", "4S"]: - lesSemsDuParcours = JuryPE.PARCOURS[parcours][ - "aggregat" - ] # les semestres du parcours : par ex. ['S1', 'S2', 'S3'] - lesFidsValidantDuParcours = [ - sesFormsemestre_idValidants[ - JuryPE.PARCOURS["4S"]["aggregat"].index(nom_sem) - ] - for nom_sem in lesSemsDuParcours # par ex. ['SEM4532', 'SEM567', ...] - ] - parcours_incomplet = ( - sum([fid == None for fid in lesFidsValidantDuParcours]) > 0 - ) - - if not parcours_incomplet: - self.PARCOURSINFO_DICT[etudid][ - parcours - ] = lesFidsValidantDuParcours[-1] - else: - self.PARCOURSINFO_DICT[etudid][parcours] = None - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print( - parcours + "=" + str(self.PARCOURSINFO_DICT[etudid][parcours]), - end="", - ) - - # if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - # print - - # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_reoriente_ou_demissionnaire(self, etudid): - """Renvoie True si l'étudiant est réorienté (NAR) ou démissionnaire (DEM)""" - from app.scodoc import sco_report - - reponse = False - etud = self.get_cache_etudInfo_d_un_etudiant(etudid) - (_, parcours) = sco_report.get_code_cursus_etud( - etud["etudid"], sems=etud["sems"] - ) - if ( - len(codes_cursus.CODES_SEM_REO & set(parcours.values())) > 0 - ): # Eliminé car NAR apparait dans le parcours - reponse = True - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(" -> à éliminer car réorienté (NAR)") - if "DEM" in list(parcours.values()): # Eliminé car DEM - reponse = True - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: - pe_tools.pe_print(" -> à éliminer car DEM") - return reponse - - # ------------------------------------------------------------------------------------------------------------------ - def est_un_etudiant_disparu(self, etudid): - """Renvoie True si l'étudiant n'a pas achevé la formation à l'IUT et a disparu des listes, sans - pour autant avoir été indiqué NAR ou DEM ; recherche son dernier semestre validé et regarde s'il - n'existe pas parmi les semestres existants dans scodoc un semestre postérieur (en terme de date de - début) de n° au moins égal à celui de son dernier semestre valide dans lequel il aurait pu - s'inscrire mais ne l'a pas fait.""" - sessems = self.get_semestresDUT_d_un_etudiant( - etudid - ) # les semestres de l'étudiant - sonDernierSidValide = self.get_dernier_semestre_id_valide_d_un_etudiant(etudid) - - sesdates = [ - pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems - ] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant - if sesdates: - lastdate = max(sesdates) # date de fin de l'inscription la plus récente - else: - return False - - # if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print(" derniere inscription = ", lastDateSem) - - if sonDernierSidValide is None: - # si l'étudiant n'a validé aucun semestre, les prend tous ? (à vérifier) - semestresSuperieurs = self.semestresDeScoDoc - else: - semestresSuperieurs = [ - sem - for sem in self.semestresDeScoDoc - if sem["semestre_id"] > sonDernierSidValide - ] # Semestre de rang plus élevé que son dernier sem valide - datesDesSemestresSuperieurs = [ - pe_tagtable.conversionDate_StrToDate(sem["date_debut"]) - for sem in semestresSuperieurs - ] - datesDesSemestresPossibles = [ - date_deb for date_deb in datesDesSemestresSuperieurs if date_deb >= lastdate - ] # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant et de niveau plus élevé que le dernier semestre valide de l'étudiant - if ( - len(datesDesSemestresPossibles) > 0 - ): # etudiant ayant disparu de la circulation - # if PETable.AFFICHAGE_DEBUG_PE == True : - # pe_tools.pe_print(" -> à éliminer car des semestres où il aurait pu s'inscrire existent ") - # pe_tools.pe_print(pe_tools.print_semestres_description( datesDesSemestresPossibles.values() )) - return True - else: - return False - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_dernier_semestre_id_valide_d_un_etudiant(self, etudid): - """Renvoie le n° (semestre_id) du dernier semestre validé par un étudiant fourni par son etudid - et None si aucun semestre n'a été validé - """ - from app.scodoc import sco_report - - etud = self.get_cache_etudInfo_d_un_etudiant(etudid) - (code, parcours) = sco_report.get_code_cursus_etud( - etud["etudid"], sems=etud["sems"] - ) # description = '1234:A', parcours = {1:ADM, 2:NAR, ...} - sonDernierSemestreValide = max( - [ - int(cle) - for (cle, code) in parcours.items() - if code in codes_cursus.CODES_SEM_VALIDES - ] - + [0] - ) # n° du dernier semestre valide, 0 sinon - return sonDernierSemestreValide if sonDernierSemestreValide > 0 else None - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_Fid_d_un_Si_valide_d_un_etudiant(self, etudid, nom_semestre): - """Récupère le formsemestre_id valide d'un étudiant fourni son etudid à un semestre DUT de n° semestre_id - donné. Si le semestre est en cours (pas encore de jury), renvoie le formsemestre_id actuel. - """ - semestre_id = JuryPE.PARCOURS["4S"]["aggregat"].index(nom_semestre) + 1 - sesSi = self.get_semestresDUT_d_un_etudiant( - etudid, semestre_id - ) # extrait uniquement les Si par ordre temporel décroissant - - if len(sesSi) > 0: # S'il a obtenu au moins une note - # mT = sesMoyennes[0] - leFid = sesSi[0]["formsemestre_id"] - for i, sem in enumerate( - sesSi - ): # Parcours des éventuels semestres précédents - nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"]) - dec = nt.get_etud_decision_sem( - etudid - ) # quelle est la décision du jury ? - if dec and (dec["code"] in codes_cursus.CODES_SEM_VALIDES): - # isinstance( sesMoyennes[i+1], float) and - # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" - leFid = sem["formsemestre_id"] - else: - leFid = None - return leFid - - # **************************************************************************************************************** # - # Traitements des semestres impliqués dans le jury - # **************************************************************************************************************** # - - # ------------------------------------------------------------------------------------------------------------------ - def get_semtags_in_jury(self): - """ - Créé les semestres tagués relatifs aux résultats des étudiants à prendre en compte dans le jury. - Calcule les moyennes et les classements de chaque semestre par tag et les statistiques de ces semestres. - """ - lesFids = self.get_formsemestreids_du_jury( - self.get_etudids_du_jury(), liste_semestres=["S1", "S2", "S3", "S4"] - ) - for i, fid in enumerate(lesFids): - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "%d) Semestre taggué %s (avec classement dans groupe)" - % (i + 1, fid) - ) - self.add_semtags_in_jury(fid) - - # ------------------------------------------------------------------------------------------------------------------ - def add_semtags_in_jury(self, fid): - """Crée si nécessaire un semtag et le mémorise dans self.semTag ; - charge également les données des nouveaux étudiants qui en font partis. - """ - # Semestre taggué avec classement dans le groupe - if fid not in self.semTagDict: - nt = self.get_cache_notes_d_un_semestre(fid) - - # Création du semestres - self.semTagDict[fid] = pe_semestretag.SemestreTag( - nt, nt.sem - ) # Création du pesemestre associé - self.semTagDict[fid].comp_data_semtag() - lesEtudids = self.semTagDict[fid].get_etudids() - - lesEtudidsManquants = [] - for etudid in lesEtudids: - if ( - etudid not in self.PARCOURSINFO_DICT - ): # Si l'étudiant n'a pas été pris en compte dans le jury car déjà diplômé ou redoublant - lesEtudidsManquants.append(etudid) - # self.get_cache_etudInfo_d_un_etudiant(etudid) - self.add_etudiants( - etudid - ) # Ajoute les élements de parcours de l'étudiant - - nbinscrit = self.semTagDict[fid].get_nbinscrits() - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - " - %d étudiants classés " % (nbinscrit) - + ": " - + ",".join( - [etudid for etudid in self.semTagDict[fid].get_etudids()] - ) - ) - if lesEtudidsManquants: - pe_tools.pe_print( - " - dont %d étudiants manquants ajoutés aux données du jury" - % (len(lesEtudidsManquants)) - + ": " - + ", ".join(lesEtudidsManquants) - ) - pe_tools.pe_print(" - Export csv") - filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv" - self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable()) - - # ---------------------------------------------------------------------------------------------------------------- - def get_formsemestreids_du_jury(self, etudids, liste_semestres="4S"): - """Renvoie la liste des formsemestre_id validants des étudiants en parcourant les semestres valides des étudiants mémorisés dans - self.PARCOURSINFO_DICT. - Les étudiants sont identifiés par leur etudic donnés dans la liste etudids (généralement self.get_etudids_in_jury() ). - La liste_semestres peut être une liste ou une chaine de caractères parmi : - * None => tous les Fids validant - * 'Si' => le ième 1 semestre - * 'iA' => l'année i = ['S1, 'S2'] ou ['S3', 'S4'] - * '3S', '4S' => fusion des semestres - * [ 'Si', 'iA' , ... ] => une liste combinant les formats précédents - """ - champs_possibles = list(JuryPE.PARCOURS.keys()) - if ( - not isinstance(liste_semestres, list) - and not isinstance(liste_semestres, str) - and liste_semestres not in champs_possibles - ): - raise ValueError( - "Probleme de paramètres d'appel dans pe_jurype.JuryPE.get_formsemestreids_du_jury" - ) - - if isinstance(liste_semestres, list): - res = [] - for elmt in liste_semestres: - res.extend(self.get_formsemestreids_du_jury(etudids, elmt)) - return list(set(res)) - - # si liste_sem est un nom de parcours - nom_sem = liste_semestres - # if nom_sem in ['1A', '2A', '3S', '4S'] : - # return self.get_formsemestreids_du_jury(etudids, JuryPE.PARCOURS[nom_sem] ) - # else : - fids = { - self.PARCOURSINFO_DICT[etudid][nom_sem] - for etudid in etudids - if self.PARCOURSINFO_DICT[etudid][nom_sem] != None - } - - return list(fids) - - # **************************************************************************************************************** # - # Traitements des parcours impliquées dans le jury - # **************************************************************************************************************** # - - # # ---------------------------------------------------------------------------------------------------------------- - # def get_antags_in_jury(self, avec_affichage_debug=True ): - # """Construit les settag associés aux années 1A et 2A du jury""" - # lesAnnees = {'1A' : ['S1', 'S2'], '2A' : ['S3', 'S4'] } - # for nom_annee in lesAnnees: - # lesAidDesAnnees = self.get_anneeids_du_jury(annee= nom_annee) # les annee_ids des étudiants du jury - # for aid in lesAidDesAnnees: - # fidSemTagFinal = JuryPE.convert_aid_en_fid( aid ) - # lesEtudisDelAnnee = self.semTagDict[ fidSemTagFinal ].get_etudids() # les etudiants sont ceux inscrits dans le semestre final de l'année - # parcoursDesEtudiants = { etudid : self.PARCOURSINFO_DICT[etudid] for etudid in lesEtudisDelAnnee } # les parcours des etudid aka quels semestres sont à prendre en compte - # - # lesFidsDesEtudiants = self.get_formsemestreids_du_jury(lesEtudisDelAnnee, nom_annee) # les formsemestres_id à prendre en compte pour les moyennes - # # Manque-t-il des semtag associés ; si oui, les créé - # pe_tools.pe_print(aid, lesFidsDesEtudiants) - # for fid in lesFidsDesEtudiants: - # self.add_semtags_in_jury(fid, avec_affichage_debug=avec_affichage_debug) - # lesSemTagDesEtudiants = { fid: self.semTagDict[fid] for fid in lesFidsDesEtudiants } - # - # # Tous les semtag nécessaires pour ses étudiants avec ajout éventuel s'ils n'ont pas été chargés - # pe_tools.pe_print(" -> Création de l'année tagguée " + str( aid )) - # #settag_id, short_name, listeEtudId, groupe, listeSemAAggreger, ParcoursEtudDict, SemTagDict, with_comp_moy=True) - # self.anTagDict[ aid ] = pe_settag.SetTag( aid, "Annee " + self.semTagDict[fidSemTagFinal].short_name, \ - # lesEtudisDelAnnee, 'groupe', lesAnnees[ nom_annee ], parcoursDesEtudiants, lesSemTagDesEtudiants ) - # self.anTagDict[ aid ].comp_data_settag() # calcul les moyennes - - # **************************************************************************************************************** # - # Traitements des moyennes sur différentes combinaisons de parcours 1A, 2A, 3S et 4S, - # impliquées dans le jury - # **************************************************************************************************************** # - - def get_settags_in_jury(self): - """Calcule les moyennes sur la totalité du parcours (S1 jusqu'à S3 ou S4) - en classant les étudiants au sein du semestre final du parcours (même S3, même S4, ...) - """ - - # Par groupe : - # combinaisons = { 'S1' : ['S1'], 'S2' : ['S2'], 'S3' : ['S3'], 'S4' : ['S4'], \ - # '1A' : ['S1', 'S2'], '2A' : ['S3', 'S4'], - # '3S' : ['S1', 'S2', 'S3'], '4S' : ['S1', 'S2', 'S3', 'S4'] } - - # ---> sur 2 parcours DUT (cas S3 fini, cas S4 fini) - combinaisons = ["1A", "2A", "3S", "4S"] - for i, nom in enumerate(combinaisons): - parcours = JuryPE.PARCOURS[nom][ - "aggregat" - ] # La liste des noms de semestres (S1, S2, ...) impliqués dans l'aggrégat - - # Recherche des parcours possibles par le biais de leur Fid final - fids_finaux = self.get_formsemestreids_du_jury( - self.get_etudids_du_jury(), nom - ) # les formsemestre_ids validant finaux des étudiants du jury - - if len(fids_finaux) > 0: # S'il existe des parcours validant - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print("%d) Fusion %s avec" % (i + 1, nom)) - - if nom not in self.setTagDict: - self.setTagDict[nom] = {} - - for fid in fids_finaux: - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print(" - semestre final %s" % (fid)) - settag = pe_settag.SetTag( - nom, parcours=parcours - ) # Le set tag fusionnant les données - etudiants = self.semTagDict[ - fid - ].get_etudids() # Les étudiants du sem final - - # ajoute les étudiants au semestre - settag.set_Etudiants( - etudiants, - self.PARCOURSINFO_DICT, - self.ETUDINFO_DICT, - nom_sem_final=self.semTagDict[fid].nom, - ) - - # manque-t-il des semestres ? Si oui, les ajoute au jurype puis au settag - for ffid in settag.get_Fids_in_settag(): - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print( - " -> ajout du semestre tagué %s" % (ffid) - ) - self.add_semtags_in_jury(ffid) - settag.set_SemTagDict( - self.semTagDict - ) # ajoute les semestres au settag - - settag.comp_data_settag() # Calcul les moyennes, les rangs, .. - - self.setTagDict[nom][fid] = settag # Mémorise le résultat - - else: - if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print("%d) Pas de fusion %s possible" % (i + 1, nom)) - - def get_promotags_in_jury(self): - """Calcule les aggrégats en interclassant les étudiants du jury (les moyennes ont déjà été calculées en amont)""" - - lesEtudids = self.get_etudids_du_jury() - - for i, nom in enumerate(JuryPE.PARCOURS.keys()): - settag = pe_settag.SetTagInterClasse(nom, diplome=self.diplome) - nbreEtudInscrits = settag.set_Etudiants( - lesEtudids, self.PARCOURSINFO_DICT, self.ETUDINFO_DICT - ) - if nbreEtudInscrits > 0: - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "%d) %s avec interclassement sur la promo" % (i + 1, nom) - ) - if nom in ["S1", "S2", "S3", "S4"]: - settag.set_SetTagDict(self.semTagDict) - else: # cas des aggrégats - settag.set_SetTagDict(self.setTagDict[nom]) - settag.comp_data_settag() - self.promoTagDict[nom] = settag - else: - if pe_tools.PE_DEBUG: - pe_tools.pe_print( - "%d) Pas d'interclassement %s sur la promo faute de notes" - % (i + 1, nom) - ) - - # **************************************************************************************************************** # - # Méthodes pour la synthèse du juryPE - # ***************************************************************************************************************** - def synthetise_juryPE(self): - """Synthétise tous les résultats du jury PE dans un dictionnaire""" - self.syntheseJury = {} - for etudid in self.get_etudids_du_jury(): - etudinfo = self.ETUDINFO_DICT[etudid] - self.syntheseJury[etudid] = { - "nom": etudinfo["nom"], - "prenom": etudinfo["prenom"], - "civilite": etudinfo["civilite"], - "civilite_str": etudinfo["civilite_str"], - "age": str(pe_tools.calcul_age(etudinfo["date_naissance"])), - "lycee": etudinfo["nomlycee"] - + ( - " (" + etudinfo["villelycee"] + ")" - if etudinfo["villelycee"] != "" - else "" - ), - "bac": etudinfo["bac"], - "code_nip": etudinfo["code_nip"], # pour la photo - "entree": self.get_dateEntree(etudid), - "promo": self.diplome, - } - # Le parcours - self.syntheseJury[etudid]["parcours"] = self.get_parcoursIUT( - etudid - ) # liste des semestres - self.syntheseJury[etudid]["nbSemestres"] = len( - self.syntheseJury[etudid]["parcours"] - ) # nombre de semestres - - # Ses résultats - for nom in JuryPE.PARCOURS: # S1, puis S2, puis 1A - # dans le groupe : la table tagguée dans les semtag ou les settag si aggrégat - self.syntheseJury[etudid][nom] = {"groupe": {}, "promo": {}} - if ( - self.PARCOURSINFO_DICT[etudid][nom] != None - ): # Un parcours valide existe - if nom in ["S1", "S2", "S3", "S4"]: - tagtable = self.semTagDict[self.PARCOURSINFO_DICT[etudid][nom]] - else: - tagtable = self.setTagDict[nom][ - self.PARCOURSINFO_DICT[etudid][nom] - ] - for tag in tagtable.get_all_tags(): - self.syntheseJury[etudid][nom]["groupe"][ - tag - ] = tagtable.get_resultatsEtud( - tag, etudid - ) # Le tuple des résultats - - # interclassé dans la promo - tagtable = self.promoTagDict[nom] - for tag in tagtable.get_all_tags(): - self.syntheseJury[etudid][nom]["promo"][ - tag - ] = tagtable.get_resultatsEtud(tag, etudid) - - def get_dateEntree(self, etudid): - """Renvoie l'année d'entrée de l'étudiant à l'IUT""" - # etudinfo = self.ETUDINFO_DICT[etudid] - semestres = self.get_semestresDUT_d_un_etudiant(etudid) - if semestres: - # le 1er sem à l'IUT - return semestres[0]["annee_debut"] - else: - return "" - - def get_parcoursIUT(self, etudid): - """Renvoie une liste d'infos sur les semestres du parcours d'un étudiant""" - # etudinfo = self.ETUDINFO_DICT[etudid] - sems = self.get_semestresDUT_d_un_etudiant(etudid) - - infos = [] - for sem in sems: - nomsem = comp_nom_semestre_dans_parcours(sem) - infos.append( - { - "nom_semestre_dans_parcours": nomsem, - "titreannee": sem["titreannee"], - "formsemestre_id": sem["formsemestre_id"], # utile dans le futur ? - } - ) - return infos - - # **************************************************************************************************************** # - # Méthodes d'affichage pour debug - # **************************************************************************************************************** # - def str_etudiants_in_jury(self, delim=";"): - # En tete: - entete = ["Id", "Nom", "Abandon", "Diplome"] - for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]: - entete += [nom_sem, "descr"] - chaine = delim.join(entete) + "\n" - - for etudid in self.PARCOURSINFO_DICT: - donnees = self.PARCOURSINFO_DICT[etudid] - # pe_tools.pe_print(etudid, donnees) - # les infos générales - descr = [ - etudid, - donnees["nom"], - str(donnees["abandon"]), - str(donnees["diplome"]), - ] - - # les semestres - for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]: - table = ( - self.semTagDict[donnees[nom_sem]].nom - if donnees[nom_sem] in self.semTagDict - else "manquant" - ) - descr += [ - donnees[nom_sem] if donnees[nom_sem] != None else "manquant", - table, - ] - - chaine += delim.join(descr) + "\n" - return chaine - - # - def export_juryPEDict(self): - """Export csv de self.PARCOURSINFO_DICT""" - fichier = "juryParcoursDict_" + str(self.diplome) - pe_tools.pe_print(" -> Export de " + fichier) - filename = self.NOM_EXPORT_ZIP + fichier + ".csv" - self.zipfile.writestr(filename, self.str_etudiants_in_jury()) - - def get_allTagForAggregat(self, nom_aggregat): - """Extrait du dictionnaire syntheseJury la liste des tags d'un semestre ou - d'un aggrégat donné par son nom (S1, S2, S3 ou S4, 1A, ...). Renvoie [] si aucun tag. - """ - taglist = set() - for etudid in self.get_etudids_du_jury(): - taglist = taglist.union( - set(self.syntheseJury[etudid][nom_aggregat]["groupe"].keys()) - ) - taglist = taglist.union( - set(self.syntheseJury[etudid][nom_aggregat]["promo"].keys()) - ) - return list(taglist) - - def get_allTagInSyntheseJury(self): - """Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag""" - allTags = set() - for nom in JuryPE.PARCOURS.keys(): - allTags = allTags.union(set(self.get_allTagForAggregat(nom))) - return sorted(list(allTags)) if len(allTags) > 0 else [] - - 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 - - # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom - donnees_tries = sorted( - [ - ( - etudid, - self.syntheseJury[etudid]["nom"] - + " " - + self.syntheseJury[etudid]["prenom"], - ) - for etudid in self.syntheseJury.keys() - ], - key=lambda c: c[1], - ) - etudids = [e[0] for e in donnees_tries] - if not etudids: # Si pas d'étudiants - T = GenTable( - columns_ids=["pas d'étudiants"], - rows=[], - titles={"pas d'étudiants": "pas d'étudiants"}, - html_sortable=True, - xls_sheet_name="dut", - ) - sT.add_genTable("dut", T) - return sT - - # Si des étudiants - maxParcours = max( - [self.syntheseJury[etudid]["nbSemestres"] for etudid in etudids] - ) - - infos = ["civilite", "nom", "prenom", "age", "nbSemestres"] - entete = ["etudid"] - entete.extend(infos) - entete.extend(["P%d" % i for i in range(1, maxParcours + 1)]) - champs = [ - "note", - "class groupe", - "class promo", - "min/moy/max groupe", - "min/moy/max promo", - ] - - # Les aggrégats à afficher par ordre tel que indiqué dans le dictionnaire parcours - aggregats = list(JuryPE.PARCOURS.keys()) # ['S1', 'S2', ..., '1A', '4S'] - aggregats = sorted( - aggregats, key=lambda t: JuryPE.PARCOURS[t]["ordre"] - ) # Tri des aggrégats - - if mode == "multiplesheet": - allSheets = ( - self.get_allTagInSyntheseJury() - ) # tous les tags de syntheseJuryDict - allSheets = sorted(allSheets) # Tri des tags par ordre alphabétique - for ( - sem - ) in aggregats: # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S'] - entete.extend(["%s %s" % (sem, champ) for champ in champs]) - else: # "singlesheet" - allSheets = ["singlesheet"] - for ( - sem - ) in aggregats: # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S'] - tags = self.get_allTagForAggregat(sem) - entete.extend( - ["%s %s %s" % (sem, tag, champ) for tag in tags for champ in champs] - ) - - columns_ids = entete # les id et les titres de colonnes sont ici identiques - titles = {i: i for i in columns_ids} - - for ( - sheet - ) in ( - allSheets - ): # Pour tous les sheets à générer (1 si singlesheet, autant que de tags si multiplesheet) - rows = [] - for etudid in etudids: - e = self.syntheseJury[etudid] - # Les info générales: - row = { - "etudid": etudid, - "civilite": e["civilite"], - "nom": e["nom"], - "prenom": e["prenom"], - "age": e["age"], - "nbSemestres": e["nbSemestres"], - } - # Les parcours: P1, P2, ... - n = 1 - for p in e["parcours"]: - row["P%d" % n] = p["titreannee"] - n += 1 - # if self.syntheseJury[etudid]['nbSemestres'] < maxParcours: - # descr += delim.join( ['']*( maxParcours -self.syntheseJury[etudid]['nbSemestres']) ) + delim - for sem in aggregats: # JuryPE.PARCOURS.keys(): - listeTags = ( - self.get_allTagForAggregat(sem) - if mode == "singlesheet" - else [sheet] - ) - for tag in listeTags: - if tag in self.syntheseJury[etudid][sem]["groupe"]: - resgroupe = self.syntheseJury[etudid][sem]["groupe"][ - tag - ] # tuple - else: - resgroupe = (None, None, None, None, None, None, None) - if tag in self.syntheseJury[etudid][sem]["promo"]: - respromo = self.syntheseJury[etudid][sem]["promo"][tag] - else: - respromo = (None, None, None, None, None, None, None) - - # note = "%2.2f" % resgroupe[0] if isinstance(resgroupe[0], float) else str(resgroupe[0]) - champ = ( - "%s %s " % (sem, tag) - if mode == "singlesheet" - else "%s " % (sem) - ) - row[champ + "note"] = scu.fmt_note(resgroupe[0]) - row[champ + "class groupe"] = "%s / %s" % ( - resgroupe[2], - resgroupe[3], - ) - row[champ + "class promo"] = "%s / %s" % ( - respromo[2], - respromo[3], - ) - row[champ + "min/moy/max groupe"] = "%s / %s / %s" % tuple( - scu.fmt_note(x) - for x in (resgroupe[6], resgroupe[4], resgroupe[5]) - ) - row[champ + "min/moy/max promo"] = "%s / %s / %s" % tuple( - scu.fmt_note(x) - for x in (respromo[6], respromo[4], respromo[5]) - ) - rows.append(row) - - T = GenTable( - columns_ids=columns_ids, - rows=rows, - titles=titles, - html_sortable=True, - xls_sheet_name=sheet, - ) - sT.add_genTable(sheet, T) - - if mode == "singlesheet": - return sT.get_genTable("singlesheet") - else: - return sT - - # **************************************************************************************************************** # - # Méthodes de classe pour gestion d'un cache de données accélérant les calculs / intérêt à débattre - # **************************************************************************************************************** # - - # ------------------------------------------------------------------------------------------------------------------ - def get_cache_etudInfo_d_un_etudiant(self, etudid): - """Renvoie les informations sur le parcours d'un étudiant soit en les relisant depuis - ETUDINFO_DICT si mémorisée soit en les chargeant et en les mémorisant - """ - if etudid not in self.ETUDINFO_DICT: - self.ETUDINFO_DICT[etudid] = sco_etud.get_etud_info( - etudid=etudid, filled=True - )[0] - return self.ETUDINFO_DICT[etudid] - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat: - """Charge la table des notes d'un formsemestre""" - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - return res_sem.load_formsemestre_results(formsemestre) - - # ------------------------------------------------------------------------------------------------------------------ - - # ------------------------------------------------------------------------------------------------------------------ - def get_semestresDUT_d_un_etudiant(self, etudid, semestre_id=None): - """Renvoie la liste des semestres DUT d'un étudiant - pour un semestre_id (parmi 1,2,3,4) donné - en fonction de ses infos d'etud (cf. sco_etud.get_etud_info( etudid=etudid, filled=True)[0]), - les semestres étant triés par ordre décroissant. - Si semestre_id == None renvoie tous les semestres""" - etud = self.get_cache_etudInfo_d_un_etudiant(etudid) - if semestre_id == None: - sesSems = [sem for sem in etud["sems"] if 1 <= sem["semestre_id"] <= 4] - else: - sesSems = [sem for sem in etud["sems"] if sem["semestre_id"] == semestre_id] - return sesSems - - # ********************************************** - def calcul_anneePromoDUT_d_un_etudiant(self, etudid) -> int: - """Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid - en fonction de ses semestres de scolarisation""" - semestres = self.get_semestresDUT_d_un_etudiant(etudid) - if semestres: - return max([get_annee_diplome_semestre(sem) for sem in semestres]) - else: - return None - - # ********************************************* - # Fonctions d'affichage pour debug - def get_resultat_d_un_etudiant(self, etudid): - chaine = "" - for nom_sem in ["S1", "S2", "S3", "S4"]: - semtagid = self.PARCOURSINFO_DICT[etudid][ - nom_sem - ] # le formsemestre_id du semestre taggué de l'étudiant - semtag = self.semTagDict[semtagid] - chaine += "Semestre " + nom_sem + str(semtagid) + "\n" - # le détail du calcul tag par tag - # chaine += "Détail du calcul du tag\n" - # chaine += "-----------------------\n" - # for tag in semtag.taglist: - # chaine += "Tag=" + tag + "\n" - # chaine += semtag.str_detail_resultat_d_un_tag(tag, etudid=etudid) + "\n" - # le bilan des tags - chaine += "Bilan des tags\n" - chaine += "--------------\n" - for tag in semtag.taglist: - chaine += ( - tag + ";" + semtag.str_resTag_d_un_etudiant(tag, etudid) + "\n" - ) - chaine += "\n" - return chaine - - def get_date_entree_etudiant(self, etudid) -> str: - """Renvoie la date d'entree d'un étudiant: "1996" """ - annees_debut = [ - int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"] - ] - if annees_debut: - return str(min(annees_debut)) - return "" - - -# ---------------------------------------------------------------------------------------- -# Fonctions - - -# ---------------------------------------------------------------------------------------- -def get_annee_diplome_semestre(sem) -> int: - """Pour un semestre donne, décrit par le biais du dictionnaire sem usuel : - sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...}, - à condition qu'il soit un semestre de formation DUT, - predit l'annee à laquelle sera remis le diplome DUT des etudiants scolarisés dans le semestre - (en supposant qu'il n'y ait plus de redoublement) et la renvoie sous la forme d'un int. - Hypothese : les semestres de 1ere partie d'annee universitaire (comme des S1 ou des S3) s'etalent - sur deux annees civiles - contrairement au semestre de seconde partie d'annee universitaire (comme - des S2 ou des S4). - Par exemple : - > S4 debutant en 2016 finissant en 2016 => diplome en 2016 - > S3 debutant en 2015 et finissant en 2016 => diplome en 2016 - > S3 (decale) debutant en 2015 et finissant en 2015 => diplome en 2016 - La regle de calcul utilise l'annee_fin du semestre sur le principe suivant : - nbreSemRestant = nombre de semestres restant avant diplome - nbreAnneeRestant = nombre d'annees restant avant diplome - 1 - delta = 0 si semestre de 1ere partie d'annee / 1 sinon - decalage = active ou desactive un increment a prendre en compte en cas de semestre decale - """ - if ( - 1 <= sem["semestre_id"] <= 4 - ): # Si le semestre est un semestre DUT => problème si formation DUT en 1 an ?? - nbreSemRestant = 4 - sem["semestre_id"] - nbreAnRestant = nbreSemRestant // 2 - delta = int(sem["annee_fin"]) - int(sem["annee_debut"]) - decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1 - increment = decalage * (1 - delta) - return int(sem["annee_fin"]) + nbreAnRestant + increment - - -# ---------------------------------------------------------------------------------------- - - -# ---------------------------------------------------------------------------------- -def get_cosemestres_diplomants(semBase, avec_meme_formation=False): - """Partant d'un semestre de Base = {'formsemestre_id': ..., 'semestre_id': ..., 'annee_debut': ...}, - renvoie la liste de tous ses co-semestres (lui-meme inclus) - Par co-semestre, s'entend les semestres : - > dont l'annee predite pour la remise du diplome DUT est la meme - > dont la formation est la même (optionnel) - > ne prenant en compte que les etudiants sans redoublement - """ - tousLesSems = ( - sco_formsemestre.do_formsemestre_list() - ) # tous les semestres memorisés dans scodoc - diplome = get_annee_diplome_semestre(semBase) - - if avec_meme_formation: # si une formation est imposee - nom_formation = str(semBase["formation_id"]) - if pe_tools.PE_DEBUG: - pe_tools.pe_print(" - avec formation imposée : ", nom_formation) - coSems = [ - sem - for sem in tousLesSems - if get_annee_diplome_semestre(sem) == diplome - and sem["formation_id"] == semBase["formation_id"] - ] - else: - if pe_tools.PE_DEBUG: - pe_tools.pe_print(" - toutes formations confondues") - coSems = [ - sem for sem in tousLesSems if get_annee_diplome_semestre(sem) == diplome - ] - - return coSems diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semtag.py similarity index 57% rename from app/pe/pe_semestretag.py rename to app/pe/pe_semtag.py index 8384551c4..dbf0c02ab 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semtag.py @@ -37,110 +37,96 @@ Created on Fri Sep 9 09:15:05 2016 """ from app import db, log -from app.comp import res_sem +from app.comp import res_sem, moy_ue, moy_sem from app.comp.res_compat import NotesTableCompat +from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre from app.models.moduleimpls import ModuleImpl -from app.pe import pe_tagtable -from app.scodoc import codes_cursus from app.scodoc import sco_tag_module -from app.scodoc import sco_utils as scu +from app.scodoc.codes_cursus import UE_SPORT +import app.pe.pe_comp as pe_comp +from app.pe.pe_tabletags import (TableTag, TAGS_RESERVES) - -class SemestreTag(pe_tagtable.TableTag): - """Un SemestreTag représente un tableau de notes (basé sur notesTable) - modélisant les résultats des étudiants sous forme de moyennes par tag. - - Attributs récupérés via des NotesTables : - - nt: le tableau de notes du semestre considéré - - nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions) - - nt.identdict: { etudid : ident } - - liste des moduleimpl { ... 'module_id', ...} - - Attributs supplémentaires : - - inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants - - _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés - - - Attributs hérités de TableTag : - - nom : - - resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...} - - rang - - statistiques - - Redéfinition : - - get_etudids() : les etudids des étudiants non défaillants ni démissionnaires +class SemestreTag(TableTag): + """Un SemestreTag représente les résultats des étudiants à un semestre, en donnant + accès aux moyennes par tag. + Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT. """ - DEBUG = True - # ----------------------------------------------------------------------------- # Fonctions d'initialisation # ----------------------------------------------------------------------------- - def __init__(self, notetable, sem): # Initialisation sur la base d'une notetable - """Instantiation d'un objet SemestreTag à partir d'un tableau de note - et des informations sur le semestre pour le dater + def __init__(self, nom: str, formsemestre_id: int): """ - pe_tagtable.TableTag.__init__( - self, - nom="S%d %s %s-%s" - % ( - sem["semestre_id"], - "ENEPS" - if "ENEPS" in sem["titre"] - else "UFA" - if "UFA" in sem["titre"] - else "FI", - sem["annee_debut"], - sem["annee_fin"], - ), - ) + Args: + nom: Nom à donner au SemestreTag + formsemestre_id: Identifiant du FormSemestre sur lequel il se base + """ + TableTag.__init__(self, nom=nom) - # Les attributs spécifiques - self.nt = notetable + """Le semestre""" + self.formsemestre_id = formsemestre_id + self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - # Les attributs hérités : la liste des étudiants - self.inscrlist = [ - etud - for etud in self.nt.inscrlist - if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT - ] - self.identdict = { - etudid: ident - for (etudid, ident) in self.nt.identdict.items() - if etudid in self.get_etudids() - } # Liste des étudiants non démissionnaires et non défaillants + """Les résultats du semestre""" + self.nt = load_formsemestre_results(self.formsemestre) - # Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards - self.modimpls = [ - modimpl - for modimpl in self.nt.formsemestre.modimpls_sorted - if modimpl.module.ue.type == codes_cursus.UE_STANDARD - ] # la liste des modules (objet modimpl) - self.somme_coeffs = sum( - [ - modimpl.module.coefficient - for modimpl in self.modimpls - if modimpl.module.coefficient is not None - ] - ) + """Les étudiants""" + self.etuds = self.nt.etuds + self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} - # ----------------------------------------------------------------------------- - def comp_data_semtag(self): - """Calcule tous les données numériques associées au semtag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.tagdict = ( - self.do_tagdict() - ) # Dictionnaire résumant les tags et les données (normalisées) des modules du semestre auxquels ils sont liés + """Les notes, les modules implémentés triés, les étudiants, les coeffs, + récupérés notamment de py:mod:`res_but` + """ + self.sem_cube = self.nt.sem_cube + self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted + self.modimpl_coefs_df = self.nt.modimpl_coefs_df - # Calcul des moyennes de chaque étudiant puis ajoute la moyenne au sens "DUT" - for tag in self.tagdict: - self.add_moyennesTag(tag, self.comp_MoyennesTag(tag, force=True)) - self.add_moyennesTag("dut", self.get_moyennes_DUT()) - self.taglist = sorted( - list(self.tagdict.keys()) + ["dut"] - ) # actualise la liste des tags + """Les inscriptions au module et les dispenses d'UE""" + self.modimpl_inscr_df = self.nt.modimpl_inscr_df + self.ues = self.nt.ues + self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() + self.dispense_ues = self.nt.dispense_ues + + """Les tags (en supprimant les tags réservés)""" + self.tags = get_synthese_tags_semestre(self.nt.formsemestre) + for tag in TAGS_RESERVES: + if tag in self.tags: + del self.tags[tag] + + """Calcul des moyennes & les classements de chaque étudiant à chaque tag""" + self.moyennes_tags = {} + + for tag in self.tags: + pe_comp.pe_print(f" -> Traitement du tag {tag}") + moy_gen_tag = self.compute_moyenne_tag(tag) + class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int + self.moyennes_tags[tag] = { + "notes": moy_gen_tag, + "classements": class_gen_tag, + "min": moy_gen_tag.min(), + "max": moy_gen_tag.max(), + "moy": moy_gen_tag.mean(), + "nb_inscrits": len(moy_gen_tag), + } + + """Ajoute les moyennes générales de BUT pour le semestre considéré""" + pe_comp.pe_print(f" -> Traitement du tag but") + moy_gen_but = self.nt.etud_moy_gen + class_gen_but = self.nt.etud_moy_gen_ranks_int + self.moyennes_tags["but"] = { + "notes": moy_gen_but, + "classements": class_gen_but, + "min": moy_gen_but.min(), + "max": moy_gen_but.max(), + "moy": moy_gen_but.mean(), + "nb_inscrits": len(moy_gen_but), + } + + """Synthétise l'ensemble des moyennes dans un dataframe""" + self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique + self.notes = self.df_tagtable() # Le dataframe synthétique des notes (=moyennes par tag) # ----------------------------------------------------------------------------- def get_etudids(self): @@ -148,89 +134,64 @@ class SemestreTag(pe_tagtable.TableTag): return [etud["etudid"] for etud in self.inscrlist] # ----------------------------------------------------------------------------- - def do_tagdict(self): - """Parcourt les modimpl du semestre (instance des modules d'un programme) et synthétise leurs données sous la - forme d'un dictionnaire reliant les tags saisis dans le programme aux - données des modules qui les concernent, à savoir les modimpl_id, les module_id, le code du module, le coeff, - la pondération fournie avec le tag (par défaut 1 si non indiquée). - { tagname1 : { modimpl_id1 : { 'module_id' : ..., 'coeff' : ..., 'coeff_norm' : ..., 'ponderation' : ..., 'module_code' : ..., 'ue_xxx' : ...}, - modimpl_id2 : .... - }, - tagname2 : ... - } - Renvoie le dictionnaire ainsi construit. + def compute_moyenne_tag(self, tag: str) -> list: + """Calcule la moyenne des étudiants pour le tag indiqué, + pour ce SemestreTag. - Rq: choix fait de repérer les modules par rapport à leur modimpl_id (valable uniquement pour un semestre), car - correspond à la majorité des calculs de moyennes pour les étudiants - (seuls ceux qui ont capitalisé des ue auront un régime de calcul différent). - """ - tagdict = {} + Sont pris en compte les modules implémentés associés au tag, + avec leur éventuel coefficient de **repondération**, en utilisant les notes + chargées pour ce SemestreTag. - for modimpl in self.modimpls: - modimpl_id = modimpl.id - # liste des tags pour le modimpl concerné: - tags = sco_tag_module.module_tag_list(modimpl.module.id) - - for ( - tag - ) in tags: # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2" - [tagname, ponderation] = sco_tag_module.split_tagname_coeff( - tag - ) # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1) - # tagname = tagname - if tagname not in tagdict: # Ajout d'une clé pour le tag - tagdict[tagname] = {} - - # Ajout du modimpl au tagname considéré - tagdict[tagname][modimpl_id] = { - "module_id": modimpl.module.id, # les données sur le module - "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre - "ponderation": ponderation, # la pondération demandée pour le tag sur le module - "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee - "ue_id": modimpl.module.ue.id, # les données sur l'ue - "ue_code": modimpl.module.ue.ue_code, - "ue_acronyme": modimpl.module.ue.acronyme, - } - return tagdict - - # ----------------------------------------------------------------------------- - def comp_MoyennesTag(self, tag, force=False) -> list: - """Calcule et renvoie les "moyennes" de tous les étudiants du SemTag - (non défaillants) à un tag donné, en prenant en compte - tous les modimpl_id concerné par le tag, leur coeff et leur pondération. Force ou non le calcul de la moyenne lorsque des notes sont manquantes. Renvoie les informations sous la forme d'une liste [ (moy, somme_coeff_normalise, etudid), ...] """ - lesMoyennes = [] - for etudid in self.get_etudids(): - ( - notes, - coeffs_norm, - ponderations, - ) = self.get_listesNotesEtCoeffsTagEtudiant( - tag, etudid - ) # les notes associées au tag - coeffs = comp_coeff_pond( - coeffs_norm, ponderations - ) # les coeff pondérés par les tags - (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme( - notes, coeffs, force=force - ) - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes - # ----------------------------------------------------------------------------- - def get_moyennes_DUT(self): - """Lit les moyennes DUT du semestre pour tous les étudiants - et les renvoie au même format que comp_MoyennesTag""" - return [ - (self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids() + """Adaptation du mask de calcul des moyennes au tag visé""" + modimpls_mask = [ + modimpl.module.ue.type != UE_SPORT + for modimpl in self.formsemestre.modimpls_sorted ] + """Désactive tous les modules qui ne sont pas pris en compte pour ce tag""" + for i, modimpl in enumerate(self.formsemestre.modimpls_sorted): + if modimpl.moduleimpl_id not in self.tags[tag]: + modimpls_mask[i] = False + + """Applique la pondération des coefficients""" + modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy() + for modimpl_id in self.tags[tag]: + ponderation = self.tags[tag][modimpl_id]["ponderation"] + modimpl_coefs_ponderes_df[modimpl_id] *= ponderation + + """Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)""" + moyennes_ues_tag = moy_ue.compute_ue_moys_apc( + self.sem_cube, + self.etuds, + self.formsemestre.modimpls_sorted, + self.modimpl_inscr_df, + modimpl_coefs_ponderes_df, + modimpls_mask, + self.dispense_ues, + block=self.formsemestre.block_moyennes, + ) + + """Les ects""" + ects = self.ues_inscr_parcours_df.fillna(0.0) * [ + ue.ects for ue in self.ues if ue.type != UE_SPORT + ] + + """Calcule la moyenne générale dans le semestre (pondérée par le ECTS)""" + moy_gen_tag = moy_sem.compute_sem_moys_apc_using_ects( + moyennes_ues_tag, + ects, + formation_id=self.formsemestre.formation_id, + skip_empty_ues=True, + ) + + return moy_gen_tag + # ----------------------------------------------------------------------------- def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2): """Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id. @@ -319,27 +280,6 @@ class SemestreTag(pe_tagtable.TableTag): return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records") return [] - # ----------------------------------------------------------------------------- - def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid): - """Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes - donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans - le calcul de la moyenne du tag. - Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée). - Les pondérations sont celles déclarées avec le tag (cf. _tagdict).""" - - notes = [] - coeffs_norm = [] - ponderations = [] - for moduleimpl_id, modimpl in self.tagdict[ - tag - ].items(): # pour chaque module du semestre relatif au tag - (note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid) - if note != None: - notes.append(note) - coeffs_norm.append(coeff_norm) - ponderations.append(modimpl["ponderation"]) - return (notes, coeffs_norm, ponderations) - # ----------------------------------------------------------------------------- # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug # ----------------------------------------------------------------------------- @@ -435,8 +375,9 @@ class SemestreTag(pe_tagtable.TableTag): return chaine def str_tagsModulesEtCoeffs(self): - """Renvoie une chaine affichant la liste des tags associés au semestre, les modules qui les concernent et les coeffs de pondération. - Plus concrêtement permet d'afficher le contenu de self._tagdict""" + """Renvoie une chaine affichant la liste des tags associés au semestre, + les modules qui les concernent et les coeffs de pondération. + Plus concrètement permet d'afficher le contenu de self._tagdict""" chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n" chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n" taglist = self.get_all_tags() @@ -463,25 +404,6 @@ class SemestreTag(pe_tagtable.TableTag): # ********************************************* -def comp_coeff_pond(coeffs, ponderations): - """ - Applique une ponderation (indiquée dans la liste ponderations) à une liste de coefficients : - ex: coeff = [2, 3, 1, None], ponderation = [1, 2, 0, 1] => [2*1, 3*2, 1*0, None] - Les coeff peuvent éventuellement être None auquel cas None est conservé ; - Les pondérations sont des floattants - """ - if ( - coeffs == None - or ponderations == None - or not isinstance(coeffs, list) - or not isinstance(ponderations, list) - or len(coeffs) != len(ponderations) - ): - raise ValueError("Erreur de paramètres dans comp_coeff_pond") - return [ - (None if coeffs[i] == None else coeffs[i] * ponderations[i]) - for i in range(len(coeffs)) - ] # ----------------------------------------------------------------------------- @@ -509,3 +431,58 @@ def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: if ue_status is None: return None return ue_status["moy"] + + +# ----------------------------------------------------------------------------- +def get_synthese_tags_semestre(formsemestre: FormSemestre): + """Etant données les implémentations des modules du semestre (modimpls), + synthétise les tags les concernant (tags saisis dans le programme pédagogique) + en les associant aux modimpls qui les concernent (modimpl_id, module_id, + le code du module, coeff et pondération fournie avec le tag (par défaut 1 si non indiquée)). + + { tagname1: { modimpl_id1: { 'module_id': ..., + 'coeff': ..., + 'coeff_norm': ..., + 'ponderation': ..., + 'module_code': ..., + 'ue_xxx': ...}, + } + } + + Args: + formsemestre: Le formsemestre à la base de la recherche des tags + """ + synthese_tags = {} + + """Instance des modules du semestre""" + modimpls = formsemestre.modimpls_sorted + + for modimpl in modimpls: + modimpl_id = modimpl.id + + """Liste des tags pour le module concerné""" + tags = sco_tag_module.module_tag_list(modimpl.module.id) + + """Traitement des tags recensés, chacun pouvant étant de la forme + "mathématiques", "théorie", "pe:0", "maths:2" + """ + for tag in tags: + """Extraction du nom du tag et du coeff de pondération""" + (tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag) + + """Ajout d'une clé pour le tag""" + if tagname not in synthese_tags: + synthese_tags[tagname] = {} + + """Ajout du module (modimpl) au tagname considéré""" + synthese_tags[tagname][modimpl_id] = { + "modimpl": modimpl, # les données sur le module + # "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre + "ponderation": ponderation, # la pondération demandée pour le tag sur le module + # "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee + # "ue_id": modimpl.module.ue.id, # les données sur l'ue + # "ue_code": modimpl.module.ue.ue_code, + # "ue_acronyme": modimpl.module.ue.acronyme, + } + + return synthese_tags diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py deleted file mode 100644 index f8c35bd42..000000000 --- a/app/pe/pe_settag.py +++ /dev/null @@ -1,324 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on Fri Sep 9 09:15:05 2016 - -@author: barasc -""" - -from app.pe.pe_tools import pe_print, PE_DEBUG -from app.pe import pe_tagtable - - -class SetTag(pe_tagtable.TableTag): - """Agrège plusieurs semestres (ou settag) taggués (SemestreTag/Settag de 1 à 4) pour extraire des moyennes - et des classements par tag pour un groupe d'étudiants donnés. - par. exemple fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S' - Le settag est identifié sur la base du dernier semestre (ici le 'S3') ; - les étudiants considérés sont donc ceux inscrits dans ce S3 - à condition qu'ils disposent d'un parcours sur tous les semestres fusionnés valides (par. ex - un etudiant non inscrit dans un S1 mais dans un S2 et un S3 n'est pas pris en compte). - """ - - # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, parcours): - pe_tagtable.TableTag.__init__(self, nom=nom_combinaison) - self.combinaison = nom_combinaison - self.parcours = parcours # Le groupe de semestres/parcours à aggréger - - # ------------------------------------------------------------------------------------------- - def set_Etudiants( - self, etudiants: list[dict], juryPEDict, etudInfoDict, nom_sem_final=None - ): - """Détermine la liste des étudiants à prendre en compte, en partant de - la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide.""" - if nom_sem_final: - self.nom += "_" + nom_sem_final - for etudid in etudiants: - parcours_incomplet = ( - sum([juryPEDict[etudid][nom_sem] is None for nom_sem in self.parcours]) - > 0 - ) # manque-t-il des formsemestre_id validant aka l'étudiant n'a pas été inscrit dans tous les semestres de l'aggrégat - if not parcours_incomplet: - self.inscrlist.append(etudInfoDict[etudid]) - self.identdict[etudid] = etudInfoDict[etudid] - - delta = len(etudiants) - len(self.inscrlist) - if delta > 0: - pe_print(self.nom + " -> " + str(delta) + " étudiants supprimés") - - # Le sous-ensemble des parcours - self.parcoursDict = {etudid: juryPEDict[etudid] for etudid in self.identdict} - - # ------------------------------------------------------------------------------------------- - def get_Fids_in_settag(self): - """Renvoie la liste des semestres (leur formsemestre_id) à prendre en compte - pour le calcul des moyennes, en considérant tous les étudiants inscrits et - tous les semestres de leur parcours""" - return list( - { - self.parcoursDict[etudid][nom_sem] - for etudid in self.identdict - for nom_sem in self.parcours - } - ) - - # --------------------------------------------------------------------------------------------- - def set_SemTagDict(self, SemTagDict): - """Mémorise les semtag nécessaires au jury.""" - self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} - if PE_DEBUG >= 1: - pe_print(" => %d semestres fusionnés" % len(self.SemTagDict)) - - # ------------------------------------------------------------------------------------------------------------------- - def comp_data_settag(self): - """Calcule tous les données numériques relatives au settag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.taglist = self.do_taglist() # la liste des tags - self.do_tagdict() # le dico descriptif des tags - # if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist )) - - # Calcul des moyennes de chaque étudiant par tag - reussiteAjoutTag = {"OK": [], "KO": []} - for tag in self.taglist: - moyennes = self.comp_MoyennesSetTag(tag, force=False) - res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne - reussiteAjoutTag["OK" if res else "KO"].append(tag) - if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG: - pe_print( - " => Fusion de %d tags : " % (len(reussiteAjoutTag["OK"])) - + ", ".join(reussiteAjoutTag["OK"]) - ) - if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG: - pe_print( - " => %d tags manquants : " % (len(reussiteAjoutTag["KO"])) - + ", ".join(reussiteAjoutTag["KO"]) - ) - - # ------------------------------------------------------------------------------------------------------------------- - def get_etudids(self): - return list(self.identdict.keys()) - - # ------------------------------------------------------------------------------------------------------------------- - def do_taglist(self): - """Parcourt les tags des semestres taggués et les synthétise sous la forme - d'une liste en supprimant les doublons - """ - ensemble = [] - for semtag in self.SemTagDict.values(): - ensemble.extend(semtag.get_all_tags()) - return sorted(list(set(ensemble))) - - # ------------------------------------------------------------------------------------------------------------------- - def do_tagdict(self): - """Synthétise la liste des modules pris en compte dans le calcul d'un tag (pour analyse des résultats)""" - self.tagdict = {} - for semtag in self.SemTagDict.values(): - for tag in semtag.get_all_tags(): - if tag != "dut": - if tag not in self.tagdict: - self.tagdict[tag] = {} - for mod in semtag.tagdict[tag]: - self.tagdict[tag][mod] = semtag.tagdict[tag][mod] - - # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): - """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) - avec notes et coeffs deux listes""" - lesSemsDeLEtudiant = [ - self.parcoursDict[etudid][nom_sem] for nom_sem in self.parcours - ] # peuvent être None - - notes = [ - self.SemTagDict[fid].get_moy_from_resultats(tag, etudid) - for fid in lesSemsDeLEtudiant - if tag in self.SemTagDict[fid].taglist - ] # eventuellement None - coeffs = [ - self.SemTagDict[fid].get_coeff_from_resultats(tag, etudid) - for fid in lesSemsDeLEtudiant - if tag in self.SemTagDict[fid].taglist - ] - return (notes, coeffs) - - # ------------------------------------------------------------------------------------------------------------------- - def comp_MoyennesSetTag(self, tag, force=False): - """Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués - de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération - appliqué dans cette moyenne. - - Force ou non le calcul de la moyenne lorsque des notes sont manquantes. - - Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...} - """ - # if tag not in self.get_all_tags() : return None - - # Calcule les moyennes - lesMoyennes = [] - for ( - etudid - ) in ( - self.get_etudids() - ): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag - (notes, coeffs_norm) = self.get_NotesEtCoeffsSetTagEtudiant( - tag, etudid - ) # lecture des notes associées au tag - (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme( - notes, coeffs_norm, force=force - ) - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes - - -class SetTagInterClasse(pe_tagtable.TableTag): - """Récupère les moyennes de SetTag aggrégant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2 - pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo - nom_combinaison = 'S1' ou '1A' - """ - - # ------------------------------------------------------------------------------------------------------------------- - def __init__(self, nom_combinaison, diplome): - pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") - self.combinaison = nom_combinaison - self.parcoursDict = {} - - # ------------------------------------------------------------------------------------------- - def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None): - """Détermine la liste des étudiants à prendre en compte, en partant de - la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée. - Renvoie le nombre d'étudiants effectivement inscrits.""" - if nom_sem_final: - self.nom += "_" + nom_sem_final - for etudid in etudiants: - if juryPEDict[etudid][self.combinaison] != None: - self.inscrlist.append(etudInfoDict[etudid]) - self.identdict[etudid] = etudInfoDict[etudid] - self.parcoursDict[etudid] = juryPEDict[etudid] - return len(self.inscrlist) - - # ------------------------------------------------------------------------------------------- - def get_Fids_in_settag(self): - """Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte - pour les moyennes, en considérant tous les étudiants inscrits""" - return list( - {self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict} - ) - - # --------------------------------------------------------------------------------------------- - def set_SetTagDict(self, SetTagDict): - """Mémorise les settag nécessaires au jury.""" - self.SetTagDict = { - fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None - } - if PE_DEBUG >= 1: - pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) - - # ------------------------------------------------------------------------------------------------------------------- - def comp_data_settag(self): - """Calcule tous les données numériques relatives au settag""" - # Attributs relatifs aux tag pour les modules pris en compte - self.taglist = self.do_taglist() - - # if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist )) - - # Calcul des moyennes de chaque étudiant par tag - reussiteAjoutTag = {"OK": [], "KO": []} - for tag in self.taglist: - moyennes = self.get_MoyennesSetTag(tag, force=False) - res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne - reussiteAjoutTag["OK" if res else "KO"].append(tag) - if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG: - pe_print( - " => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"])) - + ", ".join(reussiteAjoutTag["OK"]) - ) - if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG: - pe_print( - " => %d tags manquants : " % (len(reussiteAjoutTag["KO"])) - + ", ".join(reussiteAjoutTag["KO"]) - ) - - # ------------------------------------------------------------------------------------------------------------------- - def get_etudids(self): - return list(self.identdict.keys()) - - # ------------------------------------------------------------------------------------------------------------------- - def do_taglist(self): - """Parcourt les tags des semestres taggués et les synthétise sous la forme - d'une liste en supprimant les doublons - """ - ensemble = [] - for settag in self.SetTagDict.values(): - ensemble.extend(settag.get_all_tags()) - return sorted(list(set(ensemble))) - - # ------------------------------------------------------------------------------------------------------------------- - def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid): - """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs) - avec notes et coeffs deux listes""" - leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison] - - note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid) - coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats( - tag, etudid - ) - return (note, coeff) - - # ------------------------------------------------------------------------------------------------------------------- - def get_MoyennesSetTag(self, tag, force=False): - """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat, - et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération - appliqué dans cette moyenne. - - Force ou non le calcul de la moyenne lorsque des notes sont manquantes. - - Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...} - """ - # if tag not in self.get_all_tags() : return None - - # Calcule les moyennes - lesMoyennes = [] - for ( - etudid - ) in ( - self.get_etudids() - ): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag - (moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant( - tag, etudid - ) # lecture des notes associées au tag - lesMoyennes += [ - (moyenne, somme_coeffs, etudid) - ] # Un tuple (pour classement résumant les données) - return lesMoyennes diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tabletags.py similarity index 83% rename from app/pe/pe_tagtable.py rename to app/pe/pe_tabletags.py index 49bf22cc8..5c995095b 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tabletags.py @@ -41,36 +41,55 @@ import datetime import numpy as np from app.scodoc import sco_utils as scu +import pandas as pd + + +TAGS_RESERVES = ["but"] class TableTag(object): """ - Classe mémorisant les moyennes des étudiants à différents tag et permettant de calculer les rangs et les statistiques : - - nom : Nom représentatif des données de la Table - - inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme : + Classe mémorisant les moyennes des étudiants à différents tags et permettant de + calculer des rangs et des statistiques. + + Ses attributs sont: + + * nom : Nom représentatif des données de la Table + * inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme : { etudid : dictionnaire d'info extrait de Scodoc, ...} - - taglist : Liste triée des noms des tags - - resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée + * taglist : Liste triée des noms des tags + * resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme : { tag : { etudid: (note_moy, somme_coeff_norm), ...} - - rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme : + * rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme : { tag : {etudid: rang, ...} } - - nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags) - - statistiques : Dictionnaire donnant les stastitiques (moyenne, min, max) des résultats par tag de la forme : + * nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags) + * statistiques : Dictionnaire donnant les statistiques (moyenne, min, max) des résultats par tag de la forme : { tag : (moy, min, max), ...} """ - def __init__(self, nom=""): + def __init__(self, nom: str): + """Les attributs basiques des TagTable, qui seront initialisés + dans les classes dérivées + """ self.nom = nom - self.inscrlist = [] - self.identdict = {} - self.taglist = [] + """Les étudiants""" + self.etudiants = {} + """Les moyennes par tag""" + self.moyennes_tags = {} + + + # ----------------------------------------------------------------------------------------------------------- + def get_all_tags(self): + """Liste des tags de la table, triée par ordre alphabétique + + Returns: + Liste de tags triés par ordre alphabétique + """ + return sorted(self.moyennes_tags.keys()) - self.resultats = {} - self.rangs = {} - self.statistiques = {} # ***************************************************************************************************************** # Accesseurs @@ -80,8 +99,8 @@ class TableTag(object): def get_moy_from_resultats(self, tag, etudid): """Renvoie la moyenne obtenue par un étudiant à un tag donné au regard du format de self.resultats""" return ( - self.resultats[tag][etudid][0] - if tag in self.resultats and etudid in self.resultats[tag] + self.moyennes_tags[tag][etudid][0] + if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag] else None ) @@ -90,7 +109,7 @@ class TableTag(object): """Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats""" return ( self.rangs[tag][etudid] - if tag in self.resultats and etudid in self.resultats[tag] + if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag] else None ) @@ -100,16 +119,11 @@ class TableTag(object): au regard du format de self.resultats. """ return ( - self.resultats[tag][etudid][1] - if tag in self.resultats and etudid in self.resultats[tag] + self.moyennes_tags[tag][etudid][1] + if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag] else None ) - # ----------------------------------------------------------------------------------------------------------- - def get_all_tags(self): - """Renvoie la liste des tags du semestre triée par ordre alphabétique""" - # return self.taglist - return sorted(self.resultats.keys()) # ----------------------------------------------------------------------------------------------------------- def get_nbinscrits(self): @@ -170,10 +184,12 @@ class TableTag(object): avec calcul du rang :param tag: Un tag :param listMoyEtCoeff: Une liste donnant [ (moy, coeff, etudid) ] + + TODO:: Inutile maintenant ? """ # ajout des moyennes au dictionnaire résultat if listMoyEtCoeff: - self.resultats[tag] = { + self.moyennes_tags[tag] = { etudid: (moyenne, somme_coeffs) for (moyenne, somme_coeffs, etudid) in listMoyEtCoeff } @@ -204,11 +220,12 @@ class TableTag(object): self.statistiques """ stats = ("-NA-", "-", "-") - if tag not in self.resultats: + if tag not in self.moyennes_tags: return stats notes = [ - self.get_moy_from_resultats(tag, etudid) for etudid in self.resultats[tag] + self.get_moy_from_resultats(tag, etudid) + for etudid in self.moyennes_tags[tag] ] # les notes du tag notes_valides = [ note for note in notes if isinstance(note, float) and note != None @@ -225,7 +242,7 @@ class TableTag(object): """Renvoie une chaine de caractères (valable pour un csv) décrivant la moyenne et le rang d'un étudiant, pour un tag donné ; """ - if tag not in self.get_all_tags() or etudid not in self.resultats[tag]: + if tag not in self.get_all_tags() or etudid not in self.moyennes_tags[tag]: return "" moystr = TableTag.str_moytag( @@ -256,30 +273,32 @@ class TableTag(object): str_moytag = classmethod(str_moytag) # ----------------------------------------------------------------------- - def str_tagtable(self, delim=";", decimal_sep=","): - """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags.""" - entete = ["etudid", "nom", "prenom"] - for tag in self.get_all_tags(): - entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]] - chaine = delim.join(entete) + "\n" + def df_tagtable(self): + """Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags - for etudid in self.identdict: - descr = delim.join( - [ - etudid, - self.identdict[etudid]["nom"], - self.identdict[etudid]["prenom"], - ] - ) - descr += delim + self.str_res_d_un_etudiant(etudid, delim) - chaine += descr + "\n" - - # Ajout des stats ... à faire - - if decimal_sep != ".": - return chaine.replace(".", decimal_sep) + Returns: + Un dataframe etudids x tag (avec tag par ordre alphabétique) + """ + tags = self.get_all_tags() + if tags: + dict_series = {tag: self.moyennes_tags[tag]["notes"] for tag in tags} + df = pd.DataFrame(dict_series) + return df else: - return chaine + return None + + def str_tagtable(self): + """Renvoie une chaine de caractère listant toutes les moyennes, + les rangs des étudiants pour tous les tags.""" + + etudiants = self.etudiants + df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) + + for tag in self.get_all_tags(): + df = df.join(self.moyennes_tags[tag]["notes"].rename(f"moy {tag}")) + df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}")) + + return df.to_csv(sep=";") # ************************************************************************ diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py deleted file mode 100644 index ead3a2d18..000000000 --- a/app/pe/pe_tools.py +++ /dev/null @@ -1,960 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on Thu Sep 8 09:36:33 2016 - -@author: barasc -""" - -import os -import datetime -import re -import unicodedata - - -from flask import g - -import app.scodoc.sco_utils as scu -from app import log -from app.scodoc.sco_logos import find_logo - -PE_DEBUG = 0 - -if not PE_DEBUG: - # log to notes.log - def pe_print(*a, **kw): - # kw is ignored. log always add a newline - log(" ".join(a)) - -else: - pe_print = print # print function - - -# Generated LaTeX files are encoded as: -PE_LATEX_ENCODING = "utf-8" - -# /opt/scodoc/tools/doc_poursuites_etudes -REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/") -REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/") - -PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex" -PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex" -PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex" -PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex" - -# ---------------------------------------------------------------------------------------- - - -# ---------------------------------------------------------------------------------------- -def print_semestres_description(sems, avec_affichage_debug=False): - """Dediee a l'affichage d'un semestre pour debug du module""" - - def chaine_semestre(sem): - desc = ( - "S" - + str(sem["semestre_id"]) - + " " - + sem["modalite"] - + " " - + sem["anneescolaire"] - ) - desc += " (" + sem["annee_debut"] + "/" + sem["annee_fin"] + ") " - desc += str(sem["formation_id"]) + " / " + str(sem["formsemestre_id"]) - desc += " - " + sem["titre_num"] - return desc - - if avec_affichage_debug == True: - if isinstance(sems, list): - for sem in sems: - pe_print(chaine_semestre(sem)) - else: - pe_print(chaine_semestre(sems)) - - -# ---------------------------------------------------------------------------------------- -def calcul_age(born): - """Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'. - Aucun test de validité sur le format de la date n'est fait. - """ - if not isinstance(born, str) or born == "": - return "" - - donnees = born.split("/") - naissance = datetime.datetime(int(donnees[2]), int(donnees[1]), int(donnees[0])) - today = datetime.date.today() - return ( - today.year - - naissance.year - - ((today.month, today.day) < (naissance.month, naissance.day)) - ) - - -# ---------------------------------------------------------------------------------------- -def remove_accents(input_unicode_str): - """Supprime les accents d'une chaine unicode""" - nfkd_form = unicodedata.normalize("NFKD", input_unicode_str) - only_ascii = nfkd_form.encode("ASCII", "ignore") - return only_ascii - - -def escape_for_latex(s): - """Protège les caractères pour inclusion dans du source LaTeX""" - if not s: - return "" - conv = { - "&": r"\&", - "%": r"\%", - "$": r"\$", - "#": r"\#", - "_": r"\_", - "{": r"\{", - "}": r"\}", - "~": r"\textasciitilde{}", - "^": r"\^{}", - "\\": r"\textbackslash{}", - "<": r"\textless ", - ">": r"\textgreater ", - } - exp = re.compile( - "|".join( - re.escape(key) - for key in sorted(list(conv.keys()), key=lambda item: -len(item)) - ) - ) - return exp.sub(lambda match: conv[match.group()], s) - - -# ---------------------------------------------------------------------------------------- -def list_directory_filenames(path): - """List of regular filenames in a directory (recursive) - Excludes files and directories begining with . - """ - R = [] - for root, dirs, files in os.walk(path, topdown=True): - dirs[:] = [d for d in dirs if d[0] != "."] - R += [os.path.join(root, fn) for fn in files if fn[0] != "."] - return R - - -def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): - """Read pathname server file and add content to zip under path_in_zip""" - rooted_path_in_zip = os.path.join(ziproot, path_in_zip) - zipfile.write(filename=pathname, arcname=rooted_path_in_zip) - # data = open(pathname).read() - # zipfile.writestr(rooted_path_in_zip, data) - - -def add_refs_to_register(register, directory): - """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme - filename => pathname - """ - length = len(directory) - for pathname in list_directory_filenames(directory): - filename = pathname[length + 1 :] - register[filename] = pathname - - -def add_pe_stuff_to_zip(zipfile, ziproot): - """Add auxiliary files to (already opened) zip - Put all local files found under config/doc_poursuites_etudes/local - and config/doc_poursuites_etudes/distrib - If a file is present in both subtrees, take the one in local. - - Also copy logos - """ - register = {} - # first add standard (distrib references) - distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib") - add_refs_to_register(register=register, directory=distrib_dir) - # then add local references (some oh them may overwrite distrib refs) - local_dir = os.path.join(REP_LOCAL_AVIS, "local") - add_refs_to_register(register=register, directory=local_dir) - # at this point register contains all refs (filename, pathname) to be saved - for filename, pathname in register.items(): - add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename) - - # Logos: (add to logos/ directory in zip) - logos_names = ["header", "footer"] - for name in logos_names: - logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) - if logo is not None: - add_local_file_to_zip( - zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename - ) - - -# ---------------------------------------------------------------------------------------- -# Variable pour le debug des avislatex (en squeezant le calcul du jury souvent long) -JURY_SYNTHESE_POUR_DEBUG = { - "EID1810": { - "nom": "ROUX", - "entree": "2016", - "civilite_str": "M.", - "promo": 2016, - "S2": { - "groupe": { - "informatique": ( - 13.184230769230767, - 0.21666666666666667, - "18", - 78, - 9.731491508491509, - 18.46846153846154, - 18.46846153846154, - ), - "technique": ( - 12.975409073359078, - 0.6166666666666666, - "16", - 78, - 9.948540264387688, - 18.29285714285714, - 18.29285714285714, - ), - "pe": ( - 12.016584900684544, - 1.116666666666667, - "20", - 78, - 9.83147528118408, - 17.691755169172936, - 17.691755169172936, - ), - "mathematiques": ( - 12.25, - 0.1, - "15 ex", - 78, - 8.45153073717949, - 19.0625, - 19.0625, - ), - "dut": ( - 12.43750128724589, - 1.0, - "19", - 78, - 10.151630181286441, - 17.881104750512645, - 17.881104750512645, - ), - }, - "promo": { - "informatique": ( - 13.184230769230767, - 0.21666666666666667, - "25", - 73, - 11.696187214611871, - 18.51346153846154, - 18.51346153846154, - ), - "technique": ( - 12.975409073359078, - 0.6166666666666666, - "23", - 73, - 11.862307379173147, - 17.616047267953675, - 17.616047267953675, - ), - "pe": ( - 12.016584900684544, - 1.116666666666667, - "28", - 73, - 11.571004424603757, - 16.706338951857248, - 16.706338951857248, - ), - "mathematiques": ( - 12.25, - 0.1, - "18 ex", - 73, - 10.00886454908676, - 19.0625, - 19.0625, - ), - "dut": ( - 12.43750128724589, - 1.0, - "25", - 73, - 11.88798432763965, - 17.397627309377608, - 17.397627309377608, - ), - }, - }, - "S1": { - "groupe": { - "informatique": ( - 16.064999999999998, - 0.16666666666666669, - "11", - 82, - 11.020296296296294, - 19.325999999999997, - 19.325999999999997, - ), - "technique": ( - 14.513007894736845, - 0.6333333333333333, - "11", - 82, - 11.195082967479676, - 18.309764912280702, - 18.309764912280702, - ), - "pe": ( - 13.260301515151516, - 1.1, - "19", - 82, - 10.976036277232245, - 17.7460505050505, - 17.7460505050505, - ), - "mathematiques": ( - 11.142850000000001, - 0.13333333333333333, - "34", - 82, - 10.314605121951217, - 19.75, - 19.75, - ), - "dut": ( - 13.54367375, - 1.0, - "19", - 82, - 11.22193801880508, - 18.226902529333334, - 18.226902529333334, - ), - }, - "promo": { - "informatique": ( - 16.064999999999998, - 0.16666666666666669, - "15", - 73, - 13.265276712328768, - 19.325999999999997, - 19.325999999999997, - ), - "technique": ( - 14.513007894736845, - 0.6333333333333333, - "16", - 73, - 12.996048795361693, - 18.309764912280702, - 18.309764912280702, - ), - "pe": ( - 13.260301515151516, - 1.1, - "25", - 73, - 12.4107195879539, - 17.7460505050505, - 17.7460505050505, - ), - "mathematiques": ( - 11.142850000000001, - 0.13333333333333333, - "39", - 73, - 11.320606952054794, - 19.75, - 19.75, - ), - "dut": ( - 13.54367375, - 1.0, - "25", - 73, - 12.730581289342638, - 18.226902529333334, - 18.226902529333334, - ), - }, - }, - "4S": { - "groupe": { - "informatique": ( - 14.84359375, - 0.5333333333333333, - "2", - 19, - 10.69933552631579, - 18.28646875, - 18.28646875, - ), - "pe": ( - 12.93828572598162, - 3.75, - "4", - 19, - 11.861967145815218, - 15.737718967605682, - 15.737718967605682, - ), - "mathematiques": (None, None, "1 ex", 19, None, None, None), - "ptut": (None, None, "1 ex", 19, None, None, None), - "dut": ( - 13.511767410105122, - 4.0, - "4", - 19, - 12.573349864933606, - 15.781651391587998, - 15.781651391587998, - ), - }, - "promo": { - "informatique": ( - 16.075, - 0.1, - "4", - 73, - 10.316541095890413, - 19.333333333333336, - 19.333333333333336, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "13", - 73, - 11.657102668465479, - 16.853208080808084, - 16.853208080808084, - ), - "mathematiques": ( - None, - None, - "55 ex", - 73, - 7.705091805555555, - 19.8, - 19.8, - ), - "dut": ( - 14.425416666666665, - 1.0, - "12", - 73, - 13.188168241098825, - 16.612613522048612, - 16.612613522048612, - ), - }, - }, - "S4": { - "groupe": { - "informatique": ( - 16.075, - 0.1, - "1", - 19, - 8.799078947368422, - 16.075, - 16.075, - ), - "technique": ( - 13.835576923076923, - 0.4333333333333333, - "4", - 19, - 12.238304655870447, - 16.521153846153847, - 16.521153846153847, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "4", - 19, - 12.292846491228072, - 16.25833333333334, - 16.25833333333334, - ), - "dut": ( - 14.425416666666665, - 1.0, - "6", - 19, - 13.628367861842106, - 15.267566666666665, - 15.267566666666665, - ), - }, - "promo": { - "informatique": ( - 16.075, - 0.1, - "4", - 73, - 10.316541095890413, - 19.333333333333336, - 19.333333333333336, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "13", - 73, - 11.657102668465479, - 16.853208080808084, - 16.853208080808084, - ), - "technique": ( - 13.835576923076923, - 0.4333333333333333, - "11", - 73, - 12.086685508009952, - 17.25909420289855, - 17.25909420289855, - ), - "mathematiques": ( - None, - None, - "55 ex", - 73, - 7.705091805555555, - 19.8, - 19.8, - ), - "ptut": ( - 13.5, - 0.13333333333333333, - "50", - 73, - 13.898173515981734, - 17.083333333333332, - 17.083333333333332, - ), - "dut": ( - 14.425416666666665, - 1.0, - "12", - 73, - 13.188168241098825, - 16.612613522048612, - 16.612613522048612, - ), - }, - }, - "1A": { - "groupe": { - "informatique": ( - 14.43673913043478, - 0.38333333333333336, - "16", - 78, - 11.046040002787066, - 18.85992173913043, - 18.85992173913043, - ), - "technique": ( - 13.754459142857144, - 1.25, - "14", - 78, - 11.179785631638866, - 18.493250340136054, - 18.493250340136054, - ), - "pe": ( - 12.633767581547854, - 2.216666666666667, - "18", - 78, - 10.912253971396854, - 18.39547581699347, - 18.39547581699347, - ), - "mathematiques": ( - 11.617342857142857, - 0.23333333333333334, - "24", - 78, - 9.921286855287565, - 19.375000000000004, - 19.375000000000004, - ), - "dut": ( - 12.990587518622945, - 2.0, - "18", - 78, - 11.2117147027821, - 18.391345156695156, - 18.391345156695156, - ), - }, - "promo": { - "informatique": ( - 13.184230769230767, - 0.21666666666666667, - "25", - 73, - 11.696187214611871, - 18.51346153846154, - 18.51346153846154, - ), - "technique": ( - 12.975409073359078, - 0.6166666666666666, - "23", - 73, - 11.862307379173147, - 17.616047267953675, - 17.616047267953675, - ), - "pe": ( - 12.016584900684544, - 1.116666666666667, - "28", - 73, - 11.571004424603757, - 16.706338951857248, - 16.706338951857248, - ), - "mathematiques": ( - 12.25, - 0.1, - "18 ex", - 73, - 10.00886454908676, - 19.0625, - 19.0625, - ), - "dut": ( - 12.43750128724589, - 1.0, - "25", - 73, - 11.88798432763965, - 17.397627309377608, - 17.397627309377608, - ), - }, - }, - "2A": { - "groupe": { - "informatique": ( - 15.88333333333333, - 0.15000000000000002, - "2", - 19, - 9.805818713450288, - 17.346666666666668, - 17.346666666666668, - ), - "pe": ( - 13.378513043478259, - 1.5333333333333334, - "6", - 19, - 12.099566454042717, - 16.06209927536232, - 16.06209927536232, - ), - "technique": ( - 13.965093333333336, - 1.1666666666666665, - "5", - 19, - 12.51068332957394, - 16.472092380952386, - 16.472092380952386, - ), - "mathematiques": (None, None, "1 ex", 19, None, None, None), - "dut": ( - 14.032947301587301, - 2.0, - "4", - 19, - 13.043386086541773, - 15.574706269841268, - 15.574706269841268, - ), - }, - "promo": { - "informatique": ( - 16.075, - 0.1, - "4", - 73, - 10.316541095890413, - 19.333333333333336, - 19.333333333333336, - ), - "pe": ( - 13.52416666666667, - 0.49999999999999994, - "13", - 73, - 11.657102668465479, - 16.853208080808084, - 16.853208080808084, - ), - "technique": ( - 13.835576923076923, - 0.4333333333333333, - "11", - 73, - 12.086685508009952, - 17.25909420289855, - 17.25909420289855, - ), - "mathematiques": ( - None, - None, - "55 ex", - 73, - 7.705091805555555, - 19.8, - 19.8, - ), - "dut": ( - 14.425416666666665, - 1.0, - "12", - 73, - 13.188168241098825, - 16.612613522048612, - 16.612613522048612, - ), - }, - }, - "nbSemestres": 4, - "code_nip": "21414563", - "prenom": "Baptiste", - "age": "21", - "lycee": "PONCET", - "3S": { - "groupe": { - "informatique": ( - 14.559423076923077, - 0.43333333333333335, - "3", - 19, - 11.137856275303646, - 18.8095, - 18.8095, - ), - "pe": ( - 12.84815019664546, - 3.25, - "4", - 19, - 11.795678015751701, - 15.657624449801428, - 15.657624449801428, - ), - "technique": ( - 13.860638395358142, - 1.9833333333333334, - "3", - 19, - 12.395950358235925, - 17.340302131732695, - 17.340302131732695, - ), - "mathematiques": ( - 11.494044444444445, - 0.3, - "6", - 19, - 9.771571754385965, - 14.405358333333334, - 14.405358333333334, - ), - "dut": ( - 13.207217657917942, - 3.0, - "4", - 19, - 12.221677199297439, - 15.953012966561774, - 15.953012966561774, - ), - }, - "promo": { - "informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0), - "pe": ( - 13.308035483870967, - 1.0333333333333334, - "17", - 73, - 11.854843423685786, - 16.191317607526884, - 16.191317607526884, - ), - "technique": ( - 14.041625757575758, - 0.7333333333333333, - "10", - 73, - 11.929466899200335, - 16.6400384469697, - 16.6400384469697, - ), - "mathematiques": ( - 11.0625, - 0.06666666666666667, - "40", - 73, - 11.418430205479451, - 19.53, - 19.53, - ), - "dut": ( - 13.640477936507937, - 1.0, - "14", - 73, - 12.097377866597594, - 16.97088994741667, - 16.97088994741667, - ), - }, - }, - "bac": "STI2D", - "S3": { - "groupe": { - "informatique": (15.5, 0.05, "5", 19, 12.842105263157896, 20.0, 20.0), - "pe": ( - 13.308035483870967, - 1.0333333333333334, - "8", - 19, - 12.339608902093943, - 15.967147311827956, - 15.967147311827956, - ), - "technique": ( - 14.041625757575758, - 0.7333333333333333, - "7", - 19, - 13.128539816586922, - 16.44310151515152, - 16.44310151515152, - ), - "mathematiques": ( - 11.0625, - 0.06666666666666667, - "6", - 19, - 9.280921052631578, - 16.125, - 16.125, - ), - "dut": ( - 13.640477936507937, - 1.0, - "8", - 19, - 12.83638061385213, - 15.881845873015871, - 15.881845873015871, - ), - }, - "promo": { - "informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0), - "pe": ( - 13.308035483870967, - 1.0333333333333334, - "17", - 73, - 11.854843423685786, - 16.191317607526884, - 16.191317607526884, - ), - "technique": ( - 14.041625757575758, - 0.7333333333333333, - "10", - 73, - 11.929466899200335, - 16.6400384469697, - 16.6400384469697, - ), - "mathematiques": ( - 11.0625, - 0.06666666666666667, - "40", - 73, - 11.418430205479451, - 19.53, - 19.53, - ), - "dut": ( - 13.640477936507937, - 1.0, - "14", - 73, - 12.097377866597594, - 16.97088994741667, - 16.97088994741667, - ), - }, - }, - "parcours": [ - { - "nom_semestre_dans_parcours": "semestre 4 FAP 2016", - "titreannee": "DUT RT UFA (PPN 2013), semestre 4 FAP 2016", - }, - { - "nom_semestre_dans_parcours": "semestre 3 FAP 2015-2016", - "titreannee": "DUT RT UFA (PPN 2013), semestre 3 FAP 2015-2016", - }, - { - "nom_semestre_dans_parcours": "semestre 2 FI 2015", - "titreannee": "DUT RT, semestre 2 FI 2015", - }, - { - "nom_semestre_dans_parcours": "semestre 1 FI 2014-2015", - "titreannee": "DUT RT, semestre 1 FI 2014-2015", - }, - ], - } -} diff --git a/app/pe/pe_trajectoire.py b/app/pe/pe_trajectoire.py new file mode 100644 index 000000000..d527de3cd --- /dev/null +++ b/app/pe/pe_trajectoire.py @@ -0,0 +1,150 @@ +import app.pe.pe_comp as pe_tools +from app.models import FormSemestre +from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date + + +class Trajectoire: + """Modélise, pour un aggrégat visé (par ex: 'S2', '3S', '2A') + et un ensemble d'étudiants donnés, + la combinaison des formsemestres des étudiants amenant à un semestre + terminal visé. + + Si l'aggrégat est un semestre de type Si, elle stocke le (ou les) + formsemestres de numéro i qu'ont suivis l'étudiant pour atteindre le Si + (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) + + Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie + les semestres que les étudiants ont suivis pour les amener jusqu'au semestre + terminal de la trajectoire (par ex: ici un S3). + Ces semestres peuvent être : + * des S1+S2+S1+S2+S3 si redoublement de la 1ère année + * des S1+S2+(année de césure)+S3 si césure, ... + """ + + def __init__(self, nom_aggregat: str, semestre_final: FormSemestre): + """Modélise un ensemble de formsemestres d'étudiants + amenant à un semestre terminal + + Args: + nom_aggregat: Un nom d'aggrégat (par ex: '5S') + semestre_final: Le semestre final de l'aggrégat + """ + self.nom = nom_aggregat + self.semestre_final = semestre_final + self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id) + + """Les semestres à aggréger""" + self.semestres_aggreges = {} + + + def add_semestres_a_aggreger(self, semestres: dict[int: FormSemestre]): + """Ajoute des semestres au semestre à aggréger + + Args: + semestres: Dictionnaire ``{fid: FormSemestre(fid)} à ajouter`` + """ + self.semestres_aggreges = self.semestres_aggreges | semestres + + + + def get_repr(self): + """Représentation textuelle d'une trajectoire + basée sur ses semestres aggrégés""" + noms = [] + for fid in self.semestres_aggreges: + semestre = self.semestres_aggreges[fid] + noms.append(f"S{semestre.semestre_id}({fid})") + noms = sorted(noms) + repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}" + if noms: + repr += " - " + "+".join(noms) + return repr + + +class TrajectoiresJuryPE: + """Centralise toutes les trajectoires du jury PE""" + + def __init__(self, annee_diplome: int): + """ + Args: + annee_diplome: L'année de diplomation + """ + + self.annee_diplome = annee_diplome + """Toutes les trajectoires possibles""" + self.trajectoires: dict[tuple: Trajectoire] = {} + """Quelle trajectoires pour quel étudiant : + dictionnaire {etudid: {nom_aggregat: Trajectoire}}""" + self.suivi: dict[int: str] = {} + + + def cree_trajectoires(self, etudiants: EtudiantsJuryPE): + """Créé toutes les trajectoires, au regard du cursus des étudiants + analysés + les mémorise dans les données de l'étudiant + """ + + for nom_aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS: + + """L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)""" + noms_semestre_de_aggregat = pe_tools.PARCOURS[nom_aggregat]["aggregat"] + nom_semestre_terminal = noms_semestre_de_aggregat[-1] + + for etudid in etudiants.cursus: + if etudid not in self.suivi: + self.suivi[etudid] = {aggregat: None + for aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS} + + """Le formsemestre terminal (dernier en date) associé au + semestre marquant la fin de l'aggrégat + (par ex: son dernier S3 en date)""" + semestres = etudiants.cursus[etudid][nom_semestre_terminal] + if semestres: + formsemestre_final = get_dernier_semestre_en_date(semestres) + + """Ajout ou récupération de la trajectoire""" + trajectoire_id = (nom_aggregat, formsemestre_final.formsemestre_id) + if trajectoire_id not in self.trajectoires: + trajectoire = Trajectoire(nom_aggregat, formsemestre_final) + self.trajectoires[trajectoire_id] = trajectoire + else: + trajectoire = self.trajectoires[trajectoire_id] + + """La liste des semestres de l'étudiant à prendre en compte + pour cette trajectoire""" + semestres_a_aggreger = etudiants.get_trajectoire(etudid, formsemestre_final) + + """Ajout des semestres à la trajectoire""" + trajectoire.add_semestres_a_aggreger(semestres_a_aggreger) + + """Mémoire la trajectoire suivie par l'étudiant""" + self.suivi[etudid][nom_aggregat] = trajectoire + + """Vérifications""" + # dernier_semestre_aggregat = get_dernier_semestre_en_date(semestres_aggreges) + # assert dernier_semestre_aggregat == formsemestre_terminal + + +def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int): + """Pour un nom d'aggrégat donné (par ex: 'S3') et un semestre terminal cible + identifié par son formsemestre_id (par ex: 'S3 2022-2023'), + renvoie l'ensemble des semestres à prendre en compte dans + l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}. + + Fusionne les cursus individuels des étudiants, dont le cursus correspond + à l'aggrégat visé. + + Args: + aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S) + formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au + dernier semestre de l'aggrégat + """ + noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"] + + formsemestres = {} + for etudid in self.cursus: + cursus_etudiant = self.cursus[etudid][aggregat] + if formsemestre_id_terminal in cursus_etudiant: + formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal] + formsemestres = formsemestres | formsemestres_etudiant + return formsemestres + diff --git a/app/pe/pe_trajectoiretag.py b/app/pe/pe_trajectoiretag.py new file mode 100644 index 000000000..71b665dca --- /dev/null +++ b/app/pe/pe_trajectoiretag.py @@ -0,0 +1,217 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on Fri Sep 9 09:15:05 2016 + +@author: barasc +""" + +from app.comp import moy_sem +from app.comp.res_sem import load_formsemestre_results +from app.pe.pe_semtag import SemestreTag +from app.pe import pe_tabletags +import pandas as pd +import numpy as np +from app.pe.pe_trajectoire import Trajectoire + +from app.pe.pe_etudiant import EtudiantsJuryPE +from app.pe.pe_tabletags import TableTag + + +class TrajectoireTag(TableTag): + """Calcule les moyennes par tag d'une combinaison de semestres + (trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et + par un semestre terminal, pour extraire les classements par tag pour un + groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous + participé au semestre terminal. + + Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S' + + """ + + # ------------------------------------------------------------------------------------------------------------------- + def __init__( + self, + nom: str, + trajectoire: Trajectoire, + semestres_taggues: dict[int, SemestreTag], + donnees_etudiants: EtudiantsJuryPE, + ): + """ """ + TableTag.__init__(self, nom=nom) + + """La trajectoire associée""" + self.trajectoire_id = trajectoire.trajectoire_id + self.trajectoire = trajectoire + + """Le formsemestre terminal et les semestres aggrégés""" + self.formsemestre_terminal = trajectoire.semestre_final + nt = load_formsemestre_results(self.formsemestre_terminal) + + self.semestres_aggreges = trajectoire.semestres_aggreges + + """Les semestres tags associés aux semestres aggrégés""" + try: + self.semestres_tags_aggreges = { + frmsem_id: semestres_taggues[frmsem_id] + for frmsem_id in semestres_taggues + } + except: + raise ValueError("Semestres taggués manquants") + + """Les étudiants (état civil + cursus connu)""" + self.etuds = nt.etuds + # assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ? + self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} + + """Les tags extraits de tous les semestres""" + self.tags_sorted = self.do_taglist() + + """Construit le cube de notes""" + self.notes_cube = self.compute_notes_cube() + + """Calcul les moyennes par tag sous forme d'un dataframe""" + etudids = list(self.etudiants.keys()) + self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) + + """Synthétise les moyennes/classements par tag""" + self.moyennes_tags = {} + for tag in self.tags_sorted: + moy_gen_tag = self.notes[tag] + class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int + self.moyennes_tags[tag] = { + "notes": moy_gen_tag, + "classements": class_gen_tag, + "min": moy_gen_tag.min(), + "max": moy_gen_tag.max(), + "moy": moy_gen_tag.mean(), + "nb_inscrits": len(moy_gen_tag), + } + + def get_repr(self): + """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle + est basée)""" + return self.trajectoire.get_repr() + + def compute_notes_cube(self): + """Construit le cube de notes (etudid x tags x semestre_aggregé) + nécessaire au calcul des moyennes de l'aggrégat + """ + nb_tags = len(self.tags_sorted) + nb_etudiants = len(self.etuds) + nb_semestres = len(self.semestres_tags_aggreges) + + """Index du cube (etudids -> dim 0, tags -> dim 1)""" + etudids = [etud.etudid for etud in self.etuds] + tags = self.tags_sorted + semestres_id = list(self.semestres_tags_aggreges.keys()) + + dfs = {} + + for frmsem_id in semestres_id: + """Partant d'un dataframe vierge""" + df = pd.DataFrame(np.nan, index=etudids, columns=tags) + + """Charge les notes du semestre tag""" + notes = self.semestres_tags_aggreges[frmsem_id].notes + + """Les étudiants & les tags commun au dataframe final et aux notes du semestre)""" + etudids_communs = df.index.intersection(notes.index) + tags_communs = df.columns.intersection(notes.columns) + + """Injecte les notes par tag""" + df.loc[etudids_communs, tags_communs] = notes.loc[ + etudids_communs, tags_communs + ] + + """Stocke le df""" + dfs[frmsem_id] = df + + """Réunit les notes sous forme d'un cube etdids x tags x semestres""" + semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs] + etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1) + + return etudids_x_tags_x_semestres + + + + def do_taglist(self): + """Synthétise les tags à partir des semestres (taggués) aggrégés + + Returns: + Une liste de tags triés par ordre alphabétique + """ + tags = [] + for frmsem_id in self.semestres_tags_aggreges: + tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) + return sorted(set(tags)) + + +def compute_tag_moy(set_cube: np.array, etudids: list, tags: list): + """Calcul de la moyenne par tag sur plusieurs semestres. + La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles + + *Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag + par aggrégat de plusieurs semestres. + + Args: + set_cube: notes moyennes aux modules ndarray + (etuds x modimpls x UEs), des floats avec des NaN + etudids: liste des étudiants (dim. 0 du cube) + tags: liste des tags (dim. 1 du cube) + Returns: + Un DataFrame avec pour columns les moyennes par tags, + et pour rows les etudid + """ + nb_etuds, nb_tags, nb_semestres = set_cube.shape + assert nb_etuds == len(etudids) + assert nb_tags == len(tags) + + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + + # Enlève les NaN du cube pour les entrées manquantes + set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0) + + # Les moyennes par tag + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2) + + # Le dataFrame + etud_moy_tag_df = pd.DataFrame( + etud_moy_tag, + index=etudids, # les etudids + columns=tags, # les tags + ) + + return etud_moy_tag_df diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 7a49e7210..e464ab344 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -36,6 +36,8 @@ """ from flask import send_file, request + +from app.models import FormSemestre from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu @@ -43,18 +45,44 @@ from app.scodoc import sco_formsemestre from app.scodoc import html_sco_header from app.scodoc import sco_preferences -from app.pe import pe_tools -from app.pe import pe_jurype +from app.pe import pe_comp +from app.pe import pe_jury from app.pe import pe_avislatex def _pe_view_sem_recap_form(formsemestre_id): + sem_base = FormSemestre.get_formsemestre(formsemestre_id) + if not sem_base.formation.is_apc() or sem_base.formation.get_cursus().NB_SEM < 6: + H = [ + html_sco_header.sco_header(page_title="Avis de poursuite d'études"), + f"""

Génération des avis de poursuites d'études

+

+ Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de + poursuites d'études. +
+ De nombreux aspects sont paramétrables: + + voir la documentation + . + Cette fonction (en Scodoc9) n'est prévue que pour le BUT. +
+ Rendez-vous donc sur un semestre de BUT. +

+

Génération des avis de poursuites d'études

Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de - poursuites d'études. + poursuites d'études pour les étudiants diplômés en {diplome}.
De nombreux aspects sont paramétrables: contenu_latex + for etudid in etudids: + [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant( + jury, + etudid, + template_latex, + tag_annotation_pe, + footer_latex, + prefs, + ) + jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex) + latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico + + # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous + doc_latex = "\n% -----\n".join( + ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())] ) + jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex) - latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex - for etudid in etudids: - [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant( - jury, - etudid, - template_latex, - tag_annotation_pe, - footer_latex, - prefs, - ) - jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex) - latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico + # Ajoute image, LaTeX class file(s) and modeles + pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.nom_export_zip) - # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous - doc_latex = "\n% -----\n".join( - ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())] - ) - jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex) - - # Ajoute image, LaTeX class file(s) and modeles - pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP) data = jury.get_zipped_data() return send_file( data, mimetype="application/zip", - download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"), + download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"), as_attachment=True, ) diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index cf535e3b1..2f51e383e 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -292,24 +292,35 @@ def get_etud_tagged_modules(etudid, tagname): return R -def split_tagname_coeff(tag, separateur=":"): - """Découpe un tag saisi par un utilisateur pour en extraire un tagname - (chaine de caractère correspondant au tag) - et un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":"). - Renvoie le résultat sous la forme d'une liste [tagname, pond] où pond est un float +def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]: + """Découpage d'un tag, tel que saisi par un utilisateur dans le programme, + pour en extraire : - Auteur: CB + * son _nom de tag_ (tagname) (chaine de caractère correspondant au tag) + * un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":"). + + Args: + tag: La saisie utilisateur du tag dans le programme + separateur: Le séparateur des informations dans la saisie utilisateur + + Return: + Tuple (tagname, coeff_de_ponderation) extrait de la saisie utilisateur + (avec coeff_de_ponderation=1.0 si non mentionné) + + Author: + Cléo Baras """ if separateur in tag: temp = tag.split(":") try: pond = float(temp[1]) - return [temp[0], pond] + return (temp[0], pond) except: - return [tag, 1.0] # renvoie tout le tag si le découpage à échouer + """Renvoie tout le tag si le découpage à échouer""" + return (tag, 1.0) else: - # initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag - return [tag, 1.0] + """initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag""" + return (tag, 1.0) """Tests: