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,21 +83,18 @@ 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(
" => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids)
)
"""Analyse des parcours étudiants pour déterminer leur année effective de diplome
"""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")
@ -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)}`
@ -198,10 +203,12 @@ class EtudiantsJuryPE:
"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
"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.
@ -372,7 +378,7 @@ class EtudiantsJuryPE:
"""
nbres_semestres = []
for etudid in self.diplomes_ids:
nbres_semestres.append( self.cursus[etudid]["nb_semestres"] )
nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
return max(nbres_semestres)
@ -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)
@ -133,7 +134,7 @@ class JuryPE(object):
filename, formsemestretag.str_tagtable(), path="details_semestres"
)
"""Génère les trajectoires (combinaison de semestres suivis
"""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(
@ -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,11 +285,11 @@ 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"""
df.sort_values(by=["Nom", "Prenom"], inplace = True)
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def df_tag(self, tag):
@ -348,11 +354,11 @@ 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"""
df.sort_values(by=["Nom", "Prenom"], inplace = True)
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury
@ -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.
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: {