# -*- 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( "<br>", "\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