Compare commits

...

6 Commits

8 changed files with 112 additions and 603 deletions

View File

@ -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(
"<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

View File

@ -170,6 +170,7 @@ 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é
@ -185,11 +186,7 @@ def calcul_age(born: datetime.date) -> int:
return None
today = datetime.date.today()
return (
today.year
- born.year
- ((today.month, today.day) < (born.month, born.day))
)
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
def remove_accents(input_unicode_str):
@ -286,7 +283,9 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
# ----------------------------------------------------------------------------------------
def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int:
def get_annee_diplome_semestre(
sem_base: FormSemestre | dict, nbre_sem_formation: int = 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
@ -339,7 +338,9 @@ def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int:
return annee_fin + nbreAnRestant + increment
def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list:
def get_cosemestres_diplomants(
annee_diplome: int, formation_id: int
) -> dict[int, FormSemestre]:
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``
et s'intégrant à la formation donnée par son ``formation_id``.
@ -381,4 +382,3 @@ def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list:
cosemestres[fid] = cosem
return cosemestres

View File

@ -66,11 +66,15 @@ class EtudiantsJuryPE:
"Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)"
self.etudiants_ids = {}
"""Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)"""
self.cosemestres: dict[int, FormSemestre] = None
"Les cosemestres donnant lieu à même année de diplome"
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``.
dans la formation ``formation_id``. XXX TODO voir si on garde formation_id qui n'est pas utilisé ici
Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.
@ -79,14 +83,11 @@ class EtudiantsJuryPE:
*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(f"1) Recherche des coSemestres -> {len(cosemestres)} trouvé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(
@ -142,9 +143,13 @@ class EtudiantsJuryPE:
+ ", ".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])
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)}`
@ -201,7 +206,9 @@ class EtudiantsJuryPE:
"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
"nb_semestres": len(
semestres_etudiant
), # le nombre de semestres de l'étudiant
"abandon": False, # va être traité en dessous
}
@ -250,7 +257,6 @@ class EtudiantsJuryPE:
} # 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.
@ -411,17 +417,19 @@ def annee_diplome(identite: Identite) -> int:
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
]
)
dates_possibles_diplome = []
for sem_base in formsemestres:
annee = pe_comp.get_annee_diplome_semestre(sem_base)
if annee:
dates_possibles_diplome(annee)
if dates_possibles_diplome:
return max(dates_possibles_diplome)
else:
None
else:
return None
@ -502,8 +510,6 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b
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)}``.
@ -523,5 +529,3 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSeme
return dernier_semestre
else:
return None

View File

@ -1,4 +1,3 @@
from app.pe.pe_tabletags import TableTag
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE
@ -27,16 +26,14 @@ class AggregatInterclasseTag(TableTag):
trajectoires_jury_pe: TrajectoiresJuryPE,
trajectoires_taggues: dict[tuple, TrajectoireTag],
):
""""""
"""Table nommée au nom de l'aggrégat (par ex: 3S"""
# 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)"""
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" # TODO
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
"""
# 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]
@ -49,8 +46,8 @@ class AggregatInterclasseTag(TableTag):
trajectoire_id
]
"""Les trajectoires suivies par les étudiants du jury, en ne gardant que
celles associées aux diplomés"""
# 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]
@ -58,10 +55,10 @@ class AggregatInterclasseTag(TableTag):
"""Les tags"""
self.tags_sorted = self.do_taglist()
"""Construit la matrice de notes"""
# Construit la matrice de notes
self.notes = self.compute_notes_matrice()
"""Synthétise les moyennes/classements par tag"""
# Synthétise les moyennes/classements par tag
self.moyennes_tags = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
@ -79,7 +76,6 @@ class AggregatInterclasseTag(TableTag):
"""Une représentation textuelle"""
return f"Aggrégat {self.nom}"
def do_taglist(self):
"""Synthétise les tags à partir des trajectoires_tagguées
@ -87,39 +83,35 @@ class AggregatInterclasseTag(TableTag):
Une liste de tags triés par ordre alphabétique
"""
tags = []
for trajectoire_id in self.trajectoires_taggues:
trajectoire = self.trajectoires_taggues[trajectoire_id]
for trajectoire in self.trajectoires_taggues.values():
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)
# nb_tags = len(self.tags_sorted) unused ?
# nb_etudiants = len(self.diplomes_ids)
"""Index de la matrice (etudids -> dim 0, tags -> dim 1)"""
# Index de la matrice (etudids -> dim 0, tags -> dim 1)
etudids = list(self.diplomes_ids)
tags = self.tags_sorted
"""Partant d'un dataframe vierge"""
# 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"""
for trajectoire in self.trajectoires_taggues.values():
# Charge les moyennes par tag de la trajectoire tagguée
notes = trajectoire.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"""
# Injecte les notes par tag
df.loc[etudids_communs, tags_communs] = notes.loc[
etudids_communs, tags_communs
]
return df

View File

@ -107,13 +107,14 @@ class JuryPE(object):
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.nom_export_zip = f"Jury_PE_{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}"
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)
@ -197,7 +198,7 @@ class JuryPE(object):
open(filename, "rb").read(),
)
"""Fin !!!! Tada :)"""
# Fin !!!! Tada :)
def add_file_to_zip(self, filename: str, data, path=""):
"""Add a file to our zip
@ -256,7 +257,7 @@ class JuryPE(object):
etudids = list(self.diplomes_ids)
"""Récupération des données des étudiants"""
# Récupération des données des étudiants
administratif = {}
nbre_semestres_max = self.etudiants.nbre_etapes_max_diplomes()
@ -265,13 +266,18 @@ class JuryPE(object):
cursus = self.etudiants.cursus[etudid]
formsemestres = cursus["formsemestres"]
if cursus["diplome"]:
diplome = cursus["diplome"]
else:
diplome = "indéterminé"
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"],
"Date de diplome": diplome,
"Nbre de semestres": len(formsemestres),
}
@ -279,10 +285,10 @@ class JuryPE(object):
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
"""Construction du dataframe"""
# Construction du dataframe
df = pd.DataFrame.from_dict(administratif, orient="index")
"""Tri par nom/prénom"""
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
@ -348,10 +354,10 @@ class JuryPE(object):
}
# Fin de l'aggrégat
"""Construction du dataFrame"""
# Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index")
"""Tri par nom/prénom"""
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
@ -400,10 +406,10 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
pe_comp.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}")
"""Créé le semestre_tag et exécute les calculs de moyennes"""
# Crée le semestre_tag et exécute les calculs de moyennes
formsemestretag = SemestreTag(nom, frmsem_id)
"""Stocke le semestre taggué"""
# Stocke le semestre taggué
semestres_tags[frmsem_id] = formsemestretag
return semestres_tags

View File

@ -53,7 +53,7 @@ def _pe_view_sem_recap_form(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"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
"""<h2 class="formsemestre">Génération des avis de poursuites d'études (V2 BUT EXPERIMENTALE)</h2>
<p class="help">
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
poursuites d'études.
@ -61,13 +61,12 @@ def _pe_view_sem_recap_form(formsemestre_id):
De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes"
target="_blank" rel="noopener noreferrer">
voir la documentation
voir la documentation (en cours de révision)
</a>.
Cette fonction (en Scodoc9) n'est prévue que pour le BUT.
<br>
Rendez-vous donc sur un semestre de BUT.
</p>
<p class=
""",
]
return "\n".join(H) + html_sco_header.sco_footer()
@ -77,7 +76,13 @@ def _pe_view_sem_recap_form(formsemestre_id):
H = [
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études (V2 BUT EXPERIMENTALE)</h2>
<div class="alert-warning">
Fonction expérimentale pour le BUT : travaux en cours, merci de tester
et de faire part de vos expériences sur le Discord.
</div>
<p class="help">
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
poursuites d'études pour les étudiants diplômés en {diplome}.
@ -86,7 +91,7 @@ def _pe_view_sem_recap_form(formsemestre_id):
<a href="https://scodoc.org/AvisPoursuiteEtudes"
target="_blank" rel="noopener noreferrer">
voir la documentation
</a>.
</a> (en cours de révision).
</p>
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
enctype="multipart/form-data">

View File

@ -34,8 +34,9 @@
Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
"""
import http
import re
from flask import g, url_for
from flask import g
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
@ -43,11 +44,9 @@ from app.models import FormSemestre
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError
# Opérations à implementer:
# + liste des modules des formations de code donné (formation_code) avec ce tag
@ -100,6 +99,17 @@ class ScoTag(object):
def __repr__(self): # debug
return '<tag "%s">' % self.title
@classmethod
def check_tag_title(cls, title: str) -> bool:
""" "true si le tag est acceptable: longueur max (32), syntaxe
un_tag ou un_tag:78
"""
if not title or len(title) > 32:
return False
if re.match(r"^[A-Za-z0-9\-_$!\.]*(:[0-9]*)?$", title):
return True
return False
def delete(self):
"""Delete this tag.
This object should not be used after this call !
@ -243,10 +253,17 @@ def module_tag_set(module_id="", taglist=None):
elif isinstance(taglist, str):
taglist = taglist.split(",")
taglist = [t.strip() for t in taglist]
log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist))
log(f"module_tag_set: module_id={module_id} taglist={taglist}")
# Check tags syntax
for tag in taglist:
if not ScoTag.check_tag_title(tag):
return scu.json_error(404, "invalid tag")
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
# Sanity check:
Mod = sco_edit_module.module_list(args={"module_id": module_id})
if not Mod:
mod_dict = sco_edit_module.module_list(args={"module_id": module_id})
if not mod_dict:
raise ScoValueError("invalid module !")
newtags = set(taglist)

View File

@ -9,6 +9,8 @@ $(function () {
$.post("module_tag_set", {
module_id: field.data("module_id"),
taglist: tags.join(),
}).fail(function(jqXHR, textStatus, errorThrown) {
alert("erreur: tag non enregistré");
});
},
autocomplete: {