Compare commits
6 Commits
be7bb588cf
...
02bccb58aa
Author | SHA1 | Date |
---|---|---|
Cléo Baras | 02bccb58aa | |
Emmanuel Viennet | 4f7da8bfa4 | |
Emmanuel Viennet | a439c4c985 | |
Emmanuel Viennet | d991eb007c | |
Emmanuel Viennet | 4f10d017be | |
Emmanuel Viennet | 3a3d47ebe4 |
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue